From 5c44af3730ea717380e4d1a7b13c1f2624d684e7 Mon Sep 17 00:00:00 2001 From: chulanovskyi Date: Fri, 14 Aug 2026 16:45:18 +0300 Subject: [PATCH 1/2] feat: alter script --- forward_engineering/api.js | 13 +- forward_engineering/config.json | 14 +- .../generateContainerScript.js | 16 +- forward_engineering/generateScript.js | 9 +- .../services/alterScript/AlterScriptDto.js | 44 ++ .../alterScript/collectionAlterHelper.js | 156 +++++++ .../services/alterScript/commentHelper.js | 22 + .../services/alterScript/deltaSchemaHelper.js | 51 ++ .../services/alterScript/indexAlterHelper.js | 441 ++++++++++++++++++ .../services/alterScript/scopeAlterHelper.js | 177 +++++++ .../services/alterScriptBuilder.js | 119 +++++ .../services/applyToInstanceService.js | 16 +- .../statements/collectionsStatements.js | 24 + .../services/statements/indexesStatements.js | 107 ++++- .../services/statements/insertStatements.js | 2 +- .../services/statements/scopesStatements.js | 23 + forward_engineering/utils/indexes.js | 7 +- localization/en.json | 1 + 18 files changed, 1220 insertions(+), 22 deletions(-) create mode 100644 forward_engineering/services/alterScript/AlterScriptDto.js create mode 100644 forward_engineering/services/alterScript/collectionAlterHelper.js create mode 100644 forward_engineering/services/alterScript/commentHelper.js create mode 100644 forward_engineering/services/alterScript/deltaSchemaHelper.js create mode 100644 forward_engineering/services/alterScript/indexAlterHelper.js create mode 100644 forward_engineering/services/alterScript/scopeAlterHelper.js create mode 100644 forward_engineering/services/alterScriptBuilder.js diff --git a/forward_engineering/api.js b/forward_engineering/api.js index a943d6e..189e325 100644 --- a/forward_engineering/api.js +++ b/forward_engineering/api.js @@ -27,9 +27,19 @@ const { } = require('../shared/enums/dynamicMessages'); const { HTTP_ERROR_CODES } = require('../shared/enums/httpCodes'); const { applyScript, logApplyScriptAttempt } = require('./services/applyToInstanceService'); +const { hasDropStatements } = require('./services/alterScriptBuilder'); const { generateContainerScript } = require('./generateContainerScript'); const { generateScript } = require('./generateScript'); +/** + * @param {ConnectionInfo} connectionInfo + * @param {AppLogger} _logger + * @param {Callback} callback + */ +const isDropInStatements = (connectionInfo, _logger, callback) => { + callback(null, hasDropStatements({ connectionInfo })); +}; + /** * @param {ConnectionInfo} connectionInfo * @param {AppLogger} appLogger @@ -106,7 +116,7 @@ const applyToInstance = async (connectionInfo, appLogger, callback, app) => { /** * @param {ConnectionInfo} connectionInfo - * @param {AppLogger} logger + * @param {AppLogger} appLogger * @param {Callback} callback * @param {App} app */ @@ -131,6 +141,7 @@ const testConnection = async (connectionInfo, appLogger, callback, app) => { module.exports = { generateContainerScript, generateScript, + isDropInStatements, applyToInstance, testConnection, }; diff --git a/forward_engineering/config.json b/forward_engineering/config.json index d6dd493..fa8a3aa 100644 --- a/forward_engineering/config.json +++ b/forward_engineering/config.json @@ -15,12 +15,24 @@ "container": true, "entity": true }, + "compMode": { + "container": true, + "entity": true + }, "additionalOptions": [ { "id": "INCLUDE_SAMPLES", "value": true, "name": "Include sample data", - "align": "right" + "align": "right", + "hideInDeltaModel": true + }, + { + "id": "applyDropStatements", + "value": false, + "forUpdate": true, + "name": "Apply Drop Statements", + "isDropInStatements": true } ], "numberOfDocumentsOptions": { diff --git a/forward_engineering/generateContainerScript.js b/forward_engineering/generateContainerScript.js index 955c022..459419d 100644 --- a/forward_engineering/generateContainerScript.js +++ b/forward_engineering/generateContainerScript.js @@ -1,6 +1,7 @@ const { get } = require('lodash'); const logHelper = require('../shared/helpers/logHelper'); const ForwardEngineeringScriptBuilder = require('./services/forwardEngineeringScriptBuilder'); +const { buildAlterScript } = require('./services/alterScriptBuilder'); const { GENERATING_CONTAINER_SCRIPT } = require('../shared/enums/staticMessages'); const { includeSamples } = require('./utils/includeSamples'); @@ -8,9 +9,9 @@ const { includeSamples } = require('./utils/includeSamples'); * @param {ConnectionInfo} connectionInfo * @param {AppLogger} appLogger * @param {Callback} callback - * @param {App} app + * @param {App} _app */ -const generateContainerScript = async (connectionInfo, appLogger, callback, app) => { +const generateContainerScript = async (connectionInfo, appLogger, callback, _app) => { const logger = logHelper.createLogger({ title: GENERATING_CONTAINER_SCRIPT, hiddenKeys: connectionInfo.hiddenKeys, @@ -18,6 +19,10 @@ const generateContainerScript = async (connectionInfo, appLogger, callback, app) }); try { + if (connectionInfo.isUpdateScript) { + return callback(null, buildAlterScript({ connectionInfo })); + } + const scriptBuilder = new ForwardEngineeringScriptBuilder(); const { jsonData, collections, options } = connectionInfo; @@ -27,12 +32,15 @@ const generateContainerScript = async (connectionInfo, appLogger, callback, app) ...rawScope, bucketName: rawScope?.bucket ?? '', }; - const collectionsData = collections.map(schema => ({ + + const getCollectionData = ({ schema, scope }) => ({ ...JSON.parse(schema), namespace: scope?.namespace, bucketName: scope?.bucketName, scopeName: scope?.name, - })); + }); + + const collectionsData = collections.map(schema => getCollectionData({ schema, scope })); scriptBuilder.addScopeScript(scope); collectionsData.forEach(collection => scriptBuilder.addCollectionScripts(collection)); diff --git a/forward_engineering/generateScript.js b/forward_engineering/generateScript.js index ecac692..00ace33 100644 --- a/forward_engineering/generateScript.js +++ b/forward_engineering/generateScript.js @@ -2,15 +2,16 @@ const { get } = require('lodash'); const logHelper = require('../shared/helpers/logHelper'); const { GENERATING_ENTITY_SCRIPT } = require('../shared/enums/staticMessages'); const ForwardEngineeringScriptBuilder = require('./services/forwardEngineeringScriptBuilder'); +const { buildAlterScript } = require('./services/alterScriptBuilder'); const { includeSamples } = require('./utils/includeSamples'); /** * @param {ConnectionInfo} connectionInfo * @param {AppLogger} appLogger * @param {Callback} callback - * @param {App} app + * @param {App} _app */ -const generateScript = async (connectionInfo, appLogger, callback, app) => { +const generateScript = async (connectionInfo, appLogger, callback, _app) => { const logger = logHelper.createLogger({ title: GENERATING_ENTITY_SCRIPT, hiddenKeys: connectionInfo.hiddenKeys, @@ -18,6 +19,10 @@ const generateScript = async (connectionInfo, appLogger, callback, app) => { }); try { + if (connectionInfo.isUpdateScript) { + return callback(null, buildAlterScript({ connectionInfo })); + } + const scriptBuilder = new ForwardEngineeringScriptBuilder(); const { jsonData, jsonSchema, containerData, options } = connectionInfo; diff --git a/forward_engineering/services/alterScript/AlterScriptDto.js b/forward_engineering/services/alterScript/AlterScriptDto.js new file mode 100644 index 0000000..4cb19d2 --- /dev/null +++ b/forward_engineering/services/alterScript/AlterScriptDto.js @@ -0,0 +1,44 @@ +/** + * @param {{ + * script: string, + * isDropScript?: boolean, + * isActivated?: boolean, + * modelLevel?: 'collection' | 'index' | 'scope', + * scriptPurpose?: 'deletion' | 'add' | 'modify', + * }} params + * @returns {{ + * script: string, + * isDropScript: boolean, + * isActivated: boolean, + * modelLevel: 'collection' | 'index' | 'scope', + * scriptPurpose: 'deletion' | 'add' | 'modify', + * } | undefined} + */ +const getAlterScriptDto = ({ + script, + isDropScript = false, + isActivated = true, + modelLevel = 'collection', + scriptPurpose = 'add', +} = {}) => { + const cleanScript = script?.trim(); + if (!cleanScript) { + return; + } + + return { + script: cleanScript, + isDropScript, + isActivated, + modelLevel, + scriptPurpose, + }; +}; + +const AlterScriptDto = { + getInstance: getAlterScriptDto, +}; + +module.exports = { + AlterScriptDto, +}; diff --git a/forward_engineering/services/alterScript/collectionAlterHelper.js b/forward_engineering/services/alterScript/collectionAlterHelper.js new file mode 100644 index 0000000..1d53698 --- /dev/null +++ b/forward_engineering/services/alterScript/collectionAlterHelper.js @@ -0,0 +1,156 @@ +const { getCollectionScript, getDropCollectionScript } = require('../statements/collectionsStatements'); +const { AlterScriptDto } = require('./AlterScriptDto'); +const { getDeltaItems } = require('./deltaSchemaHelper'); + +/** + * @param {{ entity: object }} params + * @returns {object} + */ +const getCompMod = ({ entity = {} } = {}) => entity.role?.compMod || entity.compMod || {}; + +/** + * @param {{ entity: object }} params + * @returns {object} + */ +const getEntityRole = ({ entity = {} } = {}) => entity.role || entity; + +/** + * @param {{ entity: object, nameType?: 'old' | 'new' | 'current' }} params + * @returns {string} + */ +const getCollectionName = ({ entity = {}, nameType = 'current' } = {}) => { + const role = getEntityRole({ entity }); + const compMod = getCompMod({ entity }); + + if (nameType === 'old') { + return compMod.code?.old || compMod.collectionName?.old || role.code || role.collectionName || ''; + } + + if (nameType === 'new') { + return compMod.code?.new || compMod.collectionName?.new || role.code || role.collectionName || ''; + } + + return role.code || role.collectionName || entity.title || role.name || ''; +}; + +/** + * @param {{ entity: object }} params + * @returns {{ namespace: string, bucketName: string, scopeName: string, ifNotExists: boolean }} + */ +const getCollectionContext = ({ entity = {} } = {}) => { + const role = getEntityRole({ entity }); + const compMod = getCompMod({ entity }); + const bucketProperties = compMod.bucketProperties || {}; + + return { + namespace: bucketProperties.namespace, + bucketName: bucketProperties.bucket, + scopeName: compMod.keyspaceName || bucketProperties.code || bucketProperties.name, + ifNotExists: Boolean(role.ifNotExists), + }; +}; + +/** + * @param {{ entity: object, collectionName: string }} params + * @returns {AlterScriptDto | undefined} + */ +const getCreateCollectionDto = ({ entity, collectionName } = {}) => { + const context = getCollectionContext({ entity }); + const script = getCollectionScript({ + ...context, + collectionName, + }); + + return AlterScriptDto.getInstance({ script, isDropScript: false, modelLevel: 'collection', scriptPurpose: 'add' }); +}; + +/** + * @param {{ entity: object, collectionName: string }} params + * @returns {AlterScriptDto | undefined} + */ +const getDropCollectionDto = ({ entity, collectionName } = {}) => { + const context = getCollectionContext({ entity }); + const script = getDropCollectionScript({ + ...context, + collectionName, + ifExists: true, + }); + + return AlterScriptDto.getInstance({ + script, + isDropScript: true, + modelLevel: 'collection', + scriptPurpose: 'deletion', + }); +}; + +/** + * @param {{ entity: object }} params + * @returns {boolean} + */ +const isCollectionRenamed = ({ entity } = {}) => { + const compMod = getCompMod({ entity }); + const oldName = compMod.code?.old || compMod.collectionName?.old; + const newName = compMod.code?.new || compMod.collectionName?.new; + + return Boolean(oldName && newName && oldName !== newName); +}; + +/** + * @param {{ entity: object }} params + * @returns {AlterScriptDto[]} + */ +const getAddedCollectionDtos = ({ entity } = {}) => { + const collectionName = getCollectionName({ entity, nameType: 'new' }) || getCollectionName({ entity }); + return [getCreateCollectionDto({ entity, collectionName })].filter(Boolean); +}; + +/** + * @param {{ entity: object }} params + * @returns {AlterScriptDto[]} + */ +const getDeletedCollectionDtos = ({ entity } = {}) => { + const collectionName = getCollectionName({ entity, nameType: 'old' }) || getCollectionName({ entity }); + return [getDropCollectionDto({ entity, collectionName })].filter(Boolean); +}; + +/** + * @param {{ entity: object }} params + * @returns {AlterScriptDto[]} + */ +const getModifiedCollectionDtos = ({ entity } = {}) => { + if (!isCollectionRenamed({ entity })) { + return []; + } + + const oldName = getCollectionName({ entity, nameType: 'old' }); + const newName = getCollectionName({ entity, nameType: 'new' }); + + return [ + getDropCollectionDto({ entity, collectionName: oldName }), + getCreateCollectionDto({ entity, collectionName: newName }), + ].filter(Boolean); +}; + +/** + * @param {{ schema: object }} params + * @returns {AlterScriptDto[]} + */ +const getCollectionAlterScriptDtos = ({ schema } = {}) => { + const addedEntities = getDeltaItems({ schema, nameProperty: 'entities', modify: 'added' }); + const modifiedEntities = getDeltaItems({ schema, nameProperty: 'entities', modify: 'modified' }); + const deletedEntities = getDeltaItems({ schema, nameProperty: 'entities', modify: 'deleted' }); + + const addedDtos = addedEntities.flatMap(entity => getAddedCollectionDtos({ entity })); + const modifiedDtos = modifiedEntities.flatMap(entity => getModifiedCollectionDtos({ entity })); + const deletedDtos = deletedEntities.flatMap(entity => getDeletedCollectionDtos({ entity })); + + return [...deletedDtos, ...modifiedDtos, ...addedDtos].filter(Boolean); +}; + +module.exports = { + getCollectionAlterScriptDtos, + getCollectionContext, + getCollectionName, + getCompMod, +}; diff --git a/forward_engineering/services/alterScript/commentHelper.js b/forward_engineering/services/alterScript/commentHelper.js new file mode 100644 index 0000000..5b36416 --- /dev/null +++ b/forward_engineering/services/alterScript/commentHelper.js @@ -0,0 +1,22 @@ +const { joinStatements } = require('../statements/commonStatements'); + +/** + * @param {{ statement: string }} params + * @returns {string} + */ +const commentStatement = ({ statement } = {}) => { + if (!statement) { + return ''; + } + + const joinedStatement = joinStatements({ + statements: statement.split('\n').map(line => ` * ${line}`), + separator: '\n', + }); + + return `/*\n${joinedStatement}\n */`; +}; + +module.exports = { + commentStatement, +}; diff --git a/forward_engineering/services/alterScript/deltaSchemaHelper.js b/forward_engineering/services/alterScript/deltaSchemaHelper.js new file mode 100644 index 0000000..7a9e0e5 --- /dev/null +++ b/forward_engineering/services/alterScript/deltaSchemaHelper.js @@ -0,0 +1,51 @@ +/** + * @param {{ connectionInfo: object }} params + * @returns {object} + */ +const getDeltaSchema = ({ connectionInfo = {} } = {}) => { + const { jsonSchema, collections } = connectionInfo; + + if (typeof jsonSchema === 'string') { + return JSON.parse(jsonSchema); + } + + if (Array.isArray(collections) && collections.length > 0) { + const firstCollection = collections[0]; + return typeof firstCollection === 'string' ? JSON.parse(firstCollection) : firstCollection; + } + + if (jsonSchema && typeof jsonSchema === 'object') { + const firstCollection = Object.values(jsonSchema)[0]; + if (!firstCollection) { + return {}; + } + + return typeof firstCollection === 'string' ? JSON.parse(firstCollection) : firstCollection; + } + + return {}; +}; + +/** + * @param {{ schema: object, nameProperty: string, modify: 'added' | 'modified' | 'deleted' }} params + * @returns {object[]} + */ +const getDeltaItems = ({ schema = {}, nameProperty, modify } = {}) => + [schema.properties?.[nameProperty]?.properties?.[modify]?.items] + .flat() + .filter(Boolean) + .map(item => Object.values(item.properties || {})[0]) + .filter(Boolean); + +/** + * @param {{ connectionInfo: object }} params + * @returns {boolean} + */ +const shouldApplyDropStatements = ({ connectionInfo = {} } = {}) => + Boolean(connectionInfo.options?.additionalOptions?.find(option => option.id === 'applyDropStatements')?.value); + +module.exports = { + getDeltaSchema, + getDeltaItems, + shouldApplyDropStatements, +}; diff --git a/forward_engineering/services/alterScript/indexAlterHelper.js b/forward_engineering/services/alterScript/indexAlterHelper.js new file mode 100644 index 0000000..ca8eac9 --- /dev/null +++ b/forward_engineering/services/alterScript/indexAlterHelper.js @@ -0,0 +1,441 @@ +const { isEqual, omit, isEmpty } = require('lodash'); +const { getIndexKeyIdToKeyNameMap, injectKeysNamesIntoIndexKeys } = require('../../utils/indexes'); +const { getIndexScript, getDropIndexScript, getAlterIndexScript } = require('../statements/indexesStatements'); +const { commentStatement } = require('./commentHelper'); +const { AlterScriptDto } = require('./AlterScriptDto'); +const { getDeltaItems } = require('./deltaSchemaHelper'); +const { getCollectionContext, getCollectionName, getCompMod } = require('./collectionAlterHelper'); + +const NON_STRUCTURAL_INDEX_FIELDS = ['indxComments', 'indxDescription', 'id', 'GUID']; + +/** + * @param {{ entity: object }} params + * @returns {object} + */ +const getEntityRole = ({ entity = {} } = {}) => entity.role || entity; + +/** + * @param {{ entity: object }} params + * @returns {object} + */ +const getEntityProperties = ({ entity = {} } = {}) => { + const role = getEntityRole({ entity }); + return { + ...entity.properties, + ...role.properties, + }; +}; + +/** + * @param {{ entity: object, nameType?: 'old' | 'new' }} params + * @returns {object} + */ +const getKeyIdToNameMap = ({ entity, nameType = 'new' } = {}) => { + const compMod = getCompMod({ entity }); + const hashTable = nameType === 'old' ? compMod.oldIdToNameHashTable : compMod.newIdToNameHashTable; + + if (!isEmpty(hashTable)) { + return hashTable; + } + + return getIndexKeyIdToKeyNameMap(getEntityProperties({ entity })); +}; + +/** + * @param {{ index: object, keyIdToName: object }} params + * @returns {object} + */ +const prepareIndex = ({ index = {}, keyIdToName = {} } = {}) => ({ + ...index, + ...injectKeysNamesIntoIndexKeys({ index, keyIdToName }), + ifNotExists: index.ifNotExists, + isActivated: index.isActivated !== false, +}); + +/** + * @param {{ keys?: object[] }} params + * @returns {object[]} + */ +const normalizeIndexKeys = ({ keys = [] } = {}) => keys.map(key => omit(key, ['keyId', 'id', 'GUID'])); + +/** + * @param {{ nodes?: Array<{ nodeName?: string } | string> }} params + * @returns {string[]} + */ +const normalizeNodes = ({ nodes = [] } = {}) => + nodes.map(node => (typeof node === 'string' ? node : node?.nodeName)).filter(Boolean); + +/** + * @param {{ index: object }} params + * @returns {object} + */ +const getComparableIndex = ({ index = {} } = {}) => { + const comparableIndex = omit(index, NON_STRUCTURAL_INDEX_FIELDS); + + return { + ...comparableIndex, + indxKey: normalizeIndexKeys({ keys: index.indxKey }), + partitionByHashKeys: normalizeIndexKeys({ keys: index.partitionByHashKeys }), + withOptions: { + defer_build: Boolean(index.withOptions?.defer_build), + num_replica: index.withOptions?.num_replica, + nodes: normalizeNodes({ nodes: index.withOptions?.nodes }), + }, + }; +}; + +/** + * @param {{ index: object }} params + * @returns {object} + */ +const getComparableIndexDefinition = ({ index = {} } = {}) => omit(getComparableIndex({ index }), ['withOptions']); + +/** + * @param {{ oldIndex: object, newIndex: object }} params + * @returns {{ + * type: 'noop' | 'recreate' | 'alter', + * action?: 'move' | 'replica_count', + * nodes?: object[], + * num_replica?: number + * }} + */ +const classifyIndexChange = ({ oldIndex, newIndex } = {}) => { + if (isEqual(getComparableIndex({ index: oldIndex }), getComparableIndex({ index: newIndex }))) { + return { type: 'noop' }; + } + + if ( + !isEqual(getComparableIndexDefinition({ index: oldIndex }), getComparableIndexDefinition({ index: newIndex })) + ) { + return { type: 'recreate' }; + } + + const oldOptions = oldIndex.withOptions || {}; + const newOptions = newIndex.withOptions || {}; + const nodesChanged = !isEqual( + normalizeNodes({ nodes: oldOptions.nodes }), + normalizeNodes({ nodes: newOptions.nodes }), + ); + const replicaChanged = !isEqual(oldOptions.num_replica, newOptions.num_replica); + const deferBuildChanged = Boolean(oldOptions.defer_build) !== Boolean(newOptions.defer_build); + + if (deferBuildChanged || (nodesChanged && replicaChanged)) { + return { type: 'recreate' }; + } + + if (nodesChanged) { + if (normalizeNodes({ nodes: newOptions.nodes }).length === 0) { + return { type: 'recreate' }; + } + + return { type: 'alter', action: 'move', nodes: newOptions.nodes || [] }; + } + + if (replicaChanged) { + if (newOptions.num_replica === undefined || newOptions.num_replica === null || newOptions.num_replica === '') { + return { type: 'recreate' }; + } + + return { type: 'alter', action: 'replica_count', num_replica: newOptions.num_replica }; + } + + return { type: 'recreate' }; +}; + +/** + * @param {{ entity: object, collectionName: string, index: object }} params + * @returns {AlterScriptDto | undefined} + */ +const getCreateIndexDto = ({ entity, collectionName, index } = {}) => { + const { namespace, bucketName, scopeName } = getCollectionContext({ entity }); + const script = getIndexScript({ + ...index, + namespace, + bucketName, + scopeName, + collectionName, + }); + + if (!script) { + return; + } + + const preparedScript = index.isActivated === false ? commentStatement({ statement: script }) : script; + + return AlterScriptDto.getInstance({ + script: preparedScript, + isDropScript: false, + modelLevel: 'index', + scriptPurpose: 'add', + }); +}; + +/** + * @param {{ entity: object, collectionName: string, index: object }} params + * @returns {AlterScriptDto | undefined} + */ +const getDropIndexDto = ({ entity, collectionName, index } = {}) => { + const { namespace, bucketName, scopeName } = getCollectionContext({ entity }); + const script = getDropIndexScript({ + ...index, + namespace, + bucketName, + scopeName, + collectionName, + }); + + return AlterScriptDto.getInstance({ + script, + isDropScript: true, + modelLevel: 'index', + scriptPurpose: 'deletion', + }); +}; + +/** + * @param {{ + * entity: object, + * collectionName: string, + * index: object, + * action: 'move' | 'replica_count', + * nodes?: object[], + * num_replica?: number + * }} params + * @returns {AlterScriptDto | undefined} + */ +const getAlterIndexDto = ({ entity, collectionName, index, action, nodes, num_replica } = {}) => { + const { namespace, bucketName, scopeName } = getCollectionContext({ entity }); + const script = getAlterIndexScript({ + ...index, + namespace, + bucketName, + scopeName, + collectionName, + action, + nodes, + num_replica, + }); + + return AlterScriptDto.getInstance({ + script, + isDropScript: false, + modelLevel: 'index', + scriptPurpose: 'modify', + }); +}; + +/** + * @param {{ + * entity: object, + * collectionName: string, + * indexes?: object[], + * nameType?: 'old' | 'new' + * }} params + * @returns {object[]} + */ +const getPreparedIndexes = ({ entity, indexes = [], nameType = 'new' } = {}) => { + const keyIdToName = getKeyIdToNameMap({ entity, nameType }); + return indexes.map(index => prepareIndex({ index, keyIdToName })).filter(index => index.indxName); +}; + +/** + * @param {{ + * entity: object, + * collectionName: string, + * indexes?: object[], + * nameType?: 'old' | 'new' + * }} params + * @returns {AlterScriptDto[]} + */ +const getCreateIndexDtos = ({ entity, collectionName, indexes = [], nameType = 'new' } = {}) => + getPreparedIndexes({ entity, indexes, nameType }).flatMap(index => { + const dto = getCreateIndexDto({ entity, collectionName, index }); + return dto ? [dto] : []; + }); + +/** + * @param {{ + * entity: object, + * collectionName: string, + * indexes?: object[], + * nameType?: 'old' | 'new' + * }} params + * @returns {AlterScriptDto[]} + */ +const getDropIndexDtos = ({ entity, collectionName, indexes = [], nameType = 'old' } = {}) => + getPreparedIndexes({ entity, indexes, nameType }).flatMap(index => { + const dto = getDropIndexDto({ entity, collectionName, index }); + return dto ? [dto] : []; + }); + +/** + * @param {{ indexes?: object[] }} params + * @returns {Map} + */ +const getIndexesByName = ({ indexes = [] } = {}) => + new Map(indexes.filter(index => index.indxName).map(index => [index.indxName, index])); + +/** + * @param {{ + * entity: object, + * collectionName: string, + * oldIndexes?: object[], + * newIndexes?: object[] + * }} params + * @returns {AlterScriptDto[]} + */ +const getIndexChangeDtos = ({ entity, collectionName, oldIndexes = [], newIndexes = [] } = {}) => { + const preparedOldIndexes = getPreparedIndexes({ entity, indexes: oldIndexes, nameType: 'old' }); + const preparedNewIndexes = getPreparedIndexes({ entity, indexes: newIndexes, nameType: 'new' }); + const oldIndexesByName = getIndexesByName({ indexes: preparedOldIndexes }); + const newIndexesByName = getIndexesByName({ indexes: preparedNewIndexes }); + const indexNames = new Set([...oldIndexesByName.keys(), ...newIndexesByName.keys()]); + + return [...indexNames].reduce((scriptDtos, indexName) => { + const oldIndex = oldIndexesByName.get(indexName); + const newIndex = newIndexesByName.get(indexName); + + if (!oldIndex) { + scriptDtos.push(...getCreateIndexDtos({ entity, collectionName, indexes: [newIndex], nameType: 'new' })); + return scriptDtos; + } + + if (!newIndex) { + scriptDtos.push(...getDropIndexDtos({ entity, collectionName, indexes: [oldIndex], nameType: 'old' })); + return scriptDtos; + } + + const classification = classifyIndexChange({ oldIndex, newIndex }); + + if (classification.type === 'alter') { + const dto = getAlterIndexDto({ + entity, + collectionName, + index: newIndex, + action: classification.action, + nodes: classification.nodes, + num_replica: classification.num_replica, + }); + if (dto) { + scriptDtos.push(dto); + } + return scriptDtos; + } + + if (classification.type === 'recreate') { + scriptDtos.push( + ...getDropIndexDtos({ entity, collectionName, indexes: [oldIndex], nameType: 'old' }), + ...getCreateIndexDtos({ entity, collectionName, indexes: [newIndex], nameType: 'new' }), + ); + } + + return scriptDtos; + }, []); +}; + +/** + * @param {{ entity: object }} params + * @returns {boolean} + */ +const isCollectionRenamed = ({ entity } = {}) => { + const compMod = getCompMod({ entity }); + const oldName = compMod.code?.old || compMod.collectionName?.old; + const newName = compMod.code?.new || compMod.collectionName?.new; + + return Boolean(oldName && newName && oldName !== newName); +}; + +/** + * @param {{ entity: object }} params + * @returns {AlterScriptDto[]} + */ +const getAddedEntityIndexDtos = ({ entity } = {}) => { + const role = getEntityRole({ entity }); + const collectionName = getCollectionName({ entity, nameType: 'new' }) || getCollectionName({ entity }); + + return getCreateIndexDtos({ + entity, + collectionName, + indexes: role.indexes || [], + nameType: 'new', + }); +}; + +/** + * @param {{ entity: object }} params + * @returns {AlterScriptDto[]} + */ +const getDeletedEntityIndexDtos = ({ entity } = {}) => { + const role = getEntityRole({ entity }); + const collectionName = getCollectionName({ entity, nameType: 'old' }) || getCollectionName({ entity }); + + return getDropIndexDtos({ + entity, + collectionName, + indexes: role.indexes || [], + nameType: 'old', + }); +}; + +/** + * @param {{ entity: object }} params + * @returns {AlterScriptDto[]} + */ +const getModifiedEntityIndexDtos = ({ entity } = {}) => { + const role = getEntityRole({ entity }); + const compMod = getCompMod({ entity }); + const oldCollectionName = getCollectionName({ entity, nameType: 'old' }) || getCollectionName({ entity }); + const newCollectionName = getCollectionName({ entity, nameType: 'new' }) || getCollectionName({ entity }); + + if (isCollectionRenamed({ entity })) { + const oldIndexes = compMod.indexes?.old || role.indexes || []; + const newIndexes = compMod.indexes?.new || role.indexes || []; + + return [ + ...getDropIndexDtos({ + entity, + collectionName: oldCollectionName, + indexes: oldIndexes, + nameType: 'old', + }), + ...getCreateIndexDtos({ + entity, + collectionName: newCollectionName, + indexes: newIndexes, + nameType: 'new', + }), + ]; + } + + if (!compMod.indexes) { + return []; + } + + return getIndexChangeDtos({ + entity, + collectionName: newCollectionName, + oldIndexes: compMod.indexes.old || [], + newIndexes: compMod.indexes.new || [], + }); +}; + +/** + * @param {{ schema: object }} params + * @returns {AlterScriptDto[]} + */ +const getIndexAlterScriptDtos = ({ schema } = {}) => { + const addedEntities = getDeltaItems({ schema, nameProperty: 'entities', modify: 'added' }); + const modifiedEntities = getDeltaItems({ schema, nameProperty: 'entities', modify: 'modified' }); + const deletedEntities = getDeltaItems({ schema, nameProperty: 'entities', modify: 'deleted' }); + + return [ + ...deletedEntities.flatMap(entity => getDeletedEntityIndexDtos({ entity })), + ...modifiedEntities.flatMap(entity => getModifiedEntityIndexDtos({ entity })), + ...addedEntities.flatMap(entity => getAddedEntityIndexDtos({ entity })), + ].filter(Boolean); +}; + +module.exports = { + getIndexAlterScriptDtos, + classifyIndexChange, +}; diff --git a/forward_engineering/services/alterScript/scopeAlterHelper.js b/forward_engineering/services/alterScript/scopeAlterHelper.js new file mode 100644 index 0000000..e13bae2 --- /dev/null +++ b/forward_engineering/services/alterScript/scopeAlterHelper.js @@ -0,0 +1,177 @@ +const { getScopeScript, getDropScopeScript } = require('../statements/scopesStatements'); +const { AlterScriptDto } = require('./AlterScriptDto'); +const { getDeltaItems } = require('./deltaSchemaHelper'); + +/** + * @param {{ container: object }} params + * @returns {object} + */ +const getCompMod = ({ container = {} } = {}) => container.role?.compMod || container.compMod || {}; + +/** + * @param {{ container: object }} params + * @returns {object} + */ +const getContainerRole = ({ container = {} } = {}) => container.role || container; + +/** + * @param {{ + * container: object, + * nameType?: 'old' | 'new' | 'current' + * }} params + * @returns {string} + */ +const getScopeName = ({ container = {}, nameType = 'current' } = {}) => { + const role = getContainerRole({ container }); + const compMod = getCompMod({ container }); + + if (nameType === 'old') { + return compMod.code?.old || compMod.name?.old || role.code || role.name || ''; + } + + if (nameType === 'new') { + return compMod.code?.new || compMod.name?.new || role.code || role.name || ''; + } + + return role.code || role.name || container.title || container.name || ''; +}; + +/** + * @param {{ + * container: object, + * nameType?: 'old' | 'new' | 'current' + * }} params + * @returns {{ + * namespace: string, + * bucketName: string, + * name: string, + * ifNotExists: boolean + * }} + */ +const getScopeContext = ({ container = {}, nameType = 'current' } = {}) => { + const role = getContainerRole({ container }); + const compMod = getCompMod({ container }); + + const getTypedValue = ({ propertyName }) => { + if (nameType === 'old') { + return compMod[propertyName]?.old ?? role[propertyName]; + } + + if (nameType === 'new') { + return compMod[propertyName]?.new ?? role[propertyName]; + } + + return role[propertyName]; + }; + + return { + namespace: getTypedValue({ propertyName: 'namespace' }), + bucketName: getTypedValue({ propertyName: 'bucket' }), + name: getScopeName({ container, nameType }), + ifNotExists: Boolean(role.ifNotExists), + }; +}; + +/** + * @param {{ container: object }} params + * @returns {boolean} + */ +const isScopeRecreated = ({ container } = {}) => { + const compMod = getCompMod({ container }); + const oldName = compMod.code?.old || compMod.name?.old; + const newName = compMod.code?.new || compMod.name?.new; + const nameChanged = Boolean(oldName && newName && oldName !== newName); + const bucketChanged = Boolean( + compMod.bucket?.old && compMod.bucket?.new && compMod.bucket.old !== compMod.bucket.new, + ); + const namespaceChanged = Boolean( + compMod.namespace?.old && compMod.namespace?.new && compMod.namespace.old !== compMod.namespace.new, + ); + + return nameChanged || bucketChanged || namespaceChanged; +}; + +/** + * @param {{ + * container: object, + * nameType?: 'old' | 'new' | 'current' + * }} params + * @returns {AlterScriptDto | undefined} + */ +const getCreateScopeDto = ({ container, nameType = 'new' } = {}) => { + const script = getScopeScript(getScopeContext({ container, nameType })); + + return AlterScriptDto.getInstance({ + script, + isDropScript: false, + modelLevel: 'scope', + scriptPurpose: 'add', + }); +}; + +/** + * @param {{ + * container: object, + * nameType?: 'old' | 'new' | 'current' + * }} params + * @returns {AlterScriptDto | undefined} + */ +const getDropScopeDto = ({ container, nameType = 'old' } = {}) => { + const script = getDropScopeScript({ + ...getScopeContext({ container, nameType }), + ifExists: true, + }); + + return AlterScriptDto.getInstance({ + script, + isDropScript: true, + modelLevel: 'scope', + scriptPurpose: 'deletion', + }); +}; + +/** + * @param {{ container: object }} params + * @returns {AlterScriptDto[]} + */ +const getAddedScopeDtos = ({ container } = {}) => [getCreateScopeDto({ container, nameType: 'new' })].filter(Boolean); + +/** + * @param {{ container: object }} params + * @returns {AlterScriptDto[]} + */ +const getDeletedScopeDtos = ({ container } = {}) => [getDropScopeDto({ container, nameType: 'old' })].filter(Boolean); + +/** + * @param {{ container: object }} params + * @returns {AlterScriptDto[]} + */ +const getModifiedScopeDtos = ({ container } = {}) => { + if (!isScopeRecreated({ container })) { + return []; + } + + return [getDropScopeDto({ container, nameType: 'old' }), getCreateScopeDto({ container, nameType: 'new' })].filter( + Boolean, + ); +}; + +/** + * @param {{ schema: object }} params + * @returns {AlterScriptDto[]} + */ +const getScopeAlterScriptDtos = ({ schema } = {}) => { + const addedContainers = getDeltaItems({ schema, nameProperty: 'containers', modify: 'added' }); + const modifiedContainers = getDeltaItems({ schema, nameProperty: 'containers', modify: 'modified' }); + const deletedContainers = getDeltaItems({ schema, nameProperty: 'containers', modify: 'deleted' }); + + return [ + ...deletedContainers.flatMap(container => getDeletedScopeDtos({ container })), + ...modifiedContainers.flatMap(container => getModifiedScopeDtos({ container })), + ...addedContainers.flatMap(container => getAddedScopeDtos({ container })), + ].filter(Boolean); +}; + +module.exports = { + getScopeAlterScriptDtos, +}; diff --git a/forward_engineering/services/alterScriptBuilder.js b/forward_engineering/services/alterScriptBuilder.js new file mode 100644 index 0000000..637a1e7 --- /dev/null +++ b/forward_engineering/services/alterScriptBuilder.js @@ -0,0 +1,119 @@ +const { isEqual, uniqWith } = require('lodash'); +const { getCollectionAlterScriptDtos } = require('./alterScript/collectionAlterHelper'); +const { getIndexAlterScriptDtos } = require('./alterScript/indexAlterHelper'); +const { getScopeAlterScriptDtos } = require('./alterScript/scopeAlterHelper'); +const { commentStatement } = require('./alterScript/commentHelper'); +const { getDeltaSchema, shouldApplyDropStatements } = require('./alterScript/deltaSchemaHelper'); + +const SCRIPT_ORDER = [ + ['index', 'deletion'], + ['collection', 'deletion'], + ['scope', 'deletion'], + ['scope', 'add'], + ['collection', 'add'], + ['collection', 'modify'], + ['index', 'modify'], + ['index', 'add'], +]; + +/** + * @param {{ alterScriptDtos: object[], applyDropStatements: boolean }} params + * @returns {object[]} + */ +const getCommentedDropScriptDtos = ({ alterScriptDtos = [], applyDropStatements = false } = {}) => { + if (applyDropStatements) { + return alterScriptDtos; + } + + return alterScriptDtos.map(dto => { + if (!dto?.isDropScript || !dto?.script) { + return dto; + } + + return { + ...dto, + script: commentStatement({ statement: dto.script }), + }; + }); +}; + +/** + * @param {{ alterScriptDtos: object[] }} params + * @returns {object[]} + */ +const sortAlterScriptDtos = ({ alterScriptDtos = [] } = {}) => { + const ordered = SCRIPT_ORDER.reduce( + (result, [modelLevel, scriptPurpose]) => { + const matched = []; + const remaining = []; + + result.remaining.forEach(dto => { + if (dto?.modelLevel === modelLevel && dto?.scriptPurpose === scriptPurpose) { + matched.push(dto); + return; + } + remaining.push(dto); + }); + + return { + sorted: [...result.sorted, ...matched], + remaining, + }; + }, + { sorted: [], remaining: alterScriptDtos }, + ); + + return [...ordered.sorted, ...ordered.remaining]; +}; + +/** + * @param {{ alterScriptDtos: object[] }} params + * @returns {string} + */ +const joinAlterScriptDtos = ({ alterScriptDtos = [] } = {}) => + alterScriptDtos + .map(dto => dto?.script) + .filter(Boolean) + .join('\n\n'); + +/** + * @param {{ connectionInfo: object }} params + * @returns {object[]} + */ +const getAlterScriptDtos = ({ connectionInfo } = {}) => { + const schema = getDeltaSchema({ connectionInfo }); + const alterScriptDtos = uniqWith( + [ + ...getScopeAlterScriptDtos({ schema }), + ...getCollectionAlterScriptDtos({ schema }), + ...getIndexAlterScriptDtos({ schema }), + ], + isEqual, + ); + + return sortAlterScriptDtos({ alterScriptDtos }); +}; + +/** + * @param {{ connectionInfo: object }} params + * @returns {string} + */ +const buildAlterScript = ({ connectionInfo } = {}) => { + const alterScriptDtos = getAlterScriptDtos({ connectionInfo }); + const applyDropStatements = shouldApplyDropStatements({ connectionInfo }); + const scriptDtos = getCommentedDropScriptDtos({ alterScriptDtos, applyDropStatements }); + + return joinAlterScriptDtos({ alterScriptDtos: scriptDtos }); +}; + +/** + * @param {{ connectionInfo: object }} params + * @returns {boolean} + */ +const hasDropStatements = ({ connectionInfo } = {}) => + getAlterScriptDtos({ connectionInfo }).some(dto => dto?.isActivated && dto?.isDropScript); + +module.exports = { + buildAlterScript, + hasDropStatements, +}; diff --git a/forward_engineering/services/applyToInstanceService.js b/forward_engineering/services/applyToInstanceService.js index c0cb887..8bd9f77 100644 --- a/forward_engineering/services/applyToInstanceService.js +++ b/forward_engineering/services/applyToInstanceService.js @@ -37,7 +37,13 @@ const scriptReducer = (scripts, script) => { /** * - * @param {{bucketName: string, script: string, cluster: object, logger: object, callback: function}} param0 + * @param {{ + * bucketName: string, + * script: string, + * cluster: object, + * logger: object, + * callback: function + * }} params * @returns {boolean} */ const applyScript = async ({ bucketName, script, cluster, logger, callback }) => { @@ -127,7 +133,7 @@ const isIndexAlreadyCreatedError = err => { /** * - * @param {{error: object }} param0 + * @param {{ error: object }} params * @returns {boolean} */ const isCommentedStatement = ({ error }) => { @@ -136,7 +142,11 @@ const isCommentedStatement = ({ error }) => { /** * - * @param {{attemptNumber: number, bucketName: string, logger: object}} param0 + * @param {{ + * attemptNumber: number, + * bucketName: string, + * logger: object + * }} params * @returns {void} */ const logApplyScriptAttempt = ({ attemptNumber, bucketName, logger }) => { diff --git a/forward_engineering/services/statements/collectionsStatements.js b/forward_engineering/services/statements/collectionsStatements.js index 2db4beb..9877992 100644 --- a/forward_engineering/services/statements/collectionsStatements.js +++ b/forward_engineering/services/statements/collectionsStatements.js @@ -17,6 +17,30 @@ const getCollectionScript = ({ namespace, scopeName, bucketName, collectionName, return `CREATE COLLECTION ${fullPath}${wrapWithBackticks(collectionName)}${ifNotExistsClause};\n\n`; }; +/** + * + * @param {{ + * namespace: string, + * scopeName: string, + * bucketName: string, + * collectionName: string, + * ifExists?: boolean + * }} collection + * @returns {string} + */ +const getDropCollectionScript = ({ namespace, scopeName, bucketName, collectionName, ifExists }) => { + if (!collectionName) { + return ''; + } + + const fullBucketPath = getFullBucketPath({ namespace, bucketName }); + const fullPath = bucketName && scopeName ? `${fullBucketPath}.${wrapWithBackticks(scopeName)}.` : ''; + const ifExistsClause = ifExists ? ' IF EXISTS' : ''; + + return `DROP COLLECTION ${fullPath}${wrapWithBackticks(collectionName)}${ifExistsClause};`; +}; + module.exports = { getCollectionScript, + getDropCollectionScript, }; diff --git a/forward_engineering/services/statements/indexesStatements.js b/forward_engineering/services/statements/indexesStatements.js index 043172f..92209d9 100644 --- a/forward_engineering/services/statements/indexesStatements.js +++ b/forward_engineering/services/statements/indexesStatements.js @@ -100,7 +100,7 @@ const getKeys = index => { separator: ',', }); - return { script: `(${keysNames})`, canHaveIndex: Boolean(keysNames.length) }; + return { script: `(${keysNames})`, canHaveIndex: keysNames.length > 0 }; } case INDEX_TYPE.array: return { script: `(${index.arrayExpr})`, canHaveIndex: true }; @@ -132,7 +132,6 @@ const getAdditionalOptionsFunctions = index => { return [getPartitionByHashClause, getWhereClause, getUsingGSI, getWithClause]; case INDEX_TYPE.array: return [getWhereClause, getUsingGSI, getWithClause]; - case INDEX_TYPE.metadata: default: return []; } @@ -155,15 +154,14 @@ const getWhereClause = index => { const getWithClause = index => { const deferBuild = get(index, 'withOptions.defer_build') ? `"defer_build":true` : ''; - const numReplica = !isEmpty(get(index, 'withOptions.num_replica')) - ? `"num_replica":${index.withOptions.num_replica}` - : ''; + const numReplicaValue = get(index, 'withOptions.num_replica'); + const numReplica = isEmpty(numReplicaValue) ? '' : `"num_replica":${numReplicaValue}`; const nodeStatement = joinStatements({ statements: index.withOptions?.nodes?.map(node => `"${node.nodeName}"`), separator: ',', }); - const nodes = get(index, 'withOptions.nodes', []).length ? `"nodes":[${nodeStatement}]` : ''; + const nodes = get(index, 'withOptions.nodes', []).length > 0 ? `"nodes":[${nodeStatement}]` : ''; const hasWithClosure = deferBuild || numReplica || nodes; @@ -230,6 +228,103 @@ const commentStatement = statement => { return `/*\n${joinedStatement}\n */`; }; +/** + * + * @param {{ + * namespace: string, + * bucketName: string, + * scopeName: string, + * collectionName: string, + * indxName: string, + * usingGSI?: boolean, + * }} index + * @returns {string} + */ +const getDropIndexScript = ({ namespace, bucketName, scopeName, collectionName, indxName, usingGSI }) => { + if (!indxName || !collectionName) { + return ''; + } + + const keySpaceRefStatement = getKeySpaceReference({ namespace, bucketName, scopeName, collectionName }); + const usingGsiClause = usingGSI ? ' USING GSI' : ''; + + return `DROP INDEX ${wrapWithBackticks(indxName)} IF EXISTS ON ${keySpaceRefStatement}${usingGsiClause};`; +}; + +/** + * + * @param {{ + * action: 'move' | 'replica_count', + * nodes?: Array<{ nodeName?: string } | string>, + * num_replica?: number + * }} params + * @returns {string} + */ +const getAlterIndexWithClause = ({ action, nodes = [], num_replica } = {}) => { + if (action === 'move') { + const nodeStatement = joinStatements({ + statements: nodes.map(node => `"${typeof node === 'string' ? node : node.nodeName}"`), + separator: ',', + }); + + if (!nodeStatement) { + return ''; + } + + return `{"action":"move","nodes":[${nodeStatement}]}`; + } + + if (action === 'replica_count' && num_replica !== undefined && num_replica !== null && num_replica !== '') { + return `{"action":"replica_count","num_replica":${num_replica}}`; + } + + return ''; +}; + +/** + * + * @param {{ + * namespace: string, + * bucketName: string, + * scopeName: string, + * collectionName: string, + * indxName: string, + * usingGSI?: boolean, + * action: 'move' | 'replica_count', + * nodes?: Array<{ nodeName?: string } | string>, + * num_replica?: number, + * }} index + * @returns {string} + */ +const getAlterIndexScript = ({ + namespace, + bucketName, + scopeName, + collectionName, + indxName, + usingGSI, + action, + nodes = [], + num_replica, +}) => { + if (!indxName || !collectionName || !action) { + return ''; + } + + const keySpaceRefStatement = getKeySpaceReference({ namespace, bucketName, scopeName, collectionName }); + const usingGsiClause = usingGSI ? ' USING GSI' : ''; + const withClause = getAlterIndexWithClause({ action, nodes, num_replica }); + + if (!withClause) { + return ''; + } + + return `ALTER INDEX ${wrapWithBackticks(indxName)} ON ${keySpaceRefStatement}${usingGsiClause} WITH ${withClause};`; +}; + module.exports = { getIndexesScript, + getIndexScript, + getDropIndexScript, + getAlterIndexScript, }; diff --git a/forward_engineering/services/statements/insertStatements.js b/forward_engineering/services/statements/insertStatements.js index e570fcd..8c6fef4 100644 --- a/forward_engineering/services/statements/insertStatements.js +++ b/forward_engineering/services/statements/insertStatements.js @@ -23,7 +23,7 @@ const getInsertScripts = ({ jsonData, collections = [] }) => { /** * - * @param {jsonData: object, collection: object} param0 + * @param {{ jsonData: object, collection: object }} params * @returns {string} */ const getInsertScriptForCollection = ({ jsonData, collection, useUpsert = true }) => { diff --git a/forward_engineering/services/statements/scopesStatements.js b/forward_engineering/services/statements/scopesStatements.js index 32d5fc0..fdfb24e 100644 --- a/forward_engineering/services/statements/scopesStatements.js +++ b/forward_engineering/services/statements/scopesStatements.js @@ -17,6 +17,29 @@ const getScopeScript = ({ namespace, bucketName, name, ifNotExists }) => { return `CREATE SCOPE ${fullBucketPath}.${scopeName}${ifNotExistsClause};`; }; +/** + * + * @param {{ + * namespace: string, + * bucketName: string, + * name: string, + * ifExists?: boolean + * }} scope + * @returns {string} + */ +const getDropScopeScript = ({ namespace, bucketName, name, ifExists }) => { + if (!bucketName || !name) { + return ''; + } + + const fullBucketPath = getFullBucketPath({ namespace, bucketName }); + const scopeName = wrapWithBackticks(name); + const ifExistsClause = ifExists ? ' IF EXISTS' : ''; + + return `DROP SCOPE ${fullBucketPath}.${scopeName}${ifExistsClause};`; +}; + module.exports = { getScopeScript, + getDropScopeScript, }; diff --git a/forward_engineering/utils/indexes.js b/forward_engineering/utils/indexes.js index 5ecf169..e689bc0 100644 --- a/forward_engineering/utils/indexes.js +++ b/forward_engineering/utils/indexes.js @@ -9,6 +9,7 @@ const injectKeysNamesIntoIndexKeys = ({ index, keyIdToName = {} }) => ({ indxName: index.indxName, indxType: index.indxType, isActivated: index.isActivated, + ifNotExists: index.ifNotExists, partitionByHash: index.partitionByHash, functionExpr: index.functionExpr, usingGSI: index.usingGSI, @@ -30,10 +31,8 @@ const injectKeysNamesIntoIndexKeys = ({ index, keyIdToName = {} }) => ({ */ const getIndexKeyIdToKeyNameMap = collectionProperties => Object.entries(collectionProperties).reduce((keyIdToNameMap, [propertyName, propertyData]) => { - return { - ...keyIdToNameMap, - [propertyData.GUID]: propertyName, - }; + keyIdToNameMap[propertyData.GUID] = propertyName; + return keyIdToNameMap; }, {}); module.exports = { diff --git a/localization/en.json b/localization/en.json index 69c99c8..1a774f9 100644 --- a/localization/en.json +++ b/localization/en.json @@ -8,6 +8,7 @@ "MAIN_MENU___INSERT_FIELD": "Insert Field", "MAIN_MENU___APPEND_FIELD": "Append Field", "MAIN_MENU___REVERSE_DB_COLLECTIONS": "Couchbase collections...", + "MAIN_MENU___FORWARD_CHANGE_COLLECTIONS": "Couchbase Alter Script", "TOOLBAR___ADD_BUCKET": "Add Scope", "TOOLBAR___ADD_COLLECTION": "Add Collection", "TOOLBAR___ADD_VIEW": "Add View", From f4103c37a4401bfe78dd005c5b070eef2cf94d6d Mon Sep 17 00:00:00 2001 From: chulanovskyi Date: Fri, 14 Aug 2026 16:59:21 +0300 Subject: [PATCH 2/2] chore: sonar --- forward_engineering/services/statements/indexesStatements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forward_engineering/services/statements/indexesStatements.js b/forward_engineering/services/statements/indexesStatements.js index 92209d9..953da06 100644 --- a/forward_engineering/services/statements/indexesStatements.js +++ b/forward_engineering/services/statements/indexesStatements.js @@ -274,7 +274,7 @@ const getAlterIndexWithClause = ({ action, nodes = [], num_replica } = {}) => { return `{"action":"move","nodes":[${nodeStatement}]}`; } - if (action === 'replica_count' && num_replica !== undefined && num_replica !== null && num_replica !== '') { + if (action === 'replica_count' && !isEmpty(num_replica)) { return `{"action":"replica_count","num_replica":${num_replica}}`; }