This is an automated email from the ASF dual-hosted git repository.

riemer pushed a commit to branch add-multi-language-support-templates
in repository https://gitbox.apache.org/repos/asf/streampipes.git

commit 23765e02b3a0a127258caf88a753605dc313af48
Author: Dominik Riemer <[email protected]>
AuthorDate: Fri Jan 2 13:56:27 2026 +0100

    Add multi-language support to connect transformation
---
 streampipes-connect-transformer-api/pom.xml        | 10 ++-
 .../transformer/api/TransformationEngine.java      |  3 +-
 .../transformer/api/TransformationEngines.java     | 13 +++-
 .../transformer/groovy/GroovyScriptEngine.java     | 11 +++-
 .../transformer/js/GraalJsScriptEngine.java        | 16 ++++-
 .../streampipes/model/connect/ScriptMetadata.java  | 12 ++--
 .../svcdiscovery/SpServiceRegistration.java        | 23 ++++++-
 .../ExtensionsServiceEndpointGenerator.java        | 10 +--
 .../endpoint/ExtensionsServiceEndpointUtils.java   | 11 ++++
 .../TransformationScriptLanguageResource.java      | 66 +++++++++++++++++++
 .../svcdiscovery/api/ISpServiceDiscovery.java      |  4 ++
 .../svcdiscovery/SpServiceDiscoveryCore.java       | 12 +++-
 .../StreamPipesExtensionsServiceBase.java          |  9 ++-
 .../lib/apis/connect-script-languages.service.ts   | 29 +++++++++
 .../lib/apis/connect-script-templates.service.ts   |  0
 .../src/lib/model/gen/streampipes-model.ts         | 47 +++++++++++++-
 .../platform-services/src/public-api.ts            |  1 +
 .../adapter-configuration-state.service.ts         | 14 +++-
 .../configure-fields-preview.component.scss        |  4 +-
 .../configure-fields-error-message.component.html  |  9 ++-
 .../configure-fields-error-message.component.scss  |  2 +-
 .../configure-schema.component.html                | 75 ++++++++++++++++++----
 .../configure-schema.component.scss                |  4 +-
 .../configure-schema/configure-schema.component.ts | 71 ++++++++++++++++----
 24 files changed, 390 insertions(+), 66 deletions(-)

diff --git a/streampipes-connect-transformer-api/pom.xml 
b/streampipes-connect-transformer-api/pom.xml
index 1278b99369..801c646bb3 100644
--- a/streampipes-connect-transformer-api/pom.xml
+++ b/streampipes-connect-transformer-api/pom.xml
@@ -31,9 +31,15 @@
     <artifactId>streampipes-connect-transformer-api</artifactId>
 
     <properties>
-        <maven.compiler.source>21</maven.compiler.source>
-        <maven.compiler.target>21</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
 
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.streampipes</groupId>
+            <artifactId>streampipes-model</artifactId>
+            <version>0.99.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+
 </project>
diff --git 
a/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngine.java
 
b/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngine.java
index cb5b210c1f..11e34c774b 100644
--- 
a/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngine.java
+++ 
b/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngine.java
@@ -19,6 +19,7 @@
 package org.apache.streampipes.connect.transformer.api;
 
 import 
org.apache.streampipes.connect.transformer.api.exception.ScriptCompilationException;
+import org.apache.streampipes.model.connect.ScriptMetadata;
 
 /**
  * Compiles code templates into reusable transformers.
@@ -28,7 +29,7 @@ public interface TransformationEngine {
   /**
    * Identifier of the underlying scripting language.
    */
-  String language();
+  ScriptMetadata metadata();
 
   /**
    * Compile the user-provided script into an executable transformer.
diff --git 
a/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngines.java
 
b/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngines.java
index 300668bae7..99b2a8863c 100644
--- 
a/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngines.java
+++ 
b/streampipes-connect-transformer-api/src/main/java/org/apache/streampipes/connect/transformer/api/TransformationEngines.java
@@ -18,7 +18,10 @@
 
 package org.apache.streampipes.connect.transformer.api;
 
+import org.apache.streampipes.model.connect.ScriptMetadata;
+
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Supplier;
 
@@ -29,10 +32,18 @@ public enum TransformationEngines {
   private final Map<String, Supplier<TransformationEngine>> 
transformationEngines = new HashMap<>();
 
   public void registerEngine(Supplier<TransformationEngine> engineSupplier) {
-    transformationEngines.put(engineSupplier.get().language(), engineSupplier);
+    transformationEngines.put(engineSupplier.get().metadata().language(), 
engineSupplier);
   }
 
   public TransformationEngine getTransformationEngine(String language) {
     return transformationEngines.get(language).get();
   }
+
+  public List<ScriptMetadata> getAvailableEngineMetadata() {
+    return transformationEngines
+        .values()
+        .stream()
+        .map(transformationEngineSupplier -> 
transformationEngineSupplier.get().metadata())
+        .toList();
+  }
 }
diff --git 
a/streampipes-connect-transformer-groovy/src/main/java/org/apache/streampipes/connect/transformer/groovy/GroovyScriptEngine.java
 
b/streampipes-connect-transformer-groovy/src/main/java/org/apache/streampipes/connect/transformer/groovy/GroovyScriptEngine.java
index 406a33bad6..a217b8b3aa 100644
--- 
a/streampipes-connect-transformer-groovy/src/main/java/org/apache/streampipes/connect/transformer/groovy/GroovyScriptEngine.java
+++ 
b/streampipes-connect-transformer-groovy/src/main/java/org/apache/streampipes/connect/transformer/groovy/GroovyScriptEngine.java
@@ -23,6 +23,7 @@ import 
org.apache.streampipes.connect.transformer.api.TransformationEngine;
 import 
org.apache.streampipes.connect.transformer.api.exception.ScriptCompilationException;
 import 
org.apache.streampipes.connect.transformer.api.exception.ScriptExecutionException;
 import 
org.apache.streampipes.connect.transformer.api.utils.TransformationEngineConversionUtils;
+import org.apache.streampipes.model.connect.ScriptMetadata;
 
 import groovy.lang.Binding;
 import groovy.lang.GroovyShell;
@@ -35,8 +36,12 @@ import java.util.Map;
 public class GroovyScriptEngine implements TransformationEngine {
 
   @Override
-  public String language() {
-    return "groovy";
+  public ScriptMetadata metadata() {
+    return new ScriptMetadata(
+        "groovy",
+    "Groovy",
+    ""
+    );
   }
 
   @Override
@@ -61,7 +66,7 @@ public class GroovyScriptEngine implements 
TransformationEngine {
       binding.setVariable("input", input);
       Script scriptInstance = InvokerHelper.createScript(scriptClass, binding);
       Object result = scriptInstance.run();
-      return TransformationEngineConversionUtils.ensureMap(result, language());
+      return TransformationEngineConversionUtils.ensureMap(result, 
metadata().language());
     } catch (Exception e) {
       throw new ScriptExecutionException("Groovy template execution failed", 
e);
     }
diff --git 
a/streampipes-connect-transformer-js/src/main/java/org/apache/streampipes/connect/transformer/js/GraalJsScriptEngine.java
 
b/streampipes-connect-transformer-js/src/main/java/org/apache/streampipes/connect/transformer/js/GraalJsScriptEngine.java
index a677d91782..6cb9b267a7 100644
--- 
a/streampipes-connect-transformer-js/src/main/java/org/apache/streampipes/connect/transformer/js/GraalJsScriptEngine.java
+++ 
b/streampipes-connect-transformer-js/src/main/java/org/apache/streampipes/connect/transformer/js/GraalJsScriptEngine.java
@@ -22,6 +22,7 @@ import 
org.apache.streampipes.connect.transformer.api.ScriptTransformer;
 import org.apache.streampipes.connect.transformer.api.TransformationEngine;
 import 
org.apache.streampipes.connect.transformer.api.exception.ScriptCompilationException;
 import 
org.apache.streampipes.connect.transformer.api.exception.ScriptExecutionException;
+import org.apache.streampipes.model.connect.ScriptMetadata;
 
 import org.graalvm.polyglot.Context;
 import org.graalvm.polyglot.HostAccess;
@@ -34,8 +35,17 @@ import java.util.Optional;
 public class GraalJsScriptEngine implements TransformationEngine {
 
   @Override
-  public String language() {
-    return "javascript";
+  public ScriptMetadata metadata() {
+    return new ScriptMetadata(
+        "javascript",
+        "JavaScript",
+        """
+            // returns the same event
+            function transform(event) {
+              return event;
+            }
+            """
+    );
   }
 
   @Override
@@ -83,7 +93,7 @@ public class GraalJsScriptEngine implements 
TransformationEngine {
       throws ScriptExecutionException {
     try {
       Value result = transformFunction.execute(input);
-      return PolyglotResultConverter.ensureMap(result, language());
+      return PolyglotResultConverter.ensureMap(result, metadata().language());
     } catch (PolyglotException e) {
       throw new ScriptExecutionException("Graal JS script execution failed", 
e);
     }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/ScriptMetadata.java
similarity index 76%
copy from 
ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss
copy to 
streampipes-model/src/main/java/org/apache/streampipes/model/connect/ScriptMetadata.java
index 8a0f012b14..e6b6633de2 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/connect/ScriptMetadata.java
@@ -16,10 +16,12 @@
  *
  */
 
-.error-color {
-    color: #963e3e;
-}
+package org.apache.streampipes.model.connect;
+
+import org.apache.streampipes.model.shared.annotation.TsModel;
 
-.error-text {
-    font-size: 12pt;
+@TsModel
+public record ScriptMetadata(String language,
+                             String name,
+                             String template) {
 }
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/extensions/svcdiscovery/SpServiceRegistration.java
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/extensions/svcdiscovery/SpServiceRegistration.java
index 756750a8e2..e1d6f26905 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/extensions/svcdiscovery/SpServiceRegistration.java
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/extensions/svcdiscovery/SpServiceRegistration.java
@@ -17,6 +17,7 @@
  */
 package org.apache.streampipes.model.extensions.svcdiscovery;
 
+import org.apache.streampipes.model.connect.ScriptMetadata;
 import org.apache.streampipes.model.extensions.ExtensionItemDescription;
 import org.apache.streampipes.model.shared.annotation.TsModel;
 import org.apache.streampipes.model.shared.api.Storable;
@@ -24,6 +25,7 @@ import org.apache.streampipes.model.shared.api.Storable;
 import com.google.gson.annotations.SerializedName;
 
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 @TsModel
@@ -43,6 +45,7 @@ public class SpServiceRegistration implements Storable {
   private String healthCheckPath;
   private long firstTimeSeenUnhealthy = 0;
   private SpServiceStatus status = SpServiceStatus.REGISTERED;
+  private List<ScriptMetadata> supportedScriptLanguages;
 
   private Set<ExtensionItemDescription> providedExtensions;
 
@@ -55,6 +58,7 @@ public class SpServiceRegistration implements Storable {
                                String host,
                                int port,
                                Set<SpServiceTag> tags,
+                               List<ScriptMetadata> supportedScriptLanguages,
                                String healthCheckPath,
                                Set<ExtensionItemDescription> 
providedExtensions) {
     this.svcType = svcType;
@@ -63,6 +67,7 @@ public class SpServiceRegistration implements Storable {
     this.host = host;
     this.port = port;
     this.tags = tags;
+    this.supportedScriptLanguages = supportedScriptLanguages;
     this.healthCheckPath = healthCheckPath;
     this.labels = new HashSet<>();
     this.providedExtensions = providedExtensions;
@@ -74,9 +79,20 @@ public class SpServiceRegistration implements Storable {
                                            String host,
                                            Integer port,
                                            Set<SpServiceTag> tags,
+                                           List<ScriptMetadata> 
supportedScriptLanguages,
                                            String healthCheckPath,
                                            Set<ExtensionItemDescription> 
providedExtensions) {
-    return new SpServiceRegistration(svcType, svcGroup, svcId, host, port, 
tags, healthCheckPath, providedExtensions);
+    return new SpServiceRegistration(
+        svcType,
+        svcGroup,
+        svcId,
+        host,
+        port,
+        tags,
+        supportedScriptLanguages,
+        healthCheckPath,
+        providedExtensions
+    );
   }
 
   public int getWeight() {
@@ -201,4 +217,9 @@ public class SpServiceRegistration implements Storable {
   public void setProvidedExtensions(Set<ExtensionItemDescription> 
providedExtensions) {
     this.providedExtensions = providedExtensions;
   }
+
+  public List<ScriptMetadata> getSupportedScriptLanguages() {
+    return supportedScriptLanguages;
+  }
+
 }
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointGenerator.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointGenerator.java
index 5987e769c3..1a59bc8888 100644
--- 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointGenerator.java
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointGenerator.java
@@ -35,7 +35,6 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.stream.Stream;
 
 public class ExtensionsServiceEndpointGenerator implements 
IExtensionsServiceEndpointGenerator {
 
@@ -88,7 +87,7 @@ public class ExtensionsServiceEndpointGenerator implements 
IExtensionsServiceEnd
                                            Set<SpServiceTag> 
customServiceTags) {
     return SpServiceDiscovery.getServiceDiscovery()
         .getServiceEndpoints(DefaultSpServiceTypes.EXT, true,
-                             getDesiredServiceTags(appId, 
spServiceUrlProvider, customServiceTags));
+                             
ExtensionsServiceEndpointUtils.getDesiredServiceTags(appId, 
spServiceUrlProvider, customServiceTags));
   }
 
   private String getServiceURL(String appId, SpServiceUrlProvider 
spServiceUrlProvider,
@@ -106,11 +105,4 @@ public class ExtensionsServiceEndpointGenerator implements 
IExtensionsServiceEnd
   public static boolean filtersSupported(SpServiceRegistration service, String 
tag) {
     return new HashSet<>(service.getTags()).stream().anyMatch(t -> 
t.asString().equals(tag));
   }
-
-  private List<String> getDesiredServiceTags(String appId, 
SpServiceUrlProvider serviceUrlProvider,
-                                             Set<SpServiceTag> 
customServiceTags) {
-    return Stream
-        .concat(Stream.of(serviceUrlProvider.getServiceTag(appId)), 
customServiceTags.stream())
-        .map(SpServiceTag::asString).toList();
-  }
 }
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointUtils.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointUtils.java
index 7c63a315bf..ef4a028ca8 100644
--- 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointUtils.java
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/execution/endpoint/ExtensionsServiceEndpointUtils.java
@@ -19,6 +19,7 @@ package org.apache.streampipes.manager.execution.endpoint;
 
 import org.apache.streampipes.model.base.NamedStreamPipesEntity;
 import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.extensions.svcdiscovery.SpServiceTag;
 import org.apache.streampipes.model.graph.DataProcessorDescription;
 import org.apache.streampipes.model.graph.DataProcessorInvocation;
 import org.apache.streampipes.model.graph.DataSinkDescription;
@@ -26,7 +27,10 @@ import org.apache.streampipes.model.graph.DataSinkInvocation;
 import org.apache.streampipes.storage.management.StorageDispatcher;
 import org.apache.streampipes.svcdiscovery.api.model.SpServiceUrlProvider;
 
+import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.stream.Stream;
 
 public class ExtensionsServiceEndpointUtils {
 
@@ -51,6 +55,13 @@ public class ExtensionsServiceEndpointUtils {
     }
   }
 
+  public static List<String> getDesiredServiceTags(String appId, 
SpServiceUrlProvider serviceUrlProvider,
+                                            Set<SpServiceTag> 
customServiceTags) {
+    return Stream
+        .concat(Stream.of(serviceUrlProvider.getServiceTag(appId)), 
customServiceTags.stream())
+        .map(SpServiceTag::asString).toList();
+  }
+
   private static boolean isDataProcessor(NamedStreamPipesEntity entity) {
     return entity instanceof DataProcessorInvocation || entity instanceof 
DataProcessorDescription;
   }
diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/TransformationScriptLanguageResource.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/TransformationScriptLanguageResource.java
new file mode 100644
index 0000000000..5000e107cf
--- /dev/null
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/TransformationScriptLanguageResource.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.rest.impl.connect;
+
+import org.apache.streampipes.connect.transformer.api.TransformationEngines;
+import 
org.apache.streampipes.manager.execution.endpoint.ExtensionsServiceEndpointUtils;
+import org.apache.streampipes.model.connect.ScriptMetadata;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.svcdiscovery.SpServiceDiscovery;
+import org.apache.streampipes.svcdiscovery.api.model.DefaultSpServiceTypes;
+import org.apache.streampipes.svcdiscovery.api.model.SpServiceUrlProvider;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/v2/connect/master/script-languages")
+public class TransformationScriptLanguageResource {
+
+  @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+  public List<ScriptMetadata> getAll(@RequestBody AdapterDescription 
adapterDescription) {
+    var languagesSupportedByCore = 
TransformationEngines.INSTANCE.getAvailableEngineMetadata();
+    var matchingServices = SpServiceDiscovery.getServiceDiscovery()
+        .getService(DefaultSpServiceTypes.EXT, true,
+            ExtensionsServiceEndpointUtils.getDesiredServiceTags(
+                adapterDescription.getAppId(),
+                SpServiceUrlProvider.ADAPTER,
+                adapterDescription.getDeploymentConfiguration()
+                    .getDesiredServiceTags())
+        );
+
+    if (!matchingServices.isEmpty()) {
+      return matchingServices.get(0)
+          .getSupportedScriptLanguages()
+          .stream()
+          .filter(metadata -> languagesSupportedByCore
+              .stream()
+              .anyMatch(coreLanguage -> 
coreLanguage.language().equals(metadata.language()))
+          )
+          .toList();
+    } else {
+      throw new IllegalArgumentException("No supported languages found");
+    }
+  }
+}
diff --git 
a/streampipes-service-discovery-api/src/main/java/org/apache/streampipes/svcdiscovery/api/ISpServiceDiscovery.java
 
b/streampipes-service-discovery-api/src/main/java/org/apache/streampipes/svcdiscovery/api/ISpServiceDiscovery.java
index afee5cdb74..646ce8ba62 100644
--- 
a/streampipes-service-discovery-api/src/main/java/org/apache/streampipes/svcdiscovery/api/ISpServiceDiscovery.java
+++ 
b/streampipes-service-discovery-api/src/main/java/org/apache/streampipes/svcdiscovery/api/ISpServiceDiscovery.java
@@ -46,6 +46,10 @@ public interface ISpServiceDiscovery {
 
   List<SpServiceRegistration> getService(boolean restrictToHealthy);
 
+  List<SpServiceRegistration> getService(String serviceGroup,
+                                         boolean restrictToHealthy,
+                                         List<String> filterByTags);
+
   /**
    * Get all service
    * @return list of services
diff --git 
a/streampipes-service-discovery/src/main/java/org/apache/streampipes/svcdiscovery/SpServiceDiscoveryCore.java
 
b/streampipes-service-discovery/src/main/java/org/apache/streampipes/svcdiscovery/SpServiceDiscoveryCore.java
index ced6b806cd..26deec5803 100644
--- 
a/streampipes-service-discovery/src/main/java/org/apache/streampipes/svcdiscovery/SpServiceDiscoveryCore.java
+++ 
b/streampipes-service-discovery/src/main/java/org/apache/streampipes/svcdiscovery/SpServiceDiscoveryCore.java
@@ -58,7 +58,7 @@ public class SpServiceDiscoveryCore implements 
ISpServiceDiscovery {
   }
 
   @Override
-  public List<String> getServiceEndpoints(String serviceGroup,
+  public List<SpServiceRegistration> getService(String serviceGroup,
                                           boolean restrictToHealthy,
                                           List<String> filterByTags) {
     List<SpServiceRegistration> activeServices = findServices(0);
@@ -68,10 +68,18 @@ public class SpServiceDiscoveryCore implements 
ISpServiceDiscovery {
         .filter(service -> allFiltersSupported(service, filterByTags))
         .filter(service -> !restrictToHealthy
             || service.getStatus() != SpServiceStatus.UNHEALTHY)
-        .map(this::makeServiceUrl)
         .collect(Collectors.toList());
   }
 
+  @Override
+  public List<String> getServiceEndpoints(String serviceGroup,
+                                          boolean restrictToHealthy,
+                                          List<String> filterByTags) {
+    var services = getService(serviceGroup, restrictToHealthy, filterByTags);
+
+    return services.stream().map(this::makeServiceUrl).toList();
+  }
+
   @Override
   public List<SpServiceRegistration> getService(boolean restrictToHealthy) {
     List<SpServiceRegistration> activeServices = findServices(0);
diff --git 
a/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java
 
b/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java
index c11ff8e60f..aa2481afee 100644
--- 
a/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java
+++ 
b/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java
@@ -88,13 +88,15 @@ public abstract class StreamPipesExtensionsServiceBase 
extends StreamPipesServic
       serviceDef.setServiceId(serviceId);
       DeclarersSingleton.getInstance().populate(networkingConfig.getHost(), 
networkingConfig.getPort(), serviceDef);
       SpRateLimiter.INSTANCE.createRateLimiter();
-      startExtensionsService(this.getClass(), serviceDef, networkingConfig);
-      ServiceLoadDataReportGenerator.getInstance().initialize();
 
-     registerTransformationEngines(List.of(
+      registerTransformationEngines(List.of(
           GroovyScriptEngine::new,
           GraalJsScriptEngine::new
       ));
+
+      startExtensionsService(this.getClass(), serviceDef, networkingConfig);
+      ServiceLoadDataReportGenerator.getInstance().initialize();
+
     } catch (UnknownHostException e) {
       LOG.error(
           "Could not auto-resolve host address - "
@@ -129,6 +131,7 @@ public abstract class StreamPipesExtensionsServiceBase 
extends StreamPipesServic
         networkingConfig.getHost(),
         networkingConfig.getPort(),
         getServiceTags(extensions),
+        TransformationEngines.INSTANCE.getAvailableEngineMetadata(),
         getHealthCheckPath(),
         extensions);
 
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-languages.service.ts
 
b/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-languages.service.ts
new file mode 100644
index 0000000000..73cb673591
--- /dev/null
+++ 
b/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-languages.service.ts
@@ -0,0 +1,29 @@
+import { inject, Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import {
+    AdapterDescription,
+    PlatformServicesCommons,
+    ScriptMetadata,
+} from '@streampipes/platform-services';
+import { Observable } from 'rxjs';
+
+@Injectable({
+    providedIn: 'root',
+})
+export class ConnectScriptLanguagesService {
+    private http = inject(HttpClient);
+    private platformServicesCommons = inject(PlatformServicesCommons);
+
+    getAll(
+        adapterDescription: AdapterDescription,
+    ): Observable<ScriptMetadata[]> {
+        return this.http.post<ScriptMetadata[]>(
+            this.baseUrl,
+            adapterDescription,
+        );
+    }
+
+    get baseUrl(): string {
+        return 
`${this.platformServicesCommons.apiBasePath}/connect/master/script-languages`;
+    }
+}
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-templates.service.ts
 
b/ui/projects/streampipes/platform-services/src/lib/apis/connect-script-templates.service.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
index 1d042c59d3..1972c1d960 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -16,10 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2025-12-30 
09:52:24.
+// Generated using typescript-generator version 3.2.1263 on 2026-01-02 
11:22:00.
 
 export class NamedStreamPipesEntity implements Storable {
     '@class':
@@ -151,6 +152,9 @@ export class AdapterDescription extends 
VersionedNamedStreamPipesEntity {
     }
 }
 
+/**
+ * @deprecated since 0.99.0, for removal
+ */
 export class TransformationRuleDescription {
     '@class':
         | 
'org.apache.streampipes.model.connect.rules.value.ValueTransformationRuleDescription'
@@ -224,6 +228,9 @@ export class TransformationRuleDescription {
     }
 }
 
+/**
+ * @deprecated since 0.99.0, for removal
+ */
 export class ValueTransformationRuleDescription extends 
TransformationRuleDescription {
     '@class':
         | 
'org.apache.streampipes.model.connect.rules.value.ValueTransformationRuleDescription'
@@ -295,6 +302,9 @@ export class AddTimestampRuleDescription extends 
ValueTransformationRuleDescript
     }
 }
 
+/**
+ * @deprecated since 0.99.0, for removal
+ */
 export class AddValueTransformationRuleDescription extends 
ValueTransformationRuleDescription {
     '@class': 
'org.apache.streampipes.model.connect.rules.value.AddValueTransformationRuleDescription';
     'datatype': string;
@@ -757,6 +767,9 @@ export class Certificate implements Storable {
     }
 }
 
+/**
+ * @deprecated since 0.99.0, for removal
+ */
 export class ChangeDatatypeTransformationRuleDescription extends 
ValueTransformationRuleDescription {
     '@class': 
'org.apache.streampipes.model.connect.rules.value.ChangeDatatypeTransformationRuleDescription';
     'originalDatatypeXsd': string;
@@ -1800,6 +1813,9 @@ export class EventPropertyPrimitive extends EventProperty 
{
     }
 }
 
+/**
+ * @deprecated since 0.99.0, for removal
+ */
 export class StreamTransformationRuleDescription extends 
TransformationRuleDescription {
     '@class':
         | 
'org.apache.streampipes.model.connect.rules.stream.StreamTransformationRuleDescription'
@@ -1835,6 +1851,9 @@ export class StreamTransformationRuleDescription extends 
TransformationRuleDescr
     }
 }
 
+/**
+ * @deprecated since 0.99.0, for removal
+ */
 export class EventRateTransformationRuleDescription extends 
StreamTransformationRuleDescription {
     '@class': 
'org.apache.streampipes.model.connect.rules.stream.EventRateTransformationRuleDescription';
     'aggregationTimeWindow': number;
@@ -3424,6 +3443,9 @@ export class RemoveDuplicateRule {
     }
 }
 
+/**
+ * @deprecated since 0.99.0, for removal
+ */
 export class RemoveDuplicatesTransformationRuleDescription extends 
StreamTransformationRuleDescription {
     '@class': 
'org.apache.streampipes.model.connect.rules.stream.RemoveDuplicatesTransformationRuleDescription';
     'filterTimeWindow': string;
@@ -3744,6 +3766,26 @@ export class SampleData {
     }
 }
 
+export class ScriptMetadata {
+    language: string;
+    name: string;
+    template: string;
+
+    static fromData(
+        data: ScriptMetadata,
+        target?: ScriptMetadata,
+    ): ScriptMetadata {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new ScriptMetadata();
+        instance.language = data.language;
+        instance.name = data.name;
+        instance.template = data.template;
+        return instance;
+    }
+}
+
 export class SecretStaticProperty extends StaticProperty {
     '@class': 
'org.apache.streampipes.model.staticproperty.SecretStaticProperty';
     'encrypted': boolean;
@@ -4376,6 +4418,9 @@ export class TreeInputNode {
     }
 }
 
+/**
+ * @deprecated since 0.99.0, for removal
+ */
 export class UnitTransformRuleDescription extends 
ValueTransformationRuleDescription {
     '@class': 
'org.apache.streampipes.model.connect.rules.value.UnitTransformRuleDescription';
     'fromUnitRessourceURL': string;
diff --git a/ui/projects/streampipes/platform-services/src/public-api.ts 
b/ui/projects/streampipes/platform-services/src/public-api.ts
index 2eed5bd462..0897190af8 100644
--- a/ui/projects/streampipes/platform-services/src/public-api.ts
+++ b/ui/projects/streampipes/platform-services/src/public-api.ts
@@ -29,6 +29,7 @@ export * from './lib/apis/asset-management.service';
 export * from './lib/apis/compact-pipeline.service';
 export * from './lib/apis/certificate.service';
 export * from './lib/apis/chart.service';
+export * from './lib/apis/connect-script-languages.service';
 export * from './lib/apis/dashboard.service';
 export * from './lib/apis/dashboard-kiosk.service';
 export * from './lib/apis/datalake-rest.service';
diff --git 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
index 1b1cb0ad17..11c92c62e1 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/adapter-configuration-state-service/adapter-configuration-state.service.ts
@@ -110,7 +110,11 @@ export class AdapterConfigurationStateService {
                 // 3. Automatically run the script after getting the sample
                 const currentScript =
                     updatedAdapter.transformationConfig.script;
-                this.runScript(updatedAdapter, currentScript);
+                this.runScript(
+                    updatedAdapter,
+                    currentScript,
+                    updatedAdapter.transformationConfig.language,
+                );
             },
             error: (error: HttpErrorResponse) => {
                 // Update state with error AND metadata (error/idle)
@@ -122,7 +126,11 @@ export class AdapterConfigurationStateService {
         });
     }
 
-    public runScript(adapter: AdapterDescription, script: string): void {
+    public runScript(
+        adapter: AdapterDescription,
+        script: string,
+        language: string,
+    ): void {
         // 1. Prepare state for loading
         this.updateState({
             isRunningScript: true,
@@ -132,7 +140,7 @@ export class AdapterConfigurationStateService {
         // 2. Update the local adapter object with the latest script from the 
UI
         const updatedAdapter = { ...adapter };
         updatedAdapter.transformationConfig.script = script;
-        updatedAdapter.transformationConfig.language = 'javascript';
+        updatedAdapter.transformationConfig.language = language;
 
         // 3. Execute the API call
         this.restService.sampleTransform(updatedAdapter).subscribe({
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-fields/configure-fields-preview/configure-fields-preview.component.scss
 
b/ui/src/app/connect/components/adapter-configuration/configure-fields/configure-fields-preview/configure-fields-preview.component.scss
index b4c4b08df6..c4108bbd62 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-fields/configure-fields-preview/configure-fields-preview.component.scss
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-fields/configure-fields-preview/configure-fields-preview.component.scss
@@ -18,11 +18,11 @@
 
 .preview-text {
     margin: var(--space-2xs);
-    background-color: var(--color-code-bg);
+    background-color: var(--color-bg-0);
     font:
         var(--font-size-xs) Inconsolata,
         monospace;
-    color: white;
+    color: var(--color-default-text);
     padding: 10px;
     max-width: 100%;
     max-height: 300px;
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.html
 
b/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.html
index 70f5c17c8c..3f1eafffbd 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.html
@@ -18,14 +18,17 @@
 
 <div fxLayout="column" fxFlex="100">
     <div fxLayout="row" fxLayoutAlign="center center" fxFlex="100">
-        <div fxLayoutAlign="start center" class="error-text">
+        <div fxLayoutAlign="start center" class="error-text mt-10">
             &nbsp;{{
                 'There was an error while guessing the schema of your 
configured data stream'
                     | translate
             }}:
         </div>
     </div>
-    <div fxLayout="row" fxLayoutAlign="center center" class="mt-10">
-        <sp-exception-message [message]="errorMessage"></sp-exception-message>
+    <div fxLayout="row" fxLayoutAlign="center center" class="mt-10 w-100">
+        <sp-exception-message
+            [message]="errorMessage"
+            class="w-100"
+        ></sp-exception-message>
     </div>
 </div>
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss
 
b/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss
index 8a0f012b14..ac0cecd92a 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-fields/error-message/configure-fields-error-message.component.scss
@@ -21,5 +21,5 @@
 }
 
 .error-text {
-    font-size: 12pt;
+    font-size: var(--font-size-md);
 }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.html
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.html
index 564eb2780a..c7d1f9feb3 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.html
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.html
@@ -18,20 +18,52 @@
 <div fxFlex="100" fxLayout="column">
     <div fxFlex="100" fxLayout="column">
         <sp-basic-inner-panel
-            innerPadding="0"
-            [panelTitle]="'Basic Settings' | translate"
+            [panelTitle]="'Transformation' | translate"
             outerMargin="20px 0px"
         >
-            <div header fxLayoutAlign="end center" fxFlex="100">
+            <div
+                header
+                fxLayoutAlign="end center"
+                fxFlex="100"
+                fxLayoutGap="10px"
+            >
+                @if (selectedScriptMetadata() !== undefined) {
+                    <button
+                        mat-button
+                        [matMenuTriggerFor]="langMenu"
+                        aria-label="Select template language"
+                    >
+                        {{ selectedScriptMetadata().name | titlecase }}
+                        <mat-icon>arrow_drop_down</mat-icon>
+                    </button>
+
+                    <mat-menu #langMenu="matMenu">
+                        @for (
+                            script of availableScripts;
+                            track script.language
+                        ) {
+                            <button
+                                mat-menu-item
+                                (click)="onLanguageChange(script)"
+                            >
+                                <span>{{ script.name | titlecase }}</span>
+                                @if (
+                                    script.language ===
+                                    selectedScriptMetadata().language
+                                ) {
+                                    <mat-icon class="ms-auto">check</mat-icon>
+                                }
+                            </button>
+                        }
+                    </mat-menu>
+                }
                 <button
-                    color="accent"
                     mat-button
-                    matTooltip="Run script"
-                    data-cy="configure-schema-run-script-button"
-                    (click)="runScript()"
+                    data-cy="reset-script"
+                    (click)="resetScript()"
                 >
-                    <mat-icon>play_circle_filled</mat-icon>
-                    <span>Run script</span>
+                    <mat-icon>settings_backup_restore</mat-icon>
+                    <span>Reset script</span>
                 </button>
             </div>
             <div class="code-editor-outer">
@@ -43,6 +75,28 @@
                     data-cy="configure-schema-script-editor"
                 ></ngx-codemirror>
             </div>
+            <div fxLayoutGap="10px">
+                <button
+                    class="mt-sm"
+                    mat-flat-button
+                    matTooltip="Run script"
+                    data-cy="configure-schema-run-script-button"
+                    (click)="runScript()"
+                >
+                    <mat-icon>play_circle_filled</mat-icon>
+                    <span>Run script</span>
+                </button>
+                <button
+                    class="mat-basic mt-sm"
+                    mat-flat-button
+                    matTooltip="Run script"
+                    data-cy="add-script-template-button"
+                    (click)="runScript()"
+                >
+                    <mat-icon>play_circle_filled</mat-icon>
+                    <span>Create script template</span>
+                </button>
+            </div>
         </sp-basic-inner-panel>
     </div>
 
@@ -56,12 +110,11 @@
                 <div header fxLayoutAlign="end center" fxFlex="100">
                     <button
                         data-cy="connect-get-new-sample-button"
-                        color="accent"
                         mat-button
-                        matTooltip="Get New Sample"
                         (click)="getSampleEvent()"
                     >
                         <mat-icon>refresh</mat-icon>
+                        <span>{{ 'Get new sample' | translate }}</span>
                     </button>
                 </div>
                 @if (isSampleLoading()) {
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.scss
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.scss
index b6331428c8..5010c7939e 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.scss
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.scss
@@ -30,10 +30,10 @@
 
 .preview-text {
     margin: var(--space-2xs);
-    background-color: var(--color-code-bg);
+    background-color: var(--color-bg-0);
     font:
         var(--font-size-xs) Inconsolata,
         monospace;
-    color: white;
+    color: var(--color-default-text);
     padding: 10px;
 }
diff --git 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.ts
 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.ts
index 794225d259..dd8a7ae5de 100644
--- 
a/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.ts
+++ 
b/ui/src/app/connect/components/adapter-configuration/configure-schema/configure-schema.component.ts
@@ -27,7 +27,11 @@ import {
     signal,
 } from '@angular/core';
 import { MatStepper } from '@angular/material/stepper';
-import { AdapterDescription } from '@streampipes/platform-services';
+import {
+    AdapterDescription,
+    ConnectScriptLanguagesService,
+    ScriptMetadata,
+} from '@streampipes/platform-services';
 import { AdapterConfigurationStateService } from 
'../adapter-configuration-state-service/adapter-configuration-state.service';
 
 @Component({
@@ -38,6 +42,7 @@ import { AdapterConfigurationStateService } from 
'../adapter-configuration-state
 })
 export class ConfigureSchemaComponent implements OnInit {
     private stateService = inject(AdapterConfigurationStateService);
+    private scriptLanguagesService = inject(ConnectScriptLanguagesService);
 
     @Input()
     adapterDescription: AdapterDescription;
@@ -51,6 +56,8 @@ export class ConfigureSchemaComponent implements OnInit {
     @Output()
     nextEmitter: EventEmitter<MatStepper> = new EventEmitter();
 
+    availableScripts: ScriptMetadata[] = [];
+
     isSampleLoading = computed(() => 
this.stateService.state().isGettingSample);
 
     sampleErrorMessage = computed(() => this.stateService.state().sampleError);
@@ -70,10 +77,9 @@ export class ConfigureSchemaComponent implements OnInit {
                 ?.outputs?.[0] || {},
     );
 
-    script = signal(`// returns the same event
-function transform(event) {
-  return event;
-}`);
+    script = signal(undefined);
+    initialScript = signal<{ meta: ScriptMetadata; script: string 
}>(undefined);
+    selectedScriptMetadata = signal(undefined);
 
     editorOptions = {
         mode: 'javascript',
@@ -94,25 +100,64 @@ function transform(event) {
     }
 
     private initializeScriptVariable(): void {
-        const currentScript =
-            this.adapterDescription.transformationConfig.script;
-        if (currentScript) {
-            this.script.set(currentScript);
-        } else {
-            this.adapterDescription.transformationConfig.script = 
this.script();
-        }
+        this.scriptLanguagesService
+            .getAll(this.adapterDescription)
+            .subscribe(res => {
+                this.availableScripts = res;
+                const currentScript =
+                    this.adapterDescription.transformationConfig.script;
+                let meta: ScriptMetadata;
+                if (currentScript) {
+                    this.script.set(currentScript);
+                    meta = this.availableScripts.find(
+                        s =>
+                            s.language ===
+                            this.adapterDescription.transformationConfig
+                                .language,
+                    );
+                    this.initialScript.set({ meta, script: currentScript });
+                } else {
+                    meta = this.availableScripts.find(
+                        s => s.language === 'javascript',
+                    );
+                    this.adapterDescription.transformationConfig.script =
+                        meta.template;
+                    this.adapterDescription.transformationConfig.language =
+                        meta.language;
+                    this.script.set(meta.template);
+                }
+                this.selectedScriptMetadata.set(meta);
+            });
     }
 
     onCodeChange(newCode: string) {
         this.script.set(newCode);
     }
 
+    onLanguageChange(newLanguage: ScriptMetadata) {
+        this.selectedScriptMetadata.set(newLanguage);
+        this.script.set(newLanguage.template);
+    }
+
+    resetScript(): void {
+        if (this.initialScript() !== undefined) {
+            this.script.set(this.initialScript().script);
+            this.selectedScriptMetadata.set(this.initialScript().meta);
+        } else {
+            this.script.set(this.selectedScriptMetadata().template);
+        }
+    }
+
     getSampleEvent(): void {
         this.stateService.getSampleEvent(this.adapterDescription);
     }
 
     runScript(): void {
-        this.stateService.runScript(this.adapterDescription, this.script());
+        this.stateService.runScript(
+            this.adapterDescription,
+            this.script(),
+            this.selectedScriptMetadata().language,
+        );
     }
 
     public cancel() {


Reply via email to