diff --git a/workspaces/mi/mi-core/src/rpc-types/mi-diagram/index.ts b/workspaces/mi/mi-core/src/rpc-types/mi-diagram/index.ts index a2a22477709..d3b7672b8f8 100644 --- a/workspaces/mi/mi-core/src/rpc-types/mi-diagram/index.ts +++ b/workspaces/mi/mi-core/src/rpc-types/mi-diagram/index.ts @@ -264,6 +264,14 @@ import { ConfigureKubernetesRequest, ConfigureKubernetesResponse, UpdateRegistryPropertyRequest, + LoadDriverAndTestConnectionRequest, + GetDynamicFieldsRequest, + GetDynamicFieldsResponse, + GetStoredProceduresResponse, + DriverDownloadRequest, + DriverDownloadResponse, + DriverMavenCoordinatesRequest, + DriverMavenCoordinatesResponse, Property } from "./types"; @@ -444,4 +452,9 @@ export interface MiDiagramAPI { isKubernetesConfigured: () => Promise; updatePropertiesInArtifactXML: (params: UpdateRegistryPropertyRequest) => Promise; getPropertiesFromArtifactXML: (params: string) => Promise; + loadDriverAndTestConnection: (params: LoadDriverAndTestConnectionRequest) => Promise; + getDynamicFields: (params: GetDynamicFieldsRequest) => Promise; + getStoredProcedures: (params: DSSFetchTablesRequest) => Promise; + downloadDriverForConnector: (params: DriverDownloadRequest) => Promise; + getDriverMavenCoordinates: (params: DriverMavenCoordinatesRequest) => Promise; } diff --git a/workspaces/mi/mi-core/src/rpc-types/mi-diagram/rpc-type.ts b/workspaces/mi/mi-core/src/rpc-types/mi-diagram/rpc-type.ts index 98e27fe7331..11e88127fd6 100644 --- a/workspaces/mi/mi-core/src/rpc-types/mi-diagram/rpc-type.ts +++ b/workspaces/mi/mi-core/src/rpc-types/mi-diagram/rpc-type.ts @@ -266,6 +266,14 @@ import { GetMockServicesResponse, ConfigureKubernetesRequest, ConfigureKubernetesResponse, + LoadDriverAndTestConnectionRequest, + GetDynamicFieldsRequest, + GetDynamicFieldsResponse, + GetStoredProceduresResponse, + DriverDownloadRequest, + DriverDownloadResponse, + DriverMavenCoordinatesRequest, + DriverMavenCoordinatesResponse, Property, UpdateRegistryPropertyRequest } from "./types"; @@ -453,3 +461,8 @@ export const configureKubernetes: RequestType = { method: `${_preFix}/isKubernetesConfigured` }; export const updatePropertiesInArtifactXML: RequestType = { method: `${_preFix}/updatePropertiesInArtifactXML` }; export const getPropertiesFromArtifactXML: RequestType = { method: `${_preFix}/getPropertiesFromArtifactXML` }; +export const loadDriverAndTestConnection: RequestType = { method: `${_preFix}/loadDriverAndTestConnection` }; +export const getDynamicFields: RequestType = { method: `${_preFix}/getDynamicFields` }; +export const getStoredProcedures: RequestType = { method: `${_preFix}/getStoredProcedures` }; +export const downloadDriverForConnector: RequestType = { method: `${_preFix}/downloadDriverForConnector` }; +export const getDriverMavenCoordinates: RequestType = { method: `${_preFix}/getDriverMavenCoordinates` }; diff --git a/workspaces/mi/mi-core/src/rpc-types/mi-diagram/types.ts b/workspaces/mi/mi-core/src/rpc-types/mi-diagram/types.ts index 4a835958597..c63ab7e4aaa 100644 --- a/workspaces/mi/mi-core/src/rpc-types/mi-diagram/types.ts +++ b/workspaces/mi/mi-core/src/rpc-types/mi-diagram/types.ts @@ -1925,6 +1925,7 @@ export interface DSSFetchTablesRequest { username: string; password: string; url: string; + driverPath: string; } export interface DSSFetchTablesResponse { @@ -2250,3 +2251,66 @@ export interface UpdateRegistryPropertyRequest { targetFile: string; properties: Property[]; } + +export interface DynamicField { + type: string; + value: { + name: string; + displayName: string; + inputType: string; + required: string; + helpTip: string; + placeholder: string; + defaultValue: string; + }; +} + +export interface GetDynamicFieldsRequest { + connectorName: string; + operationName: string; + fieldName: string; + selectedValue: string; + connection: ConnectorConnection; +} + +export interface GetDynamicFieldsResponse { + columns: DynamicField[]; +} + +export interface GetStoredProceduresResponse { + procedures: string[]; +} + +export interface DriverDownloadRequest { + groupId: string; + artifactId: string; + version: string; +} + +export interface DriverDownloadResponse { + driverPath: string; +} +export interface DriverMavenCoordinatesRequest { + filePath: string; + connectorName: string; + connectionType: string; +} + +export interface DriverMavenCoordinatesResponse { + groupId: string; + artifactId: string; + version: string; + found: boolean; +} + +export interface LoadDriverAndTestConnectionRequest { + dbType: string; + username: string; + password: string; + host: string; + port: string; + dbName: string; + url: string; + className: string; + driverPath: string; +} diff --git a/workspaces/mi/mi-diagram/src/components/Form/DBConnector/DriverConfiguration.tsx b/workspaces/mi/mi-diagram/src/components/Form/DBConnector/DriverConfiguration.tsx new file mode 100644 index 00000000000..d9c82bc7b35 --- /dev/null +++ b/workspaces/mi/mi-diagram/src/components/Form/DBConnector/DriverConfiguration.tsx @@ -0,0 +1,276 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * 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. + */ + +// DriverConfiguration.tsx +import { ComponentCard, FormGroup, TextField, Button, Codicon, ProgressRing } from '@wso2/ui-toolkit'; +import React from 'react'; +import styled from '@emotion/styled'; + +export interface DriverConfig { + type: 'default' | 'custom' | 'maven'; + groupId?: string; + artifactId?: string; + version?: string; + driverPath?: string; +} + +interface DriverConfigProps { + config: DriverConfig; + onConfigChange: (config: DriverConfig) => void; + onSave: () => void; + onClear?: () => void; + onSelectLocation?: () => void; + isLoading?: boolean; + error?: string; + isReadOnly?: boolean; +} + +export interface DriverOption { + value: string; + label: string; + description: string; + configType: DriverConfig['type']; +} + +export const DRIVER_OPTIONS: DriverOption[] = [ + { + value: 'default', + label: 'Use Default Driver', + description: 'Use the pre-configured default driver', + configType: 'default' + }, + { + value: 'custom', + label: 'Select Local Driver', + description: 'Select a driver from local file system', + configType: 'custom' + }, + { + value: 'maven', + label: 'Add Maven Dependency', + description: 'Add driver as Maven dependency', + configType: 'maven' + } +]; + +export const cardStyle = { + display: "block", + margin: "15px 0", + padding: "0 15px 15px 15px", + width: "auto", + cursor: "auto" +}; + +const SpacedCodicon = styled(Codicon)` + margin-right: 8px; +`; + +export const DefaultDriverConfig: React.FC = ({ + config, + isReadOnly = true +}) => { + return ( + + + + + + + + + + + + + + + + + +
Group ID + +
Artifact ID + +
Version + +
+
+
+ ); +}; + +export const CustomDriverConfig: React.FC = ({ + config, + onConfigChange, + onSave, + onClear, + onSelectLocation, + isLoading, + error +}) => { + const hasDriverPath = !!config.driverPath; + return ( + + + {error && ( +
+ {error} +
+ )} + {hasDriverPath ? ( +
+ + + + + + + + + + + + + + + +
Group ID + +
Artifact ID + +
Version + +
+ +
+ + +
+
+ ) : ( + // View when no driver path is selected +
+
+ No driver selected. Please select a driver location to continue. +
+ +
+ + {isLoading && ( + + )} +
+
)} +
+
+ ); +}; + +export const MavenDriverConfig: React.FC = ({ + config, + onConfigChange, + onSave, + onClear, + error +}) => { + return ( + + + {error && ( +
+ {error} +
+ )} + + + + + + + + + + + + + + + + +
Group ID + onConfigChange({ ...config, groupId: e.target.value })} + sx={{ paddingTop: '5px', paddingBottom: '10px', width: '50%' }} + /> +
Artifact ID + onConfigChange({ ...config, artifactId: e.target.value })} + sx={{ paddingTop: '5px', paddingBottom: '10px', width: '50%' }} + /> +
Version + onConfigChange({ ...config, version: e.target.value })} + sx={{ paddingTop: '5px', paddingBottom: '10px', width: '50%' }} + /> +
+ +
+ + +
+
+
+ ); +}; \ No newline at end of file diff --git a/workspaces/mi/mi-diagram/src/components/Form/DynamicFields/DynamicFieldsHandler.tsx b/workspaces/mi/mi-diagram/src/components/Form/DynamicFields/DynamicFieldsHandler.tsx new file mode 100644 index 00000000000..112a11744e9 --- /dev/null +++ b/workspaces/mi/mi-diagram/src/components/Form/DynamicFields/DynamicFieldsHandler.tsx @@ -0,0 +1,1638 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * 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. + */ + +import { Element, getNameForController, DynamicField, DynamicFieldGroup } from '../FormGenerator'; + +interface ConnectionParameter { + name: string; + value: string; +} + +interface ConnectionInfo { + name: string; + parameters: ConnectionParameter[]; +} + +interface DbCredentials { + className: string; + username: string; + password: string; + url: string; + driverPath?: string; +} + +interface TestDbConnectionArgs extends DbCredentials { + dbType: string; + host: string; + port: string; + dbName: string; +} + +type FetchTablesResponse = Record; + +interface OnValueChangeParams { + target?: string | string[]; + rpc?: string; + queryType?: 'select' | 'insert' | 'delete' | 'call'; + resultField?: string; + preparedResultField?: string; + columnTypesField?: string; + columnNamesField?: string; + targetCombo?: string[]; + onDynamicFieldChange?: OnValueChangeConfig; +} +interface OnValueChangeConfig { + function: string; + params?: OnValueChangeParams[]; +} + +// Interface for the structure holding dynamic field values during query build/parse +interface DynamicFieldValue { + value?: string; + isExpression?: boolean; + columnType?: string; + columnName?: string; + name: string; // Original dynamic field name + displayName: string; // User-friendly column name + helpTip: string; +} + +export interface DynamicFieldsHandlerProps { + rpcClient: any; + formData: any; + getValues: (name?: string | string[]) => any; + setValue: (name: string, value: any, options?: object) => void; + setComboValues?: (elementName: string, newValues: string[]) => void; + documentUri?: string; + parameters: any; + dynamicFields: Record; + setDynamicFields: (newFields: Record) => void; + setCustomError: (fieldName: string, message: string | null) => void; + updateElements?: (elements: Element[]) => void; + connectionName?: string; + comboValuesMap?: any; +} + +// --- Constants --- +const FIELD_NAMES = { + CONFIG_KEY: 'configKey', + CONFIG_REF: 'configRef', + COLUMNS: 'columns', + QUERY_TYPE: 'queryType', + QUERY: 'query', + ORDER_BY: 'orderBy', + LIMIT: 'limit', + OFFSET: 'offset', + USER_CONSENT: 'userConsent', + DRIVER_CLASS: 'driverClass', + DB_USER: 'dbUser', + DB_PASSWORD: 'dbPassword', + DB_URL: 'dbUrl', + GROUP_ID: 'groupId', + ARTIFACT_ID: 'artifactId', + VERSION: 'version', + DRIVER_PATH: 'driverPath', + CONNECTION_TYPE: 'connectionType', + CONNECTION_NAME: 'name', + ASSISTANCE_MODE: 'assistanceMode', + RESPONSE_COLUMNS: 'responseColumns', + TABLE_NAME: 'table', +}; + +const QUERY_TYPES = { + SELECT: 'select', + INSERT: 'insert', + DELETE: 'delete', + CALL: 'call', +}; + +const UI_MODES = { + ONLINE: 'online', + OFFLINE: 'offline', +}; + +const ERROR_MESSAGES = { + COMPLEX_QUERY: 'This query structure is not supported by this simplified operation. Please use the "Execute Query" operation for complex or custom SQL.', + TABLE_NOT_FOUND: 'Query contains a table (relation) that is not present in the database. Check the table name or use the ExecuteQuery operation which supports complex/custom SQL queries.', + FIELD_NOT_FOUND: 'Query contains fields/columns that are not present in the selected table. Check the query or use the ExecuteQuery operation which supports complex/custom SQL queries.', + INSERT_MISMATCH: 'Column count does not match value count in INSERT statement.', + CALL_MISMATCH: 'Column count does not match value count in CALL statement.', + CONNECTION_FAILED: 'Database connection failed. Please check your connection parameters or DB driver. Using Offline mode.', + DRIVER_CLASS_LOAD_ERROR: 'Error loading database driver class. Please ensure the driver is correctly configured.', + CONNECTION_VALIDATION_ERROR: 'Error validating database connection.', + PERMISSION_OR_CONFIG_ERROR: 'Connection invalid, incomplete, or user consent not provided.', + GENERIC_FETCH_ERROR: 'Error fetching data.', + PARSE_WHERE_CONDITION: 'Could not parse WHERE condition', + PARSE_INSERT_VALUE: 'Error parsing INSERT values', +}; + +const REGEX = { + EXPRESSION: /(\{[^\s"][^,<>\n}]*\})/, + SYNAPSE_EXPRESSION: /^\$\{((?:.|\s)*)\}$/, + DYNAMIC_FIELD_NAME: /^dyn_param_([^_]+)_([^_]+)_(.+)$/, + SELECT: /^SELECT\s+(?.*?)\s+FROM\s+(?[\w."`\[\]]+)(?:\s+WHERE\s+(?.*?))?(?:\s+ORDER BY\s+(?.*?))?(?:\s+LIMIT\s+(?\d+|\$\{[^}]+\}))?(?:\s+OFFSET\s+(?\d+|\$\{[^}]+\}))?\s*$/i, + INSERT: /^INSERT\s+INTO\s+(?[\w."`\[\]]+)\s*\((?.*?)\)\s+VALUES\s*\((?.*?)\)\s*$/i, + DELETE: /^DELETE\s+FROM\s+(?[\w."`\[\]]+)(?:\s+WHERE\s+(?.*?))?\s*$/i, + CALL: /^CALL\s+(?[\w."`\[\]]+)\s*\((?.*?)\)\s*$/i, + WHERE_CONDITION_PAIR: /^\s*([`"'\[\]\w\s,.-]+)\s*(=|>|<|>=|<=|!=|LIKE|ILIKE|IS(?:\s+NOT)?\s+NULL)\s*(.*?)\s*$/i, + COLUMN_TYPE_FROM_HELPTIP: /Column type: (.*)/, +}; + +const EXPRESSION_TYPES = ['stringOrExpression', 'integerOrExpression', 'expression', 'keyOrExpression', 'resourceOrExpression', + 'textOrExpression', 'textAreaOrExpression', 'stringOrExpression' +]; + +const NO_QUOTES_SQL_TYPES = ['INT', 'INTEGER', 'BIGINT', 'SMALLINT', 'TINYINT', 'FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC', 'BOOLEAN', 'BIT', 'DATE', 'TIME', 'TIMESTAMP', 'DATETIME']; + +export class DynamicFieldsHandler { + private readonly rpcClient: any; + private readonly formData: any; // Or specific type + private readonly getValues: DynamicFieldsHandlerProps['getValues']; + private readonly setValue: DynamicFieldsHandlerProps['setValue']; + private readonly setComboValues: DynamicFieldsHandlerProps['setComboValues']; + private readonly documentUri: string; + private dynamicFields: Record; + private readonly setDynamicFields: DynamicFieldsHandlerProps['setDynamicFields']; + private readonly setCustomError: DynamicFieldsHandlerProps['setCustomError']; + private readonly parameters: any; + private readonly connectionName: string; + + constructor(props: DynamicFieldsHandlerProps) { + this.rpcClient = props.rpcClient; + this.formData = props.formData; + this.getValues = props.getValues; + this.setValue = props.setValue; + this.setComboValues = props.setComboValues; + this.documentUri = props.documentUri ?? ''; + this.dynamicFields = props.dynamicFields; + this.setDynamicFields = (newFields: Record) => { + this.dynamicFields = newFields + props.setDynamicFields(this.dynamicFields); + }; + this.parameters = props.parameters; + this.setCustomError = props.setCustomError; + this.connectionName = props.connectionName; + } + + /** Handles value changes for elements configured with onValueChange */ + public handleValueChange = async (value: any, fieldName: string, element: Element): Promise => { + const config = element?.onValueChange; + if (!config?.function) return; + + try { + switch (config.function) { + case 'handleDynamicContent': + if (element.inputType === 'string' && element.name === FIELD_NAMES.TABLE_NAME) { + const parentElement = this.findElementByName(this.formData.elements, FIELD_NAMES.TABLE_NAME); + const queryBuildConfig = parentElement?.onValueChange?.params?.[0]?.onDynamicFieldChange?.params?.[0]; + await this._buildQueryFromDynamicFields(FIELD_NAMES.TABLE_NAME, queryBuildConfig?.queryType , queryBuildConfig, element); + } else { + await this._handleDynamicContentChange(value, fieldName, element); + } + break; + case 'updateTargetCombo': + if (config.params?.[0]?.target && config.params?.[0]?.rpc) { + await this.onConnectionChange(config.params[0].target, config.params?.[0].rpc); + } + break; + case 'buildQuery': + const parentField = (element as any).parentField; + if (parentField) { + await this.onDynamicFieldChange(value, element, parentField); + } else { + console.warn(`'buildQuery' called without parentField for element:`, element.name); + } + break; + case 'handleAssistanceMode': + if (config.params?.[0]?.target && config.params?.[0]?.rpc) { + await this._handleAssistanceModeChange(value,config.params[0].target, config.params?.[0].rpc); + } + break; + default: + console.warn(`Unknown onValueChange function: ${config.function}`); + } + } catch (error) { + console.error(`Error handling value change for field '${fieldName}':`, error); + this.setCustomError(getNameForController(fieldName), "Error processing field change."); + } + }; + + /** Fetches dynamic fields when a related field changes */ + public fetchDynamicFields = async ( + element: Element, // The element triggering the change + selectedValue: string, + parentFieldName: string + ): Promise => { + try { + // //if offline mode do not fetch dynamic fields + // if(element.inputType === 'string' && element.name === FIELD_NAMES.TABLE_NAME) { + // return undefined; + // } + const connectionInfo = await this._getValidConnectionDetails(); + if (!connectionInfo) { + this._clearDynamicFields(parentFieldName); + return undefined; + } + // if selected value is empty + if (!selectedValue) { + this._clearDynamicFields(parentFieldName); + return null; + } + //Get columns for the table + const rpcClientInstance = this.rpcClient.getMiDiagramRpcClient(); + const response = await rpcClientInstance.getDynamicFields({ + connectorName: this._getConnectorName(), + operationName: this.formData.operationName, + fieldName: parentFieldName, + selectedValue: selectedValue, + connection: connectionInfo, + }); + const newFields = response.columns || []; + // Augment dynamic fields with necessary context for later use + const onDynamicFieldChangeConfig = element?.onValueChange?.params?.[0]?.onDynamicFieldChange; + newFields.forEach((field: any) => { + field.value.parentField = parentFieldName; + if (onDynamicFieldChangeConfig) { + field.value.onValueChange = onDynamicFieldChangeConfig; + } + }); + + this.setCustomError(getNameForController(parentFieldName), null); + return newFields; + + } catch (error) { + console.error('Error fetching dynamic fields:', error); + this.setCustomError(getNameForController(parentFieldName), ERROR_MESSAGES.GENERIC_FETCH_ERROR); + this._clearDynamicFields(parentFieldName); + return null; // Indicate error + } + }; + + /** Handles changes in the selected DB connection */ + public onConnectionChange = async (targetField: string | string[], rpc?: string): Promise => { + const targetFields = Array.isArray(targetField) ? targetField : [targetField]; + + try { + // Attempt to get connection and validate it. Errors/banners set inside. + const connectionInfo = await this._getValidConnectionDetails(); + if (!connectionInfo) { + this._updateUiForConnectionState(false, targetFields, {}); // Offline state + return; + } + + // Fetch tables only if connection is valid + const tables = await this._fetchTablesForConnection(connectionInfo, rpc, targetFields[0]); + this._updateUiForConnectionState(true, targetFields, tables); // Online state + + } catch (error) { + console.error('Error processing connection change:', error); + this.setCustomError(getNameForController(FIELD_NAMES.CONFIG_KEY), ERROR_MESSAGES.CONNECTION_VALIDATION_ERROR); + this._updateUiForConnectionState(false, targetFields, {}); // Revert to offline state on error + } + }; + + /** Builds or parses the SQL query when dynamic fields or the query field itself changes */ + public onDynamicFieldChange = async (value: any, element: any, parentField: string): Promise => { + try { + const parentElement = this.findElementByName(this.formData.elements, parentField); + const queryBuildConfig = parentElement?.onValueChange?.params?.[0]?.onDynamicFieldChange?.params?.[0]; + + if (!queryBuildConfig?.queryType || !queryBuildConfig.resultField || !queryBuildConfig.preparedResultField || !queryBuildConfig.columnNamesField || !queryBuildConfig.columnTypesField) { + console.warn(`'buildQuery' configuration is incomplete for parent field: ${parentField}`); + return; + } + const operationType = queryBuildConfig.queryType; + + // Case 1: User manually edited the main query field + if (element.name === queryBuildConfig.resultField) { + await this._handleManualQueryChange(value, parentField, operationType, queryBuildConfig, element); + } + // Case 2: A dynamic field (or param manager) changed, rebuild query + else { + await this._buildQueryFromDynamicFields(parentField, operationType, queryBuildConfig, element); + } + + } catch (error) { + console.error('Error in onDynamicFieldChange:', error); + if (!(error instanceof Error && error.message.startsWith('Query parsing error:'))) { // Avoid double banner + this.setCustomError(getNameForController(element.name), "Error processing field change."); + } + } + }; + + /** Recursively finds an element definition by name */ + public findElementByName = (elements: Element[], name: string): Element | null => { + for (const element of elements as any[]) { + if (element.type !== 'attributeGroup' && element.value.name === name) { + return element.value; + } + + if (element.type === 'attributeGroup') { + const result = this.findElementByName(element.value.elements, name); + if (result) { + return result; + } + } + } + return null; + }; + + /** Recursively sets the hidden property of an element (Warning: Mutates elements) */ + public setElementVisibility = (elements: any[] | undefined, name: string, isHidden: boolean): boolean => { + if (!elements) return false; + + for (const element of elements) { + if (element.type !== 'attributeGroup' && element.value?.name === name) { + if (name === FIELD_NAMES.TABLE_NAME || name === FIELD_NAMES.ORDER_BY) { + let updatedOffline = false; + let updatedOnline = false; + if (isHidden) { + if (element?.enableCondition?.[0]?.queryType === UI_MODES.OFFLINE) { + element.value.hidden = true; + updatedOffline = true; + } + if (element?.enableCondition?.[0]?.queryType === UI_MODES.ONLINE) { + element.value.hidden = false; + updatedOnline = true; + } + } else { + if (element?.enableCondition?.[0]?.queryType === UI_MODES.ONLINE) { + element.value.hidden = true; + updatedOnline = true; + } + if (element?.enableCondition?.[0]?.queryType === UI_MODES.OFFLINE) { + element.value.hidden = false; + updatedOffline = true; + } + } + if (updatedOffline && updatedOnline) { + return true; + } + + } else { + element.value.hidden = isHidden; + return true; // Found and set + } + } + if (element.type === 'attributeGroup' && element.value?.elements) { + if (this.setElementVisibility(element.value.elements, name, isHidden)) { + return true; // Found in subgroup + } + } + } + return false; // Not found + }; + + // --- Private Helper Methods --- + + /** Handles the specific logic for 'handleDynamicContent' onChange */ + private async _handleDynamicContentChange(value: any, fieldName: string, element: Element): Promise { + const newFields = await this.fetchDynamicFields(element, value, fieldName); + // Update dynamic fields state only if fetch was successful (newFields is not null) + if (newFields !== null && newFields !== undefined) { + //let augmentedFields = newFields; + if (element.onValueChange?.params?.[0]?.onDynamicFieldChange.params?.[0]?.queryType === QUERY_TYPES.SELECT) { + const augmentedFields: typeof newFields = []; + newFields.forEach((field: any) => { + // Original field + augmentedFields.push(field); + // Extra checkbox field for "Include in Response" + augmentedFields.push({ + ...field, + value: { + ...field.value, + name: `${field.value.name}_include`, + columnName: field.value.displayName, + helpTip: `Check to include the column '${field.value.displayName}' in the SELECT response.`, + displayName: field.value.displayName.charAt(0).toUpperCase() + field.value.displayName.slice(1), + inputType: "checkbox", + defaultValue: false, + parentField: fieldName, + }, + }); + }); + this.setDynamicFields({ + ...this.dynamicFields, [fieldName]: { + header: "Table Columns", + fields: augmentedFields, + }, + }); + } else if (element.onValueChange?.params?.[0]?.onDynamicFieldChange.params?.[0]?.queryType === QUERY_TYPES.CALL) { + this.setDynamicFields({ + ...this.dynamicFields, [fieldName]: { + header: "Parameters", + fields: newFields, + }, + }); + } else { + this.setDynamicFields({ + ...this.dynamicFields, [fieldName]: { + header: "Table Columns", + fields: newFields, + }, + }); + } + + // Update target combos if configured + const targetCombos = element.onValueChange?.params?.[0]?.targetCombo; + if (targetCombos && this.setComboValues) { + const comboOptions = newFields.map((field: DynamicField) => field.value.displayName); + targetCombos.forEach(async (comboItem: string) => { + this.setComboValues(comboItem, comboOptions); + // if combooptions is empty trigger the onDynamicFieldChange + if (comboOptions.length === 0) { + + // dummy empty element to trigger the onDynamicFieldChange + const dummyElement = { + name: comboItem, + value: { + name: comboItem, + displayName: comboItem, + value: "", + inputType: 'stringOrExpression', + hidden: false, + onValueChange: element.onValueChange?.params?.[0]?.onDynamicFieldChange, + parentField: fieldName, + }, + }; + + const tempElem = this.findElementByName(this.formData.elements, FIELD_NAMES.COLUMNS); + if (tempElem) { + await this.onDynamicFieldChange(dummyElement.value, dummyElement, fieldName); + } + } + + }); + } + } else { + + // check if opened form inside connection + if (this.connectionName && this.connectionName.trim() !== '' && newFields !== undefined) { + + // get the configRef element from formData + const configRefElement = this.findElementByName(this.formData.elements, FIELD_NAMES.CONFIG_REF); + if (configRefElement) { + // trigger onConnectionChnage since the connection element does not exist + await this.onConnectionChange(configRefElement.onValueChange.params[0].target as string, + configRefElement.onValueChange.params[0].rpc); + } + } else { + + // Fetch failed or connection invalid, clear relevant dynamic fields and combos + this._clearDynamicFields(fieldName); + const targetCombos = element.onValueChange?.params?.[0]?.targetCombo; + if (targetCombos && this.setComboValues) { + targetCombos.forEach((comboItem: string) => this.setComboValues!(comboItem, [])); + } + + } + + } + } + + /** Clears dynamic fields for a specific parent */ + private _clearDynamicFields(parentFieldName: string): void { + const newDynamicFields = { ...this.dynamicFields }; + delete newDynamicFields[parentFieldName]; + this.setDynamicFields(newDynamicFields); + } + + + /** Updates UI state based on connection validity (online/offline) */ + private _updateUiForConnectionState( + isOnline: boolean, + targetFields: string[], + tables: FetchTablesResponse + ): void { + this.setElementVisibility(this.formData.elements, FIELD_NAMES.COLUMNS, isOnline); // Hide columns if offline + this.setElementVisibility(this.formData.elements, FIELD_NAMES.RESPONSE_COLUMNS, isOnline); // Show response columns if online + + this.setElementVisibility(this.formData.elements, FIELD_NAMES.TABLE_NAME, isOnline); // Show table name if online + this.setElementVisibility(this.formData.elements, FIELD_NAMES.ORDER_BY, isOnline); // Show order by if online + //this.setElementVisibility(this.formData.elements, FIELD_NAMES.ASSISTANCE_MODE, isOnline); + //this.setElementVisibility(this.formData.elements, FIELD_NAMES.TABLE_NAME_OFFLINE, isOnline); // Show table name offline if offline + + //const userConsent = connection.parameters?.find(p => p.name === FIELD_NAMES.USER_CONSENT)?.value; + + this.setValue(FIELD_NAMES.QUERY_TYPE, isOnline ? UI_MODES.ONLINE : UI_MODES.OFFLINE); + //this.setValue(FIELD_NAMES.ASSISTANCE_MODE, this.getValues(FIELD_NAMES.QUERY_TYPE) === UI_MODES.ONLINE ? true : false); + const tableNames = isOnline ? Object.keys(tables) : []; + + if (this.setComboValues) { + + targetFields.forEach((field) => { + const currentFieldValue = this.getValues(field); + this.setComboValues!(field, tableNames); + if (!isOnline) { + this.setValue(field, undefined); // Clear current value if going offline + } + // if currentFieldValue is not in the list set it to the first item + if (currentFieldValue !== undefined && tableNames.length > 0 && !tableNames.includes(currentFieldValue)) { + this.setValue(field, tableNames[0]); + } + }); + } + + // Clear dynamic fields if going offline + if (!isOnline) { + targetFields.forEach(parentField => this._clearDynamicFields(parentField)); + } + } + + /** Gets the configured connector name */ + private _getConnectorName(): string { + const name = this.formData?.connectorName; + return typeof name === 'string' ? name.replace(/\s/g, '') : 'db-connector'; + } + + /** Fetches connection details for the currently selected configKey */ + private async _getDbConnectionDetails(): Promise { + try { + const rpcClientInstance = this.rpcClient.getMiDiagramRpcClient(); + const connectorData = await rpcClientInstance.getConnectorConnections({ + documentUri: this.documentUri, + connectorName: this._getConnectorName(), + }); + + // Prioritize connectionName prop if available and not empty + let configRefValue = this.connectionName && this.connectionName.trim() !== '' + ? this.connectionName + : this.getValues(getNameForController(FIELD_NAMES.CONFIG_KEY)); + + const connection = connectorData.connections.find((c: { name: any; }) => c.name === configRefValue); + + // if configRef is ""/connection is undefined set the first connection as default + if (!connection && connectorData.connections.length > 0) { + // get all values + this.setValue(getNameForController(FIELD_NAMES.CONFIG_KEY), connectorData.connections[0].name); + return connectorData.connections[0]; + } + + return connection; + } catch (error) { + console.error("Error fetching connection details:", error); + this.setCustomError(getNameForController(FIELD_NAMES.CONFIG_KEY), "Failed to retrieve connection details."); + return undefined; + } + } + + /** Gets connection details and validates them (permission, params, test connection) */ + private async _getValidConnectionDetails(): Promise { + const connection = await this._getDbConnectionDetails(); + + if (!connection) { + return undefined; + } + // 1. Check User Consent + const userConsent = connection.parameters?.find(p => p.name === FIELD_NAMES.USER_CONSENT)?.value; + if (userConsent !== 'true') { + this.setElementVisibility(this.formData.elements, FIELD_NAMES.ASSISTANCE_MODE, true); // Hide assistance mode if no user consent + return undefined; // Not an error, but not valid for operations + } + this.setElementVisibility(this.formData.elements, FIELD_NAMES.ASSISTANCE_MODE, false); // Show assistance mode if have user consent + if(this.findElementByName(this.formData.elements, FIELD_NAMES.QUERY_TYPE)?.currentValue) { + if(this.findElementByName(this.formData.elements, FIELD_NAMES.QUERY_TYPE)?.currentValue === UI_MODES.OFFLINE && this.getValues(FIELD_NAMES.QUERY_TYPE) === UI_MODES.OFFLINE) { + this.setValue(FIELD_NAMES.ASSISTANCE_MODE, false); + return undefined; + } else { + this.setValue(FIELD_NAMES.ASSISTANCE_MODE, true); + } + } else { + this.setValue(FIELD_NAMES.ASSISTANCE_MODE, true); + } + + // 2. Check Required Parameters exist + const requiredParams = [FIELD_NAMES.DRIVER_CLASS, FIELD_NAMES.DB_USER, FIELD_NAMES.DB_PASSWORD, FIELD_NAMES.DB_URL, FIELD_NAMES.CONNECTION_TYPE]; + const hasAllParams = requiredParams.every(paramName => + connection.parameters?.some(p => p.name === paramName && p.value) + ); + + if (!hasAllParams) { + this.setCustomError(getNameForController(FIELD_NAMES.CONFIG_KEY), + ERROR_MESSAGES.PERMISSION_OR_CONFIG_ERROR + " (Missing parameters)"); + return undefined; + } + + // 3. Test the Connection via RPC + try { + let groupId = connection.parameters.find(p => p.name === FIELD_NAMES.GROUP_ID)?.value; + let artifactId = connection.parameters.find(p => p.name === FIELD_NAMES.ARTIFACT_ID)?.value; + let version = connection.parameters.find(p => p.name === FIELD_NAMES.VERSION)?.value; + let driverPath = connection.parameters.find(p => p.name === FIELD_NAMES.DRIVER_PATH)?.value; + let connectorName = this._getConnectorName(); + let connectionType = this._getConnectionDbType(connection); + if (!groupId || !artifactId || !version) { + const driverDetails = await this.rpcClient.getMiDiagramRpcClient().getDriverMavenCoordinates({ filePath: driverPath, connectionType: connectionType, connectorName: connectorName }); + groupId = driverDetails.groupId; + artifactId = driverDetails.artifactId; + version = driverDetails.version; + } + + let isDriverDownloaded = false; + let retryCount = 0; + const maxRetries = 5; + if (!driverPath) { + while (!isDriverDownloaded && retryCount < maxRetries) { + const args = { + groupId: groupId, + artifactId: artifactId, + version: version, + }; + this.setCustomError(getNameForController(FIELD_NAMES.CONFIG_KEY), "Checking DB Driver..."); + driverPath = await this.rpcClient.getMiDiagramRpcClient().downloadDriverForConnector(args); + if (driverPath) { + isDriverDownloaded = true; + } + retryCount++; + } + } + if (!isDriverDownloaded) { + this.setCustomError(getNameForController(FIELD_NAMES.CONFIG_KEY), "Failed to download the DB driver after 5 attempts."); + } + connection.parameters.push({ + name: FIELD_NAMES.DRIVER_PATH, + value: driverPath, + }); + const testArgs: TestDbConnectionArgs = { + className: connection.parameters.find(p => p.name === FIELD_NAMES.DRIVER_CLASS)!.value, + username: connection.parameters.find(p => p.name === FIELD_NAMES.DB_USER)!.value, + password: connection.parameters.find(p => p.name === FIELD_NAMES.DB_PASSWORD)!.value, + url: connection.parameters.find(p => p.name === FIELD_NAMES.DB_URL)!.value, + driverPath: driverPath, + dbType: '', + host: '', port: '', dbName: '' + }; + const testResult = await this.rpcClient.getMiDiagramRpcClient().loadDriverAndTestConnection(testArgs); + if (!testResult.success) { + this.setCustomError(getNameForController(FIELD_NAMES.CONFIG_KEY), + ERROR_MESSAGES.CONNECTION_FAILED + (testResult.message ? `: ${testResult.message}` : '')); + this.setValue(FIELD_NAMES.ASSISTANCE_MODE, false); + this.setValue(FIELD_NAMES.QUERY_TYPE, UI_MODES.OFFLINE); + //this._handleAssistanceModeChange(false, FIELD_NAMES.ASSISTANCE_MODE, "hide"); + return undefined; // Connection failed, set error message + } else { + this.setCustomError(getNameForController(FIELD_NAMES.CONFIG_KEY), null); + } + return connection; + + } catch (error) { + console.error("Error testing DB connection:", error); + this.setCustomError(FIELD_NAMES.CONFIG_KEY, ERROR_MESSAGES.CONNECTION_FAILED + " (RPC Error)"); + this.setValue(FIELD_NAMES.ASSISTANCE_MODE, false); + this.setValue(FIELD_NAMES.QUERY_TYPE, UI_MODES.OFFLINE); + return undefined; + } + } + + /** Fetches database tables for a given valid connection */ + private async _fetchTablesForConnection(connectionInfo: ConnectionInfo, rpc?: string, elementName?: string): Promise { + try { + const credentials: DbCredentials = { + className: connectionInfo.parameters.find(p => p.name === FIELD_NAMES.DRIVER_CLASS)!.value, + username: connectionInfo.parameters.find(p => p.name === FIELD_NAMES.DB_USER)!.value, + password: connectionInfo.parameters.find(p => p.name === FIELD_NAMES.DB_PASSWORD)!.value, + url: connectionInfo.parameters.find(p => p.name === FIELD_NAMES.DB_URL)!.value, + driverPath: connectionInfo.parameters.find(p => p.name === FIELD_NAMES.DRIVER_PATH)?.value, + }; + let tables; + if (rpc === "getStoredProcedures") { + tables = await this.rpcClient.getMiDiagramRpcClient().getStoredProcedures(credentials); + tables = Object.fromEntries(tables.map((key: string) => [key, true])); + } else { + tables = await this.rpcClient.getMiDiagramRpcClient().fetchDSSTables(credentials); + } + + return tables || {}; + + } catch (error) { + this.setCustomError(getNameForController(elementName!), "Failed to fetch database tables."); + console.error('Error fetching database tables:', error); + return {}; + } + } + + /** Gets the database type (e.g., 'mysql', 'postgresql') from connection parameters */ + private _getConnectionDbType(connectionInfo: ConnectionInfo | undefined): string | undefined { + return connectionInfo?.parameters?.find(p => p.name === FIELD_NAMES.CONNECTION_TYPE)?.value; + } + + /** Collects values from the dynamic fields associated with a parent field */ + private _getDynamicFieldValues( + parentField: string, + element: Element, + operationType?: string + ): Record { + const formValues = this.getValues(); + const currentQueryType = formValues[FIELD_NAMES.QUERY_TYPE] ?? UI_MODES.OFFLINE; + const collectedValues: Record = {}; + + // Handling ParamManager (Offline Mode) + + if (currentQueryType === UI_MODES.OFFLINE) { + const paramManagerValues = formValues[FIELD_NAMES.COLUMNS] || []; + if (Array.isArray(paramManagerValues)) { + let count = 0; + for (const column of paramManagerValues) { + if (operationType === QUERY_TYPES.CALL) { column.columnName = `param_${++count}`; } + if (!column.columnName) continue; // Skip if columnName is missing + + const normalizedName = column.columnName.replace(/[^a-zA-Z0-9_]/g, '_'); + collectedValues[normalizedName] = { + value: column.columnValue?.value, + isExpression: column.columnValue?.isExpression, + columnType: column.propertyType?.value, + name: normalizedName, + displayName: column.columnName, + helpTip: '', + }; + } + } + //Handling Response Columns (SELECT only) + if ( operationType === QUERY_TYPES.SELECT && typeof formValues[FIELD_NAMES.RESPONSE_COLUMNS] === 'string' && + formValues[FIELD_NAMES.RESPONSE_COLUMNS].trim() !== '') { + const selectColumns = formValues[FIELD_NAMES.RESPONSE_COLUMNS].split(',').map((col: string) => col.trim()).filter((col: string) => col); + for (const col of selectColumns) { + const colName = col.replace(/[^a-zA-Z0-9_]/g, '_'); + collectedValues[colName + '_include'] = { + value: 'true', + isExpression: false, + columnType: '', + name: colName + '_include', + displayName: colName, + columnName: colName, + helpTip: '', + }; + } + } + + + // set UI mode to offline if not already set + if (currentQueryType !== UI_MODES.OFFLINE) { + this.setValue(FIELD_NAMES.QUERY_TYPE, UI_MODES.OFFLINE); + } + return collectedValues; + } + + // Handling dynamically generated fields (Online Mode) + if (!this.dynamicFields[parentField]) { + console.warn(`No dynamic fields found for parent field: ${parentField}`); + return {}; + } + const fieldsDefinition = this.dynamicFields[parentField].fields; + if (!fieldsDefinition) return {}; + + fieldsDefinition.forEach(field => { + const fieldDef = field.value; + const fieldCtrlName = getNameForController(fieldDef.name); + const formValue = formValues[fieldCtrlName]; + + const match = fieldDef.name.match(REGEX.DYNAMIC_FIELD_NAME); + let originalColumnName = fieldDef.displayName; + let columnType = undefined; + if (match) { + //originalColumnName = match[3]; + columnType = match[2]; + } + + const value = (typeof formValue === 'object' && formValue !== null && 'value' in formValue) ? formValue.value : formValue; + const isExpression = (typeof formValue === 'object' && formValue !== null && 'isExpression' in formValue) ? formValue.isExpression : false; + + collectedValues[originalColumnName] = { + value: value, + isExpression: isExpression, + columnType: columnType, + name: fieldDef.name, + displayName: fieldDef.displayName, + columnName: fieldDef.columnName, + helpTip: fieldDef.helpTip + }; + }); + + // set UI mode to online if not already set + if (currentQueryType !== UI_MODES.ONLINE) { + this.setValue(FIELD_NAMES.QUERY_TYPE, UI_MODES.ONLINE); + } + + return collectedValues; + } + + + /** Encodes column names based on DB type for safe use in queries */ + private _encodeColumnName(columnName: string, dbType?: string): string { + const type = dbType?.toLowerCase() ?? 'default'; + switch (type) { + case 'mysql': + return `\`${columnName.replace(/`/g, '``')}\``; + case 'postgresql': + case 'oracle': + case 'ibm db2': + return `"${columnName.replace(/"/g, '""')}"`; + case 'microsoft sql server': + return `[${columnName.replace(/]/g, ']]')}]`; + default: + return `"${columnName.replace(/"/g, '""')}"`; + } + } + + /** Checks if a value likely represents a numeric or boolean type based on helpTip */ + private _checkNoQuotesNeeded(field: DynamicFieldValue): boolean { + // 1. Check explicit columnType first if available + if (field.columnType && NO_QUOTES_SQL_TYPES.includes(field.columnType.toUpperCase())) { + return true; + } + + // 2. Fallback to helpTip parsing + const match = field.helpTip?.match(REGEX.COLUMN_TYPE_FROM_HELPTIP); + if (match) { + const typeFromHelpTip = match[1]; + if (NO_QUOTES_SQL_TYPES.includes(typeFromHelpTip.toUpperCase())) { + return true; + } + } + + // 3. Check if value itself looks numeric or boolean *if* no type info found + if (!field.columnType && !match && field.value) { + if (/^\d+(\.\d+)?$/.test(field.value) || /^(true|false)$/i.test(field.value)) { + // It looks numeric or boolean assume no quotes needed as a guess + return true; + } + } + return false; + } + + /** Sets a field value, handling simple strings and expression objects */ + private _setFieldValue(fieldName: string, value: string | number | boolean | null | undefined, isExpression: boolean = false, parentField: string = 'table'): void { + // Check if the target element expects a simple value or an object + const targetElement = this.findElementByName(this.formData.elements, getNameForController(fieldName)); + const dynamicField = this.dynamicFields[parentField]?.fields.find((f: DynamicField) => f.value.name === fieldName); + const expectsObject = targetElement != null ? EXPRESSION_TYPES.includes(targetElement?.inputType) : EXPRESSION_TYPES.includes(dynamicField?.value?.inputType); + if (expectsObject) { + this.setValue(getNameForController(fieldName), { + isExpression: isExpression, + value: value ?? '', + namespaces: [] + }); + } else { + // For simple inputs (text, combo), just set the value + const finalValue = value ?? ''; + this.setValue(getNameForController(fieldName), finalValue); + } + } + + /** Clears a field's value using the appropriate format (simple vs object) */ + private _clearFieldValue(fieldName: string): void { + const ctrlName = getNameForController(fieldName); + const fieldElement = this.findElementByName(this.formData.elements, ctrlName); + if (fieldElement) { + const expectsObject = fieldElement.inputType && EXPRESSION_TYPES.includes(fieldElement.inputType); + if (expectsObject) { + this.setValue(ctrlName, { isExpression: false, value: '', namespaces: [] }); + } else { + this.setValue(ctrlName, ''); + } + } + } + + // --- Query Building Logic --- + + /** Builds the SQL query and prepared statement based on dynamic field values */ + private async _buildQueryFromDynamicFields( + parentField: string, + operationType: 'select' | 'insert' | 'delete' | 'call', + config: OnValueChangeParams, + element: Element + ): Promise { + const connectionInfo = await this._getDbConnectionDetails(); // Get connection for DB type + const dbType = this._getConnectionDbType(connectionInfo); + const dynamicFieldValues = this._getDynamicFieldValues(parentField, element, operationType); + const activeFields = Object.entries(dynamicFieldValues) + .filter(([, field]) => field.value !== undefined && field.value !== '' && !field.name.endsWith('_include')) // Filter out empty/undefined values + .reduce((acc, [key, val]) => { acc[key] = val; return acc; }, {} as Record); + const selectFields = Object.entries(dynamicFieldValues) + .filter(([, field]) => field.value !== undefined && field.value !== '' && field.name.endsWith('_include') && field.value.toString() === 'true') // Filter out empty/undefined values + .reduce((acc, [key, val]) => { acc[key] = val; return acc; }, {} as Record); + const allFields = Object.entries(dynamicFieldValues) + .reduce((acc, [key, val]) => { acc[key] = val; return acc; }, {} as Record); + + // fill the dynamic fields with the values from the parameters if they are empty + let queryBuilt = false; + if (Object.values(dynamicFieldValues).every((field: any) => field.value === "") + && element.inputType !== 'ParamManager') { + + const resultFieldElement = this.findElementByName(this.formData.elements, getNameForController(config.resultField!)); + const columnTypesElement = this.findElementByName(this.formData.elements, getNameForController(config.columnTypesField!)); + const columnNamesElement = this.findElementByName(this.formData.elements, getNameForController(config.columnNamesField!)); + const preparedStmtElement = this.findElementByName(this.formData.elements, getNameForController(config.preparedResultField!)); + + const query = resultFieldElement?.currentValue || ''; + const columnTypes = columnTypesElement?.currentValue; + const columnNames = columnNamesElement?.currentValue; + const preparedStatement = preparedStmtElement?.currentValue; + + if (query === undefined && preparedStatement === undefined && + columnTypes === undefined && columnNames === undefined) { + return; + } + + // Wait for the next tick to allow the UI to update + await new Promise(resolve => setTimeout(resolve, 0)); + + // Populate dynamic fields from parameters if column names are available + if (columnNames !== undefined && columnTypes !== undefined) { + const columnNameList = columnNames.split(',').map((name: string) => name.trim()); + + columnNameList.forEach((columnName: string) => { + const dynamicField = dynamicFieldValues[columnName]; + if (dynamicField) { + const fieldParam = this.parameters?.paramValues.find( + (param: any) => param.key === columnName + ); + + if (fieldParam?.value) { + const fieldValue = fieldParam.value; + const isExpression = REGEX.EXPRESSION.test(fieldValue) || + REGEX.SYNAPSE_EXPRESSION.test(fieldValue); + + // Clean expression format + let processedValue = fieldValue; + if (isExpression) { + processedValue = processedValue.replace(/^\{|\}$/g, ""); + } + this.setValue(dynamicField.name, { + isExpression: isExpression, + value: processedValue, + namespaces: [] + }); + queryBuilt = true; + } + } + }); + } + + // Set the query fields + this._setFieldValue(config.resultField!, query); + this._setFieldValue(config.preparedResultField!, preparedStatement?.replace(/[{}]/g, '') || '', false); + this._setFieldValue(config.columnTypesField!, columnTypes || '', false); + this._setFieldValue(config.columnNamesField!, columnNames || '', false); + + // Handle ORDER BY field population for SELECT queries + if (operationType === QUERY_TYPES.SELECT) { + const orderByField = this.findElementByName(this.formData.elements, getNameForController(FIELD_NAMES.ORDER_BY)); + + // Prevent infinite loop if this is called by the orderBy field itself + if (orderByField && element.name !== String(orderByField.name) && columnNames && this.setComboValues) { + const orderByOptions = columnNames.split(',').map((name: string) => name.trim()); + this.setComboValues(String(orderByField.name), orderByOptions); + } + } + // Skip the regular query building since restored from existing form values - Form Edit Scenario + if (!queryBuilt && query !== "" && element.name !== FIELD_NAMES.TABLE_NAME) { + await this._handleManualQueryChange(query, parentField, operationType, config, element); + return; + } + } + + let query = ''; + let preparedStatement = ''; + const columnNames: string[] = []; + const columnTypes: string[] = []; + + const tableName = this.getValues(getNameForController(parentField)); + if (!tableName) { + console.warn("Cannot build query: Table name is missing."); + return; + } + + const encodedTableName = this._encodeColumnName(tableName, dbType); + + switch (operationType) { + case QUERY_TYPES.INSERT: + const insertColumns = Object.values(activeFields); + if (insertColumns.length > 0) { + const cols = insertColumns.map(f => this._encodeColumnName(f.displayName, dbType)).join(', '); + const placeholders = insertColumns.map(() => '?').join(', '); + const values = insertColumns.map(f => + this._checkNoQuotesNeeded(f) ? f.value : `'${f.value}'` // Handle expressions and quoting + ).join(', '); + + query = `INSERT INTO ${encodedTableName} (${cols}) VALUES (${values})`; + preparedStatement = `INSERT INTO ${encodedTableName} (${cols}) VALUES (${placeholders})`; + + // push keys of activeFields to columnNames + columnNames.push(...Object.keys(activeFields)); + columnTypes.push(...insertColumns.map(f => f.columnType ?? 'UNKNOWN')); + } else { + query = `INSERT INTO ${encodedTableName} () VALUES ()`; // Or handle as error? + preparedStatement = `INSERT INTO ${encodedTableName} () VALUES ()`; + } + + break; + + case QUERY_TYPES.DELETE: + query = `DELETE FROM ${encodedTableName}`; + preparedStatement = `DELETE FROM ${encodedTableName}`; + if (Object.keys(activeFields).length > 0) { + const where = Object.values(activeFields).map(f => { + const col = this._encodeColumnName(f.displayName, dbType); + const val = this._checkNoQuotesNeeded(f) ? f.value : `'${f.value}'`; + return `${col} = ${val}`; + }).join(' AND '); + const prepWhere = Object.values(activeFields).map(f => + `${this._encodeColumnName(f.displayName, dbType)} = ?` + ).join(' AND '); + + query += ` WHERE ${where}`; + preparedStatement += ` WHERE ${prepWhere}`; + + // push keys of activeFields to columnNames + columnNames.push(...Object.keys(activeFields)); // Columns used in WHERE + columnTypes.push(...Object.values(activeFields).map(f => f.columnType ?? 'UNKNOWN')); + } + + break; + + case QUERY_TYPES.SELECT: + query = `SELECT ${Object.keys(selectFields).length > 0 ? Object.values(selectFields).map(f => f.columnName).join(', ') : '*'} FROM ${encodedTableName}`; + preparedStatement = `SELECT ${Object.keys(selectFields).length > 0 ? Object.values(selectFields).map(f => f.columnName).join(', ') : '*'} FROM ${encodedTableName}`; + + if (Object.keys(activeFields).length > 0) { + const where = Object.values(activeFields).map(f => { + const col = this._encodeColumnName(f.displayName, dbType); + const val = this._checkNoQuotesNeeded(f) ? f.value : `'${f.value}'`; + return `${col} = ${val}`; + }).join(' AND '); + const prepWhere = Object.values(activeFields).map(f => + `${this._encodeColumnName(f.displayName, dbType)} = ?` + ).join(' AND '); + + query += ` WHERE ${where}`; + preparedStatement += ` WHERE ${prepWhere}`; + + // push keys of activeFields to columnNames + columnNames.push(...Object.keys(activeFields)); + columnTypes.push(...Object.values(activeFields).map(f => f.columnType ?? 'UNKNOWN')); + } + + // Append ORDER BY, LIMIT, OFFSET for SELECT + const orderByVal = this.getValues(getNameForController(FIELD_NAMES.ORDER_BY)); + const limitVal = this.getValues(getNameForController(FIELD_NAMES.LIMIT)); + const offsetVal = this.getValues(getNameForController(FIELD_NAMES.OFFSET)); + + if (orderByVal) { + const encodedOrderBy = this._encodeColumnName(orderByVal, dbType); + query += ` ORDER BY ${encodedOrderBy}`; + preparedStatement += ` ORDER BY ${encodedOrderBy}`; + } + + if (limitVal?.value) { + query += ` LIMIT ${limitVal.value}`; + preparedStatement += ` LIMIT ?`; + } + + if (offsetVal?.value) { + query += ` OFFSET ${offsetVal.value}`; + preparedStatement += ` OFFSET ?`; + } + + break; + + case QUERY_TYPES.CALL: + + let callTemplate; + + // Customize call template based on DB type + switch (dbType) { + case 'oracle': + callTemplate = 'BEGIN {0}({1}); END;'; + break; + case 'microsoft sql server': + callTemplate = 'EXEC {0} {1}'; + break; + // Default CALL syntax for MySQL and others + default: + callTemplate = 'CALL {0}({1})'; + break; + } + + query = callTemplate.replace('{0}', encodedTableName); + preparedStatement = query; + + const callParams = Object.values(allFields).map(f => { + // if empty return NULL + const val = f.value ? (this._checkNoQuotesNeeded(f) ? f.value : `'${f.value}'`) : 'NULL'; + return `${val}`; + }).join(', '); + + const prepParams = Object.values(allFields).map(f => '?').join(', '); + + query = query.replace('{1}', callParams); + preparedStatement = preparedStatement.replace('{1}', prepParams); + + // push keys of activeFields to columnNames + columnNames.push(...Object.keys(allFields)); + columnTypes.push(...Object.values(allFields).map(f => f.columnType ?? 'UNKNOWN')); + + break; + } + + // Set the calculated values in the form + this._setFieldValue(config.resultField!, query); + this._setFieldValue(config.preparedResultField!, preparedStatement, false); // Prepared statement is never an expression + this._setFieldValue(config.columnNamesField!, columnNames.join(', '), false); + this._setFieldValue(config.columnTypesField!, columnTypes.join(', '), false); + + } + + + // --- Query Parsing Logic --- + + /** Handles changes to the main query input field, attempting to parse it */ + private async _handleManualQueryChange( + userQuery: string, + parentField: string, // The table name field + operationType: string, + config: OnValueChangeParams, + element: Element + ): Promise { + let parseSuccess = true; + let parseErrorMessage = ''; + + const queryFieldName = getNameForController(config.resultField!); + + if (!userQuery?.trim()) { + return; + } + + const regexMap = { + [QUERY_TYPES.SELECT]: REGEX.SELECT, + [QUERY_TYPES.INSERT]: REGEX.INSERT, + [QUERY_TYPES.DELETE]: REGEX.DELETE, + [QUERY_TYPES.CALL]: REGEX.CALL, + }; + const currentRegex = regexMap[operationType]; + const match = currentRegex?.exec(userQuery); + + if (!match?.groups) { + parseSuccess = false; + this.setCustomError(queryFieldName, ERROR_MESSAGES.COMPLEX_QUERY); + // Clear dependent fields + this._clearFieldValue(config.preparedResultField!); + this._clearFieldValue(config.columnNamesField!); + this._clearFieldValue(config.columnTypesField!); + return; + } + + this.setCustomError(queryFieldName, null); // Clear previous custom error for query field + + const { tableName, columns, values, whereClause, orderBy, limit, offset } = match.groups; + const cleanTableName = tableName.replace(/[`'"\[\]]/g, ''); // Remove quotes/brackets + const parentElement = this.findElementByName(this.formData.elements, parentField); + + // --- Table Validation --- + const connectionInfo = await this._getValidConnectionDetails(); + // if (!connectionInfo) return; + + const availableTables = connectionInfo ? + await this._fetchTablesForConnection(connectionInfo, parentElement?.onValueChange?.params?.[0].rpc, element.name as string) : {}; + const availableTableNames = Object.keys(availableTables); + + if (connectionInfo && !availableTableNames.includes(cleanTableName)) { + this.setCustomError(queryFieldName, ERROR_MESSAGES.TABLE_NOT_FOUND); + // Clear dependent fields + this._clearFieldValue(config.preparedResultField!); + this._clearFieldValue(config.columnNamesField!); + this._clearFieldValue(config.columnTypesField!); + return; + } + + // --- Update Table Field and Potentially Refresh Dynamic Fields --- + const currentTableValue = this.getValues(getNameForController(parentField)); + if (currentTableValue !== cleanTableName) { + this.setValue(getNameForController(parentField), cleanTableName); + // Trigger dynamic field refresh for the new table + const parentElement = this.findElementByName(this.formData.elements, parentField); + if (connectionInfo && parentElement) { + + const elementWithType = { ...parentElement, type: '' } as Element; + await this._handleDynamicContentChange(cleanTableName, parentField, elementWithType); + + // Short delay to allow state updates related to dynamic fields + await new Promise(resolve => setTimeout(resolve, 50)); + } else { + console.warn("Could not find parent element definition for:", parentField); + } + } + + // --- Parse Clauses and Update Dynamic Fields --- + // Get the *current* dynamic fields after potential refresh + const dynamicFieldDefs = this._getDynamicFieldValues(parentField, element); + const matchedFields: Record = {}; // Fields found in the query + const selectMatchedFields: Record = {}; // Fields found in SELECT clause + + try { + switch (operationType) { + case QUERY_TYPES.SELECT: + if(columns.trim() !== '*') { + const selectResult = this._parseSelectColumns(columns, dynamicFieldDefs, connectionInfo !== undefined); + if (!selectResult.success) throw new Error(selectResult.errorMessage || ERROR_MESSAGES.FIELD_NOT_FOUND); + Object.assign(selectMatchedFields, selectResult.fields); + } + + const whereResult = this._parseWhereClause(whereClause, dynamicFieldDefs, connectionInfo !== undefined); + if (!whereResult.success) throw new Error(whereResult.errorMessage || ERROR_MESSAGES.FIELD_NOT_FOUND); + Object.assign(matchedFields, whereResult.fields); + break; + case QUERY_TYPES.DELETE: + const deleteWhereResult = this._parseWhereClause(whereClause, dynamicFieldDefs, connectionInfo !== undefined); + if (!deleteWhereResult.success) throw new Error(deleteWhereResult.errorMessage || ERROR_MESSAGES.FIELD_NOT_FOUND); + Object.assign(matchedFields, deleteWhereResult.fields); + break; + case QUERY_TYPES.INSERT: + const insertResult = this._parseInsertValues(columns, values, dynamicFieldDefs, connectionInfo !== undefined); + if (!insertResult.success) throw new Error(insertResult.errorMessage || ERROR_MESSAGES.INSERT_MISMATCH); + Object.assign(matchedFields, insertResult.fields); + break; + case QUERY_TYPES.CALL: + const callResult = this._parseCallParams(values, dynamicFieldDefs, connectionInfo !== undefined); + if (!callResult.success) throw new Error(callResult.errorMessage || ERROR_MESSAGES.FIELD_NOT_FOUND); + Object.assign(matchedFields, callResult.fields); + break; + } + + // --- Update Form Fields based on Parsed Data --- + // 1. Update dynamic fields found in the query + + if (connectionInfo) { + Object.values(dynamicFieldDefs).forEach(fieldDef => { + // Find if this field is in the matched fields from the query + const matched = Object.values(matchedFields).find(m => + fieldDef.displayName === m.displayName || + fieldDef.displayName.replace(/[^a-zA-Z0-9]/g, '') === m.displayName.replace(/[^a-zA-Z0-9]/g, '') || + fieldDef.name === m.name + ); + + const selectColMatched = Object.values(selectMatchedFields ?? {}).find(m => + fieldDef.displayName === m.displayName || + fieldDef.displayName.replace(/[^a-zA-Z0-9]/g, '') === m.displayName.replace(/[^a-zA-Z0-9]/g, '') || + fieldDef.name === m.name + '_include' + ); + if (matched) { + this._setFieldValue(fieldDef.name, matched.value, matched.isExpression); + } else { + this._clearFieldValue(fieldDef.name); + } + if (selectColMatched) { + this._setFieldValue(fieldDef.name, true, selectColMatched.isExpression); + } + }); + } else { + // If connectionInfo is false, set the values to the parameters + + // match matched fields with the param manager values, if found update and if not found add + let paramManagerTempValues: any[] = []; + Object.entries(matchedFields).forEach(([key, field]) => { + // build the param manager value and add to the array + const paramManagerValue = { + columnName: key, + columnValue: { + isExpression: field.isExpression, + value: field.value, + namespaces: [] as string[] + }, + propertyType: { + isExpression: false, + value: field.columnType, + namespaces: [] as string[] + } + }; + + paramManagerTempValues.push(paramManagerValue); + }); + + this.setValue(getNameForController(FIELD_NAMES.COLUMNS), paramManagerTempValues); + } + + // 2. Update common fields (Column Names/Types) + const columnNames = Object.keys(matchedFields).join(', '); + const columnTypes = Object.values(matchedFields).map(f => f.columnType ?? 'UNKNOWN').join(', '); + this._setFieldValue(config.columnNamesField!, columnNames, false); + this._setFieldValue(config.columnTypesField!, columnTypes, false); + + // 3. Handle optional clauses (SELECT) and clear inapplicable fields + if (operationType === QUERY_TYPES.SELECT) { + this._handleOptionalClause(orderBy, FIELD_NAMES.ORDER_BY, false, true); + this._handleOptionalClause(limit, FIELD_NAMES.LIMIT, true); + this._handleOptionalClause(offset, FIELD_NAMES.OFFSET, true); + } else { + this._clearFieldValue(FIELD_NAMES.ORDER_BY); + this._clearFieldValue(FIELD_NAMES.LIMIT); + this._clearFieldValue(FIELD_NAMES.OFFSET); + } + + // 4. Reconstruct and set the Prepared Statement + let preparedStatement = ''; + const connectionData = await this._getDbConnectionDetails(); + const dbType = this._getConnectionDbType(connectionData); + const encodedTableName = this._encodeColumnName(cleanTableName, dbType); + + switch (operationType) { + case QUERY_TYPES.SELECT: + preparedStatement = this._buildSelectPreparedStatement(columns, encodedTableName, matchedFields, orderBy, limit, offset); + break; + case QUERY_TYPES.INSERT: + preparedStatement = this._buildInsertPreparedStatement(encodedTableName, matchedFields); + break; + case QUERY_TYPES.DELETE: + preparedStatement = this._buildDeletePreparedStatement(encodedTableName, matchedFields); + break; + case QUERY_TYPES.CALL: + preparedStatement = this._buildCallPreparedStatement(encodedTableName, matchedFields); + break; + } + this._setFieldValue(config.preparedResultField!, preparedStatement, false); + // Clear any previous error message upon successful parsing + this.setCustomError(queryFieldName, null); + + } catch (error: any) { + parseErrorMessage = error.message || ERROR_MESSAGES.COMPLEX_QUERY; + console.warn(`Query parsing error: ${parseErrorMessage}`); + this.setCustomError(queryFieldName, `Query parsing error: ${parseErrorMessage}`); + + // Clear dependent fields on parse error + this._clearFieldValue(config.preparedResultField!); + this._clearFieldValue(config.columnNamesField!); + } + + } + + + /** Helper: Parses WHERE clause conditions */ + private _parseWhereClause(whereClause: string | undefined, availableFields: Record, connectionInfo: boolean): { success: boolean, fields: Record, errorMessage?: string } { + const matchedFields: Record = {}; + if (!whereClause) { + return { success: true, fields: matchedFields }; + } + + // conditions within parenthesis are considered out of scope since it introduces unwanted complexicity + const conditions = whereClause.trim().split(/\s+AND\s+/i); + + for (const condition of conditions) { + const conditionMatch = condition.match(REGEX.WHERE_CONDITION_PAIR); + + if (!conditionMatch) { + console.warn(`${ERROR_MESSAGES.PARSE_WHERE_CONDITION}: ${condition}`); + return { success: false, fields: {}, errorMessage: `${ERROR_MESSAGES.PARSE_WHERE_CONDITION}: "${condition}"` }; + } + + // Extract parts: column, operator, valueStr + const [, rawColumn, , rawValueStr] = conditionMatch; + //const columnName = rawColumn.replace(/[^a-zA-Z0-9]/g, '').trim(); + const columnName = rawColumn.trim().replace(/[`'"\[\]]/g, ''); // Remove quotes/brackets + + const valueStr = rawValueStr.trim(); + + // if connection info is false build a dummy field + const dynamicField = connectionInfo ? availableFields[columnName] : { + name: columnName, + displayName: columnName, + value: valueStr, + isExpression: false, + columnType: "VARCHAR", + helpTip: '' + }; + + if (!dynamicField) { + console.warn(`Dynamic field definition not found for WHERE column: ${columnName}`); + return { success: false, fields: {}, errorMessage: `Field "${rawColumn}" not found for this table.` }; + } + + // Remove leading/trailing single or double quotes + const fieldValue = valueStr.replace(/^['"]|['"]$/g, ''); + + matchedFields[columnName] = dynamicField; + matchedFields[columnName].value = fieldValue; + const isExpression = REGEX.SYNAPSE_EXPRESSION.test(fieldValue); + + matchedFields[columnName] = { ...dynamicField, value: fieldValue, isExpression: isExpression }; + } + return { success: true, fields: matchedFields }; + } + + /** Helper: Parses INSERT columns and values */ + private _parseInsertValues(columnsStr: string | undefined, valuesStr: string | undefined, availableFields: Record, connectionInfo: boolean): { success: boolean, fields: Record, errorMessage?: string } { + const matchedFields: Record = {}; + + if (!columnsStr || !valuesStr) { + return { success: false, fields: {}, errorMessage: ERROR_MESSAGES.INSERT_MISMATCH + " (missing columns or values)" }; + } + + // this does not support column names such as `Random, "``Name,' , .````test` + // this is an acceptable limitation since this only occurs when user manually enters the query + + const columns = columnsStr.split(',').map(col => col.trim().replace(/[`'"\[\]]/g, '')); + const values = valuesStr.split(',').map(val => val.trim().replace(/^['"]|['"]$/g, '')); // Remove quotes + + if (columns.length !== values.length || columns.length === 0) { + console.warn(ERROR_MESSAGES.INSERT_MISMATCH); + return { success: false, fields: {}, errorMessage: ERROR_MESSAGES.INSERT_MISMATCH }; + } + for (let i = 0; i < columns.length; i++) { + const columnName = columns[i]; + const valueStr = values[i]; + const dynamicField = connectionInfo ? availableFields[columnName] : { + name: columnName, + displayName: columnName, + value: valueStr, + isExpression: false, + columnType: "VARCHAR", + helpTip: '' + }; + if (!dynamicField) { + console.warn(`Dynamic field definition not found for INSERT column: ${columnName}`); + return { success: false, fields: {}, errorMessage: `Field "${columnName}" not found for this table.` }; + } + + // Detect if value is expression or literal + const isSynapseExpr = REGEX.SYNAPSE_EXPRESSION.test(valueStr); + const isLegacyExpr = REGEX.EXPRESSION.test(valueStr); + const isExpression = isSynapseExpr || isLegacyExpr; + + matchedFields[columnName] = { ...dynamicField, value: valueStr, isExpression: isExpression }; + } + + return { success: true, fields: matchedFields }; + } + + /** Helper: Parses CALL parameters */ + private _parseCallParams(valuesStr: string | undefined, availableFields: Record, connectionInfo: boolean): { success: boolean, fields: Record, errorMessage?: string } { + const matchedFields: Record = {}; + if (!valuesStr) { + return { success: false, fields: {}, errorMessage: "No parameters provided." }; + } + const values = valuesStr.split(',').map(val => val.trim().replace(/^['"]|['"]$/g, '')); + + // there should be values matching all of the dynamic fields + // iterate over availbale fields with counter + if (!connectionInfo) { + // For offline mode, iterate over values array + for (let i = 0; i < values.length; i++) { + const valueStr = values[i]; + const fieldName = `param${i + 1}`; + + const dynamicField = { + name: fieldName, + displayName: fieldName, + value: valueStr, + isExpression: false, + columnType: "VARCHAR", + helpTip: '' + }; + + const isSynapseExpr = REGEX.SYNAPSE_EXPRESSION.test(valueStr); + const isLegacyExpr = REGEX.EXPRESSION.test(valueStr); + const isExpression = isSynapseExpr || isLegacyExpr; + + matchedFields[fieldName] = { ...dynamicField, value: valueStr, isExpression: isExpression }; + } + } else { + // For online mode, iterate over available fields + let counter = 0; + for (const fieldName in availableFields) { + const dynamicField = availableFields[fieldName]; + + if (!dynamicField) { + console.warn(`Dynamic field definition not found for CALL column: ${fieldName}`); + return { success: false, fields: {}, errorMessage: `Field "${fieldName}" not found for this table.` }; + } + + // Check if value exists for this field + if (counter >= values.length) { + console.warn(`Not enough values provided for CALL parameters.`); + return { success: false, fields: {}, errorMessage: `Not enough values provided for CALL parameters.` }; + } + + const valueStr = values[counter]; + const isSynapseExpr = REGEX.SYNAPSE_EXPRESSION.test(valueStr); + const isLegacyExpr = REGEX.EXPRESSION.test(valueStr); + const isExpression = isSynapseExpr || isLegacyExpr; + + matchedFields[fieldName] = { ...dynamicField, value: valueStr, isExpression: isExpression }; + counter++; + } + } + + return { success: true, fields: matchedFields }; + } + /** Helper: Parses SELECT columns */ + private _parseSelectColumns(columnsStr: string | undefined, availableFields: Record, connectionInfo: boolean): { success: boolean, fields: Record, errorMessage?: string } { + if (!columnsStr) return { success: false, fields: {}, errorMessage: "No columns specified for SELECT." }; + // Handle SELECT * case + // if (columnsStr.trim() === '*') { + // return { success: true, fields: availableFields }; + // } + const columns = columnsStr.split(',').map(col => col.trim()); + const matchedFields: Record = {}; + for (const col of columns) { + const field = Object.values(availableFields).find(f => f.columnName && f.columnName === col); + if (!field) { + console.warn(`No matching field found for column: ${col}`); + return { success: false, fields: {}, errorMessage: `No matching field found for column: ${col}` }; + } + matchedFields[col] = field; + } + return { success: true, fields: matchedFields }; + } + + /** Helper: Handles optional clauses like ORDER BY, LIMIT, OFFSET during parsing */ + private _handleOptionalClause(clauseValue: string | undefined, fieldName: string, checkExpression: boolean = false, isCombo: boolean = false) { + + if (clauseValue) { + const cleanValue = clauseValue.replace(/[`"\[\]]/g, ''); + const isExpression = checkExpression && REGEX.SYNAPSE_EXPRESSION.test(clauseValue); + this._setFieldValue(fieldName, cleanValue, isExpression); + } else { + this._clearFieldValue(fieldName); // Clear the field if clause not present + } + } + + /** Helper: Builds a prepared statement string for SELECT */ + private _buildSelectPreparedStatement(columnsStr: string, tableName: string, matchedWhereFields: Record, orderBy?: string, limit?: string, offset?: string): string { + + const selectCols = columnsStr?.trim() || '*'; + let statement = `SELECT ${selectCols} FROM ${tableName}`; + + const wherePlaceholders = Object.keys(matchedWhereFields) + .map(key => `${this._encodeColumnName(matchedWhereFields[key].displayName)} = ?`) // Use displayName for column name + .join(' AND '); + + if (wherePlaceholders) { + statement += ` WHERE ${wherePlaceholders}`; + } + + if (orderBy) { + const cleanOrderBy = orderBy.replace(/[`"\[\]]/g, '').trim(); + statement += ` ORDER BY ${cleanOrderBy}`; + } + + if (limit) { statement += ` LIMIT ?`; } + if (offset) { statement += ` OFFSET ?`; } + + return statement; + } + + /** Helper: Builds a prepared statement string for INSERT */ + private _buildInsertPreparedStatement(tableName: string, matchedFields: Record): string { + const columns = Object.values(matchedFields).map(f => this._encodeColumnName(f.displayName)).join(', '); + const placeholders = Object.keys(matchedFields).map(() => '?').join(', '); + if (!columns) return `INSERT INTO ${tableName} DEFAULT VALUES`; // Handle case with no columns + return `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders})`; + } + + /** Helper: Builds a prepared statement string for DELETE */ + private _buildDeletePreparedStatement(tableName: string, matchedWhereFields: Record): string { + let statement = `DELETE FROM ${tableName}`; // tableName should be encoded already + + const wherePlaceholders = Object.keys(matchedWhereFields) + .map(key => `${this._encodeColumnName(matchedWhereFields[key].displayName)} = ?`) + .join(' AND '); + + if (wherePlaceholders) { + statement += ` WHERE ${wherePlaceholders}`; + } + // Allow DELETE without WHERE + + return statement; + } + + /** Helper: Builds a prepared statement string for CALL */ + private _buildCallPreparedStatement(tableName: string, matchedFields: Record): string { + const placeholders = Object.keys(matchedFields).map(() => '?').join(', '); + return `CALL ${tableName}(${placeholders})`; + } + + private async _handleAssistanceModeChange(value: any, fieldName: string, rpc?: string): Promise { + if (value === true) { + this.setValue(FIELD_NAMES.QUERY_TYPE, UI_MODES.ONLINE); + this.onConnectionChange(fieldName, rpc) + } else { + this.setValue(FIELD_NAMES.QUERY_TYPE, UI_MODES.OFFLINE); + this.setCustomError(getNameForController(FIELD_NAMES.CONFIG_KEY), null); + this._updateUiForConnectionState(false, [fieldName], {}); // Offline state + } + } +} diff --git a/workspaces/mi/mi-diagram/src/components/Form/FormGenerator.tsx b/workspaces/mi/mi-diagram/src/components/Form/FormGenerator.tsx index 18dd84e6268..f9e8c0d985d 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/FormGenerator.tsx +++ b/workspaces/mi/mi-diagram/src/components/Form/FormGenerator.tsx @@ -30,7 +30,10 @@ import { TextArea, TextField, Tooltip, - Typography + Typography, + RadioButtonGroup, + Button, + Dropdown } from '@wso2/ui-toolkit'; import styled from '@emotion/styled'; import { Controller } from 'react-hook-form'; @@ -59,7 +62,10 @@ import { HelperPaneCompletionItem, HelperPaneData } from '@wso2/mi-core'; import AIAutoFillBox from './AIAutoFillBox/AIAutoFillBox'; import { compareVersions } from '../../utils/commons'; import { RUNTIME_VERSION_440 } from '../../resources/constants'; - +import { DynamicFieldsHandler } from './DynamicFields/DynamicFieldsHandler'; +import { GenericRadioGroup } from '../Form/RadioButtonGroup/GenericRadioGroup'; +import { DriverConfig, DRIVER_OPTIONS, DefaultDriverConfig, CustomDriverConfig, MavenDriverConfig } from './DBConnector/DriverConfiguration'; +import { getValue } from './Keylookup/utils'; // Constants const XML_VALUE = 'xml'; @@ -90,15 +96,20 @@ export interface FormGeneratorProps { documentUri?: string; formData: any; connectorName?: string; + parameters?: any; sequences?: string[]; onEdit?: boolean; control: any; errors: any; setValue: any; + setComboValues?: (elementName: string, newValues: string[]) => void; + comboValuesMap?: any; reset: any; watch: any; getValues: any; skipGeneralHeading?: boolean; + connectionName?: string; + connections?: string[]; ignoreFields?: string[]; disableFields?: string[]; autoGenerateSequences?: boolean; @@ -138,6 +149,7 @@ export interface Element { artifactType?: string; isUnitTest?: boolean; skipSanitization?: boolean; + onValueChange?: any; } interface ExpressionValueWithSetter { @@ -145,6 +157,25 @@ interface ExpressionValueWithSetter { setValue: (value: ExpressionFieldValue) => void; }; +export interface DynamicField { + type: string; + value: { + columnName: string; + name: string; + displayName: string; + inputType: string; + required: string; + helpTip: string; + placeholder: string; + defaultValue: string; + }; +} + +export interface DynamicFieldGroup { + header?: string; // optional title/header + fields: DynamicField[]; // the actual fields +} + export function getNameForController(name: string | number) { if (name === 'configRef') { return 'configKey'; @@ -211,7 +242,10 @@ export function FormGenerator(props: FormGeneratorProps) { skipGeneralHeading, ignoreFields, disableFields, - range + range, + connectionName, + parameters, + setComboValues } = props; const [currentExpressionValue, setCurrentExpressionValue] = useState(null); const [expressionEditorField, setExpressionEditorField] = useState(null); @@ -234,6 +268,58 @@ export function FormGenerator(props: FormGeneratorProps) { const [numberOfDifferent, setNumberOfDifferent] = useState(0); const [idpSchemaNames, setidpSchemaNames] = useState< {fileName: string; documentUriWithFileName?: string}[]>([]); const [showFillWithAI, setShowFillWithAI] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [customErrors, setCustomErrors] = useState>({}); + const [driverError, setDriverError] = useState(""); + const dynamicFieldsHandler = useRef(null); + const [dynamicFields, setDynamicFields] = useState>({}); + const setCustomError = (fieldName: string, message: string | null) => { + setCustomErrors(prevErrors => ({ + ...prevErrors, + [fieldName]: message + })); + }; + const [driverConfig, setDriverConfig] = useState({ + type: 'default', + groupId: '', + artifactId: '', + version: '', + driverPath: '' + }); + const [isDriverConfigLoading, setIsDriverConfigLoading] = useState(true); + + useEffect(() => { + const initializeDriverConfig = () => { + setIsDriverConfigLoading(true); + + try { + const paramValues = parameters?.paramValues || []; + const getParamValue = (key: string) => paramValues.find((p: any) => p.key === key)?.value; + + const formValues = getValues(); + + const groupId = formValues.groupId || getParamValue('groupId') || ''; + const artifactId = formValues.artifactId || getParamValue('artifactId') || ''; + const version = formValues.version || getParamValue('version') || ''; + const driverPath = formValues.driverPath || getParamValue('driverPath') || ''; + + let type: DriverConfig['type'] = 'default'; + if (driverPath) { + type = 'custom'; + } else if (groupId || artifactId || version) { + type = 'maven'; + } + + setDriverConfig({ type, groupId, artifactId, version, driverPath }); + } catch (error) { + console.error('Failed to initialize driver config:', error); + } finally { + setIsDriverConfigLoading(false); + } + }; + + initializeDriverConfig(); + }, [parameters, getValues]); useEffect(() => { if (generatedFormDetails) { @@ -323,6 +409,28 @@ export function FormGenerator(props: FormGeneratorProps) { } } + useEffect(() => { + try { + if (!dynamicFieldsHandler.current) { + dynamicFieldsHandler.current = new DynamicFieldsHandler({ + rpcClient, + formData, + getValues, + setValue, + setComboValues, + documentUri: props.documentUri, + parameters, + dynamicFields, + setDynamicFields, + connectionName, + setCustomError + }); + } + } catch (error) { + console.error("Error initializing dynamicFieldsHandler:", error); + } + }, []); + useEffect(() => { setIsLoading(true); handleOnCancelExprEditorRef.current = () => { @@ -352,6 +460,224 @@ export function FormGenerator(props: FormGeneratorProps) { return connectionNames; } + // Handler to call dynamic fields handler methods + const handleValueChange = async (value: any, fieldName: string, element: Element) => { + if (!element?.onValueChange?.function) return; + if (dynamicFieldsHandler.current) { + await dynamicFieldsHandler.current.handleValueChange(value, fieldName, element); + } + }; + + const handleDriverDirSelection = async () => { + setIsProcessing(true); + const projectDirectory = await rpcClient.getMiDiagramRpcClient().askDriverPath(); + props.setValue("driverPath", projectDirectory.path); + return projectDirectory.path; + }; + + const handleDriverTypeChange = async (driverType: string, element: Element) => { + const option = DRIVER_OPTIONS.find(opt => opt.label === driverType); + if (!option) return; + + setDriverConfig(prev => ({ ...prev, type: option.configType })); + setDriverError(null); + + if (option.configType === 'default') { + handleClearDriver(); + await loadDefaultDriverDetails(); + } else { + // For custom driver, check if we already have parameters + const paramValues = parameters?.paramValues || []; + const getParamValue = (key: string) => paramValues.find((p: any) => p.key === key)?.value; + + const existingGroupId = getParamValue('groupId'); + const existingArtifactId = getParamValue('artifactId'); + const existingVersion = getParamValue('version'); + const existingDriverPath = getParamValue('driverPath'); + if (option.configType === 'custom') { + if (existingGroupId && existingArtifactId && existingVersion && existingDriverPath) { + setDriverConfig(prev => ({ + ...prev, + groupId: existingGroupId, + artifactId: existingArtifactId, + version: existingVersion, + driverPath: existingDriverPath + })); + } + } else { + if (existingGroupId && existingArtifactId && existingVersion) { + setDriverConfig(prev => ({ + ...prev, + groupId: existingGroupId, + artifactId: existingArtifactId, + version: existingVersion, + driverPath: null + })); + } else if (existingDriverPath) { + setValue("driverPath", null); + } + } + saveDriverConfig(); + } + }; + + const loadDefaultDriverDetails = async () => { + try { + const driverDetails = await rpcClient.getMiDiagramRpcClient().getDriverMavenCoordinates({ + filePath: "", + connectionType: formData?.connectionName, + connectorName: formData?.connectorName ?? connectorName.replace(/\s/g, '') + }); + + setDriverConfig(prev => ({ + ...prev, + groupId: driverDetails.groupId, + artifactId: driverDetails.artifactId, + version: driverDetails.version + })); + } catch (error) { + console.error('Failed to load default driver details:', error); + } + }; + + const saveDriverConfig = async () => { + try { + // Validate required fields based on driver type + if (driverConfig.type === 'maven' && + (!driverConfig.groupId || !driverConfig.artifactId || !driverConfig.version)) { + setDriverError("All Maven coordinates are required"); + return; + } + //check if valid coordinates + if (driverConfig.type === 'maven') { + const validDriver = await rpcClient.getMiDiagramRpcClient().downloadDriverForConnector({ + groupId: driverConfig.groupId, + artifactId: driverConfig.artifactId, + version: driverConfig.version + }); + if (!validDriver) { + setDriverError("Invalid Maven coordinates. Please check the values."); + return; + } + if (getValue("driverPath")) { + setValue("driverPath", null); + } + } + + // Save to form values + setValue("groupId", driverConfig.groupId); + setValue("artifactId", driverConfig.artifactId); + setValue("version", driverConfig.version); + if (driverConfig.type === 'custom') { + setValue("driverPath", driverConfig.driverPath); + } + + + setDriverError(null); + } catch (error) { + console.error('Failed to save driver config:', error); + setDriverError("Failed to save driver configuration"); + } + }; + + const handleClearDriver = () => { + setDriverConfig(prev => ({ + ...prev, + groupId: '', + artifactId: '', + version: '', + driverPath: '' + })); + setDriverError(null); + + // Also clear form values + setValue("groupId", null); + setValue("artifactId", null); + setValue("version", null); + setValue("driverPath", null); + }; + + const handleSelectLocation = async () => { + try { + const driveDir = await handleDriverDirSelection(); + if (!driveDir) return; + + // Clear any previous errors + setDriverError(null); + + const driverDetails = await rpcClient.getMiDiagramRpcClient().getDriverMavenCoordinates({ + filePath: driveDir, + connectionType: formData?.connectionName, + connectorName: formData?.connectorName ?? connectorName.replace(/\s/g, '') + }); + + if (driverDetails.found) { + setDriverConfig(prev => ({ + ...prev, + groupId: driverDetails.groupId, + artifactId: driverDetails.artifactId, + version: driverDetails.version, + driverPath: driveDir + })); + } else { + setDriverConfig(prev => ({ + ...prev, + driverPath: '', + groupId: '', + artifactId: '', + version: '' + })); + setDriverError("Unable to fetch maven coordinates for the selected driver. Please provide the maven coordinates manually using \"Add Maven Dependency\" option."); + } + } catch (error) { + console.error('Failed to select driver location:', error); + setDriverError("Failed to select driver location"); + } finally { + setIsProcessing(false); + } + }; + + const renderDriverConfiguration = (selectedValue: string) => { + const option = DRIVER_OPTIONS.find(opt => opt.label === selectedValue); + if (!option) return null; + + switch (option.configType) { + case 'default': + return ( + + ); + case 'custom': + return ( + + ); + case 'maven': + return ( + + ); + default: + return null; + } + }; + function getDefaultValues(elements: any[]) { const defaultValues: Record = {}; elements.forEach(async (element: any) => { @@ -560,6 +886,9 @@ export function FormGenerator(props: FormGeneratorProps) { }; function ParamManagerComponent(element: Element, isRequired: boolean, helpTipElement: JSX.Element, field: any) { + useEffect(() => { + handleValueChange(field.value, element.name.toString(), element); + }, []); // run only on mount return
{element.displayName} @@ -574,7 +903,10 @@ export function FormGenerator(props: FormGeneratorProps) { documentUri={documentUri} formData={element} parameters={field.value} - setParameters={field.onChange} + setParameters={(e: any) => { + field.onChange(e); + handleValueChange(e, element.name.toString(), element); + }} nodeRange={range} /> ; @@ -582,7 +914,10 @@ export function FormGenerator(props: FormGeneratorProps) { const ExpressionFieldComponent = ({ element, canChange, field, helpTipElement, placeholder, isRequired }: { element: Element, canChange: boolean, field: any, helpTipElement: JSX.Element, placeholder: string, isRequired: boolean }) => { const name = getNameForController(element.name); + useEffect(() => { + handleValueChange(field.value, name, element); + }, []); return expressionEditorField !== name ? ( { + field.onChange(e); + handleValueChange(e.value, name, element); + }} /> ) : ( <> @@ -625,7 +964,10 @@ export function FormGenerator(props: FormGeneratorProps) { const FormExpressionFieldComponent = (element: Element, field: any, helpTipElement: JSX.Element, isRequired: boolean, errorMsg: string) => { const name = getNameForController(element.name); - + const customError = customErrors[name]; // Get custom error + useEffect(() => { + handleValueChange(field.value, name, element); + }, []); return expressionEditorField !== name ? ( { + field.onChange(e); + handleValueChange(e.value, name, element); + }} /> ) : ( <> @@ -683,7 +1029,9 @@ export function FormGenerator(props: FormGeneratorProps) { const name = getNameForController(element.name); const isRequired = typeof element.required === 'boolean' ? element.required : element.required === 'true'; const isDisabled = disableFields?.includes(String(element.name)); - const errorMsg = errors[name] && errors[name].message.toString(); + const standardErrorMsg = errors[name] && errors[name].message.toString(); + const customErrorMsg = customErrors[name]; // Get custom error + const errorMsg = customErrorMsg ?? standardErrorMsg; // Prioritize custom error const helpTip = element.helpTip; const helpTipElement = helpTip ? ( @@ -726,6 +1074,7 @@ export function FormGenerator(props: FormGeneratorProps) { errorMsg={errorMsg} onChange={(e: any) => { field.onChange(e.target.value); + handleValueChange(e.target.value, name, element); }} /> {generatedFormDetails && visibleDetails[element.name] && generatedFormDetails[element.name] !== getValues(element.name) && ( @@ -798,6 +1147,7 @@ export function FormGenerator(props: FormGeneratorProps) { } onChange={(checked: boolean) => { field.onChange(checked); + handleValueChange(checked, name, element); }} /> {generatedFormDetails && visibleDetails[element.name] && generatedFormDetails[element.name].toString().toLowerCase() !== getValues(element.name).toString().toLowerCase() && element.name !== "responseVariable" && element.name !== "overwriteBody" && ( @@ -844,10 +1194,13 @@ export function FormGenerator(props: FormGeneratorProps) { case 'booleanOrExpression': case 'comboOrExpression': case 'combo': - const items = element.inputType === 'booleanOrExpression' ? ["true", "false"] : element.comboValues; + const items = element.inputType === 'booleanOrExpression' ? ["true", "false"] : (props.comboValuesMap?.[name] || element.comboValues); const allowItemCreate = element.inputType === 'comboOrExpression'; + useEffect(() => { + handleValueChange(field.value, name, element); + }, [props.comboValuesMap?.[name]]); // run on mount and on props.comboValuesMap return ( -
+ <> { field.onChange(e); + handleValueChange(e, name, element); }} required={isRequired} allowItemCreate={allowItemCreate} /> - {generatedFormDetails && visibleDetails[element.name] && generatedFormDetails[element.name] !== getValues(element.name) && ( - { - if (generatedFormDetails) { - field.onChange(generatedFormDetails[element.name]); - setVisibleDetails((prev) => ({ ...prev, [element.name]: false })); - setNumberOfDifferent(numberOfDifferent - 1); - } - }} - handleOnClickClose={() => { - setIsClickedDropDown(false); - setIsGenerating(false); - setVisibleDetails((prev) => ({ ...prev, [element.name]: false })); - setNumberOfDifferent(numberOfDifferent - 1); - }} - /> - )} -
+ + {dynamicFields[name]?.fields?.length > 0 && ( + <> + {dynamicFields[name].header} + {/* String/Expression fields */} + {dynamicFields[name].fields.some((el: any) => el.value.inputType === "stringOrExpression") && ( + <> + {dynamicFields[name].fields + .filter((el: any) => el.value.inputType === "stringOrExpression") + .map((dynamicElement: any) => ( +
+ {renderController(dynamicElement)} +
+ ))} + + )} + + {/* Checkbox fields */} + {dynamicFields[name].fields.some((el: any) => el.value.inputType === "checkbox") && ( + <> + {dynamicFields[name].fields + .filter((el: any) => el.value.inputType === "checkbox") + .map((dynamicElement: any) => ( +
+ {renderController(dynamicElement)} +
+ ))} + + )} + + )} + ); case 'key': case 'keyOrExpression': @@ -1085,6 +1451,9 @@ export function FormGenerator(props: FormGeneratorProps) { ); } case 'connection': + useEffect(() => { + handleValueChange(field.value, name, element); + }, []); if (isDisabled && getValues("configRef")) { field.value = getValues("configRef"); } @@ -1132,11 +1501,12 @@ export function FormGenerator(props: FormGeneratorProps) {
{ field.onChange(e); + handleValueChange(e, name, element); }} disabled={isDisabled} required={element.required === 'true'} @@ -1180,6 +1550,7 @@ export function FormGenerator(props: FormGeneratorProps) { errorMsg={errorMsg} onChange={(e: any) => { field.onChange(e.target.value); + handleValueChange(e, name, element); }} /> ); @@ -1229,6 +1600,49 @@ export function FormGenerator(props: FormGeneratorProps) { ); + case 'radio': + if (element.name === 'driverSelectOption') { + return ( + <> + ({ + value: val, + label: val + }))} + value={field.value} + helpTip={element.helpTip} + onChange={(value) => { + field.onChange(value); + handleDriverTypeChange(value, element); + }} + required={true} + /> + + {renderDriverConfiguration(field.value)} + + ); + } + // For generic radio inputs + return ( + ({ + value: val, + label: val + }))} + value={field.value} + onChange={(value) => { + field.onChange(value); + if (element.onValueChange) { + handleValueChange(value, name, element); + } + }} + required={isRequired} + /> + ); case 'idpSchemaGenerateView': const onCreateSchemaButtonClick = async (name?: string) => { const fetchItems = async () => { diff --git a/workspaces/mi/mi-diagram/src/components/Form/RadioButtonGroup/GenericRadioGroup.tsx b/workspaces/mi/mi-diagram/src/components/Form/RadioButtonGroup/GenericRadioGroup.tsx new file mode 100644 index 00000000000..81f66c866f4 --- /dev/null +++ b/workspaces/mi/mi-diagram/src/components/Form/RadioButtonGroup/GenericRadioGroup.tsx @@ -0,0 +1,59 @@ +// GenericRadioGroup.tsx +import { Icon, RadioButtonGroup, Tooltip } from '@wso2/ui-toolkit'; +import React from 'react'; + +export interface RadioOption { + value: string; + label: string; + description?: string; +} + +export interface GenericRadioGroupProps { + name: string; + label: string; + options: RadioOption[]; + value: string; + helpTip?: string; + onChange: (value: string) => void; + required?: boolean; + orientation?: 'vertical' | 'horizontal'; +} + +// The helpTipElement prop is already passed to the component. +export const GenericRadioGroup: React.FC = ({ + name, + label, + options, + value, + helpTip, + onChange, + required, + orientation = 'vertical', + +}) => { + return ( + <> +
+ + + {helpTip && + + } +
+ ({ + content: option.label, + value: option.value + }))} + value={value} + onChange={(e: any) => onChange(e.target.value)} + required={required} + /> + + ); +}; \ No newline at end of file diff --git a/workspaces/mi/mi-diagram/src/components/sidePanel/connectors/AddConnector.tsx b/workspaces/mi/mi-diagram/src/components/sidePanel/connectors/AddConnector.tsx index 8e79f851275..a28af2e1ee4 100644 --- a/workspaces/mi/mi-diagram/src/components/sidePanel/connectors/AddConnector.tsx +++ b/workspaces/mi/mi-diagram/src/components/sidePanel/connectors/AddConnector.tsx @@ -67,6 +67,17 @@ const AddConnector = (props: AddConnectorProps) => { const [connections, setConnections] = useState([] as any); const handleOnCancelExprEditorRef = useRef(() => { }); const [parameters, setParameters] = useState(props.parameters); + const [comboValuesMap, setComboValuesMap] = useState>({}); + const setComboValues = (elementName: string, newValues: string[]) => { + setComboValuesMap(prev => ({ + ...prev, + [elementName]: newValues + })); + }; + const [params, setParams] = useState({ + paramValues: [], + paramFields: [] + }); const fetchConnectionsForTemplateConnection = async () => { if (!props.formData) { @@ -74,7 +85,6 @@ const AddConnector = (props: AddConnectorProps) => { documentUri: props.documentUri, connectorName: props.formData?.connectorName ?? props.connectorName.replace(/\s/g, '') }); - // Fetch connections for old connectors (No ConnectionType) const connectionsNames = connectionsData.connections.map(connection => connection.name); setConnections(connectionsNames); @@ -109,30 +119,40 @@ const AddConnector = (props: AddConnectorProps) => { }, [props.formData]); useEffect(() => { - if (!sidePanelContext.formValues && Object.keys(sidePanelContext.formValues).length > 0 && sidePanelContext.formValues?.parameters) { - if (!sidePanelContext.formValues.form) { - //Handle connectors without uischema - fetchParameters(sidePanelContext.formValues.operationName); - - sidePanelContext.formValues?.parameters.forEach((param: any) => { - param.name = getNameForController(param.name); - if (param.isExpression) { - let namespacesArray: any[] = []; - if (param.namespaces) { - namespacesArray = Object.entries(param.namespaces).map(([prefix, uri]) => ({ prefix: prefix.split(':')[1], uri: uri })); + try { + if (sidePanelContext.formValues && Object.keys(sidePanelContext.formValues).length > 0 && sidePanelContext.formValues?.parameters) { + if (sidePanelContext.formValues.form) { + sidePanelContext.formValues?.parameters.forEach((param: any) => { + param.name = getNameForController(param.name); + if (param.isExpression) { + let namespacesArray: any[] = []; + if (param.namespaces) { + namespacesArray = Object.entries(param.namespaces).map(([prefix, uri]) => ({ prefix: prefix.split(':')[1], uri: uri })); + } + setValue(param.name, { isExpression: true, value: param.value.replace(/[{}]/g, ''), namespaces: namespacesArray }); + } else { + param.namespaces = []; + setValue(param.name, param); } - setValue(param.name, { isExpression: true, value: param.value.replace(/[{}]/g, ''), namespaces: namespacesArray }); - } else { - param.namespaces = []; - setValue(param.name, param); - } - }); - } + }); + } else { + //Handle connectors without uischema + fetchParameters(sidePanelContext.formValues.operationName); + } + const modifiedParams = { + ...params, paramValues: generateParams(sidePanelContext.formValues.parameters) + }; + setParams(modifiedParams); - if (sidePanelContext.formValues?.connectionName) { - setValue('configKey', sidePanelContext.formValues?.connectionName); + if (sidePanelContext.formValues?.connectionName) { + setValue('configKey', sidePanelContext.formValues?.connectionName); + } } + + } catch { + console.error("Error setting form values from sidePanelContext"); } + }, [sidePanelContext.formValues]); const findAllowedConnectionTypes = (elements: any): string[] | undefined => { @@ -202,10 +222,13 @@ const AddConnector = (props: AddConnectorProps) => { if (props.connectionName) { values.configKey = props.connectionName; } - + let valuesRequired = values; + if (connectorName === "db") { + valuesRequired = valuesForSynapseConfig(values); + } rpcClient.getMiDiagramRpcClient().updateMediator({ mediatorType: `${connectorName}.${operationName}`, - values: values as Record, + values: valuesRequired as Record, oldValues: sidePanelContext.formValues as Record, dirtyFields: Object.keys(dirtyFields), documentUri, @@ -227,6 +250,35 @@ const AddConnector = (props: AddConnectorProps) => { }; + function valuesForSynapseConfig(values: any) { + const filteredValues: any = {}; + Object.keys(values).forEach(key => { + if (!key.startsWith('dyn_param') && key !== 'preparedStmt') { // Exclude fields starting with 'dyn_param' + filteredValues[key] = values[key]; + } + }); + return filteredValues; + } + + function generateParams(parameters: any[]) { + return parameters.map((param: any, id) => { + return { + id: id, + key: param.name, + value: param.value ?? param.expression, + icon: "query", + paramValues: [ + { + value: param.name, + }, + { + value: param.value ?? param.expression, + }, + ] + } + }); + } + if (isLoading) { return ; } @@ -324,6 +376,12 @@ const AddConnector = (props: AddConnectorProps) => { { return this.sendRequest('synapse/getDependencyStatusList'); - } + } + + async loadDriverAndTestConnection(req: LoadDriverAndTestConnectionRequest): Promise { + return this.sendRequest("synapse/loadDriverAndTestConnection", req); + } + + async getDynamicFields(req: GetDynamicFieldsRequest): Promise { + return this.sendRequest("synapse/getDynamicFields", req); + } + + async getStoredProcedures(req: DSSQueryGenRequest): Promise { + return this.sendRequest("synapse/getStoredProcedures", req); + } + + async downloadDriverForConnector(params: DriverDownloadRequest): Promise { + return this.sendRequest("synapse/downloadDriverForConnector", params); + } + + async getDriverMavenCoordinates(params: DriverMavenCoordinatesRequest): Promise { + return this.sendRequest("synapse/getDriverMavenCoordinates", params); + } } diff --git a/workspaces/mi/mi-extension/src/rpc-managers/mi-diagram/rpc-handler.ts b/workspaces/mi/mi-extension/src/rpc-managers/mi-diagram/rpc-handler.ts index 3732e1f468f..9c41a962da5 100644 --- a/workspaces/mi/mi-extension/src/rpc-managers/mi-diagram/rpc-handler.ts +++ b/workspaces/mi/mi-extension/src/rpc-managers/mi-diagram/rpc-handler.ts @@ -318,7 +318,19 @@ import { UpdateRegistryPropertyRequest, updatePropertiesInArtifactXML, getPropertiesFromArtifactXML, - formatPomFile + formatPomFile, + getDynamicFields, + GetDynamicFieldsRequest, + getStoredProcedures, + GetStoredProceduresResponse, + DriverDownloadRequest, + DriverDownloadResponse, + DriverMavenCoordinatesRequest, + DriverMavenCoordinatesResponse, + downloadDriverForConnector, + getDriverMavenCoordinates, + loadDriverAndTestConnection, + LoadDriverAndTestConnectionRequest // getBackendRootUrl - REMOVED: Backend URLs deprecated, all AI features use local LLM } from "@wso2/mi-core"; import { Messenger } from "vscode-messenger"; @@ -506,4 +518,9 @@ export function registerMiDiagramRpcHandlers(messenger: Messenger, projectUri: s messenger.onRequest(isKubernetesConfigured, () => rpcManger.isKubernetesConfigured()); messenger.onRequest(updatePropertiesInArtifactXML, (args: UpdateRegistryPropertyRequest) => rpcManger.updatePropertiesInArtifactXML(args)); messenger.onRequest(getPropertiesFromArtifactXML, (args: string) => rpcManger.getPropertiesFromArtifactXML(args)); + messenger.onRequest(loadDriverAndTestConnection, (args: LoadDriverAndTestConnectionRequest) => rpcManger.loadDriverAndTestConnection(args)); + messenger.onRequest(getDynamicFields, (args: GetDynamicFieldsRequest) => rpcManger.getDynamicFields(args)); + messenger.onRequest(getStoredProcedures, (args: DSSFetchTablesRequest) => rpcManger.getStoredProcedures(args)); + messenger.onRequest(downloadDriverForConnector, (args: DriverDownloadRequest) => rpcManger.downloadDriverForConnector(args)); + messenger.onRequest(getDriverMavenCoordinates, (args: DriverMavenCoordinatesRequest) => rpcManger.getDriverMavenCoordinates(args)); } diff --git a/workspaces/mi/mi-extension/src/rpc-managers/mi-diagram/rpc-manager.ts b/workspaces/mi/mi-extension/src/rpc-managers/mi-diagram/rpc-manager.ts index b044e61ffa5..09c34b72268 100644 --- a/workspaces/mi/mi-extension/src/rpc-managers/mi-diagram/rpc-manager.ts +++ b/workspaces/mi/mi-extension/src/rpc-managers/mi-diagram/rpc-manager.ts @@ -288,7 +288,15 @@ import { GetMockServicesResponse, ConfigureKubernetesRequest, ConfigureKubernetesResponse, - UpdateRegistryPropertyRequest + UpdateRegistryPropertyRequest, + LoadDriverAndTestConnectionRequest, + GetDynamicFieldsRequest, + GetDynamicFieldsResponse, + GetStoredProceduresResponse, + DriverDownloadRequest, + DriverDownloadResponse, + DriverMavenCoordinatesRequest, + DriverMavenCoordinatesResponse } from "@wso2/mi-core"; import axios from 'axios'; import { error } from "console"; @@ -6074,6 +6082,68 @@ ${keyValuesXML}`; return updatedXml; } + async getDynamicFields(params: GetDynamicFieldsRequest): Promise { + return new Promise(async (resolve) => { + try { + const langClient = getStateMachine(this.projectUri).context().langClient!; + const response = await langClient.getDynamicFields({ + connectorName: params.connectorName, + operationName: params.operationName, + fieldName: params.fieldName, + selectedValue: params.selectedValue, + connection: params.connection + }); + + if (!response || !response.columns || !response.columns.length) { + resolve({ columns: [] }); + return; + } + + resolve(response); + } catch (error) { + console.error(`Error getting dynamic fields: ${error}`); + resolve({ columns: [] }); + } + }); + } + + async getStoredProcedures(params: DSSFetchTablesRequest): Promise { + return new Promise(async (resolve) => { + const langClient = getStateMachine(this.projectUri).context().langClient!; + const res = await langClient.getStoredProcedures({ + ...params, tableData: "", datasourceName: "" + }); + resolve(res); + }); + } + + async downloadDriverForConnector(params: DriverDownloadRequest): Promise { + return new Promise(async (resolve) => { + const langClient = getStateMachine(this.projectUri).context().langClient!; + const res = await langClient.downloadDriverForConnector(params); + resolve(res); + }); + } + + async loadDriverAndTestConnection(req: LoadDriverAndTestConnectionRequest): Promise { + + return new Promise(async (resolve) => { + const langClient = getStateMachine(this.projectUri).context().langClient; + const response = await langClient?.loadDriverAndTestConnection(req); + resolve({ success: response ? response.success : false }); + }); + } + + async getDriverMavenCoordinates(params: DriverMavenCoordinatesRequest): Promise { + return new Promise(async (resolve) => { + + const langClient = getStateMachine(this.projectUri).context().langClient!; + const res = await langClient.getDriverMavenCoordinates(params); + resolve(res); + + }); + } + async getPropertiesFromArtifactXML(targetFile: string): Promise { if (!targetFile) { await window.showInformationMessage( diff --git a/workspaces/mi/mi-rpc-client/src/rpc-clients/mi-diagram/rpc-client.ts b/workspaces/mi/mi-rpc-client/src/rpc-clients/mi-diagram/rpc-client.ts index afb18ab1012..996c9e77b94 100644 --- a/workspaces/mi/mi-rpc-client/src/rpc-clients/mi-diagram/rpc-client.ts +++ b/workspaces/mi/mi-rpc-client/src/rpc-clients/mi-diagram/rpc-client.ts @@ -449,7 +449,20 @@ import { Property, updatePropertiesInArtifactXML, getPropertiesFromArtifactXML, - formatPomFile + formatPomFile, + GetDynamicFieldsRequest, + GetDynamicFieldsResponse, + getDynamicFields, + GetStoredProceduresResponse, + getStoredProcedures, + DriverDownloadRequest, + DriverDownloadResponse, + DriverMavenCoordinatesRequest, + DriverMavenCoordinatesResponse, + downloadDriverForConnector, + getDriverMavenCoordinates, + LoadDriverAndTestConnectionRequest, + loadDriverAndTestConnection } from "@wso2/mi-core"; import { HOST_EXTENSION } from "vscode-messenger-common"; import { Messenger } from "vscode-messenger-webview"; @@ -1184,4 +1197,23 @@ export class MiDiagramRpcClient implements MiDiagramAPI { getPropertiesFromArtifactXML(params: string): Promise { return this._messenger.sendRequest(getPropertiesFromArtifactXML, HOST_EXTENSION, params); } + + getDynamicFields(params: GetDynamicFieldsRequest): Promise { + return this._messenger.sendRequest(getDynamicFields, HOST_EXTENSION, params); + } + + getStoredProcedures(params: DSSFetchTablesRequest): Promise { + return this._messenger.sendRequest(getStoredProcedures, HOST_EXTENSION, params); + } + + downloadDriverForConnector(params: DriverDownloadRequest): Promise { + return this._messenger.sendRequest(downloadDriverForConnector, HOST_EXTENSION, params); + } + getDriverMavenCoordinates(params: DriverMavenCoordinatesRequest): Promise { + return this._messenger.sendRequest(getDriverMavenCoordinates, HOST_EXTENSION, params); + } + + loadDriverAndTestConnection(params: LoadDriverAndTestConnectionRequest): Promise { + return this._messenger.sendRequest(loadDriverAndTestConnection, HOST_EXTENSION, params); + } } diff --git a/workspaces/mi/mi-visualizer/src/views/Forms/ConnectionForm/ConnectionFormGenerator.tsx b/workspaces/mi/mi-visualizer/src/views/Forms/ConnectionForm/ConnectionFormGenerator.tsx index 6add298a1cb..2da710375ca 100644 --- a/workspaces/mi/mi-visualizer/src/views/Forms/ConnectionForm/ConnectionFormGenerator.tsx +++ b/workspaces/mi/mi-visualizer/src/views/Forms/ConnectionForm/ConnectionFormGenerator.tsx @@ -132,12 +132,18 @@ export function AddConnection(props: AddConnectionProps) { setConnectionType(connectionFound.connectionType); setConnectionName(props.connectionName); setFormData(connectionSchema); + const parameters = connectionFound.parameters + const driverParams = parameters.filter((param: { name: string; }) => param.name === 'groupId' || param.name === 'artifactId' || param.name === 'version' || param.name === 'driverPath'); + // populate parameters that does not exist in uischema + const generatedParams = { + ...params, paramValues: generateParams(driverParams) + }; + setParams(generatedParams); reset({ name: props.connectionName, connectionType: connectionType }); - const parameters = connectionFound.parameters // Populate form with existing values if (connectionSchema === undefined) { @@ -205,7 +211,7 @@ export function AddConnection(props: AddConnectionProps) { // Fill the values Object.keys(values).forEach((key: string) => { - if ((key !== 'configRef' && key !== 'connectionType' && key !== 'connectionName') && values[key]) { + if ((key !== 'configRef' && key !== 'connectionType' && key !== 'connectionName') && values[key] != null) { if (typeof values[key] === 'object' && values[key] !== null) { if (Array.isArray(values[key])) { // Handle param manager input type @@ -438,6 +444,7 @@ export function AddConnection(props: AddConnectionProps) { <> {