diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/FunctionBuilderRouter.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/FunctionBuilderRouter.java index c622c6c57b..592ef30f9a 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/FunctionBuilderRouter.java +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/FunctionBuilderRouter.java @@ -32,6 +32,7 @@ import io.ballerina.servicemodelgenerator.extension.builder.function.KafkaFunctionBuilder; import io.ballerina.servicemodelgenerator.extension.builder.function.McpFunctionBuilder; import io.ballerina.servicemodelgenerator.extension.builder.function.MssqlCdcFunctionBuilder; +import io.ballerina.servicemodelgenerator.extension.builder.function.PostgresqlCdcFunctionBuilder; import io.ballerina.servicemodelgenerator.extension.builder.function.RabbitMQFunctionBuilder; import io.ballerina.servicemodelgenerator.extension.builder.function.SolaceFunctionBuilder; import io.ballerina.servicemodelgenerator.extension.model.Codedata; @@ -57,6 +58,7 @@ import static io.ballerina.servicemodelgenerator.extension.util.Constants.MCP; import static io.ballerina.servicemodelgenerator.extension.util.Constants.MSSQL; import static io.ballerina.servicemodelgenerator.extension.util.Constants.OBJECT_METHOD; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.POSTGRESQL; import static io.ballerina.servicemodelgenerator.extension.util.Constants.RABBITMQ; import static io.ballerina.servicemodelgenerator.extension.util.Constants.SOLACE; import static io.ballerina.servicemodelgenerator.extension.util.ServiceModelUtils.deriveServiceType; @@ -75,6 +77,7 @@ public class FunctionBuilderRouter { put(KAFKA, KafkaFunctionBuilder::new); put(SOLACE, SolaceFunctionBuilder::new); put(MSSQL, MssqlCdcFunctionBuilder::new); + put(POSTGRESQL, PostgresqlCdcFunctionBuilder::new); }}; private static NodeBuilder getFunctionBuilder(String protocol) { diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/ServiceBuilderRouter.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/ServiceBuilderRouter.java index 0647c1e36d..fdf40418fa 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/ServiceBuilderRouter.java +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/ServiceBuilderRouter.java @@ -33,6 +33,7 @@ import io.ballerina.servicemodelgenerator.extension.builder.service.KafkaServiceBuilder; import io.ballerina.servicemodelgenerator.extension.builder.service.McpServiceBuilder; import io.ballerina.servicemodelgenerator.extension.builder.service.MssqlCdcServiceBuilder; +import io.ballerina.servicemodelgenerator.extension.builder.service.PostgresqlCdcServiceBuilder; import io.ballerina.servicemodelgenerator.extension.builder.service.RabbitMQServiceBuilder; import io.ballerina.servicemodelgenerator.extension.builder.service.SolaceServiceBuilder; import io.ballerina.servicemodelgenerator.extension.builder.service.TCPServiceBuilder; @@ -65,6 +66,7 @@ import static io.ballerina.servicemodelgenerator.extension.util.Constants.KAFKA; import static io.ballerina.servicemodelgenerator.extension.util.Constants.MCP; import static io.ballerina.servicemodelgenerator.extension.util.Constants.MSSQL; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.POSTGRESQL; import static io.ballerina.servicemodelgenerator.extension.util.Constants.RABBITMQ; import static io.ballerina.servicemodelgenerator.extension.util.Constants.SOLACE; import static io.ballerina.servicemodelgenerator.extension.util.Constants.TCP; @@ -88,6 +90,7 @@ public class ServiceBuilderRouter { put(ASB, AsbServiceBuilder::new); put(SOLACE, SolaceServiceBuilder::new); put(MSSQL, MssqlCdcServiceBuilder::new); + put(POSTGRESQL, PostgresqlCdcServiceBuilder::new); put(FTP, FTPServiceBuilder::new); }}; diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/function/PostgresqlCdcFunctionBuilder.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/function/PostgresqlCdcFunctionBuilder.java new file mode 100644 index 0000000000..a6b520e5e5 --- /dev/null +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/function/PostgresqlCdcFunctionBuilder.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com) + * + * WSO2 LLC. 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 io.ballerina.servicemodelgenerator.extension.builder.function; + +import io.ballerina.servicemodelgenerator.extension.model.Function; +import io.ballerina.servicemodelgenerator.extension.model.Parameter; +import io.ballerina.servicemodelgenerator.extension.model.context.AddModelContext; +import io.ballerina.servicemodelgenerator.extension.model.context.UpdateModelContext; +import org.eclipse.lsp4j.TextEdit; + +import java.util.List; +import java.util.Map; + +import static io.ballerina.servicemodelgenerator.extension.builder.service.PostgresqlCdcServiceBuilder.AFTER_ENTRY_FIELD; +import static io.ballerina.servicemodelgenerator.extension.builder.service.PostgresqlCdcServiceBuilder.BEFORE_ENTRY_FIELD; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.DATA_BINDING; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.POSTGRESQL; + +/** + * Represents the PostgreSQL CDC function builder of the service model generator. Handles special-case logic for + * onUpdate function which has two databinding parameters (beforeEntry and afterEntry) that must share the same type. + * + * @since 1.6.0 + */ +public final class PostgresqlCdcFunctionBuilder extends AbstractFunctionBuilder { + + private static final String ON_UPDATE_FUNCTION = "onUpdate"; + + @Override + public Map> addModel(AddModelContext context) throws Exception { + Function function = context.function(); + if (ON_UPDATE_FUNCTION.equals(function.getName().getValue())) { + expandDatabindingParams(function); + } + + // Call parent which will generate code with expanded parameters + return super.addModel(context); + } + + @Override + public Map> updateModel(UpdateModelContext context) { + Function function = context.function(); + if (ON_UPDATE_FUNCTION.equals(function.getName().getValue())) { + expandDatabindingParams(function); + } + + // Call parent which will generate code with expanded parameters + return super.updateModel(context); + } + + /** + * Expands the single databinding parameter (afterEntry) to two parameters (beforeEntry and afterEntry) for the + * onUpdate function. This is called before code generation to ensure both parameters are present in the generated + * signature with the same type. + *

+ * This method looks for the case where afterEntry is enabled (visible in UI) and beforeEntry is disabled (hidden + * from UI). It then enables beforeEntry and copies the type from afterEntry. + *

+ * + * @param function The onUpdate function to process + */ + private void expandDatabindingParams(Function function) { + List parameters = function.getParameters(); + Parameter beforeEntry = null; + Parameter afterEntry = null; + + for (Parameter param : parameters) { + if (!DATA_BINDING.equals(param.getKind())) { + continue; + } + String paramName = param.getName().getValue(); + if (BEFORE_ENTRY_FIELD.equals(paramName)) { + beforeEntry = param; + } else if (AFTER_ENTRY_FIELD.equals(paramName)) { + afterEntry = param; + } + } + + if (beforeEntry != null && afterEntry != null && !beforeEntry.isEnabled() && afterEntry.isEnabled()) { + beforeEntry.setEnabled(true); + beforeEntry.getType().setValue(afterEntry.getType().getValue()); + } + + } + + @Override + public String kind() { + return POSTGRESQL; + } +} diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/AbstractCdcServiceBuilder.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/AbstractCdcServiceBuilder.java new file mode 100644 index 0000000000..7849261fd9 --- /dev/null +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/AbstractCdcServiceBuilder.java @@ -0,0 +1,624 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. 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 io.ballerina.servicemodelgenerator.extension.builder.service; + +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.openapi.core.generators.common.exception.BallerinaOpenApiException; +import io.ballerina.servicemodelgenerator.extension.model.Codedata; +import io.ballerina.servicemodelgenerator.extension.model.Function; +import io.ballerina.servicemodelgenerator.extension.model.MetaData; +import io.ballerina.servicemodelgenerator.extension.model.Option; +import io.ballerina.servicemodelgenerator.extension.model.Parameter; +import io.ballerina.servicemodelgenerator.extension.model.PropertyType; +import io.ballerina.servicemodelgenerator.extension.model.Service; +import io.ballerina.servicemodelgenerator.extension.model.ServiceInitModel; +import io.ballerina.servicemodelgenerator.extension.model.Value; +import io.ballerina.servicemodelgenerator.extension.model.context.AddServiceInitModelContext; +import io.ballerina.servicemodelgenerator.extension.model.context.GetServiceInitModelContext; +import io.ballerina.servicemodelgenerator.extension.model.context.ModelFromSourceContext; +import io.ballerina.servicemodelgenerator.extension.util.ListenerUtil; +import io.ballerina.servicemodelgenerator.extension.util.Utils; +import org.ballerinalang.formatter.core.FormatterException; +import org.ballerinalang.langserver.commons.eventsync.exceptions.EventSyncException; +import org.ballerinalang.langserver.commons.workspace.WorkspaceDocumentException; +import org.eclipse.lsp4j.TextEdit; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static io.ballerina.servicemodelgenerator.extension.util.Constants.ARG_TYPE_LISTENER_PARAM_INCLUDED_DEFAULTABLE_FIELD; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.ARG_TYPE_LISTENER_PARAM_INCLUDED_FIELD; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.ARG_TYPE_LISTENER_PARAM_REQUIRED; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.AT; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.BALLERINAX; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.CLOSE_BRACE; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.COLON; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.DATA_BINDING; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.NEW_LINE; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.ON; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.OPEN_BRACE; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.SERVICE; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.SERVICE_TYPE; +import static io.ballerina.servicemodelgenerator.extension.util.Constants.SPACE; +import static io.ballerina.servicemodelgenerator.extension.util.DatabindUtil.extractParameterKinds; +import static io.ballerina.servicemodelgenerator.extension.util.DatabindUtil.restoreAndUpdateDataBindingParams; +import static io.ballerina.servicemodelgenerator.extension.util.ServiceModelUtils.extractFunctionsFromSource; +import static io.ballerina.servicemodelgenerator.extension.util.ServiceModelUtils.getProtocol; +import static io.ballerina.servicemodelgenerator.extension.util.Utils.getImportStmt; +import static io.ballerina.servicemodelgenerator.extension.util.Utils.importExists; + +/** + * Abstract base class for CDC (Change Data Capture) service builders. + * Provides common functionality for all CDC database implementations (MSSQL, PostgreSQL, etc.). + * + * @since 1.5.0 + */ +public abstract class AbstractCdcServiceBuilder extends AbstractServiceBuilder { + + // Public field name constants (used in databinding) + public static final String AFTER_ENTRY_FIELD = "afterEntry"; + public static final String BEFORE_ENTRY_FIELD = "beforeEntry"; + + protected static final String DATABINDING_PARAM_LABEL = "Database Entry"; + + // Property keys and values + protected static final String DEFAULT_TYPE_TAB_PROPERTY = "defaultTypeTab"; + protected static final String DEFAULT_TYPE_TAB_VALUE = "create-from-scratch"; + + // Module names + protected static final String CDC_MODULE_NAME = "cdc"; + protected static final String UNNAMED_IMPORT_SUFFIX = " as _"; + + // Property keys + protected static final String KEY_LISTENER_VAR_NAME = "listenerVarName"; + protected static final String KEY_HOST = "host"; + protected static final String KEY_PORT = "port"; + protected static final String KEY_USERNAME = "username"; + protected static final String KEY_PASSWORD = "password"; + protected static final String KEY_DATABASE = "database"; + protected static final String KEY_SCHEMAS = "schemas"; + protected static final String KEY_SECURE_SOCKET = "secureSocket"; + protected static final String KEY_OPTIONS = "options"; + protected static final String KEY_CONFIGURE_LISTENER = "configureListener"; + protected static final String KEY_SELECT_LISTENER = "selectListener"; + protected static final String KEY_TABLE = "table"; + + // Choice indices + protected static final int CHOICE_SELECT_EXISTING_LISTENER = 0; + protected static final int CHOICE_CONFIGURE_NEW_LISTENER = 1; + + // Type and annotation names + protected static final String TYPE_CDC_LISTENER = "CdcListener"; + protected static final String ANNOTATION_CDC_SERVICE_CONFIG = "cdc:ServiceConfig"; + + // Field names + protected static final String FIELD_TABLES = "tables: "; + + // Function names + protected static final String FUNCTION_ON_UPDATE = "onUpdate"; + + // Argument types + protected static final String ARG_TYPE_DATABASE_CONFIG = "databaseConfig"; + + // Validation pattern + protected final Pattern emptyStringTemplate = Pattern.compile("^string\\s*`\\s*`$|^\"\"$"); + + /** + * Data holder for listener information. + * + * @param name The listener variable name + * @param declaration The listener declaration code + */ + protected record ListenerInfo(String name, String declaration) { } + + /** + * Data holder for import information. + * + * @param org The organization name + * @param module The module name + * @param unnamed Whether the import is unnamed (i.e., uses "as _" suffix) + */ + protected record Import(String org, String module, boolean unnamed) { } + + /** + * Returns the location of the CDC service model JSON resource file. + * @return Resource path (e.g., "services/cdc_mssql.json") + */ + protected abstract String getCdcServiceModelLocation(); + + /** + * Returns the CDC driver module name for imports. + * @return Module name (e.g., "mssql.cdc.driver") + */ + protected abstract String getCdcDriverModuleName(); + + /** + * Returns the list of listener field keys for this CDC type. + * MSSQL includes KEY_DATABASE_INSTANCE, PostgreSQL does not. + * @return List of field keys + */ + protected abstract List getListenerFields(); + + /** + * Returns the kind identifier for this CDC service type. + * @return Kind identifier (e.g., "mssql", "postgresql") + */ + @Override + public abstract String kind(); + + @Override + public ServiceInitModel getServiceInitModel(GetServiceInitModelContext context) { + InputStream resourceStream = this.getClass().getClassLoader() + .getResourceAsStream(getCdcServiceModelLocation()); + if (resourceStream == null) { + return null; + } + try (JsonReader reader = new JsonReader(new InputStreamReader(resourceStream, StandardCharsets.UTF_8))) { + ServiceInitModel serviceInitModel = new Gson().fromJson(reader, ServiceInitModel.class); + Map properties = serviceInitModel.getProperties(); + Set listeners = ListenerUtil.getCompatibleListeners(context.moduleName(), + context.semanticModel(), context.project()); + if (listeners.isEmpty()) { + formatInitModelForNewListener(properties); + } else { + formatInitModelForExistingListener(listeners, properties); + } + return serviceInitModel; + } catch (IOException e) { + return null; + } + } + + @Override + public Map> addServiceInitSource(AddServiceInitModelContext context) + throws WorkspaceDocumentException, FormatterException, IOException, BallerinaOpenApiException, + EventSyncException { + ServiceInitModel serviceInitModel = context.serviceInitModel(); + Map properties = serviceInitModel.getProperties(); + + ModulePartNode modulePartNode = context.document().syntaxTree().rootNode(); + List edits = new ArrayList<>(); + + // Add necessary imports + Import[] imports = new Import[]{ + new Import(BALLERINAX, CDC_MODULE_NAME, false), + new Import(serviceInitModel.getOrgName(), serviceInitModel.getModuleName(), false), + new Import(BALLERINAX, getCdcDriverModuleName(), true) + }; + addImportTextEdits(modulePartNode, imports, edits); + + // Get listener information and add declaration + ListenerInfo listenerInfo = getListenerInfo(context, properties); + addListenerDeclarationEdit(context, listenerInfo.declaration(), edits); + + // Add service declaration + addServiceTextEdits(context, listenerInfo.name(), edits); + + return Map.of(context.filePath(), edits); + } + + private void formatInitModelForNewListener(Map properties) { + properties.remove(KEY_CONFIGURE_LISTENER); + } + + private void formatInitModelForExistingListener(Set listenerNames, Map properties) { + Value configureListenerValue = properties.get(KEY_CONFIGURE_LISTENER); + + updateListenerNameSuffix(listenerNames, properties); + + // fill the existing listeners values + Value selectListenerTemplate = configureListenerValue.getChoices().get(CHOICE_SELECT_EXISTING_LISTENER) + .getProperties().get(KEY_SELECT_LISTENER); + Value existingListenerOptions = new Value.ValueBuilder() + .metadata(selectListenerTemplate.getMetadata().label(), + selectListenerTemplate.getMetadata().description()) + .value(listenerNames.iterator().next()) + .types(List.of(PropertyType.types(Value.FieldType.SINGLE_SELECT, Option.of(listenerNames)))) + .enabled(true) + .editable(true) + .setAdvanced(false) + .build(); + configureListenerValue.getChoices().get(CHOICE_SELECT_EXISTING_LISTENER) + .getProperties().put(KEY_SELECT_LISTENER, existingListenerOptions); + + // Add all listener properties to choice 1 of configureListener + getListenerFields().forEach(key -> { + Value value = properties.get(key); + configureListenerValue.getChoices().get(CHOICE_CONFIGURE_NEW_LISTENER).getProperties().put(key, value); + }); + // Remove all listener properties from outside + getListenerFields().forEach(properties::remove); + } + + private void updateListenerNameSuffix(Set listenerNames, Map properties) { + Value listenerVarName = properties.get(ServiceInitModel.KEY_LISTENER_VAR_NAME); + // add a number suffix if there are existing listeners + String baseListenerName = listenerVarName.getValue(); + int suffix = listenerNames.size() + 1; + while (listenerNames.contains(baseListenerName + suffix)) { + suffix++; + } + listenerVarName.setValue(baseListenerName + suffix); + } + + private void addImportTextEdits(ModulePartNode modulePartNode, Import[] imports, List edits) { + for (Import im : imports) { + String org = im.org(); + String module = im.module(); + if (!importExists(modulePartNode, org, module)) { + if (im.unnamed()) { + module += UNNAMED_IMPORT_SUFFIX; + } + String importText = getImportStmt(org, module); + edits.add(new TextEdit(Utils.toRange(modulePartNode.lineRange().startLine()), importText)); + } + } + } + + private void addServiceTextEdits(AddServiceInitModelContext context, String listenerName, List edits) { + Map properties = context.serviceInitModel().getProperties(); + Value tableValue = properties.get(KEY_TABLE); + String serviceDeclaration = + buildServiceConfigurations(tableValue) + + NEW_LINE + + SERVICE + SPACE + CDC_MODULE_NAME + COLON + SERVICE_TYPE + + SPACE + ON + SPACE + listenerName + SPACE + + OPEN_BRACE + + NEW_LINE + + CLOSE_BRACE + NEW_LINE; + ModulePartNode modulePartNode = context.document().syntaxTree().rootNode(); + edits.add(new TextEdit(Utils.toRange(modulePartNode.lineRange().endLine()), serviceDeclaration)); + + } + + private ListenerDTO buildCdcListenerDTO(String moduleName, Map properties) { + List requiredParams = new ArrayList<>(); + List includedParams = new ArrayList<>(); + for (Map.Entry entry : properties.entrySet()) { + Value value = entry.getValue(); + if (value.getCodedata() == null) { + continue; + } + Codedata codedata = value.getCodedata(); + String argType = codedata.getArgType(); + if (Objects.isNull(argType) || argType.isEmpty()) { + continue; + } + if (argType.equals(ARG_TYPE_LISTENER_PARAM_REQUIRED)) { + requiredParams.add(value.getValue()); + } else if (argType.equals(ARG_TYPE_LISTENER_PARAM_INCLUDED_FIELD) + || argType.equals(ARG_TYPE_LISTENER_PARAM_INCLUDED_DEFAULTABLE_FIELD)) { + includedParams.add(entry.getKey() + " = " + value.getValue()); + } + } + String listenerProtocol = getProtocol(moduleName); + String listenerVarName = properties.get(ServiceInitModel.KEY_LISTENER_VAR_NAME).getValue(); + requiredParams.addAll(includedParams); + String args = String.join(", ", requiredParams); + String listenerDeclaration = String.format("listener %s:%s %s = new (%s);", + listenerProtocol, TYPE_CDC_LISTENER, listenerVarName, args); + return new ListenerDTO(listenerProtocol, listenerVarName, listenerDeclaration); + } + + /** + * Applies listener-specific configurations by building database config and validating options. + * + * @param properties The listener properties map + */ + private void applyListenerConfigurations(Map properties) { + addDatabaseConfiguration(properties); + validateAndRemoveInvalidOptions(properties); + } + + private String buildServiceConfigurations(Value tableValue) { + String tableField = tableValue.getValue(); + if (isInvalidValue(tableField)) { + return ""; + } + return AT + ANNOTATION_CDC_SERVICE_CONFIG + SPACE + OPEN_BRACE + NEW_LINE + + " " + FIELD_TABLES + + tableField + + CLOSE_BRACE + NEW_LINE; + } + + private String buildDatabaseConfig(Map properties) { + List dbFields = new ArrayList<>(); + + properties.forEach((key, value) -> { + if (value.getCodedata() == null || !ARG_TYPE_DATABASE_CONFIG.equals(value.getCodedata().getArgType())) { + return; + } + if (value.getValues() != null && !value.getValues().isEmpty()) { + List valueArray = value.getValues().stream() + .filter(v -> v != null && !isInvalidValue(v)) + .toList(); + if (!valueArray.isEmpty()) { + dbFields.add(value.getCodedata().getOriginalName() + " : [" + String.join(", ", valueArray) + "]"); + } + return; + } + if (value.getValue() != null && !isInvalidValue(value.getValue())) { + dbFields.add(value.getCodedata().getOriginalName() + " : " + value.getValue()); + } + }); + return OPEN_BRACE + String.join(", ", dbFields) + CLOSE_BRACE; + } + + private Map getListenerProperties(Map properties, boolean listenerExists) { + if (listenerExists) { + return properties.get(KEY_CONFIGURE_LISTENER).getChoices().get(CHOICE_CONFIGURE_NEW_LISTENER) + .getProperties(); + } + return properties; + } + + private boolean isInvalidValue(String value) { + return value.isBlank() || emptyStringTemplate.matcher(value.trim()).matches(); + } + + private void validateAndRemoveInvalidOptions(Map properties) { + Value optionsValue = properties.get(KEY_OPTIONS); + if (optionsValue != null && isInvalidValue(optionsValue.getValue())) { + properties.remove(KEY_OPTIONS); + } + } + + private void addDatabaseConfiguration(Map properties) { + String databaseConfig = buildDatabaseConfig(properties); + Value databaseValue = new Value.ValueBuilder() + .value(databaseConfig) + .types(List.of(PropertyType.types(Value.FieldType.EXPRESSION))) + .enabled(true) + .editable(false) + .setCodedata(new Codedata(null, ARG_TYPE_LISTENER_PARAM_INCLUDED_FIELD)) + .build(); + properties.put(KEY_DATABASE, databaseValue); + } + + /** + * Determines listener information based on whether an existing listener is used or a new one is created. + * + * @param context The service initialization context + * @param properties The service properties + * @return ListenerInfo containing the listener name and declaration + */ + private ListenerInfo getListenerInfo(AddServiceInitModelContext context, Map properties) { + Set listeners = ListenerUtil.getCompatibleListeners( + context.serviceInitModel().getModuleName(), + context.semanticModel(), + context.project() + ); + + boolean listenerExists = !listeners.isEmpty(); + Map listenerProperties = getListenerProperties(properties, listenerExists); + applyListenerConfigurations(listenerProperties); + + boolean useExistingListener = listenerExists + && !properties.get(KEY_CONFIGURE_LISTENER) + .getChoices().get(CHOICE_CONFIGURE_NEW_LISTENER).isEnabled(); + + String listenerDeclaration; + String listenerName; + + if (!listenerExists || !useExistingListener) { + ListenerDTO listenerDTO = buildCdcListenerDTO( + context.serviceInitModel().getModuleName(), + listenerProperties + ); + listenerDeclaration = NEW_LINE + listenerDTO.listenerDeclaration(); + listenerName = listenerDTO.listenerVarName(); + } else { + listenerDeclaration = ""; + listenerName = properties.get(KEY_CONFIGURE_LISTENER) + .getChoices().get(CHOICE_SELECT_EXISTING_LISTENER) + .getProperties().get(KEY_SELECT_LISTENER).getValue(); + } + + return new ListenerInfo(listenerName, listenerDeclaration); + } + + /** + * Adds listener declaration text edits to the module. + * + * @param context The service initialization context + * @param listenerDeclaration The listener declaration code + * @param edits The list of text edits to add to + */ + private void addListenerDeclarationEdit( + AddServiceInitModelContext context, + String listenerDeclaration, + List edits + ) { + if (!listenerDeclaration.isEmpty()) { + ModulePartNode modulePartNode = context.document().syntaxTree().rootNode(); + edits.add(new TextEdit( + Utils.toRange(modulePartNode.lineRange().endLine()), + listenerDeclaration + )); + } + } + + @Override + public Service getModelFromSource(ModelFromSourceContext context) { + // First, create the base service model to get the original template with DATA_BINDING kinds + Service baseServiceModel = createBaseServiceModel(context); + if (baseServiceModel == null) { + return null; + } + + // Store original parameter kinds before standard consolidation modifies them + Map> originalParameterKinds = extractParameterKinds(baseServiceModel); + + // Run standard consolidation (this may overwrite DATA_BINDING kinds to REQUIRED) + Service serviceModel = super.getModelFromSource(context); + + if (serviceModel == null) { + return null; + } + + ServiceDeclarationNode serviceNode = (ServiceDeclarationNode) context.node(); + + List sourceFunctions = extractFunctionsFromSource(serviceNode); + + // Create a map for quick lookup by function name + Map sourceFunctionMap = sourceFunctions.stream() + .collect(Collectors.toMap(f -> f.getName().getValue(), f -> f)); + + // Process each function to update DATA_BINDING parameters + for (Function function : serviceModel.getFunctions()) { + String functionName = function.getName().getValue(); + + // Add defaultTypeTab property + function.addProperty(DEFAULT_TYPE_TAB_PROPERTY, + new Value.ValueBuilder().value(DEFAULT_TYPE_TAB_VALUE).build() + ); + + // Set module name on function codedata so router can correctly invoke the appropriate FunctionBuilder + setModuleName(function); + + // Find matching source function + Function sourceFunction = sourceFunctionMap.get(functionName); + if (sourceFunction == null) { + updateDatabindingParameterMetadata(function); + continue; + } + + restoreAndUpdateDataBindingParams(function, sourceFunction, originalParameterKinds.get(functionName)); + updateDatabindingParameterMetadata(function); + } + + // After all consolidation and databinding processing is complete, + // apply onUpdate parameter combining to all functions + applyOnUpdateCombining(serviceModel); + + return serviceModel; + } + + /** + * Updates the metadata label to "Database Entry" for all databinding parameters in the given function. + * + * @param function The function whose parameters should be updated + */ + private void updateDatabindingParameterMetadata(Function function) { + for (Parameter param : function.getParameters()) { + if (DATA_BINDING.equals(param.getKind())) { + param.setMetadata(new MetaData( + DATABINDING_PARAM_LABEL, + param.getMetadata() != null ? param.getMetadata().description() : "" + )); + } + } + } + + /** + * Ensures the function has a Codedata object and sets the module name. This is required for the router to correctly + * invoke the appropriate CdcFunctionBuilder. + * + * @param function The function to process + */ + private void setModuleName(Function function) { + if (function.getCodedata() == null) { + function.setCodedata(new Codedata()); + } + function.getCodedata().setModuleName(kind()); + } + + /** + * Applies onUpdate parameter combining to all functions in the service model. This ensures that onUpdate function + * shows only one databinding parameter in the UI, regardless of whether the function exists in source code or is + * being added from the UI. + * + * @param serviceModel The service model to process + */ + private void applyOnUpdateCombining(Service serviceModel) { + for (Function function : serviceModel.getFunctions()) { + if (FUNCTION_ON_UPDATE.equals(function.getName().getValue())) { + combineDatabindingParams(function); + } + } + } + + /** + * Combines the two DATA_BINDING parameters (beforeEntry and afterEntry) for onUpdate function. The UI will show + * only afterEntry as the databinding parameter. beforeEntry is kept in the model but marked as hidden + * (enabled=false) so it can be expanded during code generation. + *

+ * This method handles two scenarios: 1. Template case: Both parameters disabled (template initial state) - keeps + * beforeEntry disabled 2. Source case: Both parameters enabled with same type - disables beforeEntry This ensures + * UI shows only ONE databinding button/field for onUpdate. + *

+ * + * @param function The onUpdate function to process + */ + private void combineDatabindingParams(Function function) { + List parameters = function.getParameters(); + Parameter beforeEntry = null; + Parameter afterEntry = null; + + // Find the two DATA_BINDING parameters + for (Parameter param : parameters) { + if (!DATA_BINDING.equals(param.getKind())) { + continue; + } + String paramName = param.getName().getValue(); + if (BEFORE_ENTRY_FIELD.equals(paramName)) { + beforeEntry = param; + } else if (AFTER_ENTRY_FIELD.equals(paramName)) { + afterEntry = param; + } + } + + // Both parameters must exist + if (beforeEntry == null || afterEntry == null) { + return; + } + + // Case 1: Both disabled - keep beforeEntry disabled, afterEntry disabled + if (!beforeEntry.isEnabled() && !afterEntry.isEnabled()) { + // Nothing to do - already in desired state (beforeEntry hidden) + return; + } + + // Case 2: Both enabled with same type (from source code) - disable beforeEntry + if (beforeEntry.isEnabled() && afterEntry.isEnabled()) { + String beforeType = beforeEntry.getType().getValue(); + String afterType = afterEntry.getType().getValue(); + + // Combine if both have the same type (whether "record {}" or custom type) + if (beforeType.equals(afterType)) { + // Hide beforeEntry - it will be expanded during code generation + beforeEntry.setEnabled(false); + // afterEntry stays enabled and represents both parameters in the UI + } + } + + } +} diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/MssqlCdcServiceBuilder.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/MssqlCdcServiceBuilder.java index 94ea7c94aa..505fb0fded 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/MssqlCdcServiceBuilder.java +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/MssqlCdcServiceBuilder.java @@ -18,123 +18,33 @@ package io.ballerina.servicemodelgenerator.extension.builder.service; -import com.google.gson.Gson; -import com.google.gson.stream.JsonReader; -import io.ballerina.compiler.syntax.tree.ModulePartNode; -import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; -import io.ballerina.openapi.core.generators.common.exception.BallerinaOpenApiException; -import io.ballerina.servicemodelgenerator.extension.model.Codedata; -import io.ballerina.servicemodelgenerator.extension.model.Function; -import io.ballerina.servicemodelgenerator.extension.model.MetaData; -import io.ballerina.servicemodelgenerator.extension.model.Option; -import io.ballerina.servicemodelgenerator.extension.model.Parameter; -import io.ballerina.servicemodelgenerator.extension.model.PropertyType; -import io.ballerina.servicemodelgenerator.extension.model.Service; -import io.ballerina.servicemodelgenerator.extension.model.ServiceInitModel; -import io.ballerina.servicemodelgenerator.extension.model.Value; -import io.ballerina.servicemodelgenerator.extension.model.context.AddServiceInitModelContext; -import io.ballerina.servicemodelgenerator.extension.model.context.GetServiceInitModelContext; -import io.ballerina.servicemodelgenerator.extension.model.context.ModelFromSourceContext; -import io.ballerina.servicemodelgenerator.extension.util.ListenerUtil; -import io.ballerina.servicemodelgenerator.extension.util.Utils; -import org.ballerinalang.formatter.core.FormatterException; -import org.ballerinalang.langserver.commons.eventsync.exceptions.EventSyncException; -import org.ballerinalang.langserver.commons.workspace.WorkspaceDocumentException; -import org.eclipse.lsp4j.TextEdit; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static io.ballerina.servicemodelgenerator.extension.util.Constants.ARG_TYPE_LISTENER_PARAM_INCLUDED_DEFAULTABLE_FIELD; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.ARG_TYPE_LISTENER_PARAM_INCLUDED_FIELD; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.ARG_TYPE_LISTENER_PARAM_REQUIRED; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.AT; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.BALLERINAX; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.CLOSE_BRACE; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.COLON; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.DATA_BINDING; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.NEW_LINE; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.ON; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.OPEN_BRACE; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.SERVICE; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.SERVICE_TYPE; -import static io.ballerina.servicemodelgenerator.extension.util.Constants.SPACE; -import static io.ballerina.servicemodelgenerator.extension.util.DatabindUtil.extractParameterKinds; -import static io.ballerina.servicemodelgenerator.extension.util.DatabindUtil.restoreAndUpdateDataBindingParams; -import static io.ballerina.servicemodelgenerator.extension.util.ServiceModelUtils.extractFunctionsFromSource; -import static io.ballerina.servicemodelgenerator.extension.util.ServiceModelUtils.getProtocol; -import static io.ballerina.servicemodelgenerator.extension.util.Utils.getImportStmt; -import static io.ballerina.servicemodelgenerator.extension.util.Utils.importExists; /** * Builder class for Microsoft SQL Server CDC service. * * @since 1.5.0 */ -public final class MssqlCdcServiceBuilder extends AbstractServiceBuilder { - - // Public field name constants (used in databinding) - public static final String AFTER_ENTRY_FIELD = "afterEntry"; - public static final String BEFORE_ENTRY_FIELD = "beforeEntry"; - - private static final String DATABINDING_PARAM_LABEL = "Database Entry"; +public final class MssqlCdcServiceBuilder extends AbstractCdcServiceBuilder { - // Property keys and values - private static final String DEFAULT_TYPE_TAB_PROPERTY = "defaultTypeTab"; - private static final String DEFAULT_TYPE_TAB_VALUE = "create-from-scratch"; - - // Resource location private static final String CDC_MSSQL_SERVICE_MODEL_LOCATION = "services/cdc_mssql.json"; - - // Module names - private static final String CDC_MODULE_NAME = "cdc"; private static final String MSSQL_CDC_DRIVER_MODULE_NAME = "mssql.cdc.driver"; - private static final String UNNAMED_IMPORT_SUFFIX = " as _"; - - // Property keys - private static final String KEY_LISTENER_VAR_NAME = "listenerVarName"; - private static final String KEY_HOST = "host"; - private static final String KEY_PORT = "port"; - private static final String KEY_USERNAME = "username"; - private static final String KEY_PASSWORD = "password"; - private static final String KEY_DATABASES = "databases"; - private static final String KEY_SCHEMAS = "schemas"; private static final String KEY_DATABASE_INSTANCE = "databaseInstance"; - private static final String KEY_SECURE_SOCKET = "secureSocket"; - private static final String KEY_OPTIONS = "options"; - private static final String KEY_CONFIGURE_LISTENER = "configureListener"; - private static final String KEY_SELECT_LISTENER = "selectListener"; - private static final String KEY_TABLE = "table"; - private static final String KEY_DATABASE = "database"; - - // Choice indices - private static final int CHOICE_SELECT_EXISTING_LISTENER = 0; - private static final int CHOICE_CONFIGURE_NEW_LISTENER = 1; - - // Type and annotation names - private static final String TYPE_CDC_LISTENER = "CdcListener"; - private static final String ANNOTATION_CDC_SERVICE_CONFIG = "cdc:ServiceConfig"; - - // Field names - private static final String FIELD_TABLES = "tables: "; + private static final String KEY_DATABASES = "databases"; - // Function names - private static final String FUNCTION_ON_UPDATE = "onUpdate"; + @Override + protected String getCdcServiceModelLocation() { + return CDC_MSSQL_SERVICE_MODEL_LOCATION; + } - // Argument types - private static final String ARG_TYPE_DATABASE_CONFIG = "databaseConfig"; + @Override + protected String getCdcDriverModuleName() { + return MSSQL_CDC_DRIVER_MODULE_NAME; + } - // Listener field list - private final List listenerFields = List.of( + @Override + protected List getListenerFields() { + return List.of( KEY_LISTENER_VAR_NAME, KEY_HOST, KEY_PORT, @@ -142,477 +52,10 @@ public final class MssqlCdcServiceBuilder extends AbstractServiceBuilder { KEY_PASSWORD, KEY_DATABASES, KEY_SCHEMAS, - KEY_DATABASE_INSTANCE, + KEY_DATABASE_INSTANCE, // MSSQL-specific field KEY_SECURE_SOCKET, KEY_OPTIONS - ); - - // Validation pattern - Pattern emptyStringTemplate = Pattern.compile("^string\\s*`\\s*`$|^\"\"$"); - - /** - * Data holder for listener information. - * - * @param name The listener variable name - * @param declaration The listener declaration code - */ - private record ListenerInfo(String name, String declaration) { } - - /** - * Data holder for import information. - * - * @param org The organization name - * @param module The module name - * @param unnamed Whether the import is unnamed (i.e., uses "as _" suffix) - */ - private record Import(String org, String module, boolean unnamed) { } - - @Override - public ServiceInitModel getServiceInitModel(GetServiceInitModelContext context) { - InputStream resourceStream = MssqlCdcServiceBuilder.class.getClassLoader() - .getResourceAsStream(CDC_MSSQL_SERVICE_MODEL_LOCATION); - if (resourceStream == null) { - return null; - } - try (JsonReader reader = new JsonReader(new InputStreamReader(resourceStream, StandardCharsets.UTF_8))) { - ServiceInitModel serviceInitModel = new Gson().fromJson(reader, ServiceInitModel.class); - Map properties = serviceInitModel.getProperties(); - Set listeners = ListenerUtil.getCompatibleListeners(context.moduleName(), - context.semanticModel(), context.project()); - if (listeners.isEmpty()) { - formatInitModelForNewListener(properties); - } else { - formatInitModelForExistingListener(listeners, properties); - } - return serviceInitModel; - } catch (IOException e) { - return null; - } - } - - @Override - public Map> addServiceInitSource(AddServiceInitModelContext context) - throws WorkspaceDocumentException, FormatterException, IOException, BallerinaOpenApiException, - EventSyncException { - ServiceInitModel serviceInitModel = context.serviceInitModel(); - Map properties = serviceInitModel.getProperties(); - - ModulePartNode modulePartNode = context.document().syntaxTree().rootNode(); - List edits = new ArrayList<>(); - - // Add necessary imports - Import[] imports = new Import[]{ - new Import(BALLERINAX, CDC_MODULE_NAME, false), - new Import(serviceInitModel.getOrgName(), serviceInitModel.getModuleName(), false), - new Import(BALLERINAX, MSSQL_CDC_DRIVER_MODULE_NAME, true) - }; - addImportTextEdits(modulePartNode, imports, edits); - - // Get listener information and add declaration - ListenerInfo listenerInfo = getListenerInfo(context, properties); - addListenerDeclarationEdit(context, listenerInfo.declaration(), edits); - - // Add service declaration - addServiceTextEdits(context, listenerInfo.name(), edits); - - return Map.of(context.filePath(), edits); - } - - private void formatInitModelForNewListener(Map properties) { - properties.remove(KEY_CONFIGURE_LISTENER); - } - - private void formatInitModelForExistingListener(Set listenerNames, Map properties) { - Value configureListenerValue = properties.get(KEY_CONFIGURE_LISTENER); - - updateListenerNameSuffix(listenerNames, properties); - - // fill the existing listeners values - Value selectListenerTemplate = configureListenerValue.getChoices().get(CHOICE_SELECT_EXISTING_LISTENER) - .getProperties().get(KEY_SELECT_LISTENER); - Value existingListenerOptions = new Value.ValueBuilder() - .metadata(selectListenerTemplate.getMetadata().label(), - selectListenerTemplate.getMetadata().description()) - .value(listenerNames.iterator().next()) - .types(List.of(PropertyType.types(Value.FieldType.SINGLE_SELECT, Option.of(listenerNames)))) - .enabled(true) - .editable(true) - .setAdvanced(false) - .build(); - configureListenerValue.getChoices().get(CHOICE_SELECT_EXISTING_LISTENER) - .getProperties().put(KEY_SELECT_LISTENER, existingListenerOptions); - - // Add all listener properties to choice 1 of configureListener - listenerFields.forEach(key -> { - Value value = properties.get(key); - configureListenerValue.getChoices().get(CHOICE_CONFIGURE_NEW_LISTENER).getProperties().put(key, value); - }); - // Remove all listener properties from outside - listenerFields.forEach(properties::remove); - } - - private void updateListenerNameSuffix(Set listenerNames, Map properties) { - Value listenerVarName = properties.get(ServiceInitModel.KEY_LISTENER_VAR_NAME); - // add a number suffix if there are existing listeners - String baseListenerName = listenerVarName.getValue(); - int suffix = listenerNames.size() + 1; - while (listenerNames.contains(baseListenerName + suffix)) { - suffix++; - } - listenerVarName.setValue(baseListenerName + suffix); - } - - private void addImportTextEdits(ModulePartNode modulePartNode, Import[] imports, List edits) { - for (Import im : imports) { - String org = im.org(); - String module = im.module(); - if (!importExists(modulePartNode, org, module)) { - if (im.unnamed()) { - module += UNNAMED_IMPORT_SUFFIX; - } - String importText = getImportStmt(org, module); - edits.add(new TextEdit(Utils.toRange(modulePartNode.lineRange().startLine()), importText)); - } - } - } - - private void addServiceTextEdits(AddServiceInitModelContext context, String listenerName, List edits) { - Map properties = context.serviceInitModel().getProperties(); - Value tableValue = properties.get(KEY_TABLE); - String serviceDeclaration = - buildServiceConfigurations(tableValue) + - NEW_LINE + - SERVICE + SPACE + CDC_MODULE_NAME + COLON + SERVICE_TYPE + - SPACE + ON + SPACE + listenerName + SPACE + - OPEN_BRACE + - NEW_LINE + - CLOSE_BRACE + NEW_LINE; - ModulePartNode modulePartNode = context.document().syntaxTree().rootNode(); - edits.add(new TextEdit(Utils.toRange(modulePartNode.lineRange().endLine()), serviceDeclaration)); - - } - - private ListenerDTO buildCdcListenerDTO(String moduleName, Map properties) { - List requiredParams = new ArrayList<>(); - List includedParams = new ArrayList<>(); - for (Map.Entry entry : properties.entrySet()) { - Value value = entry.getValue(); - if (value.getCodedata() == null) { - continue; - } - Codedata codedata = value.getCodedata(); - String argType = codedata.getArgType(); - if (Objects.isNull(argType) || argType.isEmpty()) { - continue; - } - if (argType.equals(ARG_TYPE_LISTENER_PARAM_REQUIRED)) { - requiredParams.add(value.getValue()); - } else if (argType.equals(ARG_TYPE_LISTENER_PARAM_INCLUDED_FIELD) - || argType.equals(ARG_TYPE_LISTENER_PARAM_INCLUDED_DEFAULTABLE_FIELD)) { - includedParams.add(entry.getKey() + " = " + value.getValue()); - } - } - String listenerProtocol = getProtocol(moduleName); - String listenerVarName = properties.get(ServiceInitModel.KEY_LISTENER_VAR_NAME).getValue(); - requiredParams.addAll(includedParams); - String args = String.join(", ", requiredParams); - String listenerDeclaration = String.format("listener %s:%s %s = new (%s);", - listenerProtocol, TYPE_CDC_LISTENER, listenerVarName, args); - return new ListenerDTO(listenerProtocol, listenerVarName, listenerDeclaration); - } - - /** - * Applies listener-specific configurations by building database config and validating options. - * - * @param properties The listener properties map - */ - private void applyListenerConfigurations(Map properties) { - addDatabaseConfiguration(properties); - validateAndRemoveInvalidOptions(properties); - } - - private String buildServiceConfigurations(Value tableValue) { - String tableField = tableValue.getValue(); - if (isInvalidValue(tableField)) { - return ""; - } - return AT + ANNOTATION_CDC_SERVICE_CONFIG + SPACE + OPEN_BRACE + NEW_LINE + - " " + FIELD_TABLES + - tableField + - CLOSE_BRACE + NEW_LINE; - } - - private String buildDatabaseConfig(Map properties) { - List dbFields = new ArrayList<>(); - - properties.forEach((key, value) -> { - if (value.getCodedata() == null || !ARG_TYPE_DATABASE_CONFIG.equals(value.getCodedata().getArgType())) { - return; - } - if (value.getValues() != null && !value.getValues().isEmpty()) { - List valueArray = value.getValues().stream() - .filter(v -> v != null && !isInvalidValue(v)) - .toList(); - if (!valueArray.isEmpty()) { - dbFields.add(value.getCodedata().getOriginalName() + " : [" + String.join(", ", valueArray) + "]"); - } - return; - } - if (value.getValue() != null && !isInvalidValue(value.getValue())) { - dbFields.add(value.getCodedata().getOriginalName() + " : " + value.getValue()); - } - }); - return OPEN_BRACE + String.join(", ", dbFields) + CLOSE_BRACE; - } - - private Map getListenerProperties(Map properties, boolean listenerExists) { - if (listenerExists) { - return properties.get(KEY_CONFIGURE_LISTENER).getChoices().get(CHOICE_CONFIGURE_NEW_LISTENER) - .getProperties(); - } - return properties; - } - - private boolean isInvalidValue(String value) { - return value.isBlank() || emptyStringTemplate.matcher(value.trim()).matches(); - } - - private void validateAndRemoveInvalidOptions(Map properties) { - Value optionsValue = properties.get(KEY_OPTIONS); - if (optionsValue != null && isInvalidValue(optionsValue.getValue())) { - properties.remove(KEY_OPTIONS); - } - } - - private void addDatabaseConfiguration(Map properties) { - String databaseConfig = buildDatabaseConfig(properties); - Value databaseValue = new Value.ValueBuilder() - .value(databaseConfig) - .types(List.of(PropertyType.types(Value.FieldType.EXPRESSION))) - .enabled(true) - .editable(false) - .setCodedata(new Codedata(null, ARG_TYPE_LISTENER_PARAM_INCLUDED_FIELD)) - .build(); - properties.put(KEY_DATABASE, databaseValue); - } - - /** - * Determines listener information based on whether an existing listener is used or a new one is created. - * - * @param context The service initialization context - * @param properties The service properties - * @return ListenerInfo containing the listener name and declaration - */ - private ListenerInfo getListenerInfo(AddServiceInitModelContext context, Map properties) { - Set listeners = ListenerUtil.getCompatibleListeners( - context.serviceInitModel().getModuleName(), - context.semanticModel(), - context.project() ); - - boolean listenerExists = !listeners.isEmpty(); - Map listenerProperties = getListenerProperties(properties, listenerExists); - applyListenerConfigurations(listenerProperties); - - boolean useExistingListener = listenerExists - && !properties.get(KEY_CONFIGURE_LISTENER) - .getChoices().get(CHOICE_CONFIGURE_NEW_LISTENER).isEnabled(); - - String listenerDeclaration; - String listenerName; - - if (!listenerExists || !useExistingListener) { - ListenerDTO listenerDTO = buildCdcListenerDTO( - context.serviceInitModel().getModuleName(), - listenerProperties - ); - listenerDeclaration = NEW_LINE + listenerDTO.listenerDeclaration(); - listenerName = listenerDTO.listenerVarName(); - } else { - listenerDeclaration = ""; - listenerName = properties.get(KEY_CONFIGURE_LISTENER) - .getChoices().get(CHOICE_SELECT_EXISTING_LISTENER) - .getProperties().get(KEY_SELECT_LISTENER).getValue(); - } - - return new ListenerInfo(listenerName, listenerDeclaration); - } - - /** - * Adds listener declaration text edits to the module. - * - * @param context The service initialization context - * @param listenerDeclaration The listener declaration code - * @param edits The list of text edits to add to - */ - private void addListenerDeclarationEdit( - AddServiceInitModelContext context, - String listenerDeclaration, - List edits - ) { - if (!listenerDeclaration.isEmpty()) { - ModulePartNode modulePartNode = context.document().syntaxTree().rootNode(); - edits.add(new TextEdit( - Utils.toRange(modulePartNode.lineRange().endLine()), - listenerDeclaration - )); - } - } - - @Override - public Service getModelFromSource(ModelFromSourceContext context) { - // First, create the base service model to get the original template with DATA_BINDING kinds - Service baseServiceModel = createBaseServiceModel(context); - if (baseServiceModel == null) { - return null; - } - - // Store original parameter kinds before standard consolidation modifies them - Map> originalParameterKinds = extractParameterKinds(baseServiceModel); - - // Run standard consolidation (this may overwrite DATA_BINDING kinds to REQUIRED) - Service serviceModel = super.getModelFromSource(context); - - if (serviceModel == null) { - return null; - } - - ServiceDeclarationNode serviceNode = (ServiceDeclarationNode) context.node(); - - List sourceFunctions = extractFunctionsFromSource(serviceNode); - - // Create a map for quick lookup by function name - Map sourceFunctionMap = sourceFunctions.stream() - .collect(Collectors.toMap(f -> f.getName().getValue(), f -> f)); - - // Process each function to update DATA_BINDING parameters - for (Function function : serviceModel.getFunctions()) { - String functionName = function.getName().getValue(); - - // Add defaultTypeTab property - function.addProperty(DEFAULT_TYPE_TAB_PROPERTY, - new Value.ValueBuilder().value(DEFAULT_TYPE_TAB_VALUE).build() - ); - - // Set module name on function codedata so router can correctly invoke MssqlCdcFunctionBuilder - setModuleName(function); - - // Find matching source function - Function sourceFunction = sourceFunctionMap.get(functionName); - if (sourceFunction == null) { - updateDatabindingParameterMetadata(function); - continue; - } - - restoreAndUpdateDataBindingParams(function, sourceFunction, originalParameterKinds.get(functionName)); - updateDatabindingParameterMetadata(function); - } - - // After all consolidation and databinding processing is complete, - // apply onUpdate parameter combining to all functions - applyOnUpdateCombining(serviceModel); - - return serviceModel; - } - - /** - * Updates the metadata label to "Database Entry" for all databinding parameters in the given function. - * - * @param function The function whose parameters should be updated - */ - private void updateDatabindingParameterMetadata(Function function) { - for (Parameter param : function.getParameters()) { - if (DATA_BINDING.equals(param.getKind())) { - param.setMetadata(new MetaData( - DATABINDING_PARAM_LABEL, - param.getMetadata() != null ? param.getMetadata().description() : "" - )); - } - } - } - - /** - * Ensures the function has a Codedata object and sets the module name. This is required for the router to correctly - * invoke MssqlCdcFunctionBuilder. - * - * @param function The function to process - */ - private void setModuleName(Function function) { - if (function.getCodedata() == null) { - function.setCodedata(new Codedata()); - } - function.getCodedata().setModuleName(kind()); - } - - /** - * Applies onUpdate parameter combining to all functions in the service model. This ensures that onUpdate function - * shows only one databinding parameter in the UI, regardless of whether the function exists in source code or is - * being added from the UI. - * - * @param serviceModel The service model to process - */ - private void applyOnUpdateCombining(Service serviceModel) { - for (Function function : serviceModel.getFunctions()) { - if (FUNCTION_ON_UPDATE.equals(function.getName().getValue())) { - combineDatabindingParams(function); - } - } - } - - /** - * Combines the two DATA_BINDING parameters (beforeEntry and afterEntry) for onUpdate function. The UI will show - * only afterEntry as the databinding parameter. beforeEntry is kept in the model but marked as hidden - * (enabled=false) so it can be expanded during code generation. - *

- * This method handles two scenarios: 1. Template case: Both parameters disabled (template initial state) - keeps - * beforeEntry disabled 2. Source case: Both parameters enabled with same type - disables beforeEntry This ensures - * UI shows only ONE databinding button/field for onUpdate. - *

- * - * @param function The onUpdate function to process - */ - private void combineDatabindingParams(Function function) { - List parameters = function.getParameters(); - Parameter beforeEntry = null; - Parameter afterEntry = null; - - // Find the two DATA_BINDING parameters - for (Parameter param : parameters) { - if (!DATA_BINDING.equals(param.getKind())) { - continue; - } - String paramName = param.getName().getValue(); - if (BEFORE_ENTRY_FIELD.equals(paramName)) { - beforeEntry = param; - } else if (AFTER_ENTRY_FIELD.equals(paramName)) { - afterEntry = param; - } - } - - // Both parameters must exist - if (beforeEntry == null || afterEntry == null) { - return; - } - - // Case 1: Both disabled - keep beforeEntry disabled, afterEntry disabled - if (!beforeEntry.isEnabled() && !afterEntry.isEnabled()) { - // Nothing to do - already in desired state (beforeEntry hidden) - return; - } - - // Case 2: Both enabled with same type (from source code) - disable beforeEntry - if (beforeEntry.isEnabled() && afterEntry.isEnabled()) { - String beforeType = beforeEntry.getType().getValue(); - String afterType = afterEntry.getType().getValue(); - - // Combine if both have the same type (whether "record {}" or custom type) - if (beforeType.equals(afterType)) { - // Hide beforeEntry - it will be expanded during code generation - beforeEntry.setEnabled(false); - // afterEntry stays enabled and represents both parameters in the UI - } - } - } @Override diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/PostgresqlCdcServiceBuilder.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/PostgresqlCdcServiceBuilder.java new file mode 100644 index 0000000000..e7f70ba2e2 --- /dev/null +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/builder/service/PostgresqlCdcServiceBuilder.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. 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 io.ballerina.servicemodelgenerator.extension.builder.service; + +import java.util.List; + +import static io.ballerina.servicemodelgenerator.extension.util.Constants.POSTGRESQL; + +/** + * Builder class for PostgreSQL CDC service. + * + * @since 1.6.0 + */ +public final class PostgresqlCdcServiceBuilder extends AbstractCdcServiceBuilder { + + private static final String CDC_POSTGRESQL_SERVICE_MODEL_LOCATION = "services/cdc_postgresql.json"; + private static final String POSTGRESQL_CDC_DRIVER_MODULE_NAME = "postgresql.cdc.driver"; + + @Override + protected String getCdcServiceModelLocation() { + return CDC_POSTGRESQL_SERVICE_MODEL_LOCATION; + } + + @Override + protected String getCdcDriverModuleName() { + return POSTGRESQL_CDC_DRIVER_MODULE_NAME; + } + + @Override + protected List getListenerFields() { + return List.of( + KEY_LISTENER_VAR_NAME, + KEY_HOST, + KEY_PORT, + KEY_USERNAME, + KEY_PASSWORD, + KEY_DATABASE, + KEY_SCHEMAS, + KEY_SECURE_SOCKET, + KEY_OPTIONS + // Note: NO KEY_DATABASE_INSTANCE (PostgreSQL doesn't use it) + ); + } + + @Override + public String kind() { + return POSTGRESQL; + } +} diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/util/Constants.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/util/Constants.java index 492b176e34..42f8ee2c0b 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/util/Constants.java +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/util/Constants.java @@ -67,6 +67,7 @@ public class Constants { public static final String SF = "salesforce"; public static final String TRIGGER_GITHUB = "trigger.github"; public static final String MSSQL = "mssql"; + public static final String POSTGRESQL = "postgresql"; public static final String FTP = "ftp"; public static final String FILE = "file"; diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/services/cdc_postgresql.json b/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/services/cdc_postgresql.json new file mode 100644 index 0000000000..58af03591c --- /dev/null +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/services/cdc_postgresql.json @@ -0,0 +1,382 @@ +{ + "id": "18", + "displayName": "PostgreSQL CDC Integration", + "description": "Create a service to capture data changes from a PostgreSQL database using CDC", + "orgName": "ballerinax", + "packageName": "postgresql", + "moduleName": "postgresql", + "version": "1.16.2", + "type": "event", + "properties": { + "host": { + "metadata": { + "label": "Host", + "description": "The hostname of the PostgreSQL Server" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "hostname" + }, + "placeholder": "localhost", + "value": "\"localhost\"", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": false + }, + "port": { + "metadata": { + "label": "Port", + "description": "The port number of the PostgreSQL Server" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "port" + }, + "placeholder": "5432", + "value": "5432", + "types": [ + { + "fieldType": "NUMBER", + "ballerinaType": "int", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "int", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": false + }, + "username": { + "metadata": { + "label": "Username", + "description": "The username for the PostgreSQL Server connection" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "username" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true, + "pattern": "^string `.+`$|^\".+\"$", + "patternErrorMessage": "Username cannot be empty" + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": false, + "advanced": false + }, + "password": { + "metadata": { + "label": "Password", + "description": "The password for the PostgreSQL Server connection" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "password" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true, + "pattern": "^string `.+`$|^\".+\"$", + "patternErrorMessage": "Password cannot be empty" + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": false, + "advanced": false + }, + "database": { + "metadata": { + "label": "Database", + "description": "The PostgreSQL database name to capture changes from" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "databaseName" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true, + "pattern": "^string `.+`$|^\".+\"$", + "patternErrorMessage": "Database name cannot be empty" + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": false, + "advanced": false + }, + "schemas": { + "metadata": { + "label": "Schemas", + "description": "A list of regular expressions that match names of schemas to capture changes from" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "includedSchemas" + }, + "placeholder": "", + "types": [ + { + "fieldType": "TEXT_SET", + "ballerinaType": "string", + "minItems": 0, + "defaultItems": 0 + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": false + }, + "table": { + "metadata": { + "label": "Table", + "description": "The fully-qualified table name to capture events from. Format: `..`" + }, + "codedata": { + "argType": "annotationParam", + "originalName": "tables" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true, + "pattern": "^$|^\"\"$|^string `(|([^.\\[\\]]+|\\[[^\\]]+\\])\\.([^.\\[\\]]+|\\[[^\\]]+\\])\\.([^.\\[\\]]+|\\[[^\\]]+\\]))`$|^\"([^.\\[\\]]+|\\[[^\\]]+\\])\\.([^.\\[\\]]+|\\[[^\\]]+\\])\\.([^.\\[\\]]+|\\[[^\\]]+\\])\"$", + "patternErrorMessage": "Table must be in the format: ..
. Use square brackets for names with dots (e.g., db.[schema.name].table)" + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": false + }, + "configureListener": { + "metadata": { + "label": "Use Existing Listener or Create New Listener", + "description": "Use Existing Listener or Create New Listener" + }, + "placeholder": "", + "value": "0", + "types": [ + { + "fieldType": "CHOICE" + } + ], + "choices": [ + { + "metadata": { + "label": "Use Existing Listener", + "description": "Use Existing Listener" + }, + "placeholder": "true", + "value": "true", + "types": [ + { + "fieldType": "FORM" + } + ], + "enabled": true, + "editable": true, + "optional": false, + "advanced": false, + "properties": { + "selectListener": { + "metadata": { + "label": "Select Listener", + "description": "Select from the existing PostgreSQL CDC listeners" + }, + "codedata": { + "type": "LISTENER_VAR_NAME" + }, + "placeholder": "", + "value": "postgresqlCdcListener", + "types": [ + { + "fieldType": "SINGLE_SELECT", + "ballerinaType": "string", + "selected": true + } + ], + "enabled": true, + "editable": true, + "optional": false, + "advanced": false + } + } + }, + { + "metadata": { + "label": "Create New Listener", + "description": "Create New Listener" + }, + "placeholder": "true", + "value": "true", + "properties": {}, + "types": [ + { + "fieldType": "FORM" + } + ], + "isType": false, + "enabled": false, + "editable": true, + "optional": false, + "advanced": false + } + ], + "enabled": true, + "editable": true, + "optional": false, + "advanced": true + }, + "listenerVarName": { + "metadata": { + "label": "Listener Name", + "description": "Provide a name for the listener being created" + }, + "codedata": { + "argType": "LISTENER_VAR_NAME" + }, + "value": "postgresqlCdcListener", + "placeholder": "postgresqlCdcListener", + "types": [ + { + "fieldType": "IDENTIFIER", + "ballerinaType": "CdcListener" + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": true + }, + "secureSocket": { + "metadata": { + "label": "Secure Socket", + "description": "Configure SSL/TLS configuration for secure connection" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "secure" + }, + "value": "", + "placeholder": "", + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "cdc:SecureDatabaseConnection", + "typeMembers": [ + { + "type": "SecureDatabaseConnection", + "packageInfo": "ballerinax:cdc:1.0.3", + "kind": "RECORD_TYPE", + "selected": false + } + ], + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "cdc:SecureDatabaseConnection", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": true + }, + "options": { + "metadata": { + "label": "Options", + "description": "Additional options for the CDC engine" + }, + "codedata": { + "argType": "LISTENER_PARAM_INCLUDED_FIELD" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "cdc:Options", + "typeMembers": [ + { + "type": "Options", + "packageInfo": "ballerinax:cdc:1.0.3", + "kind": "RECORD_TYPE", + "selected": true + } + ], + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "cdc:Options", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": true + } + } +} diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/trigger_properties.json b/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/trigger_properties.json index cc82121ece..413de278e8 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/trigger_properties.json +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/trigger_properties.json @@ -127,5 +127,16 @@ "cdc", "event" ] + }, + "13": { + "name": "postgresql", + "orgName": "ballerinax", + "packageName": "postgresql", + "keywords": [ + "postgresql", + "postgres", + "cdc", + "event" + ] } } diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_service_init_model/config/cdc_service_model.json b/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_service_init_model/config/cdc_mssql_service_model.json similarity index 100% rename from service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_service_init_model/config/cdc_service_model.json rename to service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_service_init_model/config/cdc_mssql_service_model.json diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_service_init_model/config/cdc_postgresql_service_model.json b/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_service_init_model/config/cdc_postgresql_service_model.json new file mode 100644 index 0000000000..9e12d09890 --- /dev/null +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_service_init_model/config/cdc_postgresql_service_model.json @@ -0,0 +1,314 @@ +{ + "description": "Test getting cdc postgresql service init model", + "filePath": "sample1/main.bal", + "orgName": "ballerinax", + "pkgName": "postgresql", + "moduleName": "postgresql", + "response": { + "serviceInitModel": { + "id": "18", + "displayName": "PostgreSQL CDC Integration", + "description": "Create a service to capture data changes from a PostgreSQL database using CDC", + "orgName": "ballerinax", + "packageName": "postgresql", + "moduleName": "postgresql", + "version": "1.16.2", + "type": "event", + "properties": { + "host": { + "metadata": { + "label": "Host", + "description": "The hostname of the PostgreSQL Server" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "hostname" + }, + "placeholder": "localhost", + "value": "\"localhost\"", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": false + }, + "port": { + "metadata": { + "label": "Port", + "description": "The port number of the PostgreSQL Server" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "port" + }, + "placeholder": "5432", + "value": "5432", + "types": [ + { + "fieldType": "NUMBER", + "ballerinaType": "int", + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "int", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": false + }, + "username": { + "metadata": { + "label": "Username", + "description": "The username for the PostgreSQL Server connection" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "username" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true, + "pattern": "^string `.+`$|^\".+\"$", + "patternErrorMessage": "Username cannot be empty" + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": false, + "advanced": false + }, + "password": { + "metadata": { + "label": "Password", + "description": "The password for the PostgreSQL Server connection" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "password" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true, + "pattern": "^string `.+`$|^\".+\"$", + "patternErrorMessage": "Password cannot be empty" + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": false, + "advanced": false + }, + "database": { + "metadata": { + "label": "Database", + "description": "The PostgreSQL database name to capture changes from" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "databaseName" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true, + "pattern": "^string `.+`$|^\".+\"$", + "patternErrorMessage": "Database name cannot be empty" + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": false, + "advanced": false + }, + "schemas": { + "metadata": { + "label": "Schemas", + "description": "A list of regular expressions that match names of schemas to capture changes from" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "includedSchemas" + }, + "placeholder": "", + "types": [ + { + "fieldType": "TEXT_SET", + "ballerinaType": "string", + "selected": false, + "minItems": 0, + "defaultItems": 0 + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": false + }, + "table": { + "metadata": { + "label": "Table", + "description": "The fully-qualified table name to capture events from. Format: `..
`" + }, + "codedata": { + "argType": "annotationParam", + "originalName": "tables" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "TEXT", + "ballerinaType": "string", + "selected": true, + "pattern": "^$|^\"\"$|^string `(|([^.\\[\\]]+|\\[[^\\]]+\\])\\.([^.\\[\\]]+|\\[[^\\]]+\\])\\.([^.\\[\\]]+|\\[[^\\]]+\\]))`$|^\"([^.\\[\\]]+|\\[[^\\]]+\\])\\.([^.\\[\\]]+|\\[[^\\]]+\\])\\.([^.\\[\\]]+|\\[[^\\]]+\\])\"$", + "patternErrorMessage": "Table must be in the format: ..
. Use square brackets for names with dots (e.g., db.[schema.name].table)" + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "string", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": false + }, + "listenerVarName": { + "metadata": { + "label": "Listener Name", + "description": "Provide a name for the listener being created" + }, + "codedata": { + "argType": "LISTENER_VAR_NAME" + }, + "placeholder": "postgresqlCdcListener", + "value": "postgresqlCdcListener", + "types": [ + { + "fieldType": "IDENTIFIER", + "ballerinaType": "CdcListener", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": true + }, + "secureSocket": { + "metadata": { + "label": "Secure Socket", + "description": "Configure SSL/TLS configuration for secure connection" + }, + "codedata": { + "argType": "databaseConfig", + "originalName": "secure" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "cdc:SecureDatabaseConnection", + "typeMembers": [ + { + "type": "SecureDatabaseConnection", + "packageInfo": "ballerinax:cdc:1.0.3", + "kind": "RECORD_TYPE", + "selected": false + } + ], + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "cdc:SecureDatabaseConnection", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": true + }, + "options": { + "metadata": { + "label": "Options", + "description": "Additional options for the CDC engine" + }, + "codedata": { + "argType": "LISTENER_PARAM_INCLUDED_FIELD" + }, + "placeholder": "", + "value": "", + "types": [ + { + "fieldType": "RECORD_MAP_EXPRESSION", + "ballerinaType": "cdc:Options", + "typeMembers": [ + { + "type": "Options", + "packageInfo": "ballerinax:cdc:1.0.3", + "kind": "RECORD_TYPE", + "selected": true + } + ], + "selected": true + }, + { + "fieldType": "EXPRESSION", + "ballerinaType": "cdc:Options", + "selected": false + } + ], + "enabled": true, + "editable": true, + "optional": true, + "advanced": true + } + } + } + } +} diff --git a/service-model-generator/modules/service-model-index-generator/src/main/resources/service_artifacts.json b/service-model-generator/modules/service-model-index-generator/src/main/resources/service_artifacts.json index 75bca60f91..d6efd15668 100644 --- a/service-model-generator/modules/service-model-index-generator/src/main/resources/service_artifacts.json +++ b/service-model-generator/modules/service-model-index-generator/src/main/resources/service_artifacts.json @@ -1837,6 +1837,244 @@ "kind": "ANNOTATION" } ] + }, + { + "name": "postgresql", + "version": "1.16.2", + "serviceTypeSkipList": [], + "serviceDeclaration": { + "displayName": "CDC for PostgreSQL", + "description": "Create an integration that captures PostgreSQL database changes in real-time", + "optionalTypeDescriptor": 1, + "typeDescriptorLabel": "Service Type", + "typeDescriptorDescription": "", + "typeDescriptorDefaultValue": "Service", + "addDefaultTypeDescriptor": 0, + "optionalAbsoluteResourcePath": 1, + "absoluteResourcePathLabel": "Service Base Path", + "absoluteResourcePathDescription": "", + "absoluteResourcePathDefaultValue": "", + "optionalStringLiteral": 1, + "stringLiteralLabel": "Service Base Path", + "stringLiteralDescription": "", + "stringLiteralDefaultValue": "", + "listenerKind": "MULTIPLE_SELECT_LISTENER", + "kind": "event" + }, + "serviceTypes": { + "Service": { + "name": "Service", + "description": "", + "functions": [ + { + "name": "onRead", + "description": "Triggers initially to read existing records from the monitored table", + "accessor": "", + "kind": "REMOTE", + "returnType": "error?", + "returnError": 1, + "returnTypeEditable": 0, + "enable": 0, + "optional": 1, + "parameters": [ + { + "name": "afterEntry", + "label": "After Entry", + "description": "The existing record from the monitored table", + "kind": "DATA_BINDING", + "type": "record {}", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 1 + }, + { + "name": "tableName", + "label": "Table Name", + "description": "The name of the table where the record was created", + "kind": "OPTIONAL", + "type": "string", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 0 + } + ] + }, + { + "name": "onCreate", + "description": "Triggers when a new record is created in the monitored table", + "accessor": "", + "kind": "REMOTE", + "returnType": "error?", + "returnError": 1, + "returnTypeEditable": 0, + "enable": 0, + "optional": 1, + "parameters": [ + { + "name": "afterEntry", + "label": "After Entry", + "description": "The new record created in the monitored table", + "kind": "DATA_BINDING", + "type": "record {}", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 1 + }, + { + "name": "tableName", + "label": "Table Name", + "description": "The name of the table where the record was created", + "kind": "OPTIONAL", + "type": "string", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 0 + } + ] + }, + { + "name": "onUpdate", + "description": "Triggers when an existing record is updated in the monitored table", + "accessor": "", + "kind": "REMOTE", + "returnType": "error?", + "returnError": 1, + "returnTypeEditable": 0, + "enable": 0, + "optional": 1, + "parameters": [ + { + "name": "beforeEntry", + "label": "Before Entry", + "description": "The record before the update in the monitored table", + "kind": "DATA_BINDING", + "type": "record {}", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 1 + }, + { + "name": "afterEntry", + "label": "After Entry", + "description": "The record after the update in the monitored table", + "kind": "DATA_BINDING", + "type": "record {}", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 1 + }, + { + "name": "tableName", + "label": "Table Name", + "description": "The name of the table where the record was created", + "kind": "OPTIONAL", + "type": "string", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 0 + } + ] + }, + { + "name": "onDelete", + "description": "Triggers when a record is deleted from the monitored table", + "accessor": "", + "kind": "REMOTE", + "returnType": "error?", + "returnError": 1, + "returnTypeEditable": 0, + "enable": 0, + "optional": 1, + "parameters": [ + { + "name": "beforeEntry", + "label": "Before Entry", + "description": "The record before the deletion in the monitored table", + "kind": "DATA_BINDING", + "type": "record {}", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 1 + }, + { + "name": "tableName", + "label": "Table Name", + "description": "The name of the table where the record was created", + "kind": "OPTIONAL", + "type": "string", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 0 + } + ] + }, + { + "name": "onTruncate", + "description": "Triggers when a monitored table is truncated", + "accessor": "", + "kind": "REMOTE", + "returnType": "error?", + "returnError": 1, + "returnTypeEditable": 0, + "enable": 0, + "optional": 1, + "parameters": [ + { + "name": "tableName", + "label": "Table Name", + "description": "The name of the table which was truncated", + "kind": "OPTIONAL", + "type": "string", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 0 + } + ] + }, + { + "name": "onError", + "description": "Triggers when an error occurs during event processing before mandatory functions are invoked", + "accessor": "", + "kind": "REMOTE", + "returnType": "", + "returnError": 0, + "returnTypeEditable": 0, + "enable": 0, + "optional": 1, + "parameters": [ + { + "name": "cdcError", + "label": "CDC Error", + "description": "The error occurred during message processing", + "kind": "REQUIRED", + "type": "cdc:Error", + "defaultValue": "", + "importStatements": "", + "nameEditable": 1, + "typeEditable": 0 + } + ] + } + ] + } + }, + "readOnlyMetadata": [ + { + "key": "tables", + "displayName": "Table Name", + "kind": "ANNOTATION" + } + ] } ] }