diff --git a/.gitignore b/.gitignore index 07d1ff8d3..4bf61fce2 100644 --- a/.gitignore +++ b/.gitignore @@ -366,4 +366,13 @@ app.json *extracted_files* *MigrationData* *.zip -app.json +*extracted_files* +*.tsbuildinfo +*drupalMigrationData* +# Snyk Security Extension - AI Rules (auto-generated) +.cursor/rules/snyk_rules.mdc +/aem_data_structure + +*.csv +contentful.json +test-* \ No newline at end of file diff --git a/api/.eslintrc.js b/api/.eslintrc.js new file mode 100644 index 000000000..eeb8fccea --- /dev/null +++ b/api/.eslintrc.js @@ -0,0 +1,36 @@ +const path = require('path'); + +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + }, + extends: [ + 'prettier', + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2022, + project: [path.resolve(__dirname, 'tsconfig.json')], + sourceType: 'module', + }, + plugins: ['@typescript-eslint'], + rules: { + 'operator-linebreak': [ + 'error', + 'after', + { + overrides: { + ':': 'before', + }, + }, + ], + 'func-names': [0], + 'no-console': ['error', { allow: ['warn', 'error', 'info'] }], + '@typescript-eslint/no-explicit-any': 'warn', + }, +}; + diff --git a/api/.eslintrc.json b/api/.eslintrc.json deleted file mode 100644 index 011dfe9dd..000000000 --- a/api/.eslintrc.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "root": true, - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "prettier", - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2022, - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": { - "operator-linebreak": [ - "error", - "after", - { - "overrides": { - ":": "before" - } - } - ], - "func-names": [0], - "no-console": ["error", { "allow": ["warn", "error", "info"] }], - "@typescript-eslint/no-explicit-any": "warn" - } -} diff --git a/api/nodemon.json b/api/nodemon.json new file mode 100644 index 000000000..8d9d481c1 --- /dev/null +++ b/api/nodemon.json @@ -0,0 +1,24 @@ +{ + "watch": ["src"], + "ext": "ts,js,json", + "ignore": [ + "database/**/*", + "logs/**/*", + "*.log", + "node_modules/**/*", + "dist/**/*", + "build/**/*", + "cmsMigrationData/**/*", + "test_output.log", + "combine.log", + "sample.log", + "upload-api/**/*", + "ui/**/*" + ], + "exec": "tsx ./src/server.ts", + "env": { + "NODE_ENV": "production" + }, + "delay": "1000", + "verbose": true +} diff --git a/api/package-lock.json b/api/package-lock.json index 3454dedf5..cbba9ef41 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -31,8 +31,11 @@ "jsonwebtoken": "^9.0.2", "lowdb": "^7.0.1", "mkdirp": "^3.0.1", + "mysql2": "^3.14.3", "p-limit": "^6.2.0", "path-to-regexp": "^8.2.0", + "php-serialize": "^5.1.3", + "php-unserialize": "^0.0.1", "router": "^2.0.0", "shelljs": "^0.9.0", "socket.io": "^4.7.5", @@ -58,6 +61,7 @@ "eslint-config-airbnb": "^19.0.0", "eslint-config-prettier": "^8.3.0", "lodash": "^4.17.21", + "nodemon": "^3.0.0", "prettier": "^2.4.1", "tsx": "^4.7.1", "typescript": "^5.4.3" @@ -3743,6 +3747,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.10.3", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", @@ -4785,6 +4797,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6795,6 +6815,14 @@ "node": ">= 0.6.0" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7291,6 +7319,12 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/immer": { "version": "10.1.3", "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", @@ -8323,6 +8357,11 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -9443,6 +9482,11 @@ "node": ">= 12.0.0" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9473,6 +9517,20 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", @@ -9688,6 +9746,44 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -9775,6 +9871,77 @@ "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==" }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -12821,6 +12988,22 @@ "node": ">=8" } }, + "node_modules/php-serialize": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/php-serialize/-/php-serialize-5.1.3.tgz", + "integrity": "sha512-p7zXX8xjGgddgP6byN+KmGKM0x6uoMZBRZteBa9LonqgrDV3LyMxUeGVX7RTFYwWaUAnTEsUWJfHI3N7eKvJgw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/php-unserialize": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/php-unserialize/-/php-unserialize-0.0.1.tgz", + "integrity": "sha512-aZWuX3gQ30Dui+Lff19q0jeu+3DHpSYXFEQPkeAx4WAyDtAp5VI30ZPC5wb4OrcHy6KUiZIRFRTWkvK8l8l6Rw==", + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -13102,6 +13285,12 @@ "url": "https://github.com/sponsors/lupomontero" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -13266,42 +13455,6 @@ "integrity": "sha512-a2kMzcfr+ntT0bObNLY22EUNV6Z6WeZ+DybRmPOUXVWzGcqhRcrK74tpgrYt3FdzTlSh85pqoryAPmrNkwLc0g==", "optional": true }, - "node_modules/recheck-linux-x64": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/recheck-linux-x64/-/recheck-linux-x64-4.4.5.tgz", - "integrity": "sha512-s8OVPCpiSGw+tLCxH3eei7Zp2AoL22kXqLmEtWXi0AnYNwfuTjZmeLn2aQjW8qhs8ZPSkxS7uRIRTeZqR5Fv/Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/recheck-macos-x64": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/recheck-macos-x64/-/recheck-macos-x64-4.4.5.tgz", - "integrity": "sha512-Ouup9JwwoKCDclt3Na8+/W2pVbt8FRpzjkDuyM32qTR2TOid1NI+P1GA6/VQAKEOjvaxgGjxhcP/WqAjN+EULA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/recheck-windows-x64": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/recheck-windows-x64/-/recheck-windows-x64-4.4.5.tgz", - "integrity": "sha512-mkpzLHu9G9Ztjx8HssJh9k/Xm1d1d/4OoT7etHqFk+k1NGzISCRXBD22DqYF9w8+J4QEzTAoDf8icFt0IGhOEQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -13820,6 +13973,11 @@ "node": ">= 0.8" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -14030,6 +14188,18 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sinon": { "version": "19.0.5", "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.5.tgz", @@ -14333,6 +14503,14 @@ "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz", "integrity": "sha512-lgxErLl/7A5+vgIIXsh9MbeukOaCb2axgQ+bKCdIE+ibNT4XNYGNCR1qFEGq6F+YDASXK3Fh/c5FgtZchFolxw==" }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -14755,6 +14933,15 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -15060,6 +15247,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/underscore": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", diff --git a/api/package.json b/api/package.json index f21d77aad..063fb2f8f 100644 --- a/api/package.json +++ b/api/package.json @@ -5,8 +5,10 @@ "exports": "./src/server.ts", "scripts": { "build": "npx tsc", - "start": "NODE_ENV=production node dist/server.js", - "dev": "NODE_ENV=production tsx ./src/server.ts", + "start": "NODE_ENV=production tsx ./src/server.ts", + "start:prod": "NODE_ENV=production node dist/server.js", + "dev": "NODE_ENV=production nodemon --exec tsx ./src/server.ts", + "test:profile-refs": "node test-profile-references.js", "prettify": "prettier --write .", "windev": "SET NODE_ENV=production&& tsx watch ./src/server.ts", "winstart": "SET NODE_ENV=production&& node dist/server.js", @@ -47,8 +49,11 @@ "jsonwebtoken": "^9.0.2", "lowdb": "^7.0.1", "mkdirp": "^3.0.1", + "mysql2": "^3.14.3", "p-limit": "^6.2.0", "path-to-regexp": "^8.2.0", + "php-serialize": "^5.1.3", + "php-unserialize": "^0.0.1", "router": "^2.0.0", "shelljs": "^0.9.0", "socket.io": "^4.7.5", @@ -74,6 +79,7 @@ "eslint-config-airbnb": "^19.0.0", "eslint-config-prettier": "^8.3.0", "lodash": "^4.17.21", + "nodemon": "^3.0.0", "prettier": "^2.4.1", "tsx": "^4.7.1", "typescript": "^5.4.3" diff --git a/api/src/constants/index.ts b/api/src/constants/index.ts index 777712bd6..52351c6d2 100644 --- a/api/src/constants/index.ts +++ b/api/src/constants/index.ts @@ -1,29 +1,38 @@ -export const CS_REGIONS = ["NA", "EU", "AZURE_NA", "AZURE_EU", "GCP_NA", "AU", "GCP_EU"]; +export const CS_REGIONS = [ + 'NA', + 'EU', + 'AZURE_NA', + 'AZURE_EU', + 'GCP_NA', + 'AU', + 'GCP_EU', +]; export const DEVURLS: any = { - NA: "developerhub-api.contentstack.com", - EU: "eu-developerhub-api.contentstack.com", - AZURE_NA: "azure-na-developerhub-api.contentstack.com", - AZURE_EU: "azure-eu-developerhub-api.contentstack.com", - GCP_NA: "gcp-na-developerhub-api.contentstack.com", - AU: "au-developerhub-api.contentstack.com", - GCP_EU: "gcp-eu-developerhub-api.contentstack.com", + NA: 'developerhub-api.contentstack.com', + EU: 'eu-developerhub-api.contentstack.com', + AZURE_NA: 'azure-na-developerhub-api.contentstack.com', + AZURE_EU: 'azure-eu-developerhub-api.contentstack.com', + GCP_NA: 'gcp-na-developerhub-api.contentstack.com', + AU: 'au-developerhub-api.contentstack.com', + GCP_EU: 'gcp-eu-developerhub-api.contentstack.com', }; export const CMS = { - CONTENTFUL: "contentful", - SITECORE_V8: "sitecore v8", - SITECORE_V9: "sitecore v9", - SITECORE_V10: "sitecore v10", - WORDPRESS: "wordpress", - AEM: "aem", + CONTENTFUL: 'contentful', + SITECORE_V8: 'sitecore v8', + SITECORE_V9: 'sitecore v9', + SITECORE_V10: 'sitecore v10', + WORDPRESS: 'wordpress', + DRUPAL: 'drupal', + AEM: 'aem', }; export const MODULES = [ - "Project", - "Migration", - "Content Mapping", - "Legacy CMS", - "Destination Stack", + 'Project', + 'Migration', + 'Content Mapping', + 'Legacy CMS', + 'Destination Stack', ]; -export const MODULES_ACTIONS = ["Create", "Update", "Delete"]; +export const MODULES_ACTIONS = ['Create', 'Update', 'Delete']; export const AXIOS_TIMEOUT = 60 * 1000; export const HTTP_CODES = { OK: 200, @@ -40,19 +49,19 @@ export const HTTP_CODES = { }; export const HTTP_TEXTS = { UNAUTHORIZED: "You're unauthorized to access this resource.", - S3_ERROR: "Something went wrong while handing the file", - INTERNAL_ERROR: "Internal server error, please try again later.", + S3_ERROR: 'Something went wrong while handing the file', + INTERNAL_ERROR: 'Internal server error, please try again later.', SOMETHING_WENT_WRONG: - "Something went wrong while processing your request, please try again.", - CS_ERROR: "Contentstack API error", - NO_CS_USER: "No user found with the credentials", - SUCCESS_LOGIN: "Login Successful.", - TOKEN_ERROR: "Error occurred during token generation.", - LOGIN_ERROR: "Error occurred during login", - ROUTE_ERROR: "Sorry, the requested resource is not available.", - PROJECT_NOT_FOUND: "Sorry, the requested project does not exists.", - PROJECT_CREATION_FAILED: "Error occurred while creating project.", - NO_PROJECT: "resource not found with the given ID(s).", + 'Something went wrong while processing your request, please try again.', + CS_ERROR: 'Contentstack API error', + NO_CS_USER: 'No user found with the credentials', + SUCCESS_LOGIN: 'Login Successful.', + TOKEN_ERROR: 'Error occurred during token generation.', + LOGIN_ERROR: 'Error occurred during login', + ROUTE_ERROR: 'Sorry, the requested resource is not available.', + PROJECT_NOT_FOUND: 'Sorry, the requested project does not exists.', + PROJECT_CREATION_FAILED: 'Error occurred while creating project.', + NO_PROJECT: 'resource not found with the given ID(s).', AFFIX_UPDATED: "Project's Affix updated successfully", AFFIX_CONFIRMATION_UPDATED: "Project's Affix confirmation updated successfully", @@ -65,82 +74,82 @@ export const HTTP_TEXTS = { FILE_FORMAT_UPDATED: "Project's migration file format updated successfully", DESTINATION_STACK_UPDATED: "Project's migration destination stack updated successfully", - DESTINATION_STACK_NOT_FOUND: "Destination stack does not exist", - DESTINATION_STACK_ERROR: "Error occurred during verifying destination stack", - INVALID_ID: "Provided $ ID is invalid.", - CONTENT_TYPE_NOT_FOUND: "ContentType does not exist", - CONTENT_TYPE_MISSING: "ContentType is missing in request.", - INVALID_CONTENT_TYPE: "Provide valid ContentType data", + DESTINATION_STACK_NOT_FOUND: 'Destination stack does not exist', + DESTINATION_STACK_ERROR: 'Error occurred during verifying destination stack', + INVALID_ID: 'Provided $ ID is invalid.', + CONTENT_TYPE_NOT_FOUND: 'ContentType does not exist', + CONTENT_TYPE_MISSING: 'ContentType is missing in request.', + INVALID_CONTENT_TYPE: 'Provide valid ContentType data', RESET_CONTENT_MAPPING: - "ContentType has been successfully restored to its initial mapping", - UPLOAD_SUCCESS: "File uploaded successfully", + 'ContentType has been successfully restored to its initial mapping', + UPLOAD_SUCCESS: 'File uploaded successfully', CANNOT_UPDATE_LEGACY_CMS: - "Updating the legacy CMS is not possible as the migration process is either in progress or has already been successfully completed.", + 'Updating the legacy CMS is not possible as the migration process is either in progress or has already been successfully completed.', CANNOT_UPDATE_FILE_FORMAT: - "Updating the file format is not possible as the migration process is either in progress or has already been successfully completed.", + 'Updating the file format is not possible as the migration process is either in progress or has already been successfully completed.', CANNOT_UPDATE_DESTINATION_STACK: - "Updating the destination stack is restricted. Please verify the status and review preceding actions.", + 'Updating the destination stack is restricted. Please verify the status and review preceding actions.', CANNOT_PROCEED_LEGACY_CMS: - "You cannot proceed if the project is not in draft or if any Legacy CMS details are missing.", + 'You cannot proceed if the project is not in draft or if any Legacy CMS details are missing.', CANNOT_PROCEED_DESTINATION_STACK: - "You cannot proceed if the project is not in draft or if any Legacy CMS or Destination Stack details are missing.", + 'You cannot proceed if the project is not in draft or if any Legacy CMS or Destination Stack details are missing.', CANNOT_PROCEED_CONTENT_MAPPING: - "You cannot proceed if the project is not in draft or if any Legacy CMS or Destination Stack or Content Mapping details are missing.", + 'You cannot proceed if the project is not in draft or if any Legacy CMS or Destination Stack or Content Mapping details are missing.', CANNOT_PROCEED_TEST_MIGRATION: - "You cannot proceed if the project is not in draft or if any Legacy CMS or Destination Stack or Content Mapping or Test Migration details are missing.", + 'You cannot proceed if the project is not in draft or if any Legacy CMS or Destination Stack or Content Mapping or Test Migration details are missing.', CANNOT_PROCEED_MIGRATION: - "You cannot proceed if the project is not in draft or if any Legacy CMS or Destination Stack or Content Mapping or Test Migration details are missing or Migration is not completed.", + 'You cannot proceed if the project is not in draft or if any Legacy CMS or Destination Stack or Content Mapping or Test Migration details are missing or Migration is not completed.', CANNOT_UPDATE_CONTENT_MAPPING: - "Updating the content mapping is restricted. Please verify the status and review preceding actions.", + 'Updating the content mapping is restricted. Please verify the status and review preceding actions.', CANNOT_RESET_CONTENT_MAPPING: - "Reseting the content mapping is restricted. Please verify the status and review preceding actions.", + 'Reseting the content mapping is restricted. Please verify the status and review preceding actions.', CONTENTMAPPER_NOT_FOUND: - "Sorry, the requested content mapper id does not exists.", + 'Sorry, the requested content mapper id does not exists.', ADMIN_LOGIN_ERROR: "Sorry, You Don't have admin access in any of the Organisation", - PROJECT_DELETE: "Project Deleted Successfully", - PROJECT_REVERT: "Project Reverted Successfully", - LOGS_NOT_FOUND: "Sorry, no logs found for requested stack migration.", + PROJECT_DELETE: 'Project Deleted Successfully', + PROJECT_REVERT: 'Project Reverted Successfully', + LOGS_NOT_FOUND: 'Sorry, no logs found for requested stack migration.', MIGRATION_EXECUTION_KEY_UPDATED: "Project's migration execution key updated successfully", - CONTENT_TYPE_INVALID: "Invalid contentTypes: Expected an array." + CONTENT_TYPE_INVALID: 'Invalid contentTypes: Expected an array.', }; export const HTTP_RESPONSE_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - Connection: "close", + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + Connection: 'close', }; export const METHODS_TO_INCLUDE_DATA_IN_AXIOS = [ - "PUT", - "POST", - "DELETE", - "PATCH", + 'PUT', + 'POST', + 'DELETE', + 'PATCH', ]; export const VALIDATION_ERRORS = { - INVALID_EMAIL: "Given email ID is invalid.", + INVALID_EMAIL: 'Given email ID is invalid.', EMAIL_LIMIT: "Email's max limit reached.", LENGTH_LIMIT: "$'s max limit reached.", - STRING_REQUIRED: "Provided $ should be a string.", - BOOLEAN_REQUIRED: "Provided $ should be a boolean.", + STRING_REQUIRED: 'Provided $ should be a string.', + BOOLEAN_REQUIRED: 'Provided $ should be a boolean.', INVALID_REGION: "Provided region doesn't exists.", FIELD_REQUIRED: "Field '$' is required.", - INVALID_AFFIX: "Invalid affix format", + INVALID_AFFIX: 'Invalid affix format', }; -export const POPULATE_CONTENT_MAPPER = "content_mapper"; -export const POPULATE_FIELD_MAPPING = "fieldMapping"; +export const POPULATE_CONTENT_MAPPER = 'content_mapper'; +export const POPULATE_FIELD_MAPPING = 'fieldMapping'; export const CONTENT_TYPE_POPULATE_FIELDS = - "otherCmsTitle otherCmsUid isUpdated updateAt contentstackTitle contentstackUid"; + 'otherCmsTitle otherCmsUid isUpdated updateAt contentstackTitle contentstackUid'; export const PROJECT_UNSELECTED_FIELDS = - "-content_mapper -legacy_cms -destination_stack_id -execution_log"; -export const EXCLUDE_CONTENT_MAPPER = "-content_mapper -execution_log"; + '-content_mapper -legacy_cms -destination_stack_id -execution_log'; +export const EXCLUDE_CONTENT_MAPPER = '-content_mapper -execution_log'; export const AFFIX_REGEX = /^[a-zA-Z][a-zA-Z0-9]{1,4}$/; export const PROJECT_STATUS = { - DRAFT: "Draft", - READY: "Ready", - INPROGRESS: "InProgress", - FAILED: "Failed", - SUCCESS: "Success", + DRAFT: 'Draft', + READY: 'Ready', + INPROGRESS: 'InProgress', + FAILED: 'Failed', + SUCCESS: 'Success', }; export const STEPPER_STEPS: any = { LEGACY_CMS: 1, @@ -150,11 +159,11 @@ export const STEPPER_STEPS: any = { MIGRATION: 5, }; export const PREDEFINED_STATUS = [ - "Draft", - "Ready", - "InProgress", - "Failed", - "Success", + 'Draft', + 'Ready', + 'InProgress', + 'Failed', + 'Success', ]; export const PREDEFINED_STEPS = [1, 2, 3, 4, 5]; @@ -178,123 +187,123 @@ export const CONTENT_TYPE_STATUS = { export const LOCALE_MAPPER: any = { //not more than one locale mapping in master locale masterLocale: { - "en-us": "en", + 'en-us': 'en', }, - locales: { fr: "fr-fr", } + locales: { fr: 'fr-fr' }, }; export const CHUNK_SIZE = 1048576; -export const LIST_EXTENSION_UID = "bltc44e51cc9f4b0d80"; +export const LIST_EXTENSION_UID = 'bltc44e51cc9f4b0d80'; export const KEYTOREMOVE = [ - "update", - "fetch", - "delete", - "oauth", - "hosting", - "install", - "reinstall", - "upgrade", - "getRequests", - "authorize", - "authorization", - "listInstallations", + 'update', + 'fetch', + 'delete', + 'oauth', + 'hosting', + 'install', + 'reinstall', + 'upgrade', + 'getRequests', + 'authorize', + 'authorization', + 'listInstallations', ]; export const MIGRATION_DATA_CONFIG = { - DATA: "./cmsMigrationData", + DATA: './cmsMigrationData', - BACKUP_DATA: "migration-data", - BACKUP_LOG_DIR: "logs", - BACKUP_FOLDER_NAME: "import", - BACKUP_FILE_NAME: "success.log", + BACKUP_DATA: 'migration-data', + BACKUP_LOG_DIR: 'logs', + BACKUP_FOLDER_NAME: 'import', + BACKUP_FILE_NAME: 'success.log', - LOCALE_DIR_NAME: "locales", - LOCALE_FILE_NAME: "locales.json", - LOCALE_MASTER_LOCALE: "master-locale.json", - LOCALE_CF_LANGUAGE: "language.json", + LOCALE_DIR_NAME: 'locales', + LOCALE_FILE_NAME: 'locales.json', + LOCALE_MASTER_LOCALE: 'master-locale.json', + LOCALE_CF_LANGUAGE: 'language.json', - WEBHOOKS_DIR_NAME: "webhooks", - WEBHOOKS_FILE_NAME: "webhooks.json", + WEBHOOKS_DIR_NAME: 'webhooks', + WEBHOOKS_FILE_NAME: 'webhooks.json', - ENVIRONMENTS_DIR_NAME: "environments", - ENVIRONMENTS_FILE_NAME: "environments.json", + ENVIRONMENTS_DIR_NAME: 'environments', + ENVIRONMENTS_FILE_NAME: 'environments.json', - CONTENT_TYPES_DIR_NAME: "content_types", - EXTENSIONS_MAPPER_DIR_NAME: "extension-mapper.json", - CUSTOM_MAPPER_FILE_NAME: "custmon-mapper.json", - CONTENT_TYPES_FILE_NAME: "contenttype.json", - CONTENT_TYPES_MASTER_FILE: "contenttypes.json", - CONTENT_TYPES_SCHEMA_FILE: "schema.json", - MARKETPLACE_APPS_DIR_NAME: "marketplace_apps", - MARKETPLACE_APPS_FILE_NAME: "marketplace_apps.json", - EXTENSION_APPS_DIR_NAME: "extensions", - EXTENSION_APPS_FILE_NAME: "extensions.json", - REFERENCES_DIR_NAME: "reference", - REFERENCES_FILE_NAME: "reference.json", - TAXONOMIES_DIR_NAME: "taxonomies", - TAXONOMIES_FILE_NAME: "taxonomies.json", + CONTENT_TYPES_DIR_NAME: 'content_types', + EXTENSIONS_MAPPER_DIR_NAME: 'extension-mapper.json', + CUSTOM_MAPPER_FILE_NAME: 'custmon-mapper.json', + CONTENT_TYPES_FILE_NAME: 'contenttype.json', + CONTENT_TYPES_MASTER_FILE: 'contenttypes.json', + CONTENT_TYPES_SCHEMA_FILE: 'schema.json', + MARKETPLACE_APPS_DIR_NAME: 'marketplace_apps', + MARKETPLACE_APPS_FILE_NAME: 'marketplace_apps.json', + EXTENSION_APPS_DIR_NAME: 'extensions', + EXTENSION_APPS_FILE_NAME: 'extensions.json', + REFERENCES_DIR_NAME: 'reference', + REFERENCES_FILE_NAME: 'reference.json', + TAXONOMIES_DIR_NAME: 'taxonomies', + TAXONOMIES_FILE_NAME: 'taxonomies.json', - RTE_REFERENCES_DIR_NAME: "rteReference", - RTE_REFERENCES_FILE_NAME: "rteReference.json", + RTE_REFERENCES_DIR_NAME: 'rteReference', + RTE_REFERENCES_FILE_NAME: 'rteReference.json', - ASSETS_DIR_NAME: "assets", - ASSETS_FILE_NAME: "assets.json", + ASSETS_DIR_NAME: 'assets', + ASSETS_FILE_NAME: 'assets.json', // ASSETS_SCHEMA_FILE : "index.json", - ASSETS_SCHEMA_FILE: "index.json", - ASSETS_FAILED_FILE: "cs_failed.json", - ASSETS_METADATA_FILE: "metadata.json", - ASSETS_FOLDER_FILE_NAME: "folders.json", + ASSETS_SCHEMA_FILE: 'index.json', + ASSETS_FAILED_FILE: 'cs_failed.json', + ASSETS_METADATA_FILE: 'metadata.json', + ASSETS_FOLDER_FILE_NAME: 'folders.json', - ENTRIES_DIR_NAME: "entries", - ENTRIES_MASTER_FILE: "index.json", + ENTRIES_DIR_NAME: 'entries', + ENTRIES_MASTER_FILE: 'index.json', - AUTHORS_DIR_NAME: "authors", - AUTHORS_FILE_NAME: "en-us.json", - AUTHORS_MASTER_FILE: "authors.json", + AUTHORS_DIR_NAME: 'authors', + AUTHORS_FILE_NAME: 'en-us.json', + AUTHORS_MASTER_FILE: 'authors.json', - CATEGORIES_DIR_NAME: "categories", - CATEGORIES_FILE_NAME: "en-us.json", - CATEGORIES_MASTER_FILE: "categories.json", + CATEGORIES_DIR_NAME: 'categories', + CATEGORIES_FILE_NAME: 'en-us.json', + CATEGORIES_MASTER_FILE: 'categories.json', - TAG_DIR_NAME: "tag", - TAG_FILE_NAME: "en-us.json", - TAG_MASTER_FILE: "tag.json", + TAG_DIR_NAME: 'tag', + TAG_FILE_NAME: 'en-us.json', + TAG_MASTER_FILE: 'tag.json', - TERMS_DIR_NAME: "terms", - TERMS_FILE_NAME: "en-us.json", - TERMS_MASTER_FILE: "terms.json", + TERMS_DIR_NAME: 'terms', + TERMS_FILE_NAME: 'en-us.json', + TERMS_MASTER_FILE: 'terms.json', - POSTS_DIR_NAME: "posts", - POSTS_FOLDER_NAME: "en-us", - POSTS_FILE_NAME: "en-us.json", - POSTS_MASTER_FILE: "posts.json", + POSTS_DIR_NAME: 'posts', + POSTS_FOLDER_NAME: 'en-us', + POSTS_FILE_NAME: 'en-us.json', + POSTS_MASTER_FILE: 'posts.json', - PAGES_DIR_NAME: "pages", - PAGES_FOLDER_NAME: "en-us", - PAGES_FILE_NAME: "en-us.json", - PAGES_MASTER_FILE: "pages.json", + PAGES_DIR_NAME: 'pages', + PAGES_FOLDER_NAME: 'en-us', + PAGES_FILE_NAME: 'en-us.json', + PAGES_MASTER_FILE: 'pages.json', - CHUNKS_DIR_NAME: "chunks", + CHUNKS_DIR_NAME: 'chunks', - GLOBAL_FIELDS_DIR_NAME: "global_fields", - GLOBAL_FIELDS_FILE_NAME: "globalfields.json", + GLOBAL_FIELDS_DIR_NAME: 'global_fields', + GLOBAL_FIELDS_FILE_NAME: 'globalfields.json', - EXPORT_INFO_FILE: "export-info.json", + EXPORT_INFO_FILE: 'export-info.json', - AEM_DAM_DIR: 'dam-downloads' + AEM_DAM_DIR: 'dam-downloads', }; export const GET_AUDIT_DATA = { - MIGRATION: "migration-v2", - API_DIR: "api", - MIGRATION_DATA_DIR: "migration-data", - LOGS_DIR: "logs", - AUDIT_DIR: "audit", - AUDIT_REPORT: "audit-report", - FILTERALL: "all", -} + MIGRATION: 'migration-v2', + API_DIR: 'api', + MIGRATION_DATA_DIR: 'migration-data', + LOGS_DIR: 'logs', + AUDIT_DIR: 'audit', + AUDIT_REPORT: 'audit-report', + FILTERALL: 'all', +}; export const RESERVED_FIELD_MAPPINGS: Record = { - 'locale': 'cm_locale' + locale: 'cm_locale', // Add other reserved fields if needed -}; \ No newline at end of file +}; diff --git a/api/src/controllers/projects.contentMapper.controller.ts b/api/src/controllers/projects.contentMapper.controller.ts index af49095fa..3e3ede696 100644 --- a/api/src/controllers/projects.contentMapper.controller.ts +++ b/api/src/controllers/projects.contentMapper.controller.ts @@ -1,5 +1,5 @@ -import { Request, Response } from "express"; -import { contentMapperService } from "../services/contentMapper.service.js"; +import { Request, Response } from 'express'; +import { contentMapperService } from '../services/contentMapper.service.js'; /** * Handles the PUT request to update test data. * @@ -132,22 +132,43 @@ const getSingleContentTypes = async ( * @param res - The response object. * @returns A Promise that resolves to void. */ -const getSingleGlobalField = async(req: Request, res: Response): Promise => { +const getSingleGlobalField = async ( + req: Request, + res: Response +): Promise => { const resp = await contentMapperService.getSingleGlobalField(req); res.status(201).json(resp); -} +}; -/** -* update content mapping details a project. -* -* @param req - The request object. -* @param res - The response object. -* @returns A Promise that resolves to void. -*/ -const updateContentMapper = async (req: Request, res: Response): Promise => { +/** + * update content mapping details a project. + * + * @param req - The request object. + * @param res - The response object. + * @returns A Promise that resolves to void. + */ +const updateContentMapper = async ( + req: Request, + res: Response +): Promise => { const project = await contentMapperService.updateContentMapper(req); res.status(project.status).json(project); - } +}; + +/** + * Retrieves existing taxonomies from both source and destination. + * + * @param {Request} req - The request object. + * @param {Response} res - The response object. + * @returns {Promise} - A promise that resolves when the operation is complete. + */ +const getExistingTaxonomies = async ( + req: Request, + res: Response +): Promise => { + const resp = await contentMapperService.getExistingTaxonomies(req); + res.status(resp?.status || 200).json(resp); +}; export const contentMapperController = { getContentTypes, @@ -158,8 +179,9 @@ export const contentMapperController = { resetContentType, // removeMapping, getSingleContentTypes, + getExistingTaxonomies, removeContentMapper, updateContentMapper, getExistingGlobalFields, - getSingleGlobalField + getSingleGlobalField, }; diff --git a/api/src/helper/index.ts b/api/src/helper/index.ts new file mode 100644 index 000000000..109066dcc --- /dev/null +++ b/api/src/helper/index.ts @@ -0,0 +1,57 @@ +import mysql from 'mysql2'; +import customLogger from '../utils/custom-logger.utils.js'; + +const createDbConnection = async (config: any, projectId: string = '', stackId: string = ''): Promise => { + try { + // 🔍 DEBUG: Log config received in helper + console.info(`🔍 helper/index.ts createDbConnection - Received config:`, { + host: config?.host, + user: config?.user, + database: config?.database, + port: config?.port, + hasPassword: !!config?.password + }); + + // Create the connection with config values + const connection = mysql.createConnection({ + host: config?.host, + user: config?.user, + password: config?.password, + database: config?.database, + port: Number(config?.port) + }); + + // Test the connection by wrapping the connect method in a promise + return new Promise((resolve, reject) => { + connection.connect(async (err) => { + if (err) { + await customLogger(projectId, stackId, 'error', `Database connection failed: ${err.message}`); + reject(err); + return; + } + + await customLogger(projectId, stackId, 'info', 'Database connection established successfully'); + resolve(connection); + }); + }); + } catch (error: any) { + await customLogger(projectId, stackId, 'error', `Failed to create database connection: ${error.message}`); + return null; + } +}; + +// Usage example +const getDbConnection = async (config: any, projectId: string = '', stackId: string = '') => { + try { + const connection = await createDbConnection(config, projectId, stackId); + if (!connection) { + throw new Error('Could not establish database connection'); + } + return connection; + } catch (error: any) { + await customLogger(projectId, stackId, 'error', `Database connection error: ${error.message}`); + throw error; // Re-throw so caller can handle it + } +}; + +export { createDbConnection, getDbConnection }; \ No newline at end of file diff --git a/api/src/models/project-lowdb.ts b/api/src/models/project-lowdb.ts index e995da368..fd9b09026 100644 --- a/api/src/models/project-lowdb.ts +++ b/api/src/models/project-lowdb.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { JSONFile } from "lowdb/node"; -import LowWithLodash from "../utils/lowdb-lodash.utils.js"; +import { JSONFile } from 'lowdb/node'; +import LowWithLodash from '../utils/lowdb-lodash.utils.js'; /** * Represents the LegacyCMS object. @@ -23,6 +23,18 @@ interface LegacyCMS { bucketName: string; buketKey: string; }; + mySQLDetails: { + host: string; + user: string; + password?: string; + database: string; + port?: number; + }; + assetsConfig?: { + base_url?: string; + public_path?: string; + }; + is_sql: boolean; file_path: string; is_fileValid: boolean; is_localPath: boolean; @@ -74,8 +86,13 @@ interface Project { mapperKeys: {}; extract_path: string; isMigrationStarted: boolean; - isMigrationCompleted:boolean; + isMigrationCompleted: boolean; migration_execution: boolean; + taxonomies?: any[]; // Taxonomies from source CMS + source_locales?: string[]; // Source locales from legacy CMS + master_locale?: Record; // Master locale mapping { source: destination } + locales?: Record; // Non-master locale mappings { source: destination } + localeMapping?: Record; // Direct locale mapping from UI } interface ProjectDocument { @@ -88,8 +105,10 @@ const defaultData: ProjectDocument = { projects: [] }; * Represents the database instance for the project. */ const db = new LowWithLodash( - new JSONFile(path.join(process.cwd(), "database", "project.json")), + new JSONFile( + path.join(process.cwd(), 'database', 'project.json') + ), defaultData ); -export default db; \ No newline at end of file +export default db; diff --git a/api/src/routes/contentMapper.routes.ts b/api/src/routes/contentMapper.routes.ts index d9fbfd8c1..f2464d6fb 100644 --- a/api/src/routes/contentMapper.routes.ts +++ b/api/src/routes/contentMapper.routes.ts @@ -1,6 +1,6 @@ -import express from "express"; -import { contentMapperController } from "../controllers/projects.contentMapper.controller.js"; -import { asyncRouter } from "../utils/async-router.utils.js"; +import express from 'express'; +import { contentMapperController } from '../controllers/projects.contentMapper.controller.js'; +import { asyncRouter } from '../utils/async-router.utils.js'; const router = express.Router({ mergeParams: true }); @@ -9,7 +9,7 @@ const router = express.Router({ mergeParams: true }); * @route POST /createDummyData/:projectId */ router.post( - "/createDummyData/:projectId", + '/createDummyData/:projectId', asyncRouter(contentMapperController.putTestData) ); @@ -18,7 +18,7 @@ router.post( * @route GET /contentTypes/:projectId/:skip/:limit/:searchText? */ router.get( - "/contentTypes/:projectId/:skip/:limit/:searchText?", + '/contentTypes/:projectId/:skip/:limit/:searchText?', asyncRouter(contentMapperController.getContentTypes) ); @@ -27,7 +27,7 @@ router.get( * @route GET /fieldMapping/:contentTypeId/:skip/:limit/:searchText? */ router.get( - "/fieldMapping/:projectId/:contentTypeId/:skip/:limit/:searchText?", + '/fieldMapping/:projectId/:contentTypeId/:skip/:limit/:searchText?', asyncRouter(contentMapperController.getFieldMapping) ); @@ -36,7 +36,7 @@ router.get( * @route GET /:projectId */ router.get( - "/:projectId/contentTypes/:contentTypeUid?", + '/:projectId/contentTypes/:contentTypeUid?', asyncRouter(contentMapperController.getExistingContentTypes) ); @@ -45,16 +45,25 @@ router.get( * @route GET /:projectId */ router.get( - "/:projectId/globalFields/:globalFieldUid?", + '/:projectId/globalFields/:globalFieldUid?', asyncRouter(contentMapperController.getExistingGlobalFields) ); +/** + * Get Existing Taxonomies from source and destination + * @route GET /:projectId/taxonomies + */ +router.get( + '/:projectId/taxonomies', + asyncRouter(contentMapperController.getExistingTaxonomies) +); + /** * Update FieldMapping or contentType * @route PUT /contentTypes/:orgId/:projectId/:contentTypeId */ router.put( - "/contentTypes/:orgId/:projectId/:contentTypeId", + '/contentTypes/:orgId/:projectId/:contentTypeId', asyncRouter(contentMapperController.putContentTypeFields) ); @@ -63,7 +72,7 @@ router.put( * @route PUT /resetFields/:orgId/:projectId/:contentTypeId */ router.put( - "/resetFields/:orgId/:projectId/:contentTypeId", + '/resetFields/:orgId/:projectId/:contentTypeId', asyncRouter(contentMapperController.resetContentType) ); @@ -81,7 +90,7 @@ router.put( * @route GET /:orgId/:projectId/content-mapper */ router.get( - "/:orgId/:projectId/content-mapper", + '/:orgId/:projectId/content-mapper', asyncRouter(contentMapperController.removeContentMapper) ); @@ -89,7 +98,10 @@ router.get( * Update content mapper * @route GET /:orgId/:projectId */ -router.patch("/:orgId/:projectId/mapper_keys", asyncRouter(contentMapperController.updateContentMapper)); +router.patch( + '/:orgId/:projectId/mapper_keys', + asyncRouter(contentMapperController.updateContentMapper) +); /** * Get Single Global Field data diff --git a/api/src/services/contentMapper.service.ts b/api/src/services/contentMapper.service.ts index 13692542a..c76cfa55e 100644 --- a/api/src/services/contentMapper.service.ts +++ b/api/src/services/contentMapper.service.ts @@ -1,9 +1,11 @@ -import { Request } from "express"; -import { getLogMessage, isEmpty, safePromise } from "../utils/index.js"; +import { Request } from 'express'; +import fs from 'fs'; +import path from 'path'; +import { getLogMessage, isEmpty, safePromise } from '../utils/index.js'; import { BadRequestError, ExceptionFunction, -} from "../utils/custom-errors.utils.js"; +} from '../utils/custom-errors.utils.js'; import { HTTP_TEXTS, HTTP_CODES, @@ -11,18 +13,19 @@ import { NEW_PROJECT_STATUS, CONTENT_TYPE_STATUS, VALIDATION_ERRORS, -} from "../constants/index.js"; -import logger from "../utils/logger.js"; -import { config } from "../config/index.js"; -import https from "../utils/https.utils.js"; -import getAuthtoken from "../utils/auth.utils.js"; -import getProjectUtil from "../utils/get-project.utils.js"; -import fetchAllPaginatedData from "../utils/pagination.utils.js"; -import ProjectModelLowdb from "../models/project-lowdb.js"; -import FieldMapperModel from "../models/FieldMapper.js"; -import { v4 as uuidv4 } from "uuid"; -import ContentTypesMapperModelLowdb from "../models/contentTypesMapper-lowdb.js"; -import { ContentTypesMapper } from "../models/contentTypesMapper-lowdb.js"; + MIGRATION_DATA_CONFIG, +} from '../constants/index.js'; +import logger from '../utils/logger.js'; +import { config } from '../config/index.js'; +import https from '../utils/https.utils.js'; +import getAuthtoken from '../utils/auth.utils.js'; +import getProjectUtil from '../utils/get-project.utils.js'; +import fetchAllPaginatedData from '../utils/pagination.utils.js'; +import ProjectModelLowdb from '../models/project-lowdb.js'; +import FieldMapperModel from '../models/FieldMapper.js'; +import { v4 as uuidv4 } from 'uuid'; +import ContentTypesMapperModelLowdb from '../models/contentTypesMapper-lowdb.js'; +import { ContentTypesMapper } from '../models/contentTypesMapper-lowdb.js'; // Developer service to create dummy contentmapping data /** @@ -36,7 +39,6 @@ const putTestData = async (req: Request) => { const contentTypes = req.body.contentTypes; try { - /* this code snippet is iterating over an array called contentTypes and transforming each element by adding a unique identifier (id) if it doesn't already exist. @@ -49,7 +51,7 @@ const putTestData = async (req: Request) => { } const contentIds: any[] = []; const contentType = contentTypes.map((item: any) => { - const id = item?.id?.replace(/[{}]/g, "")?.toLowerCase() || uuidv4(); + const id = item?.id?.replace(/[{}]/g, '')?.toLowerCase() || uuidv4(); item.id = id; contentIds.push(id); return { ...item, id, projectId }; @@ -63,8 +65,6 @@ const putTestData = async (req: Request) => { }); }); - - /* this code snippet iterates over an array of contentTypes and performs some operations on each element. @@ -77,12 +77,46 @@ const putTestData = async (req: Request) => { await FieldMapperModel.read(); contentTypes.map((type: any, index: any) => { const fieldIds: string[] = []; - const fields = Array?.isArray?.(type?.fieldMapping) ? type?.fieldMapping?.filter((field: any) => field)?.map?.((field: any) => { - const id = field?.id ? field?.id?.replace(/[{}]/g, "")?.toLowerCase() : uuidv4(); - field.id = id; - fieldIds.push(id); - return { id, projectId, contentTypeId: type?.id, isDeleted: false, ...field }; - }) : []; + const fields = Array?.isArray?.(type?.fieldMapping) + ? type?.fieldMapping + ?.filter((field: any) => field) + ?.map?.((field: any) => { + const id = field?.id + ? field?.id?.replace(/[{}]/g, '')?.toLowerCase() + : uuidv4(); + field.id = id; + fieldIds.push(id); + + // Initialize referenceTo from advanced data (upload-api) + let referenceTo: string[] = []; + if (field?.backupFieldType === 'reference') { + // Reference fields use embedObjects OR reference_to (filter out profile) + const rawReferences = + field?.advanced?.embedObjects || + field?.advanced?.reference_to || + []; + referenceTo = rawReferences.filter( + (ref: string) => ref && ref.toLowerCase() !== 'profile' + ); + } else if ( + field?.backupFieldType === 'taxonomy' && + field?.advanced?.taxonomies + ) { + referenceTo = field.advanced.taxonomies.map( + (t: any) => t.taxonomy_uid || t + ); + } + + return { + id, + projectId, + contentTypeId: type?.id, + isDeleted: false, + ...field, + referenceTo, // Initialize referenceTo field + }; + }) + : []; FieldMapperModel.update((data: any) => { data.field_mapper = [...(data?.field_mapper ?? []), ...(fields ?? [])]; @@ -97,8 +131,6 @@ const putTestData = async (req: Request) => { } }); - - await ContentTypesMapperModelLowdb.update((data: any) => { data.ContentTypesMappers = [ ...(data?.ContentTypesMappers ?? []), @@ -108,36 +140,71 @@ const putTestData = async (req: Request) => { await ProjectModelLowdb.read(); const index = ProjectModelLowdb.chain - .get("projects") + .get('projects') .findIndex({ id: projectId }) .value(); if (index > -1 && contentIds?.length) { ProjectModelLowdb.data.projects[index].content_mapper = contentIds; - ProjectModelLowdb.data.projects[index].extract_path = req?.body?.extractPath; + ProjectModelLowdb.data.projects[index].extract_path = + req?.body?.extractPath; + + // Update assetsConfig and mySQLDetails if provided + if ( + req?.body?.assetsConfig && + ProjectModelLowdb.data.projects[index].legacy_cms + ) { + ( + ProjectModelLowdb.data.projects[index].legacy_cms as any + ).assetsConfig = req.body.assetsConfig; + } else { + } + + if ( + req?.body?.mySQLDetails && + ProjectModelLowdb.data.projects[index].legacy_cms + ) { + ( + ProjectModelLowdb.data.projects[index].legacy_cms as any + ).mySQLDetails = req.body.mySQLDetails; + } + + // Store taxonomies if provided + if (req?.body?.taxonomies && Array.isArray(req.body.taxonomies)) { + ProjectModelLowdb.data.projects[index].taxonomies = req.body.taxonomies; + logger.info( + `✓ Stored ${req.body.taxonomies.length} taxonomies for project ${projectId}` + ); + } else { + } + await ProjectModelLowdb.write(); + + // Re-read from disk to verify persistence + await ProjectModelLowdb.read(); + const verifyIndex = ProjectModelLowdb.chain + .get('projects') + .findIndex({ id: projectId }) + .value(); + const verifyProject = ProjectModelLowdb.data.projects[verifyIndex]; } else { throw new BadRequestError(HTTP_TEXTS.CONTENT_TYPE_NOT_FOUND); } const pData = ProjectModelLowdb.chain - .get("projects") + .get('projects') .find({ id: projectId }) .value(); return { status: HTTP_CODES?.OK, - data: pData - } - + data: pData, + }; } catch (error: any) { - throw new ExceptionFunction( error?.message || HTTP_TEXTS.INTERNAL_ERROR, error?.statusCode || error?.status || HTTP_CODES.SERVER_ERROR ); - } - }; /** @@ -146,7 +213,7 @@ const putTestData = async (req: Request) => { * @returns An object containing the total count and the array of content types. */ const getContentTypes = async (req: Request) => { - const sourceFn = "getContentTypes"; + const sourceFn = 'getContentTypes'; const projectId = req?.params?.projectId; const skip: any = req?.params?.skip; const limit: any = req?.params?.limit; @@ -157,7 +224,7 @@ const getContentTypes = async (req: Request) => { try { await ProjectModelLowdb.read(); const projectDetails = ProjectModelLowdb.chain - .get("projects") + .get('projects') .find({ id: projectId }) .value(); @@ -177,7 +244,7 @@ const getContentTypes = async (req: Request) => { const content_mapper: any = []; contentMapperId.map((data: any) => { const contentMapperData = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .find({ id: data, projectId: projectId }) .value(); content_mapper.push(contentMapperData); @@ -207,15 +274,14 @@ const getContentTypes = async (req: Request) => { return { status: HTTP_CODES?.OK, count: totalCount, - contentTypes: result + contentTypes: result, }; - } catch (error: any) { // Log error message logger.error( getLogMessage( sourceFn, - "Error occurred while while getting contentTypes of projects", + 'Error occurred while while getting contentTypes of projects', error ) ); @@ -224,10 +290,7 @@ const getContentTypes = async (req: Request) => { error?.message || HTTP_TEXTS.INTERNAL_ERROR, error?.statusCode || error?.status || HTTP_CODES.SERVER_ERROR ); - } - - }; /** @@ -237,7 +300,7 @@ const getContentTypes = async (req: Request) => { * @throws BadRequestError if the content type is not found. */ const getFieldMapping = async (req: Request) => { - const srcFunc = "getFieldMapping"; + const srcFunc = 'getFieldMapping'; const contentTypeId = req?.params?.contentTypeId; const projectId = req?.params?.projectId; const skip: any = req?.params?.skip; @@ -252,7 +315,7 @@ const getFieldMapping = async (req: Request) => { await ContentTypesMapperModelLowdb.read(); const contentType = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .find({ id: contentTypeId, projectId: projectId }) .value(); @@ -268,8 +331,12 @@ const getFieldMapping = async (req: Request) => { await FieldMapperModel.read(); const fieldData = contentType?.fieldMapping?.map?.((fields: any) => { const fieldMapper = FieldMapperModel.chain - .get("field_mapper") - .find({ id: fields, projectId: projectId, contentTypeId: contentTypeId }) + .get('field_mapper') + .find({ + id: fields, + projectId: projectId, + contentTypeId: contentTypeId, + }) .value(); return fieldMapper; @@ -299,15 +366,14 @@ const getFieldMapping = async (req: Request) => { return { status: HTTP_CODES?.OK, count: totalCount, - fieldMapping: result + fieldMapping: result, }; - } catch (error: any) { // Log error message logger.error( getLogMessage( srcFunc, - "Error occurred while getting field mapping of projects", + 'Error occurred while getting field mapping of projects', error ) ); @@ -316,9 +382,7 @@ const getFieldMapping = async (req: Request) => { error?.message || HTTP_TEXTS.INTERNAL_ERROR, error?.statusCode || error?.status || HTTP_CODES.SERVER_ERROR ); - } - }; /** @@ -339,7 +403,7 @@ const getExistingContentTypes = async (req: Request) => { await ProjectModelLowdb.read(); const project = ProjectModelLowdb.chain - .get("projects") + .get('projects') .find({ id: projectId }) .value(); const stackId = project?.destination_stack_id; @@ -381,7 +445,6 @@ const getExistingContentTypes = async (req: Request) => { }) ); - selectedContentType = { title: res?.data?.content_type?.title, uid: res?.data?.content_type?.uid, @@ -426,7 +489,10 @@ const getExistingGlobalFields = async (req: Request) => { } try { - const authtoken = await getAuthtoken(tokenPayload.region, tokenPayload.user_id); + const authtoken = await getAuthtoken( + tokenPayload.region, + tokenPayload.user_id + ); await ProjectModelLowdb.read(); const project = ProjectModelLowdb.chain @@ -450,7 +516,9 @@ const getExistingGlobalFields = async (req: Request) => { }; } - const baseUrl = `${config.CS_API[tokenPayload.region as keyof typeof config.CS_API]}/global_fields`; + const baseUrl = `${ + config.CS_API[tokenPayload.region as keyof typeof config.CS_API] + }/global_fields`; const headers = { api_key: stackId, authtoken, @@ -458,7 +526,13 @@ const getExistingGlobalFields = async (req: Request) => { // Step 1: Fetch the updated list of all global fields - const globalFields = await fetchAllPaginatedData(baseUrl, headers, 100, 'getExistingGlobalFields', 'global_fields'); + const globalFields = await fetchAllPaginatedData( + baseUrl, + headers, + 100, + 'getExistingGlobalFields', + 'global_fields' + ); const processedGlobalFields = globalFields.map((global: any) => ({ title: global.title, @@ -510,7 +584,7 @@ const getExistingGlobalFields = async (req: Request) => { * @throws ExceptionFunction if an error occurs while updating the content type. */ const updateContentType = async (req: Request) => { - const srcFun = "updateContentType"; + const srcFun = 'updateContentType'; const { orgId, projectId, contentTypeId } = req.params; const { contentTypeData, token_payload } = req.body; const fieldMapping = contentTypeData?.fieldMapping; @@ -565,7 +639,7 @@ const updateContentType = async (req: Request) => { try { await ContentTypesMapperModelLowdb.read(); const updateIndex = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .findIndex({ id: contentTypeId, projectId: projectId }) .value(); @@ -573,16 +647,16 @@ const updateContentType = async (req: Request) => { for (const field of fieldMapping) { if ( !field.contentstackFieldType || - field.contentstackFieldType === "" || - field.contentstackFieldType === "No matches found" || - field.contentstackFieldUid === "" + field.contentstackFieldType === '' || + field.contentstackFieldType === 'No matches found' || + field.contentstackFieldUid === '' ) { logger.error( getLogMessage( srcFun, `${VALIDATION_ERRORS.STRING_REQUIRED.replace( - "$", - "contentstackFieldType or contentstackFieldUid" + '$', + 'contentstackFieldType or contentstackFieldUid' )}` ) ); @@ -593,15 +667,15 @@ const updateContentType = async (req: Request) => { await ContentTypesMapperModelLowdb.read(); const updatedContentType = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .find({ id: contentTypeId, projectId: projectId }) .value(); return { data: updatedContentType, status: 400, message: `${VALIDATION_ERRORS.STRING_REQUIRED.replace( - "$", - "contentstackFieldType or contentstackFieldUid" + '$', + 'contentstackFieldType or contentstackFieldUid' )}`, }; } @@ -644,18 +718,30 @@ const updateContentType = async (req: Request) => { if (Array?.isArray?.(fieldMapping) && !isEmpty(fieldMapping)) { await FieldMapperModel.read(); + + // Log reference/taxonomy fields being updated + const refTaxFields = fieldMapping.filter( + (f: any) => + (f.backupFieldType === 'reference' || + f.backupFieldType === 'taxonomy') && + f.referenceTo && + f.referenceTo.length > 0 + ); + if (refTaxFields.length > 0) { + refTaxFields.forEach((f: any) => {}); + } + fieldMapping.forEach((field: any) => { const fieldIndex = FieldMapperModel.data.field_mapper.findIndex( - (f: any) => f?.id === field?.id && f?.contentTypeId === field?.contentTypeId + (f: any) => + f?.id === field?.id && f?.contentTypeId === field?.contentTypeId ); - if (fieldIndex > -1 && field?.contentstackFieldType !== "") { + if (fieldIndex > -1 && field?.contentstackFieldType !== '') { FieldMapperModel.update((data: any) => { const existingField = data?.field_mapper?.[fieldIndex]; const preservedInitial = existingField?.advanced?.initial; - data.field_mapper[fieldIndex] = field; - if (preservedInitial && field?.advanced) { data.field_mapper[fieldIndex].advanced.initial = preservedInitial; @@ -671,7 +757,7 @@ const updateContentType = async (req: Request) => { // Fetch and return updated content type await ContentTypesMapperModelLowdb.read(); const updatedContentType = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .find({ id: contentTypeId, projectId: projectId }) .value(); @@ -704,7 +790,7 @@ const updateContentType = async (req: Request) => { * @throws {ExceptionFunction} If an error occurs while resetting the field mapping. */ const resetToInitialMapping = async (req: Request) => { - const srcFunc = "resetToInitialMapping"; + const srcFunc = 'resetToInitialMapping'; const { orgId, projectId, contentTypeId } = req.params; const { token_payload } = req.body; @@ -743,15 +829,15 @@ const resetToInitialMapping = async (req: Request) => { await ContentTypesMapperModelLowdb.read(); const contentTypeData = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .find({ id: contentTypeId, projectId: projectId }) .value(); await FieldMapperModel.read(); const fieldMappingData = contentTypeData.fieldMapping.map((itemId: any) => { const fieldData = FieldMapperModel.chain - .get("field_mapper") - .find({ id: itemId, projectId: projectId, contentTypeId: contentTypeId}) + .get('field_mapper') + .find({ id: itemId, projectId: projectId, contentTypeId: contentTypeId }) .value(); return fieldData; }); @@ -771,28 +857,30 @@ const resetToInitialMapping = async (req: Request) => { //await FieldMapperModel.read(); (fieldMappingData || []).forEach((field: any) => { const fieldIndex = FieldMapperModel.data.field_mapper.findIndex( - (f: any) => f?.id === field?.id && f?.projectId === projectId && f?.contentTypeId === contentTypeId + (f: any) => + f?.id === field?.id && + f?.projectId === projectId && + f?.contentTypeId === contentTypeId ); if (fieldIndex > -1) { FieldMapperModel.update((data: any) => { - - data.field_mapper[fieldIndex] = { - ...field, - contentstackField: field?.otherCmsField, - contentstackFieldUid: field?.backupFieldUid, - contentstackFieldType: field?.backupFieldType, - advanced: { - ...field?.advanced?.initial, - initial: field?.advanced?.initial, - } - } + data.field_mapper[fieldIndex] = { + ...field, + contentstackField: field?.otherCmsField, + contentstackFieldUid: field?.backupFieldUid, + contentstackFieldType: field?.backupFieldType, + advanced: { + ...field?.advanced?.initial, + initial: field?.advanced?.initial, + }, + }; }); } }); } const contentIndex = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .findIndex({ id: contentTypeId, projectId: projectId }) .value(); // if (contentIndex > -1) { @@ -809,9 +897,8 @@ const resetToInitialMapping = async (req: Request) => { return { status: HTTP_CODES?.OK, message: HTTP_TEXTS.RESET_CONTENT_MAPPING, - data: contentTypeData + data: contentTypeData, }; - } catch (error: any) { logger.error( getLogMessage( @@ -836,11 +923,11 @@ const resetToInitialMapping = async (req: Request) => { * @throws {ExceptionFunction} If an error occurs while resetting the content types mapping. */ const resetAllContentTypesMapping = async (projectId: string) => { - const srcFunc = "resetAllContentTypesMapping"; + const srcFunc = 'resetAllContentTypesMapping'; await ProjectModelLowdb.read(); const projectDetails = ProjectModelLowdb.chain - .get("projects") + .get('projects') .find({ id: projectId }) .value(); @@ -866,7 +953,7 @@ const resetAllContentTypesMapping = async (projectId: string) => { await ContentTypesMapperModelLowdb.read(); const cData = contentMapperId.map((cId: any) => { const contentTypeData = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .find({ id: cId, projectId: projectId }) .value(); return contentTypeData; @@ -879,11 +966,11 @@ const resetAllContentTypesMapping = async (projectId: string) => { for (const field of contentType.fieldMapping) { await FieldMapperModel.read(); const fieldData = FieldMapperModel.chain - .get("field_mapper") + .get('field_mapper') .find({ id: field, projectId: projectId }) .value(); const fieldIndex = FieldMapperModel.chain - .get("field_mapper") + .get('field_mapper') .findIndex({ id: field, projectId: projectId }) .value(); @@ -891,8 +978,8 @@ const resetAllContentTypesMapping = async (projectId: string) => { await FieldMapperModel.update((fData: any) => { fData.field_mapper[fieldIndex] = { ...fieldData, - contentstackField: "", - contentstackFieldUid: "", + contentstackField: '', + contentstackFieldUid: '', contentstackFieldType: fieldData.backupFieldType, }; }); @@ -902,13 +989,13 @@ const resetAllContentTypesMapping = async (projectId: string) => { await ContentTypesMapperModelLowdb.read(); if (!isEmpty(contentType?.id)) { const cIndex = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .findIndex({ id: contentType?.id, projectId: projectId }) .value(); if (cIndex > -1) { await ContentTypesMapperModelLowdb.update((data: any) => { - data.ContentTypesMappers[cIndex].contentstackTitle = ""; - data.ContentTypesMappers[cIndex].contentstackUid = ""; + data.ContentTypesMappers[cIndex].contentstackTitle = ''; + data.ContentTypesMappers[cIndex].contentstackUid = ''; }); } } @@ -938,10 +1025,10 @@ const resetAllContentTypesMapping = async (projectId: string) => { * @throws {ExceptionFunction} If an error occurs while removing the content mapping. */ const removeMapping = async (projectId: string) => { - const srcFunc = "removeMapping"; + const srcFunc = 'removeMapping'; await ProjectModelLowdb.read(); const projectDetails = ProjectModelLowdb.chain - .get("projects") + .get('projects') .find({ id: projectId }) .value(); @@ -957,7 +1044,7 @@ const removeMapping = async (projectId: string) => { await ContentTypesMapperModelLowdb.read(); const cData = projectDetails?.content_mapper.map((cId: any) => { const contentTypeData = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .find({ id: cId, projectId: projectId }) .value(); return contentTypeData; @@ -972,7 +1059,7 @@ const removeMapping = async (projectId: string) => { for (const field of contentType.fieldMapping) { await FieldMapperModel.read(); const fieldIndex = FieldMapperModel.chain - .get("field_mapper") + .get('field_mapper') .findIndex({ id: field, projectId: projectId }) .value(); if (fieldIndex > -1) { @@ -985,7 +1072,7 @@ const removeMapping = async (projectId: string) => { await ContentTypesMapperModelLowdb.read(); if (!isEmpty(contentType?.id)) { const cIndex = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .findIndex({ id: contentType?.id, projectId: projectId }) .value(); if (cIndex > -1) { @@ -998,7 +1085,7 @@ const removeMapping = async (projectId: string) => { await ProjectModelLowdb.read(); const projectIndex = ProjectModelLowdb.chain - .get("projects") + .get('projects') .findIndex({ id: projectId }) .value(); @@ -1039,14 +1126,14 @@ const getSingleContentTypes = async (req: Request) => { ); await ProjectModelLowdb.read(); const project = ProjectModelLowdb.chain - .get("projects") + .get('projects') .find({ id: projectId }) .value(); const stackId = project?.destination_stack_id; const [err, res] = await safePromise( https({ - method: "GET", + method: 'GET', url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/content_types/${contentTypeUID}`, @@ -1066,7 +1153,7 @@ const getSingleContentTypes = async (req: Request) => { return { title: res?.data?.content_type?.title, uid: res?.data?.content_type?.uid, - schema: res?.data?.content_type?.schema + schema: res?.data?.content_type?.schema, }; }; @@ -1086,14 +1173,14 @@ const getSingleGlobalField = async (req: Request) => { ); await ProjectModelLowdb.read(); const project = ProjectModelLowdb.chain - .get("projects") + .get('projects') .find({ id: projectId }) .value(); const stackId = project?.destination_stack_id; const [err, res] = await safePromise( https({ - method: "GET", + method: 'GET', url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/global_fields/${globalFieldUID}`, @@ -1113,9 +1200,9 @@ const getSingleGlobalField = async (req: Request) => { return { title: res?.data?.global_field?.title, uid: res?.data?.global_field?.uid, - schema: res?.data?.global_field?.schema + schema: res?.data?.global_field?.schema, }; -} +}; /** * Removes the content mapping for a project. * @param req - The request object containing the project ID. @@ -1125,10 +1212,10 @@ const getSingleGlobalField = async (req: Request) => { */ const removeContentMapper = async (req: Request) => { const projectId = req?.params?.projectId; - const srcFunc = "removeMapping"; + const srcFunc = 'removeMapping'; await ProjectModelLowdb.read(); const projectDetails = ProjectModelLowdb.chain - .get("projects") + .get('projects') .find({ id: projectId }) .value(); @@ -1146,7 +1233,7 @@ const removeContentMapper = async (req: Request) => { (cId: string) => { const contentTypeData: ContentTypesMapper = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .find({ id: cId, projectId: projectId }) .value(); return contentTypeData; @@ -1162,7 +1249,7 @@ const removeContentMapper = async (req: Request) => { for (const field of contentType.fieldMapping) { await FieldMapperModel.read(); const fieldIndex = FieldMapperModel.chain - .get("field_mapper") + .get('field_mapper') .findIndex({ id: field, projectId: projectId }) .value(); if (fieldIndex > -1) { @@ -1175,7 +1262,7 @@ const removeContentMapper = async (req: Request) => { await ContentTypesMapperModelLowdb.read(); if (!isEmpty(contentType?.id)) { const cIndex = ContentTypesMapperModelLowdb.chain - .get("ContentTypesMappers") + .get('ContentTypesMappers') .findIndex({ id: contentType?.id, projectId: projectId }) .value(); if (cIndex > -1) { @@ -1188,7 +1275,7 @@ const removeContentMapper = async (req: Request) => { await ProjectModelLowdb.read(); const projectIndex = ProjectModelLowdb.chain - .get("projects") + .get('projects') .findIndex({ id: projectId }) .value(); @@ -1225,7 +1312,7 @@ const removeContentMapper = async (req: Request) => { const updateContentMapper = async (req: Request) => { const { orgId, projectId } = req.params; const { token_payload, content_mapper } = req.body; - const srcFunc = "updateContentMapper"; + const srcFunc = 'updateContentMapper'; await ProjectModelLowdb.read(); const projectIndex = (await getProjectUtil( @@ -1275,6 +1362,159 @@ const updateContentMapper = async (req: Request) => { } }; +/** + * Retrieves existing taxonomies from the destination Contentstack stack + * and source taxonomy data from migration files. + * @param req - The request object containing the project ID and token payload. + * @returns An object containing source taxonomies and destination taxonomies. + */ +const getExistingTaxonomies = async (req: Request) => { + const projectId = req?.params?.projectId; + const { token_payload } = req.body; + + try { + // Get project details + await ProjectModelLowdb.read(); + const project = ProjectModelLowdb.chain + .get('projects') + .find({ id: projectId }) + .value(); + + if (!project) { + return { + data: 'Project not found', + status: 404, + }; + } + + const stackId = project?.destination_stack_id; + + // Step 1: Get source taxonomies from project database (sent by upload-api) + let sourceTaxonomies: any[] = []; + + if (project?.taxonomies && Array.isArray(project.taxonomies)) { + // Taxonomies stored in project database (sent from upload-api during validation) + sourceTaxonomies = project.taxonomies.map((taxonomy: any) => ({ + uid: taxonomy.uid, + name: taxonomy.name || taxonomy.uid, + description: taxonomy.description || '', + source: 'source_cms', + })); + logger.info( + `✓ Found ${sourceTaxonomies.length} source taxonomies in project database` + ); + } else { + // Fallback: Try reading from migration-data files + logger.warn( + 'No taxonomies found in project database, checking fallback paths...' + ); + + // Path 1: Check api/migration-data (processed taxonomies) + // Sanitize stackId to prevent path traversal + const sanitizedStackId = path.basename(stackId); + + const apiMigrationDataPath = path.join( + MIGRATION_DATA_CONFIG.DATA, + sanitizedStackId, + MIGRATION_DATA_CONFIG.TAXONOMIES_DIR_NAME, + MIGRATION_DATA_CONFIG.TAXONOMIES_FILE_NAME + ); + + // Resolve to absolute path and validate it's within allowed directory + const baseDirectory = path.resolve(MIGRATION_DATA_CONFIG.DATA); + const resolvedPath = path.resolve(apiMigrationDataPath); + + // Ensure the resolved path is within the base directory + if (!resolvedPath.startsWith(baseDirectory)) { + logger.error( + `Path traversal attempt detected: ${resolvedPath} is outside ${baseDirectory}` + ); + throw new BadRequestError('Invalid file path'); + } + + try { + if (fs.existsSync(resolvedPath)) { + const taxonomiesData = await fs.promises.readFile( + resolvedPath, + 'utf8' + ); + const taxonomiesObject = JSON.parse(taxonomiesData); + + // Convert object to array with proper structure + const apiTaxonomies = Object.entries(taxonomiesObject).map( + ([uid, data]: [string, any]) => ({ + uid: data.uid || uid, + name: data.name || uid, + description: data.description || '', + source: 'source_cms', + }) + ); + sourceTaxonomies.push(...apiTaxonomies); + } + } catch (fileError: any) { + logger.error( + `Error reading migration-data taxonomies: ${fileError.message}` + ); + } + } + + // Step 2: Get destination taxonomies from Contentstack (if stack exists) + let destinationTaxonomies: any[] = []; + + if (token_payload?.region && token_payload?.user_id && stackId) { + try { + const authtoken = await getAuthtoken( + token_payload.region, + token_payload.user_id + ); + + const baseUrl = `${config.CS_API[ + token_payload?.region as keyof typeof config.CS_API + ]!}/taxonomies`; + + const headers = { + api_key: stackId, + authtoken, + }; + + // Fetch taxonomies from Contentstack + const taxonomies = await fetchAllPaginatedData( + baseUrl, + headers, + 100, + 'getExistingTaxonomies', + 'taxonomies' + ); + + destinationTaxonomies = taxonomies.map((taxonomy: any) => ({ + uid: taxonomy.uid, + name: taxonomy.name, + description: taxonomy.description || '', + source: 'destination_stack', + })); + } catch (apiError: any) { + logger.error( + `Error fetching destination taxonomies: ${apiError.message}` + ); + } + } + + const response = { + sourceTaxonomies, + destinationTaxonomies, + status: 201, + }; + + return response; + } catch (error: any) { + logger.error(`Error in getExistingTaxonomies: ${error.message}`); + return { + data: error.message, + status: error.status || 500, + }; + } +}; + export const contentMapperService = { putTestData, getContentTypes, @@ -1288,5 +1528,6 @@ export const contentMapperService = { getSingleContentTypes, updateContentMapper, getExistingGlobalFields, - getSingleGlobalField + getSingleGlobalField, + getExistingTaxonomies, }; diff --git a/api/src/services/contentful.service.ts b/api/src/services/contentful.service.ts index 2ecb4970f..91e67c7fd 100644 --- a/api/src/services/contentful.service.ts +++ b/api/src/services/contentful.service.ts @@ -749,6 +749,17 @@ const createEnvironment = async (packagePath: any, destination_stack_id: string, */ const createEntry = async (packagePath: any, destination_stack_id: string, projectId: string, contentTypes: any, mapperKeys: any, master_locale: string, project: any): Promise => { const srcFunc = 'createEntry'; + + // 🔍 DEBUG: Log master_locale parameter received + console.info('🔍 Contentful createEntry - master_locale parameter:', { + master_locale, + master_locale_type: typeof master_locale, + master_locale_isLowercase: master_locale === master_locale?.toLowerCase?.(), + master_locale_toLowerCase: master_locale?.toLowerCase?.(), + project_master_locale: project?.master_locale, + project_locales: project?.locales, + }); + try { const entriesSave = path.join(DATA, destination_stack_id, ENTRIES_DIR_NAME); const assetsSave = path.join(DATA, destination_stack_id, ASSETS_DIR_NAME); @@ -757,6 +768,13 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje const entries = JSON.parse(data)?.entries; const content = JSON.parse(data)?.contentTypes; const LocaleMapper = { masterLocale: project?.master_locale ?? LOCALE_MAPPER?.masterLocale, ...project?.locales ?? {} }; + + // 🔍 DEBUG: Log LocaleMapper + console.info('🔍 Contentful createEntry - LocaleMapper:', { + LocaleMapper, + LocaleMapper_masterLocale: LocaleMapper.masterLocale, + LocaleMapper_keys: Object.keys(LocaleMapper), + }); if (entries && entries.length > 0) { const assetId = await readFile(assetsSave, ASSETS_SCHEMA_FILE) ?? []; const entryId = await readFile(path.join(DATA, destination_stack_id, REFERENCES_DIR_NAME), REFERENCES_FILE_NAME); @@ -949,8 +967,37 @@ const createLocale = async (packagePath: string, destination_stack_id: string, p await customLogger(projectId, destination_stack_id, 'error', message); } const fallbackMapLocales: any = { ...project?.master_locale ?? {}, ...project?.locales ?? {} } + + // 🔍 DEBUG: Log Contentful locale data + console.info('🔍 Contentful createLocale - Raw localeData from Contentful:', locales?.map((l: any) => ({ + code: l?.code, + code_type: typeof l?.code, + code_isLowercase: l?.code === l?.code?.toLowerCase(), + fallbackCode: l?.fallbackCode + }))); + console.info('🔍 Contentful createLocale - project.master_locale:', project?.master_locale); + console.info('🔍 Contentful createLocale - project.locales:', project?.locales); + console.info('🔍 Contentful createLocale - fallbackMapLocales:', fallbackMapLocales); + await Promise?.all(locales?.map?.(async (localeData: any) => { + // 🔍 DEBUG: Log each locale processing + console.info(`🔍 Processing Contentful locale:`, { + raw_code: localeData?.code, + raw_code_type: typeof localeData?.code, + raw_code_isLowercase: localeData?.code === localeData?.code?.toLowerCase(), + raw_code_toLowerCase: localeData?.code?.toLowerCase?.(), + }); + const currentMapLocale = getKeyByValue?.(fallbackMapLocales, localeData?.code) ?? `${localeData?.code?.toLowerCase?.()}`; + + // 🔍 DEBUG: Log mapped locale + console.info(`🔍 Contentful locale mapping result:`, { + raw_code: localeData?.code, + currentMapLocale, + currentMapLocale_type: typeof currentMapLocale, + currentMapLocale_isLowercase: currentMapLocale === currentMapLocale?.toLowerCase(), + }); + const title = localeData?.sys?.id; const newLocale: Locale = { code: currentMapLocale, @@ -959,6 +1006,21 @@ const createLocale = async (packagePath: string, destination_stack_id: string, p uid: `${title}`, }; const masterLocaleCode = getKeyByValue(project?.master_locale, localeData?.code); + + // 🔍 DEBUG: Log master locale detection + if (masterLocaleCode !== undefined) { + console.info(`🔍 ✅ Contentful MASTER LOCALE detected:`, { + localeData_code: localeData?.code, + localeData_code_type: typeof localeData?.code, + localeData_code_isLowercase: localeData?.code === localeData?.code?.toLowerCase(), + masterLocaleCode, + masterLocaleCode_type: typeof masterLocaleCode, + masterLocaleCode_isLowercase: masterLocaleCode === masterLocaleCode?.toLowerCase(), + newLocale_code: newLocale.code, + newLocale_code_isLowercase: newLocale.code === newLocale.code?.toLowerCase(), + }); + } + if (masterLocaleCode !== undefined) { msLocale[title] = newLocale; const message = getLogMessage( @@ -982,6 +1044,20 @@ const createLocale = async (packagePath: string, destination_stack_id: string, p localeList[title] = newLocale; })); const masterLocaleData = Object?.values(msLocale)?.[0]; + + // 🔍 DEBUG: Log final master locale + if (masterLocaleData) { + console.info('🔍 Contentful createLocale - Final masterLocaleData:', { + code: masterLocaleData.code, + code_type: typeof masterLocaleData.code, + code_isLowercase: masterLocaleData.code === masterLocaleData.code?.toLowerCase(), + name: masterLocaleData.name, + uid: masterLocaleData.uid + }); + } else { + console.warn('⚠️ Contentful createLocale - No master locale found!'); + } + if (masterLocaleData) { for (const [key, value] of Object.entries(allLocales) ?? {}) { if (value?.code === masterLocaleData?.fallback_locale) { diff --git a/api/src/services/drupal.service.ts b/api/src/services/drupal.service.ts new file mode 100644 index 000000000..9cd1d5f2b --- /dev/null +++ b/api/src/services/drupal.service.ts @@ -0,0 +1,70 @@ +// Import modular Drupal services +import { createAssets } from './drupal/assets.service.js'; +import { createEntry } from './drupal/entries.service.js'; +import { createLocale } from './drupal/locales.service.js'; +import { createRefrence } from './drupal/references.service.js'; +import { createTaxonomy } from './drupal/taxonomy.service.js'; +import { createVersionFile } from './drupal/version.service.js'; +import { createQuery, createQueryConfig } from './drupal/query.service.js'; +import { generateContentTypeSchemas } from './drupal/content-types.service.js'; + +/** + * Drupal migration service with SQL-based data extraction. + * + * All functions use direct database connections to extract data from Drupal + * following the original migration patterns. + * + * IMPORTANT: Run in this order for proper dependency resolution: + * 1. createQuery - Generate dynamic queries from database analysis (MUST RUN FIRST) + * 2. generateContentTypeSchemas - Convert upload-api schema to API content types (MUST RUN AFTER upload-api) + * 3. createAssets - Extract assets first (needed by entries) + * 4. createRefrence - Create reference mappings (needed by entries) + * 5. createTaxonomy - Extract taxonomies (needed by entries for taxonomy references) + * 6. createEntry - Process entries (uses assets, references, and taxonomies) + * 7. createLocale - Create locale configurations + * 8. createVersionFile - Create version metadata file + */ +export const drupalService = { + createQuery, // Generate dynamic queries from database analysis (MUST RUN FIRST) + createQueryConfig, // Helper: Create query configuration file for dynamic SQL + generateContentTypeSchemas, // Convert upload-api schema to API content types (MUST RUN AFTER upload-api) + createAssets: ( + dbConfig: any, + destination_stack_id: string, + projectId: string, + isTest = false, + assetsConfig?: any + ) => { + return createAssets( + dbConfig, + destination_stack_id, + projectId, + assetsConfig?.base_url || '', + assetsConfig?.public_path || '', + isTest + ); + }, + createRefrence, // Create reference mappings for relationships (run before entries) + createTaxonomy, // Extract and process Drupal taxonomies (vocabularies and terms) + createEntry: ( + dbConfig: any, + destination_stack_id: string, + projectId: string, + isTest = false, + masterLocale = 'en-us', + contentTypeMapping: any[] = [], + project: any = null + ) => { + return createEntry( + dbConfig, + destination_stack_id, + projectId, + isTest, + masterLocale, + contentTypeMapping, + project + ); + }, + createLocale, // Create locale configurations + createVersionFile, // Create version metadata file +}; diff --git a/api/src/services/drupal/assets.service.ts b/api/src/services/drupal/assets.service.ts new file mode 100644 index 000000000..24a29eb89 --- /dev/null +++ b/api/src/services/drupal/assets.service.ts @@ -0,0 +1,908 @@ +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; +import pLimit from 'p-limit'; +import mysql from 'mysql2'; +import { MIGRATION_DATA_CONFIG } from '../../constants/index.js'; +import { getLogMessage } from '../../utils/index.js'; +import customLogger from '../../utils/custom-logger.utils.js'; +import { getDbConnection } from '../../helper/index.js'; +import { processBatches } from '../../utils/batch-processor.utils.js'; + +const { + DATA, + ASSETS_DIR_NAME, + ASSETS_FILE_NAME, + ASSETS_SCHEMA_FILE, + ASSETS_FAILED_FILE, +} = MIGRATION_DATA_CONFIG; + +interface AssetMetaData { + uid: string; + url: string; + filename: string; +} + +interface DrupalAsset { + fid: string | number; + uri: string; + filename: string; + filesize: string | number; + filemime?: string; + status?: string | number; + uid?: string | number; + timestamp?: string | number; + id?: string | number; // For file_usage table + count?: string | number; // For file_usage table +} + +/** + * Interface to track asset download URLs and their status + */ +interface AssetUrlTracker { + success: Array<{ + uid: string; + url: string; + filename: string; + }>; + failed: Array<{ + uid: string; + url: string; + filename: string; + reason: string; + }>; +} + +/** + * Writes data to a specified file, ensuring the target directory exists. + */ +async function writeFile(dirPath: string, filename: string, data: any) { + let fileHandle; + try { + await fs.promises.mkdir(dirPath, { recursive: true }); + const filePath = path.join(dirPath, filename); + + // Use file handle for better control over file operations + fileHandle = await fs.promises.open(filePath, 'w'); + await fileHandle.writeFile(JSON.stringify(data), 'utf8'); + } catch (err) { + console.error(`Error writing ${dirPath}/${filename}::`, err); + throw err; // Re-throw to handle upstream + } finally { + // Ensure file handle is always closed + if (fileHandle) { + try { + await fileHandle.close(); + } catch (closeErr) { + console.error( + `Error closing file handle for ${dirPath}/${filename}:`, + closeErr + ); + } + } + } +} + +/** + * Executes SQL query and returns results as Promise + */ +const executeQuery = ( + connection: mysql.Connection, + query: string +): Promise => { + return new Promise((resolve, reject) => { + connection.query(query, (error, results) => { + if (error) { + reject(error); + } else { + resolve(results as any[]); + } + }); + }); +}; + +const publicPathCache = new Map(); + +// AUTO-DETECT PUBLIC PATH FROM DATABASE +const detectPublicPath = async ( + connection: mysql.Connection, + baseUrl: string, + projectId: string, + destination_stack_id: string +): Promise => { + const srcFunc = 'detectPublicPath'; + + try { + // Try to get public file path from Drupal's system table + const configQuery = ` + SELECT value + FROM config + WHERE name = 'system.file' + LIMIT 1 + `; + + try { + const configResults = await executeQuery(connection, configQuery); + if (configResults.length > 0) { + const config = JSON.parse(configResults[0].value); + if (config.path && config.path.public) { + const detectedPath = config.path.public; + return detectedPath.endsWith('/') ? detectedPath : `${detectedPath}/`; + } + } + } catch (configErr) {} + + // Final fallback: Try to detect from an actual file by testing URLs + const sampleFileQuery = ` + SELECT uri, filename + FROM file_managed + WHERE uri LIKE 'public://%' + LIMIT 5 + `; + + const sampleResults = await executeQuery(connection, sampleFileQuery); + if (sampleResults.length > 0) { + // Try common Drupal paths with the user-provided baseUrl + const commonPaths = [ + '/sites/default/files/', + '/sites/all/files/', + '/files/', + ]; + + // Also try to extract path patterns from the database URIs + for (const sampleFile of sampleResults) { + const sampleUri = sampleFile.uri; + + for (const testPath of commonPaths) { + const testUrl = `${baseUrl}${testPath}${sampleUri.replace( + 'public://', + '' + )}`; + try { + const response = await axios.get(testUrl, { + timeout: 5000, + maxContentLength: 1024, // Only download first 1KB to test + headers: { + 'User-Agent': 'Contentstack-Drupal-Migration/1.0', + }, + }); + if (response.status === 200) { + const message = getLogMessage( + srcFunc, + `Auto-detected public path: ${testPath}`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + message + ); + return testPath; + } + } catch (err) { + // Continue to next path + } + } + } + + // If common paths don't work, try to extract from URI patterns + // Look for patterns like /sites/[site]/files/ in the database + const uriPatternQuery = ` + SELECT DISTINCT uri + FROM file_managed + WHERE uri LIKE 'public://%' + LIMIT 10 + `; + + const uriResults = await executeQuery(connection, uriPatternQuery); + const pathPatterns = new Set(); + + uriResults.forEach((row) => { + const uri = row.uri; + // Extract potential path patterns from URIs + const matches = uri.match(/public:\/\/(?:sites\/([^\/]+)\/)?files\//); + if (matches) { + pathPatterns.add(`/sites/${matches[1]}/files/`); + } + }); + + // Test extracted patterns + for (const pattern of pathPatterns) { + const patternStr = pattern as string; + for (const sampleFile of sampleResults.slice(0, 2)) { + // Test with fewer files + const testUrl = `${baseUrl}${patternStr}${sampleFile.uri.replace( + 'public://', + '' + )}`; + try { + const response = await axios.get(testUrl, { + timeout: 5000, + maxContentLength: 1024, // Only download first 1KB to test + headers: { + 'User-Agent': 'Contentstack-Drupal-Migration/1.0', + }, + }); + if (response.status === 200) { + const message = getLogMessage( + srcFunc, + `Auto-detected public path from patterns: ${patternStr}`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + message + ); + return patternStr; + } + } catch (err) { + // Continue to next pattern + } + } + } + } + + // Ultimate fallback + + const message = getLogMessage( + srcFunc, + `Could not auto-detect public path. Using default: /sites/default/files/`, + {} + ); + await customLogger(projectId, destination_stack_id, 'warn', message); + return '/sites/default/files/'; + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error detecting public path: ${error.message}. Using default.`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'warn', message); + return '/sites/default/files/'; + } +}; + +// URL VALIDATION AND NORMALIZATION +const normalizeUrlConfig = ( + baseUrl: string, + publicPath: string +): { baseUrl: string; publicPath: string } => { + // Validate inputs - allow empty values for auto-detection + if (!baseUrl && !publicPath) { + throw new Error( + `Invalid URL configuration: Both baseUrl and publicPath are empty. At least one must be provided.` + ); + } + + // Normalize baseUrl (handle empty case) + let normalizedBaseUrl = baseUrl ? baseUrl.trim() : ''; + + if (normalizedBaseUrl) { + // Remove trailing slash from baseUrl + normalizedBaseUrl = normalizedBaseUrl.replace(/\/+$/, ''); + + // Ensure baseUrl has protocol + if ( + !normalizedBaseUrl.startsWith('http://') && + !normalizedBaseUrl.startsWith('https://') + ) { + normalizedBaseUrl = `https://${normalizedBaseUrl}`; + } + + // Validate baseUrl format + try { + new URL(normalizedBaseUrl); + } catch (error) { + throw new Error( + `Invalid baseUrl format: "${baseUrl}" → "${normalizedBaseUrl}". Please provide a valid URL.` + ); + } + } + + // Normalize publicPath (handle empty case) + let normalizedPublicPath = publicPath ? publicPath.trim() : ''; + + if (normalizedPublicPath) { + // Ensure publicPath starts with / + if (!normalizedPublicPath.startsWith('/')) { + normalizedPublicPath = `/${normalizedPublicPath}`; + } + + // Ensure publicPath ends with / + if (!normalizedPublicPath.endsWith('/')) { + normalizedPublicPath = `${normalizedPublicPath}/`; + } + + // Remove duplicate slashes + normalizedPublicPath = normalizedPublicPath.replace(/\/+/g, '/'); + + // Validate publicPath doesn't contain invalid characters + if ( + normalizedPublicPath.includes('..') || + normalizedPublicPath.includes('//') + ) { + throw new Error( + `Invalid publicPath format: "${publicPath}" → "${normalizedPublicPath}". Path contains invalid characters.` + ); + } + } + + return { + baseUrl: normalizedBaseUrl, + publicPath: normalizedPublicPath, + }; +}; + +// DYNAMIC URL CONSTRUCTION - HANDLES MULTIPLE PATH FORMATS +const constructAssetUrl = ( + uri: string, + baseUrl: string, + publicPath: string +): string => { + try { + // Normalize the input URLs first + const { baseUrl: cleanBaseUrl, publicPath: cleanPublicPath } = + normalizeUrlConfig(baseUrl, publicPath); + + // Already a full URL - return as is + if (uri.startsWith('http://') || uri.startsWith('https://')) { + return uri; + } + + // Handle public:// scheme + if (uri.startsWith('public://')) { + const relativePath = uri.replace('public://', ''); + + // Check if we have valid baseUrl and publicPath + if (!cleanBaseUrl || !cleanPublicPath) { + throw new Error( + `Cannot construct URL: baseUrl="${cleanBaseUrl}", publicPath="${cleanPublicPath}". Both are required for public:// URIs.` + ); + } + + const fullUrl = `${cleanBaseUrl}${cleanPublicPath}${relativePath}`; + return fullUrl; + } + + // Handle private:// scheme + if (uri.startsWith('private://')) { + const relativePath = uri.replace('private://', ''); + + if (!cleanBaseUrl) { + throw new Error( + `Cannot construct URL: baseUrl="${cleanBaseUrl}". Base URL is required for private:// URIs.` + ); + } + + return `${cleanBaseUrl}/system/files/${relativePath}`; + } + + // Handle relative paths + const path = uri.startsWith('/') ? uri : `/${uri}`; + + if (!cleanBaseUrl) { + throw new Error( + `Cannot construct URL: baseUrl="${cleanBaseUrl}". Base URL is required for relative paths.` + ); + } + + return `${cleanBaseUrl}${path}`; + } catch (error: any) { + console.error(`❌ URL Construction Error: ${error.message}`); + throw new Error(`Failed to construct asset URL: ${error.message}`); + } +}; + +// IMPROVED SAVE ASSET WITH BETTER ERROR HANDLING +const saveAsset = async ( + assets: DrupalAsset, + failedJSON: any, + assetData: any, + metadata: AssetMetaData[], + projectId: string, + destination_stack_id: string, + baseUrl: string = '', + publicPath: string = '/sites/default/files/', + retryCount = 0, + authHeaders: any = {}, // Support for authentication + urlTracker?: AssetUrlTracker, // Track successful and failed URLs + userProvidedPublicPath?: string // Original user-provided path (for failed URL tracking) +): Promise => { + try { + const srcFunc = 'saveAsset'; + const assetsSave = path.join( + DATA, + destination_stack_id, + ASSETS_DIR_NAME, + 'files' + ); + + const assetId = `assets_${assets.fid}`; + const fileName = assets.filename; + const fileUrl = constructAssetUrl(assets.uri, baseUrl, publicPath); + + // Check if asset already exists + if (fs.existsSync(path.resolve(assetsSave, assetId, fileName))) { + return assetId; + } + + try { + const response = await axios.get(fileUrl, { + responseType: 'arraybuffer', + timeout: 120000, // Increased to 2 minutes + maxContentLength: 500 * 1024 * 1024, // 500MB max + headers: { + 'User-Agent': 'Contentstack-Drupal-Migration/1.0', + ...authHeaders, // Spread any authentication headers + }, + validateStatus: (status) => status === 200, // Only accept 200 + }); + + const assetPath = path.resolve(assetsSave, assetId); + + // Create asset data + assetData[assetId] = { + uid: assetId, + urlPath: `/assets/${assetId}`, + status: true, + content_type: assets.filemime || 'application/octet-stream', + file_size: assets.filesize.toString(), + tag: [], + filename: fileName, + url: fileUrl, + is_dir: false, + parent_uid: null, + _version: 1, + title: fileName, + publish_details: [], + }; + + await fs.promises.mkdir(assetPath, { recursive: true }); + await fs.promises.writeFile( + path.join(assetPath, fileName), + Buffer.from(response.data), + 'binary' + ); + await writeFile( + assetPath, + `_contentstack_${assetId}.json`, + assetData[assetId] + ); + + metadata.push({ uid: assetId, url: fileUrl, filename: fileName }); + + // Track successful download + if (urlTracker) { + urlTracker.success.push({ + uid: assetId, + url: fileUrl, + filename: fileName, + }); + } + + if (failedJSON[assetId]) { + delete failedJSON[assetId]; + } + + const message = getLogMessage( + srcFunc, + `✅ Asset "${fileName}" (${assets.fid}) downloaded successfully.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + return assetId; + } catch (err: any) { + // Retry logic with exponential backoff + if (retryCount < 3) { + // Increased to 3 retries + const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s + + await new Promise((resolve) => setTimeout(resolve, delay)); + + return await saveAsset( + assets, + failedJSON, + assetData, + metadata, + projectId, + destination_stack_id, + baseUrl, + publicPath, + retryCount + 1, + authHeaders, + urlTracker, + userProvidedPublicPath + ); + } else { + // After 3 retries failed, try fallback paths (if not already tried) + const commonPaths = [ + '/sites/default/files/', + '/sites/all/files/', + '/sites/g/files/bxs2566/files/', // Rice University specific + 'sites/default/files/', + 'sites/all/files/', + ]; + + // Only try fallback if current path is the user-provided path + const isUserProvidedPath = + publicPath === (userProvidedPublicPath || publicPath); + + if (isUserProvidedPath) { + // Try each common path + for (const fallbackPath of commonPaths) { + // Skip if already tried + if (fallbackPath === publicPath) { + continue; + } + + try { + // Attempt download with fallback path (reset retry count) + const result = await saveAsset( + assets, + failedJSON, + assetData, + metadata, + projectId, + destination_stack_id, + baseUrl, + fallbackPath, + 0, // Reset retry count for fallback attempt + authHeaders, + urlTracker, + userProvidedPublicPath || publicPath // Keep original user path for tracking + ); + + // Check if asset was actually saved (exists in assetData) + if (assetData[assetId]) { + return result; // Successfully downloaded with fallback path + } + } catch (fallbackErr) { + // Continue to next fallback path + continue; + } + } + } + + // All attempts failed - log failure + const errorDetails = { + status: err.response?.status, + statusText: err.response?.statusText, + message: err.message, + url: fileUrl, + }; + + // Use user-provided public path for the failed URL + const failedUrl = constructAssetUrl( + assets.uri, + baseUrl, + userProvidedPublicPath || publicPath + ); + + failedJSON[assetId] = { + failedUid: assets.fid, + name: fileName, + url: failedUrl, + file_size: assets.filesize, + reason_for_error: JSON.stringify(errorDetails), + }; + + // Track failed download with user-provided URL + if (urlTracker) { + urlTracker.failed.push({ + uid: assetId, + url: failedUrl, + filename: fileName, + reason: `${err.response?.status || 'Network error'}: ${ + err.message + }`, + }); + } + + const message = getLogMessage( + srcFunc, + `❌ Failed to download "${fileName}" (${assets.fid}) after all attempts: ${err.message}`, + {}, + err + ); + await customLogger(projectId, destination_stack_id, 'error', message); + + return assetId; + } + } + } catch (error) { + console.error('❌ Error in saveAsset:', error); + return `assets_${assets.fid}`; + } +}; + +// ASSETS QUERY - FETCH ALL FILES +const fetchAssetsFromDB = async ( + connection: mysql.Connection, + projectId: string, + destination_stack_id: string +): Promise => { + const srcFunc = 'fetchAssetsFromDB'; + + // Query to fetch ALL files from Drupal + const assetsQuery = ` + SELECT + fm.fid, + fm.filename, + fm.uri, + fm.filesize, + fm.filemime + FROM file_managed fm + ORDER BY fm.fid ASC + `; + + try { + const results = await executeQuery(connection, assetsQuery); + + const message = getLogMessage( + srcFunc, + `Fetched ${results.length} total assets from database.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + return results as DrupalAsset[]; + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Failed to fetch assets from database: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Retries failed assets using FID query + */ +const retryFailedAssets = async ( + connection: mysql.Connection, + failedAssetIds: string[], + failedJSON: any, + assetData: any, + metadata: AssetMetaData[], + projectId: string, + destination_stack_id: string, + baseUrl: string = '', + publicPath: string = '/sites/default/files/', + authHeaders: any = {}, + urlTracker?: AssetUrlTracker, + userProvidedPublicPath?: string +): Promise => { + const srcFunc = 'retryFailedAssets'; + + if (failedAssetIds.length === 0) { + return; + } + + try { + const assetsFIDQuery = `SELECT a.fid, a.filename, a.uri, a.filesize, a.filemime, b.id, b.count FROM file_managed a, file_usage b WHERE a.fid IN (${failedAssetIds.join( + ',' + )})`; + const results = await executeQuery(connection, assetsFIDQuery); + + if (results.length > 0) { + const limit = pLimit(1); // Reduce to 1 for large datasets to prevent EMFILE errors + const tasks = results.map((asset: DrupalAsset) => + limit(() => + saveAsset( + asset, + failedJSON, + assetData, + metadata, + projectId, + destination_stack_id, + baseUrl, + publicPath, + 0, + authHeaders, + urlTracker, + userProvidedPublicPath + ) + ) + ); + + await Promise.all(tasks); + + const message = getLogMessage( + srcFunc, + `Retried ${results.length} failed assets.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + } + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error retrying failed assets: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + } +}; + +// UPDATED createAssets WITH AUTO-DETECTION +export const createAssets = async ( + dbConfig: any, + destination_stack_id: string, + projectId: string, + baseUrl: string = '', + publicPath: string = '', // Now optional - will auto-detect if empty + isTest = false +) => { + const srcFunc = 'createAssets'; + let connection: mysql.Connection | null = null; + + try { + // Create database connection first + connection = await getDbConnection( + dbConfig, + projectId, + destination_stack_id + ); + + // Auto-detect public path if not provided or empty + let detectedPublicPath = publicPath; + if (!publicPath || publicPath.trim() === '') { + detectedPublicPath = await detectPublicPath( + connection, + baseUrl, + projectId, + destination_stack_id + ); + } else { + } + + const assetsSave = path.join(DATA, destination_stack_id, ASSETS_DIR_NAME); + const assetMasterFolderPath = path.join( + DATA, + destination_stack_id, + 'logs', + ASSETS_DIR_NAME + ); + + await fs.promises.mkdir(assetsSave, { recursive: true }); + await fs.promises.mkdir(assetMasterFolderPath, { recursive: true }); + + const failedJSON: any = {}; + const assetData: any = {}; + const metadata: AssetMetaData[] = []; + const fileMeta = { '1': ASSETS_SCHEMA_FILE }; + const failedAssetIds: string[] = []; + + // Initialize URL tracker for assets_url.json + const urlTracker: AssetUrlTracker = { + success: [], + failed: [], + }; + + const message = getLogMessage( + srcFunc, + `Exporting assets using base URL: ${baseUrl} and public path: ${detectedPublicPath}`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + // Fetch assets from database + const assetsData = await fetchAssetsFromDB( + connection, + projectId, + destination_stack_id + ); + + if (assetsData && assetsData.length > 0) { + let assets = assetsData; + if (isTest) { + assets = assets.slice(0, 10); + } + + const batchSize = assets.length > 10000 ? 100 : 1000; + const results = await processBatches( + assets, + async (asset: DrupalAsset) => { + try { + return await saveAsset( + asset, + failedJSON, + assetData, + metadata, + projectId, + destination_stack_id, + baseUrl, + detectedPublicPath, // Use detected path + 0, + {}, // authHeaders + urlTracker, // Pass URL tracker + publicPath || detectedPublicPath // Use original user-provided path for tracking + ); + } catch (error) { + failedAssetIds.push(asset.fid.toString()); + return `assets_${asset.fid}`; + } + }, + { + batchSize, + concurrency: 5, // Increased from 1 for better performance + delayBetweenBatches: 200, + }, + (batchIndex, totalBatches, batchResults) => { + if (batchIndex % 10 === 0) { + } + } + ); + + // Retry failed assets + if (failedAssetIds.length > 0) { + await retryFailedAssets( + connection, + failedAssetIds, + failedJSON, + assetData, + metadata, + projectId, + destination_stack_id, + baseUrl, + detectedPublicPath, // Use detected path + {}, // authHeaders + urlTracker, // Pass URL tracker + publicPath || detectedPublicPath // User-provided path for tracking + ); + } + + await writeFile(assetsSave, ASSETS_SCHEMA_FILE, assetData); + await writeFile(assetsSave, ASSETS_FILE_NAME, fileMeta); + + if (Object.keys(failedJSON).length > 0) { + await writeFile(assetMasterFolderPath, ASSETS_FAILED_FILE, failedJSON); + } + + // Write assets_url.json with successful and failed URLs + await writeFile(assetsSave, 'assets_url.json', urlTracker); + + const successMessage = getLogMessage( + srcFunc, + `Successfully processed ${ + Object.keys(assetData).length + } assets out of ${assets.length} total assets.`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + successMessage + ); + + return results; + } else { + const message = getLogMessage(srcFunc, `No assets found.`, {}); + await customLogger(projectId, destination_stack_id, 'info', message); + return []; + } + } catch (err) { + const message = getLogMessage( + srcFunc, + `Error encountered while creating assets.`, + {}, + err + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw err; + } finally { + if (connection) { + connection.end(); + } + } +}; diff --git a/api/src/services/drupal/content-types.service.ts b/api/src/services/drupal/content-types.service.ts new file mode 100644 index 000000000..f61453392 --- /dev/null +++ b/api/src/services/drupal/content-types.service.ts @@ -0,0 +1,357 @@ +import fs from 'fs'; +import path from 'path'; +import { MIGRATION_DATA_CONFIG } from '../../constants/index.js'; +import { convertToSchemaFormate } from '../../utils/content-type-creator.utils.js'; +import { getLogMessage } from '../../utils/index.js'; +import customLogger from '../../utils/custom-logger.utils.js'; +import FieldMapperModel from '../../models/FieldMapper.js'; +import ContentTypesMapperModelLowdb from '../../models/contentTypesMapper-lowdb.js'; + +const { DATA, CONTENT_TYPES_DIR_NAME, CONTENT_TYPES_SCHEMA_FILE } = + MIGRATION_DATA_CONFIG; + +/** + * Generates API content types from upload-api drupal schema + * This service reads the upload-api generated schema and converts it to final API content types + * following the same pattern as other CMS services (Contentful, WordPress, Sitecore) + */ +export const generateContentTypeSchemas = async ( + destination_stack_id: string, + projectId: string +): Promise => { + const srcFunc = 'generateContentTypeSchemas'; + + try { + const message = getLogMessage( + srcFunc, + `Generating content type schemas from upload-api drupal schema...`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + // Path to upload-api generated schema + const uploadApiSchemaPath = path.join( + process.cwd(), + '..', + 'upload-api', + 'drupalMigrationData', + 'drupalSchema' + ); + + // Path to API content types directory + const apiContentTypesPath = path.join( + DATA, + destination_stack_id, + CONTENT_TYPES_DIR_NAME + ); + + // Ensure API content types directory exists + await fs.promises.mkdir(apiContentTypesPath, { recursive: true }); + + if (!fs.existsSync(uploadApiSchemaPath)) { + throw new Error( + `Upload-API schema not found at: ${uploadApiSchemaPath}. Please run upload-api migration first.` + ); + } + + // Read all schema files from upload-api + const schemaFiles = fs + .readdirSync(uploadApiSchemaPath) + .filter((file) => file.endsWith('.json')); + + if (schemaFiles.length === 0) { + throw new Error( + `No schema files found in upload-api directory: ${uploadApiSchemaPath}` + ); + } + + // Load saved field mappings from database to get UI selections + await FieldMapperModel.read(); + await ContentTypesMapperModelLowdb.read(); + + const savedFieldMappings = FieldMapperModel.data.field_mapper.filter( + (field: any) => field && field.projectId === projectId + ); + + // Log fields with UI changes + const fieldsWithTypeChanges = savedFieldMappings.filter( + (field: any) => + field.contentstackFieldType && + field.backupFieldType !== field.contentstackFieldType + ); + + if (fieldsWithTypeChanges.length > 0) { + fieldsWithTypeChanges.forEach((field: any) => {}); + } + + // Build complete schema array (NO individual files) + const allApiSchemas = []; + + for (const schemaFile of schemaFiles) { + try { + const uploadApiSchemaFilePath = path.join( + uploadApiSchemaPath, + schemaFile + ); + const uploadApiSchema = JSON.parse( + fs.readFileSync(uploadApiSchemaFilePath, 'utf8') + ); + + // Convert upload-api schema to API format WITH saved field mappings from UI + const apiSchema = convertUploadApiSchemaToApiSchema( + uploadApiSchema, + savedFieldMappings, + projectId + ); + + // Add to combined schema array (NO individual files) + allApiSchemas.push(apiSchema); + + const fieldMessage = getLogMessage( + srcFunc, + `Converted content type ${uploadApiSchema.uid} with ${ + uploadApiSchema.schema?.length || 0 + } fields`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + fieldMessage + ); + } catch (error: any) { + const errorMessage = getLogMessage( + srcFunc, + `Failed to convert schema file ${schemaFile}: ${error.message}`, + {}, + error + ); + await customLogger( + projectId, + destination_stack_id, + 'error', + errorMessage + ); + } + } + + // Write ONLY the combined schema.json file + const combinedSchemaPath = path.join( + apiContentTypesPath, + CONTENT_TYPES_SCHEMA_FILE + ); + await fs.promises.writeFile( + combinedSchemaPath, + JSON.stringify(allApiSchemas, null, 2), + 'utf8' + ); + + const successMessage = getLogMessage( + srcFunc, + `Successfully generated ${schemaFiles.length} content type schemas from upload-api`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', successMessage); + } catch (error: any) { + const errorMessage = getLogMessage( + srcFunc, + `Failed to generate content type schemas: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', errorMessage); + throw error; + } +}; + +/** + * Converts upload-api drupal schema format to API content type format + * This preserves the original field types and user selections from the upload-api + * AND applies user's UI selections from Content Mapper for reference/taxonomy fields + */ +function convertUploadApiSchemaToApiSchema( + uploadApiSchema: any, + savedFieldMappings: any[] = [], + projectId?: string +): any { + const apiSchema = { + title: uploadApiSchema.title, + uid: uploadApiSchema.uid, + schema: [] as any[], + }; + + if (!uploadApiSchema.schema || !Array.isArray(uploadApiSchema.schema)) { + return apiSchema; + } + + // Convert each field from upload-api format to API format + for (const uploadField of uploadApiSchema.schema) { + try { + // Find saved field mapping from database FIRST to get user's field type selection + const savedMapping = savedFieldMappings.find( + (mapping: any) => + mapping.contentstackFieldUid === uploadField.contentstackFieldUid || + mapping.contentstackFieldUid === uploadField.uid || + mapping.uid === uploadField.contentstackFieldUid || + mapping.uid === uploadField.uid + ); + + // Use UI-selected field type if available, otherwise use upload-api type + const fieldType = + savedMapping?.contentstackFieldType || + uploadField.contentstackFieldType; + + // Map upload-api field to API format using convertToSchemaFormate + const apiField = convertToSchemaFormate({ + field: { + title: uploadField.contentstackField || uploadField.otherCmsField, + uid: uploadField.contentstackFieldUid, + contentstackFieldType: fieldType, // Use UI selection if available + advanced: { + ...uploadField.advanced, + mandatory: uploadField.advanced?.mandatory || false, + multiple: uploadField.advanced?.multiple || false, + unique: uploadField.advanced?.unique || false, + nonLocalizable: uploadField.advanced?.non_localizable || false, + default_value: uploadField.advanced?.default_value || '', + validationRegex: uploadField.advanced?.format || '', + validationErrorMessage: uploadField.advanced?.error_message || '', + }, + // For reference fields, preserve reference_to from upload-api + refrenceTo: + uploadField.advanced?.reference_to || + uploadField.advanced?.embedObjects || + [], + // For taxonomy fields, preserve taxonomies from upload-api + taxonomies: uploadField.advanced?.taxonomies || [], + }, + advanced: true, + }); + + if (apiField) { + // Use UI selections if available, otherwise fall back to upload-api data + // Check against the FINAL field type (which may have been changed in UI) + if (fieldType === 'reference') { + if ( + savedMapping?.referenceTo && + Array.isArray(savedMapping.referenceTo) && + savedMapping.referenceTo.length > 0 + ) { + // MERGE: Combine old upload-api data with new UI selections (no duplicates) + // Check both embedObjects AND reference_to + const oldReferences = + uploadField.advanced?.embedObjects || + uploadField.advanced?.reference_to || + []; + const newReferences = savedMapping.referenceTo || []; + const mergedReferences = [ + ...new Set([...oldReferences, ...newReferences]), + ].filter((ref) => ref && ref.toLowerCase() !== 'profile'); // Filter out profile + + apiField.reference_to = mergedReferences; + } else { + // Fall back to upload-api data only (check both embedObjects and reference_to) + const fallbackReferences = ( + uploadField.advanced?.embedObjects || + uploadField.advanced?.reference_to || + [] + ).filter((ref: string) => ref && ref.toLowerCase() !== 'profile'); // Filter out profile + + if (fallbackReferences && fallbackReferences.length > 0) { + apiField.reference_to = fallbackReferences; + } + } + } + + if (fieldType === 'taxonomy') { + if ( + savedMapping?.referenceTo && + Array.isArray(savedMapping.referenceTo) && + savedMapping.referenceTo.length > 0 + ) { + // MERGE: Combine old upload-api taxonomies with new UI selections (no duplicates) + const oldTaxonomyUIDs = ( + uploadField.advanced?.taxonomies || [] + ).map((t: any) => t.taxonomy_uid || t); + const newTaxonomyUIDs = savedMapping.referenceTo || []; + const mergedTaxonomyUIDs = [ + ...new Set([...oldTaxonomyUIDs, ...newTaxonomyUIDs]), + ]; + + // Convert UIDs to taxonomy format + apiField.taxonomies = mergedTaxonomyUIDs.map((taxUid: string) => ({ + taxonomy_uid: taxUid, + mandatory: uploadField.advanced?.mandatory || false, + multiple: uploadField.advanced?.multiple !== false, // Default to true for taxonomies + non_localizable: uploadField.advanced?.non_localizable || false, + })); + } else if (uploadField.advanced?.taxonomies) { + // Fall back to upload-api data only + apiField.taxonomies = uploadField.advanced.taxonomies; + } + } + + // Preserve field metadata for proper field type conversion + if (uploadField.advanced?.multiline !== undefined) { + apiField.field_metadata = apiField.field_metadata || {}; + apiField.field_metadata.multiline = uploadField.advanced.multiline; + } + + apiSchema.schema.push(apiField); + } + } catch (error: any) { + // Fallback: create basic field structure + apiSchema.schema.push({ + display_name: + uploadField.contentstackField || + uploadField.otherCmsField || + uploadField.uid, + uid: uploadField.contentstackFieldUid || uploadField.uid, + data_type: mapFieldTypeToDataType(uploadField.contentstackFieldType), + mandatory: uploadField.advanced?.mandatory || false, + unique: uploadField.advanced?.unique || false, + field_metadata: { _default: true }, + format: '', + error_messages: { format: '' }, + multiple: uploadField.advanced?.multiple || false, + non_localizable: uploadField.advanced?.non_localizable || false, + }); + } + } + + return apiSchema; +} + +/** + * Maps upload-api field types to API data types + * This ensures proper field type preservation from upload-api to API + */ +function mapFieldTypeToDataType(fieldType: string): string { + const fieldTypeMap: { [key: string]: string } = { + single_line_text: 'text', + multi_line_text: 'text', + text: 'text', + html: 'html', + json: 'json', + markdown: 'text', + number: 'number', + boolean: 'boolean', + isodate: 'isodate', + file: 'file', + reference: 'reference', + taxonomy: 'taxonomy', + link: 'link', + dropdown: 'text', + radio: 'text', + checkbox: 'boolean', + global_field: 'global_field', + group: 'group', + url: 'text', + }; + + return fieldTypeMap[fieldType] || 'text'; +} + +// Removed regenerateCombinedSchemaFromIndividualFiles function +// We now generate ONLY schema.json directly, no individual files diff --git a/api/src/services/drupal/entries.service.ts b/api/src/services/drupal/entries.service.ts new file mode 100644 index 000000000..d6b406e15 --- /dev/null +++ b/api/src/services/drupal/entries.service.ts @@ -0,0 +1,1717 @@ +import fs from 'fs'; +import path from 'path'; +import mysql from 'mysql2'; +import { v4 as uuidv4 } from 'uuid'; +import { JSDOM } from 'jsdom'; +import _ from 'lodash'; +import { + htmlToJson, + jsonToHtml, + jsonToMarkdown, +} from '@contentstack/json-rte-serializer'; +import { CHUNK_SIZE, MIGRATION_DATA_CONFIG } from '../../constants/index.js'; +import { getLogMessage } from '../../utils/index.js'; +import customLogger from '../../utils/custom-logger.utils.js'; +import { getDbConnection } from '../../helper/index.js'; +import { + analyzeFieldTypes, + isTaxonomyField, + isReferenceField, + isAssetField, + type TaxonomyFieldMapping, + type ReferenceFieldMapping, + type AssetFieldMapping, +} from './field-analysis.service.js'; +import FieldFetcherService from './field-fetcher.service.js'; +import { mapDrupalLocales } from './locales.service.js'; +// Dynamic import for phpUnserialize will be used in the function + +// Local utility functions (extracted from entries-field-creator.utils.ts patterns) +// Default prefix fallback if none provided +const DEFAULT_PREFIX = 'cs'; + +function startsWithNumber(str: string) { + return /^\d/.test(str); +} + +const uidCorrector = ({ + uid, + id, + prefix, +}: { + uid?: string; + id?: string; + prefix?: string; +}) => { + const value = uid || id; + if (!value) return ''; + + const effectivePrefix = prefix || DEFAULT_PREFIX; + + if (startsWithNumber(value)) { + return `${effectivePrefix}_${_.replace( + value, + new RegExp('[ -]', 'g'), + '_' + )?.toLowerCase()}`; + } + return _.replace(value, new RegExp('[ -]', 'g'), '_')?.toLowerCase(); +}; + +interface TaxonomyReference { + drupal_term_id: number; + taxonomy_uid: string; + term_uid: string; +} + +interface TaxonomyFieldOutput { + taxonomy_uid: string; + term_uid: string; +} + +const { + DATA, + ENTRIES_DIR_NAME, + ASSETS_DIR_NAME, + REFERENCES_DIR_NAME, + REFERENCES_FILE_NAME, + TAXONOMIES_DIR_NAME, +} = MIGRATION_DATA_CONFIG; + +interface DrupalFieldConfig { + field_name: string; + field_type: string; + settings?: { + handler?: string; + [key: string]: any; + }; + [key: string]: any; +} + +interface DrupalEntry { + nid: number; + title: string; + langcode: string; + created: number; + type: string; + [key: string]: any; +} + +interface QueryConfig { + page: { + [contentType: string]: string; + }; + count: { + [contentTypeCount: string]: string; + }; +} + +const LIMIT = 5; // Pagination limit + +// NOTE: Hardcoded queries have been REMOVED. All queries are now generated dynamically +// by the query.service.ts based on actual database field analysis. + +/** + * Executes SQL query and returns results as Promise + */ +const executeQuery = ( + connection: mysql.Connection, + query: string +): Promise => { + return new Promise((resolve, reject) => { + connection.query(query, (error, results) => { + if (error) { + reject(error); + } else { + resolve(results as any[]); + } + }); + }); +}; + +/** + * Load taxonomy reference mappings from taxonomyReference.json + */ +const loadTaxonomyReferences = async ( + referencesPath: string +): Promise> => { + try { + const taxonomyRefPath = path.join(referencesPath, 'taxonomyReference.json'); + + if (!fs.existsSync(taxonomyRefPath)) { + return {}; + } + + const taxonomyReferences: TaxonomyReference[] = JSON.parse( + fs.readFileSync(taxonomyRefPath, 'utf8') + ); + + // Create lookup map: drupal_term_id -> {taxonomy_uid, term_uid} + const lookup: Record = {}; + + taxonomyReferences.forEach((ref) => { + lookup[ref.drupal_term_id] = { + taxonomy_uid: ref.taxonomy_uid, + term_uid: ref.term_uid, + }; + }); + + return lookup; + } catch (error) { + console.error('Could not load taxonomy references:', error); + return {}; + } +}; + +/** + * Writes data to a specified file, ensuring the target directory exists. + */ +async function writeFile(dirPath: string, filename: string, data: any) { + try { + await fs.promises.mkdir(dirPath, { recursive: true }); + const filePath = path.join(dirPath, filename); + await fs.promises.writeFile(filePath, JSON.stringify(data), 'utf8'); + } catch (err) { + console.error(`Error writing ${dirPath}/${filename}::`, err); + } +} + +/** + * Reads a file and returns its JSON content. + */ +async function readFile(filePath: string, fileName: string) { + try { + const data = await fs.promises.readFile( + path.join(filePath, fileName), + 'utf8' + ); + return JSON.parse(data); + } catch (err) { + return {}; + } +} + +/** + * Fetches field configurations from Drupal database + */ +const fetchFieldConfigs = async ( + connection: mysql.Connection, + projectId: string, + destination_stack_id: string +): Promise => { + const srcFunc = 'fetchFieldConfigs'; + const contentTypeQuery = + "SELECT *, CONVERT(data USING utf8) as data FROM config WHERE name LIKE '%field.field.node%'"; + + try { + const results = await executeQuery(connection, contentTypeQuery); + + const fieldConfigs: DrupalFieldConfig[] = []; + for (const row of results) { + try { + const { unserialize } = await import('php-serialize'); + const configData = unserialize(row.data); + if (configData && typeof configData === 'object') { + fieldConfigs.push(configData as DrupalFieldConfig); + } + } catch (parseError) { + console.error( + `Failed to parse field config for ${row.name}:`, + parseError + ); + } + } + + const message = getLogMessage( + srcFunc, + `Fetched ${fieldConfigs.length} field configurations from database.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + return fieldConfigs; + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Failed to fetch field configurations: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Determines the source field type based on the value structure + */ +const determineSourceFieldType = (value: any): string => { + if (typeof value === 'object' && value !== null && value.type === 'doc') { + return 'json_rte'; + } + if (typeof value === 'string' && /<\/?[a-z][\s\S]*>/i.test(value)) { + return 'html_rte'; + } + if (typeof value === 'string') { + // Simple heuristic: if it has line breaks, consider it multi-line + return value.includes('\n') || value.includes('\r') + ? 'multi_line' + : 'single_line'; + } + return 'unknown'; +}; + +/** + * Checks if conversion is allowed based on the exact rules: + * 1. Single-line text → Single-line/Multi-line/HTML RTE/JSON RTE + * 2. Multi-line text → Multi-line/HTML RTE/JSON RTE (NOT Single-line) + * 3. HTML RTE → HTML RTE/JSON RTE (NOT Single-line or Multi-line) + * 4. JSON RTE → JSON RTE/HTML RTE (NOT Single-line or Multi-line) + */ +const isConversionAllowed = ( + sourceType: string, + targetType: string +): boolean => { + const conversionRules: { [key: string]: string[] } = { + // ✅ Single line can upgrade to multi-line, HTML RTE, or JSON RTE + single_line: [ + 'single_line_text', + 'text', + 'multi_line_text', + 'html', + 'json', + ], + // ✅ Multi-line can upgrade to HTML RTE or JSON RTE (but not downgrade to single-line) + multi_line: ['multi_line_text', 'text', 'html', 'json'], + // ✅ HTML RTE can only convert to JSON RTE (no downgrades to text fields) + html_rte: ['html', 'json'], + // ✅ JSON RTE can only convert to HTML RTE (no downgrades to text fields) + json_rte: ['json', 'html'], + }; + + return conversionRules[sourceType]?.includes(targetType) || false; +}; + +/** + * Processes field values based on content type mapping and field type switching + * Follows proper conversion rules for field type compatibility + */ +const processFieldByType = ( + value: any, + fieldMapping: any, + assetId: any, + referenceId: any +): any => { + if (!fieldMapping || !fieldMapping.contentstackFieldType) { + return value; + } + + // Determine source field type + const sourceType = determineSourceFieldType(value); + const targetType = fieldMapping.contentstackFieldType; + + // Check if conversion is allowed + if (!isConversionAllowed(sourceType, targetType)) { + console.error( + `Conversion not allowed: ${sourceType} → ${targetType}. Keeping original value.` + ); + return value; + } + + switch (targetType) { + case 'single_line_text': { + // Convert to single line text + if (typeof value === 'object' && value !== null && value.type === 'doc') { + // JSON RTE to plain text (extract text content) + try { + const htmlContent = jsonToHtml(value) || ''; + // Strip HTML tags and convert to single line + const textContent = htmlContent + .replace(/<[^>]*>/g, '') + .replace(/\s+/g, ' ') + .trim(); + return textContent; + } catch (error) { + console.error( + 'Failed to convert JSON RTE to single line text:', + error + ); + return String(value); + } + } else if (typeof value === 'string') { + if (/<\/?[a-z][\s\S]*>/i.test(value)) { + // HTML to plain text + const textContent = value + .replace(/<[^>]*>/g, '') + .replace(/\s+/g, ' ') + .trim(); + return textContent; + } + // Multi-line to single line + return value.replace(/\s+/g, ' ').trim(); + } + return String(value); + } + + case 'text': + case 'multi_line_text': { + // Convert to multi-line text + if (typeof value === 'object' && value !== null && value.type === 'doc') { + // JSON RTE to HTML (preserving structure) + try { + return ( + jsonToHtml(value, { + customElementTypes: { + 'social-embed': (attrs, child, jsonBlock) => { + return `${child}`; + }, + }, + customTextWrapper: { + color: (child, value) => { + return `${child}`; + }, + }, + }) || '' + ); + } catch (error) { + console.error('Failed to convert JSON RTE to HTML:', error); + return String(value); + } + } + // HTML and plain text can stay as-is for multi-line + return typeof value === 'string' ? value : String(value || ''); + } + + case 'json': { + // Convert to JSON RTE + if (typeof value === 'string' && /<\/?[a-z][\s\S]*>/i.test(value)) { + // HTML to JSON RTE + try { + const dom = new JSDOM(value); + const htmlDoc = dom.window.document.querySelector('body'); + if (htmlDoc) { + htmlDoc.innerHTML = value; + return htmlToJson(htmlDoc); + } + } catch (error) { + console.error('Failed to convert HTML to JSON RTE:', error); + } + } else if (typeof value === 'string') { + // Plain text to JSON RTE + try { + const dom = new JSDOM(`

${value}

`); + const htmlDoc = dom.window.document.querySelector('body'); + if (htmlDoc) { + return htmlToJson(htmlDoc); + } + } catch (error) { + console.error('Failed to convert text to JSON RTE:', error); + } + } + // If already JSON RTE or conversion failed, return as-is + return value; + } + + case 'html': { + // Convert to HTML RTE + if (typeof value === 'object' && value !== null && value.type === 'doc') { + // JSON RTE to HTML + try { + return ( + jsonToHtml(value, { + customElementTypes: { + 'social-embed': (attrs, child, jsonBlock) => { + return `${child}`; + }, + }, + customTextWrapper: { + color: (child, value) => { + return `${child}`; + }, + }, + }) || '

' + ); + } catch (error) { + console.error('Failed to convert JSON RTE to HTML:', error); + return value; + } + } else if (typeof value === 'string') { + // Check if it's already HTML + if (/<\/?[a-z][\s\S]*>/i.test(value)) { + // Already HTML, return as-is + return value; + } else { + // Plain text to HTML - wrap in paragraph tags + return `

${value}

`; + } + } + return typeof value === 'string' ? value : String(value || ''); + } + + case 'markdown': { + // Convert to Markdown + if (typeof value === 'object' && value !== null && value.type === 'doc') { + try { + return jsonToMarkdown(value); + } catch (error) { + console.error('Failed to convert JSON RTE to Markdown:', error); + return value; + } + } + return typeof value === 'string' ? value : String(value || ''); + } + + case 'file': { + // File/Asset processing with proper validation and cleanup + if (fieldMapping.advanced?.multiple) { + // Multiple files + if (Array.isArray(value)) { + const validAssets = value + .map((assetRef) => { + const assetKey = `assets_${assetRef}`; + const assetReference = assetId[assetKey]; + + if (assetReference && typeof assetReference === 'object') { + return assetReference; + } + + console.error( + `Asset ${assetKey} not found or invalid, excluding from array` + ); + return null; + }) + .filter((asset) => asset !== null); // Remove null entries + + return validAssets.length > 0 ? validAssets : undefined; // Return undefined if no valid assets + } + } else { + // Single file + const assetKey = `assets_${value}`; + const assetReference = assetId[assetKey]; + + if (assetReference && typeof assetReference === 'object') { + return assetReference; + } + + console.error(`Asset ${assetKey} not found or invalid, removing field`); + return undefined; // Return undefined to indicate field should be removed + } + return value; + } + + case 'reference': { + // Reference processing + if (fieldMapping.advanced?.multiple) { + // Multiple references + if (Array.isArray(value)) { + return value.map( + (refId) => + referenceId[`content_type_entries_title_${refId}`] || refId + ); + } + } else { + // Single reference + return [referenceId[`content_type_entries_title_${value}`] || value]; + } + return value; + } + + case 'number': { + // Number processing + if (typeof value === 'string') { + const parsed = parseInt(value); + return isNaN(parsed) ? 0 : parsed; + } + return typeof value === 'number' ? value : 0; + } + + case 'boolean': { + // Boolean processing + if (typeof value === 'string') { + return value === '1' || value.toLowerCase() === 'true'; + } + return Boolean(value); + } + + case 'isodate': { + // Date processing + if (typeof value === 'number') { + return new Date(value * 1000).toISOString(); + } + return value; + } + + default: { + // Default processing - handle HTML content + if (typeof value === 'string' && /<\/?[a-z][\s\S]*>/i.test(value)) { + try { + const dom = new JSDOM(value); + const htmlDoc = dom.window.document.querySelector('body'); + return htmlToJson(htmlDoc); + } catch (error) { + return value; + } + } + return value; + } + } +}; + +/** + * Consolidates all taxonomy fields into a single 'taxonomies' field with unique term_uid validation + * Uses the same pattern as entries-field-creator.utils.ts + */ +const consolidateTaxonomyFields = ( + processedEntry: any, + contentType: string, + taxonomyFieldMapping: TaxonomyFieldMapping +): any => { + const consolidatedTaxonomies: Array<{ + taxonomy_uid: string; + term_uid: string; + }> = []; + const fieldsToRemove: string[] = []; + const seenTermUids = new Set(); // Track unique term_uid values + + // Iterate through all fields in the processed entry + for (const [fieldKey, fieldValue] of Object.entries(processedEntry)) { + // Extract field name from key (remove _target_id suffix) + const fieldName = fieldKey.replace(/_target_id$/, ''); + + // Check if this is a taxonomy field using field analysis + if (isTaxonomyField(fieldName, contentType, taxonomyFieldMapping)) { + // Validate that field value is an array with taxonomy structure + if (Array.isArray(fieldValue)) { + for (const taxonomyItem of fieldValue) { + // Validate taxonomy structure + if ( + taxonomyItem && + typeof taxonomyItem === 'object' && + taxonomyItem.taxonomy_uid && + taxonomyItem.term_uid + ) { + // Check for unique term_uid (avoid duplicates) + if (!seenTermUids.has(taxonomyItem.term_uid)) { + consolidatedTaxonomies.push({ + taxonomy_uid: taxonomyItem.taxonomy_uid, + term_uid: taxonomyItem.term_uid, + }); + seenTermUids.add(taxonomyItem.term_uid); + } + } + } + } + + // Mark this field for removal + fieldsToRemove.push(fieldKey); + } + } + + // Create new entry object without the original taxonomy fields + const consolidatedEntry = { ...processedEntry }; + + // Remove original taxonomy fields + for (const fieldKey of fieldsToRemove) { + delete consolidatedEntry[fieldKey]; + } + + // Add consolidated taxonomy field if we have any taxonomies + if (consolidatedTaxonomies.length > 0) { + consolidatedEntry.taxonomies = consolidatedTaxonomies; + } + + return consolidatedEntry; +}; + +/** + * Processes field values based on field configuration - following original Drupal logic + */ +const processFieldData = async ( + entryData: DrupalEntry, + fieldConfigs: DrupalFieldConfig[], + assetId: any, + referenceId: any, + taxonomyId: any, + taxonomyFieldMapping: TaxonomyFieldMapping, + referenceFieldMapping: ReferenceFieldMapping, + assetFieldMapping: any, + taxonomyReferenceLookup: Record, + contentType: string, + prefix: string = DEFAULT_PREFIX +): Promise => { + const fieldNames = Object.keys(entryData); + const isoDate = new Date(); + const processedData: any = {}; + const skippedFields = new Set(); // Track fields that should be skipped entirely + const processedFields = new Set(); // Track fields that have been processed to avoid duplicates + + // Process each field in the entry data + for (const [dataKey, value] of Object.entries(entryData)) { + // Extract field name from dataKey (remove _target_id suffix) + const fieldName = dataKey + .replace(/_target_id$/, '') + .replace(/_value$/, '') + .replace(/_status$/, '') + .replace(/_uri$/, ''); + + // Handle asset fields using field analysis + if ( + dataKey.endsWith('_target_id') && + isAssetField(fieldName, contentType, assetFieldMapping) + ) { + const assetKey = `assets_${value}`; + if (assetKey in assetId) { + // Transform to proper Contentstack asset reference format + const assetReference = assetId[assetKey]; + if (assetReference && typeof assetReference === 'object') { + processedData[dataKey] = assetReference; + } + // If asset reference is not properly structured, skip the field + } + // If asset not found in assets index, mark field as skipped + skippedFields.add(dataKey); + continue; // Skip further processing for this field + } + + // Handle entity references (taxonomy and node references) using field analysis + // NOTE: value can be a number (single reference) or string (GROUP_CONCAT comma-separated IDs) + if ( + dataKey.endsWith('_target_id') && + (typeof value === 'number' || typeof value === 'string') + ) { + // Check if this is a taxonomy field using our field analysis + if (isTaxonomyField(fieldName, contentType, taxonomyFieldMapping)) { + // Handle both single ID (number) and GROUP_CONCAT result (comma-separated string) + const targetIds = + typeof value === 'string' + ? value + .split(',') + .map((id) => parseInt(id.trim())) + .filter((id) => !isNaN(id)) + : [value]; + + const transformedTaxonomies: Array<{ + taxonomy_uid: string; + term_uid: string; + }> = []; + + for (const tid of targetIds) { + // Look up taxonomy reference using drupal_term_id + const taxonomyRef = taxonomyReferenceLookup[tid]; + + if (taxonomyRef) { + transformedTaxonomies.push({ + taxonomy_uid: taxonomyRef.taxonomy_uid, + term_uid: taxonomyRef.term_uid, + }); + } else { + console.warn( + `⚠️ Taxonomy term ${tid} not found in reference lookup for field ${fieldName}` + ); + } + } + + if (transformedTaxonomies.length > 0) { + processedData[dataKey] = transformedTaxonomies; + } else { + // Fallback to original value if no lookups succeeded + processedData[dataKey] = value; + } + + // Mark field as processed so it doesn't get overwritten by ctValue loop + processedFields.add(dataKey); + skippedFields.add(dataKey); // Also skip in ctValue loop + + continue; // Skip further processing for this field + } else if ( + isReferenceField(fieldName, contentType, referenceFieldMapping) + ) { + // Handle node reference fields using field analysis + // Handle both single ID (number) and GROUP_CONCAT result (comma-separated string) + const targetIds = + typeof value === 'string' + ? value + .split(',') + .map((id) => parseInt(id.trim())) + .filter((id) => !isNaN(id)) + : [value]; + + const transformedReferences: any[] = []; + + for (const nid of targetIds) { + const referenceKey = `content_type_entries_title_${nid}`; + if (referenceKey in referenceId) { + transformedReferences.push(referenceId[referenceKey]); + } + } + + if (transformedReferences.length > 0) { + processedData[dataKey] = transformedReferences; + } else { + // If no references found, mark field as skipped + skippedFields.add(dataKey); + } + + // Mark field as processed so it doesn't get overwritten by ctValue loop + processedFields.add(dataKey); + skippedFields.add(dataKey); // Also skip in ctValue loop + + continue; // Skip further processing for this field + } + } + + // Handle other field types by checking field configs + const matchingFieldConfig = fieldConfigs.find( + (fc) => + dataKey === `${fc.field_name}_value` || + dataKey === `${fc.field_name}_status` || + dataKey === fc.field_name + ); + + if (matchingFieldConfig) { + // Handle datetime and timestamps + if ( + matchingFieldConfig.field_type === 'datetime' || + matchingFieldConfig.field_type === 'timestamp' + ) { + if (dataKey === `${matchingFieldConfig.field_name}_value`) { + if (typeof value === 'number') { + processedData[dataKey] = new Date(value * 1000).toISOString(); + } else { + processedData[dataKey] = isoDate.toISOString(); + } + // Mark field as processed to avoid duplicate processing in second loop + processedFields.add(dataKey); + processedFields.add(matchingFieldConfig.field_name); + continue; + } + } + + // Handle boolean fields + if (matchingFieldConfig.field_type === 'boolean') { + if ( + dataKey === `${matchingFieldConfig.field_name}_value` && + typeof value === 'number' + ) { + processedData[dataKey] = value === 1; + // Mark field as processed to avoid duplicate processing in second loop + processedFields.add(dataKey); + processedFields.add(matchingFieldConfig.field_name); + continue; + } + } + + // Handle comment fields + if (matchingFieldConfig.field_type === 'comment') { + if ( + dataKey === `${matchingFieldConfig.field_name}_status` && + typeof value === 'number' + ) { + processedData[dataKey] = `${value}`; + // Mark field as processed to avoid duplicate processing in second loop + processedFields.add(dataKey); + processedFields.add(matchingFieldConfig.field_name); + continue; + } + } + } + + // Remove null, undefined, and empty values + if (value === null || value === undefined || value === '') { + // Skip null, undefined, and empty string values + continue; + } + + // Default case: copy field to processedData if it wasn't handled by special processing above + if (!(dataKey in processedData)) { + processedData[dataKey] = value; + } + } + + // Process standard field transformations + const ctValue: any = {}; + + for (const fieldName of fieldNames) { + // Skip fields that were intentionally excluded in the main processing loop + if (skippedFields.has(fieldName)) { + continue; + } + + const value = entryData[fieldName]; + + if (fieldName === 'created') { + ctValue[fieldName] = new Date(value * 1000).toISOString(); + } else if (fieldName === 'uid_name') { + ctValue[fieldName] = [value]; + } else if (fieldName.endsWith('_tid')) { + ctValue[fieldName] = [value]; + } else if (fieldName === 'nid') { + ctValue.uid = uidCorrector({ + id: `content_type_entries_title_${value}`, + prefix, + }); + } else if (fieldName === 'langcode') { + // Use the actual langcode from the entry for proper multilingual support + ctValue.locale = value || 'en-us'; // fallback to en-us if langcode is empty + } else if (fieldName.endsWith('_uri')) { + // Skip if this field has already been processed + if (processedFields.has(fieldName)) { + continue; + } + + const baseFieldName = fieldName.replace('_uri', ''); + const titleFieldName = `${baseFieldName}_title`; + + // Check if we also have title data + const titleValue = entryData[titleFieldName]; + + if (value) { + ctValue[baseFieldName] = { + title: titleValue || value, // Use title if available, fallback to URI + href: value, + }; + } else { + ctValue[baseFieldName] = { + title: titleValue || '', + href: '', + }; + } + + // Mark title field as processed to avoid duplicate processing + if (titleValue) { + processedFields.add(titleFieldName); + } + } else if (fieldName.endsWith('_title')) { + // Skip _title fields as they're handled with _uri fields + if (processedFields.has(fieldName)) { + continue; + } + + // Check if there's a corresponding _uri field + const baseFieldName = fieldName.replace('_title', ''); + const uriFieldName = `${baseFieldName}_uri`; + + if (entryData[uriFieldName]) { + // URI field will handle this, skip processing here + continue; + } else { + // No URI field found, process title field standalone (rare case) + ctValue[baseFieldName] = { + title: value || '', + href: '', + }; + } + } else if (fieldName.endsWith('_value')) { + // Skip if this field was already processed in the main loop (avoid duplicates) + const baseFieldName = fieldName.replace('_value', ''); + if ( + processedFields.has(fieldName) || + processedFields.has(baseFieldName) + ) { + continue; + } + + // Check if content contains HTML + if (/<\/?[a-z][\s\S]*>/i.test(value)) { + const dom = new JSDOM(value); + const htmlDoc = dom.window.document.querySelector('body'); + const jsonValue = htmlToJson(htmlDoc); + ctValue[baseFieldName] = jsonValue; + } else { + ctValue[baseFieldName] = value; + } + + // Mark both the original and base field as processed to avoid duplicates + processedFields.add(fieldName); + processedFields.add(baseFieldName); + } else if (fieldName.endsWith('_status')) { + // Skip if this field was already processed in the main loop (avoid duplicates) + const baseFieldName = fieldName.replace('_status', ''); + if ( + processedFields.has(fieldName) || + processedFields.has(baseFieldName) + ) { + continue; + } + + ctValue[baseFieldName] = value; + + // Mark both the original and base field as processed to avoid duplicates + processedFields.add(fieldName); + processedFields.add(baseFieldName); + } else { + // Check if content contains HTML + if (typeof value === 'string' && /<\/?[a-z][\s\S]*>/i.test(value)) { + const dom = new JSDOM(value); + const htmlDoc = dom.window.document.querySelector('body'); + const jsonValue = htmlToJson(htmlDoc); + ctValue[fieldName] = jsonValue; + } else { + ctValue[fieldName] = value; + } + } + } + + // Apply processed field data, but prioritize ctValue (processed without suffixes) over processedData (with suffixes) + // This prevents duplicate fields like both 'body' and 'body_value' from appearing + const mergedData = { ...processedData, ...ctValue }; + + // Final cleanup: remove any null, undefined, or empty values from the final result + // Also remove duplicate fields where both suffixed and non-suffixed versions exist + const cleanedEntry: any = {}; + for (const [key, val] of Object.entries(mergedData)) { + if (val !== null && val !== undefined && val !== '') { + // Check if this is a suffixed field (_value, _status, _uri) and if a non-suffixed version exists + const isValueField = key.endsWith('_value'); + const isStatusField = key.endsWith('_status'); + const isUriField = key.endsWith('_uri'); + + if (isValueField) { + const baseFieldName = key.replace('_value', ''); + // Only include the _value field if the base field doesn't exist + if (!mergedData.hasOwnProperty(baseFieldName)) { + cleanedEntry[key] = val; + } + // If base field exists, skip the _value field (base field takes priority) + } else if (isStatusField) { + const baseFieldName = key.replace('_status', ''); + // Only include the _status field if the base field doesn't exist + if (!mergedData.hasOwnProperty(baseFieldName)) { + cleanedEntry[key] = val; + } + // If base field exists, skip the _status field (base field takes priority) + } else if (isUriField) { + const baseFieldName = key.replace('_uri', ''); + // Only include the _uri field if the base field doesn't exist + if (!mergedData.hasOwnProperty(baseFieldName)) { + cleanedEntry[key] = val; + } + // If base field exists, skip the _uri field (base field takes priority) + } else { + // For non-suffixed fields, always include them + cleanedEntry[key] = val; + } + } + } + + return cleanedEntry; +}; + +/** + * Processes entries for a specific content type and pagination offset + */ +const processEntries = async ( + connection: mysql.Connection, + contentType: string, + skip: number, + queryPageConfig: QueryConfig, + fieldConfigs: DrupalFieldConfig[], + assetId: any, + referenceId: any, + taxonomyId: any, + taxonomyFieldMapping: TaxonomyFieldMapping, + referenceFieldMapping: ReferenceFieldMapping, + assetFieldMapping: AssetFieldMapping, + taxonomyReferenceLookup: Record, + projectId: string, + destination_stack_id: string, + masterLocale: string, + contentTypeMapping: any[] = [], + isTest: boolean = false, + project: any = null +): Promise<{ [key: string]: any } | null> => { + const srcFunc = 'processEntries'; + + try { + // Following original pattern: queryPageConfig['page']['' + pagename + ''] + const baseQuery = queryPageConfig['page'][contentType]; + if (!baseQuery) { + throw new Error(`No query found for content type: ${contentType}`); + } + + // Check if this is an optimized query (content type with many fields) + const isOptimizedQuery = baseQuery.includes('/* OPTIMIZED_NO_JOINS:'); + let entries: any[] = []; + + if (isOptimizedQuery) { + // Handle content types with many fields using optimized approach + const fieldCountMatch = baseQuery.match( + /\/\* OPTIMIZED_NO_JOINS:(\d+) \*\// + ); + const fieldCount = fieldCountMatch ? parseInt(fieldCountMatch[1]) : 0; + + const optimizedMessage = getLogMessage( + srcFunc, + `Processing ${contentType} with optimized field fetching (${fieldCount} fields)`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + optimizedMessage + ); + + // Execute base query without field JOINs + const effectiveLimit = isTest ? 1 : LIMIT; + const cleanBaseQuery = baseQuery + .replace(/\/\* OPTIMIZED_NO_JOINS:\d+ \*\//, '') + .trim(); + const query = cleanBaseQuery + ` LIMIT ${skip}, ${effectiveLimit}`; + const baseEntries = await executeQuery(connection, query); + + if (baseEntries.length === 0) { + return null; + } + + // Fetch field data separately using FieldFetcherService + const fieldFetcher = new FieldFetcherService( + connection, + projectId, + destination_stack_id + ); + const nodeIds = baseEntries.map((entry) => entry.nid); + const fieldsForType = await fieldFetcher.getFieldsForContentType( + contentType + ); + + if (fieldsForType.length > 0) { + const fieldData = await fieldFetcher.fetchFieldDataForContentType( + contentType, + nodeIds, + fieldsForType + ); + + // Merge base entries with field data + entries = fieldFetcher.mergeNodeAndFieldData(baseEntries, fieldData); + + const mergeMessage = getLogMessage( + srcFunc, + `Merged ${baseEntries.length} base entries with field data for ${contentType}`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + mergeMessage + ); + } else { + entries = baseEntries; + } + } else { + // Handle content types with few fields using traditional approach + const effectiveLimit = isTest ? 1 : LIMIT; + const query = baseQuery + ` LIMIT ${skip}, ${effectiveLimit}`; + entries = await executeQuery(connection, query); + + if (entries.length === 0) { + return null; + } + } + + // Group entries by their actual locale (langcode) for proper multilingual support + const entriesByLocale: { [locale: string]: any[] } = {}; + + // Group entries by their langcode + entries.forEach((entry) => { + const entryLocale = entry.langcode || masterLocale; // fallback to masterLocale if no langcode + if (!entriesByLocale[entryLocale]) { + entriesByLocale[entryLocale] = []; + } + entriesByLocale[entryLocale].push(entry); + }); + + // Map source locales to destination locales using user-selected mapping from UI + // This replaces the old hardcoded transformation rules with dynamic user mapping + const transformedEntriesByLocale: { [locale: string]: any[] } = {}; + const allLocales = Object.keys(entriesByLocale); + const hasEn = allLocales.includes('en'); + const hasEnUs = allLocales.includes('en-us'); + + // Get locale mapping configuration from project + const localeMapping = project?.localeMapping || {}; + const localesFromProject = { + masterLocale: project?.master_locale || {}, + ...(project?.locales || {}), + }; + + // Get source master locale from database query + // Use the masterLocale parameter which was fetched from Drupal's system.site config + const sourceMasterLocale = masterLocale || 'en'; + + // Get destination master locale from project configuration + // Priority: localeMapping -> master_locale values -> stackDetails.master_locale -> masterLocale + const masterLocaleKey = `${sourceMasterLocale}-master_locale`; + const destinationMasterLocale = + localeMapping?.[masterLocaleKey] || + Object.values(project?.master_locale || {})?.[0] || // ✅ Use values() not keys()! + project?.stackDetails?.master_locale || + masterLocale || + 'en-us'; + // Apply source locale transformation rules first (und → en-us, etc.) + // Then map the transformed source locale to destination locale using user's selection + Object.entries(entriesByLocale).forEach(([originalLocale, entries]) => { + // Step 1: Apply Drupal-specific transformation rules (same as before) + let transformedSourceLocale = originalLocale; + + if (originalLocale === 'und') { + if (hasEn && hasEnUs) { + // Rule 4: All three "en" + "und" + "en-us" → all three stays + transformedSourceLocale = 'und'; + } else if (hasEnUs && !hasEn) { + // Rule 2: "und" + "en-us" → "und" become "en", "en-us" stays + transformedSourceLocale = 'en'; + } else if (hasEn && !hasEnUs) { + // Rule 3: "en" + "und" → "und" becomes "en-us", "en" stays + transformedSourceLocale = 'en-us'; + } else if (!hasEn && !hasEnUs) { + // Rule 1: "und" alone → "en-us" + transformedSourceLocale = 'en-us'; + } else { + // Keep as is for any other combinations + transformedSourceLocale = 'und'; + } + } else if (originalLocale === 'en-us') { + // "en-us" always stays as "en-us" in all rules + transformedSourceLocale = 'en-us'; + } else if (originalLocale === 'en') { + // "en" always stays as "en" in all rules (never transforms to "und") + transformedSourceLocale = 'en'; + } + + // Step 2: Map transformed source locale to destination locale using user's mapping + const destinationLocale = mapDrupalLocales({ + masterLocale, + locale: transformedSourceLocale, + locales: localesFromProject, + localeMapping, + sourceMasterLocale, + destinationMasterLocale, + }); + + // Merge entries if destination locale already has entries + if (transformedEntriesByLocale[destinationLocale]) { + transformedEntriesByLocale[destinationLocale] = [ + ...transformedEntriesByLocale[destinationLocale], + ...entries, + ]; + } else { + transformedEntriesByLocale[destinationLocale] = entries; + } + }); + + // Find content type mapping for field type switching + const currentContentTypeMapping = contentTypeMapping.find( + (ct) => + ct.otherCmsUid === contentType || ct.contentstackUid === contentType + ); + + const allProcessedContent: { [key: string]: any } = {}; + + // Process entries for each transformed locale separately + for (const [currentLocale, localeEntries] of Object.entries( + transformedEntriesByLocale + )) { + // Create folder structure: entries/contentType/locale/ + const contentTypeFolderPath = path.join( + MIGRATION_DATA_CONFIG.DATA, + destination_stack_id, + MIGRATION_DATA_CONFIG.ENTRIES_DIR_NAME, + contentType + ); + const localeFolderPath = path.join(contentTypeFolderPath, currentLocale); + await fs.promises.mkdir(localeFolderPath, { recursive: true }); + + // Read existing content for this locale or initialize + const localeFileName = `${currentLocale}.json`; + const existingLocaleContent = + (await readFile(localeFolderPath, localeFileName)) || {}; + + // Extract prefix from project for UID correction + const prefix = project?.legacy_cms?.affix || DEFAULT_PREFIX; + + // Process each entry in this locale + for (const entry of localeEntries) { + let processedEntry = await processFieldData( + entry, + fieldConfigs, + assetId, + referenceId, + taxonomyId, + taxonomyFieldMapping, + referenceFieldMapping, + assetFieldMapping, + taxonomyReferenceLookup, + contentType, + prefix + ); + + // 🏷️ TAXONOMY CONSOLIDATION: Merge all taxonomy fields into single 'taxonomies' field + processedEntry = consolidateTaxonomyFields( + processedEntry, + contentType, + taxonomyFieldMapping + ); + + // Apply field type switching based on user's UI selections (from content type schema) + const enhancedEntry: any = {}; + + // Process each field with type switching support + for (const [fieldName, fieldValue] of Object.entries(processedEntry)) { + let fieldMapping = null; + + // PRIORITY 1: Read from generated content type schema (has UI-selected field types) + // This is checked FIRST because it contains the final field types after user's UI changes + // Load the content type schema to get user's field type selections + try { + const contentTypeSchemaPath = path.join( + MIGRATION_DATA_CONFIG.DATA, + destination_stack_id, + 'content_types', + `${contentType}.json` + ); + const contentTypeSchema = JSON.parse( + await fs.promises.readFile(contentTypeSchemaPath, 'utf8') + ); + + // Find field in schema + const schemaField = contentTypeSchema.schema?.find( + (field: any) => + field.uid === fieldName || + field.uid === fieldName.replace(/_target_id$/, '') || + field.uid === fieldName.replace(/_value$/, '') || + fieldName.includes(field.uid) + ); + + if (schemaField) { + // Determine the proper field type based on schema configuration + let targetFieldType = schemaField.data_type; + + // Handle HTML RTE fields (text with allow_rich_text: true) + if ( + schemaField.data_type === 'text' && + schemaField.field_metadata?.allow_rich_text === true + ) { + targetFieldType = 'html'; // ✅ HTML RTE field + } + // Handle JSON RTE fields + else if (schemaField.data_type === 'json') { + targetFieldType = 'json'; // ✅ JSON RTE field + } + // Handle text fields with multiline metadata + else if ( + schemaField.data_type === 'text' && + schemaField.field_metadata?.multiline + ) { + targetFieldType = 'multi_line_text'; // ✅ Multi-line text field + } + + // Create a mapping from schema field + fieldMapping = { + uid: fieldName, + contentstackFieldType: targetFieldType, + backupFieldType: schemaField.data_type, + advanced: schemaField, + }; + } + } catch (error: any) { + // Schema not found, will try fallback below + } + + // FALLBACK: If schema not found, try UI content type mapping + if ( + !fieldMapping && + currentContentTypeMapping && + currentContentTypeMapping.fieldMapping + ) { + fieldMapping = currentContentTypeMapping.fieldMapping.find( + (fm: any) => + fm.uid === fieldName || + fm.otherCmsField === fieldName || + fieldName.startsWith(fm.uid) || + fieldName.includes(fm.uid) + ); + } + + if (fieldMapping) { + // Apply field type processing based on user's selection + const processedValue = processFieldByType( + fieldValue, + fieldMapping, + assetId, + referenceId + ); + + // Only add field if processed value is not undefined (undefined means remove field) + if (processedValue !== undefined) { + enhancedEntry[fieldName] = processedValue; + + // Log field type processing + if ( + fieldMapping.contentstackFieldType !== + fieldMapping.backupFieldType + ) { + const message = getLogMessage( + srcFunc, + `Field ${fieldName} processed as ${fieldMapping.contentstackFieldType} (switched from ${fieldMapping.backupFieldType})`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + message + ); + } + } else { + // Log field removal + const message = getLogMessage( + srcFunc, + `Field ${fieldName} removed due to missing or invalid asset reference`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'warn', + message + ); + } + } else { + // Keep original value if no mapping found + enhancedEntry[fieldName] = fieldValue; + } + } + + processedEntry = enhancedEntry; + + // Add publish_details as an empty array to the end of entry creation + processedEntry.publish_details = []; + + if (typeof entry.nid === 'number') { + const entryUid = uidCorrector({ + id: `content_type_entries_title_${entry.nid}`, + prefix, + }); + existingLocaleContent[entryUid] = processedEntry; + allProcessedContent[entryUid] = processedEntry; + } + + // Log each entry transformation + const message = getLogMessage( + srcFunc, + `Entry with uid ${entry.nid} (locale: ${currentLocale}) for content type ${contentType} has been successfully transformed.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + } + + // Write processed content for this specific locale + await writeFile(localeFolderPath, localeFileName, existingLocaleContent); + + const localeMessage = getLogMessage( + srcFunc, + `Successfully processed ${localeEntries.length} entries for locale ${currentLocale} in content type ${contentType}`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + localeMessage + ); + } + + // 📁 Create mandatory index.json files for each transformed locale directory + for (const [currentLocale, localeEntries] of Object.entries( + transformedEntriesByLocale + )) { + if (localeEntries.length > 0) { + const contentTypeFolderPath = path.join( + MIGRATION_DATA_CONFIG.DATA, + destination_stack_id, + MIGRATION_DATA_CONFIG.ENTRIES_DIR_NAME, + contentType + ); + const localeFolderPath = path.join( + contentTypeFolderPath, + currentLocale + ); + const localeFileName = `${currentLocale}.json`; + + // Create mandatory index.json file that maps to the locale file + const indexData = { '1': localeFileName }; + await writeFile(localeFolderPath, 'index.json', indexData); + } + } + + return allProcessedContent; + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error processing entries for ${contentType}: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Gets count and processes all entries for a specific content type + */ +const processContentType = async ( + connection: mysql.Connection, + contentType: string, + queryPageConfig: QueryConfig, + fieldConfigs: DrupalFieldConfig[], + assetId: any, + referenceId: any, + taxonomyId: any, + taxonomyFieldMapping: TaxonomyFieldMapping, + referenceFieldMapping: ReferenceFieldMapping, + assetFieldMapping: AssetFieldMapping, + taxonomyReferenceLookup: Record, + projectId: string, + destination_stack_id: string, + masterLocale: string, + contentTypeMapping: any[] = [], + isTest: boolean = false, + project: any = null +): Promise => { + const srcFunc = 'processContentType'; + + try { + // Get total count for pagination (if count query exists) + const countKey = `${contentType}Count`; + let totalCount = 1; // Default to process at least one batch + + if (queryPageConfig.count && queryPageConfig.count[countKey]) { + const countQuery = queryPageConfig.count[countKey]; + const countResults = await executeQuery(connection, countQuery); + totalCount = countResults[0]?.countentry || 0; + } + + if (totalCount === 0) { + const message = getLogMessage( + srcFunc, + `No entries found for content type ${contentType}.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + return; + } + + // 🧪 Process entries in batches (test migration: single entry, main migration: all entries) + const effectiveLimit = isTest ? 1 : LIMIT; + + for ( + let i = 0; + i < (isTest ? effectiveLimit : totalCount + LIMIT); + i += effectiveLimit + ) { + const result = await processEntries( + connection, + contentType, + i, + queryPageConfig, + fieldConfigs, + assetId, + referenceId, + taxonomyId, + taxonomyFieldMapping, + referenceFieldMapping, + assetFieldMapping, + taxonomyReferenceLookup, + projectId, + destination_stack_id, + masterLocale, + contentTypeMapping, + isTest, + project + ); + + // If no entries returned, break the loop + if (!result) { + break; + } + } + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error processing content type ${contentType}: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Reads dynamic query configuration file generated by query.service.ts + * Following original pattern: helper.readFile(path.join(process.cwd(), config.data, 'query', 'index.json')) + * + * NOTE: No fallback to hardcoded queries - dynamic queries MUST be generated first + */ +async function readQueryConfig( + destination_stack_id: string +): Promise { + try { + const queryPath = path.join( + DATA, + destination_stack_id, + 'query', + 'index.json' + ); + const data = await fs.promises.readFile(queryPath, 'utf8'); + return JSON.parse(data); + } catch (err) { + // No fallback - dynamic queries must be generated first by createQuery() service + throw new Error( + `❌ No dynamic query configuration found at query/index.json. Dynamic queries must be generated first using createQuery() service. Original error: ${err}` + ); + } +} + +/** + * Creates and processes entries from Drupal database for migration to Contentstack. + * Based on the original Drupal v8 migration logic with direct SQL queries. + * + * Supports dynamic SQL queries from query/index.json file following original pattern: + * var queryPageConfig = helper.readFile(path.join(process.cwd(), config.data, 'query', 'index.json')); + * var query = queryPageConfig['page']['' + pagename + '']; + */ +export const createEntry = async ( + dbConfig: any, + destination_stack_id: string, + projectId: string, + isTest = false, + masterLocale = 'en-us', + contentTypeMapping: any[] = [], + project: any = null +): Promise => { + const srcFunc = 'createEntry'; + let connection: mysql.Connection | null = null; + + try { + const entriesSave = path.join(DATA, destination_stack_id, ENTRIES_DIR_NAME); + const assetsSave = path.join(DATA, destination_stack_id, ASSETS_DIR_NAME); + const referencesSave = path.join( + DATA, + destination_stack_id, + REFERENCES_DIR_NAME + ); + + // Initialize directories + await fs.promises.mkdir(entriesSave, { recursive: true }); + + const message = getLogMessage(srcFunc, `Exporting entries...`, {}); + await customLogger(projectId, destination_stack_id, 'info', message); + + // Read query configuration (following original pattern) + const queryPageConfig = await readQueryConfig(destination_stack_id); + + // Create database connection + connection = await getDbConnection( + dbConfig, + projectId, + destination_stack_id + ); + + // Analyze field types to identify taxonomy, reference, and asset fields + const { + taxonomyFields: taxonomyFieldMapping, + referenceFields: referenceFieldMapping, + assetFields: assetFieldMapping, + } = await analyzeFieldTypes(dbConfig, destination_stack_id, projectId); + + // Fetch field configurations + const fieldConfigs = await fetchFieldConfigs( + connection, + projectId, + destination_stack_id + ); + + // Read supporting data - following original page.js pattern + // Load assets from index.json (your new format) + const assetId = (await readFile(assetsSave, 'index.json')) || {}; + + const referenceId = + (await readFile(referencesSave, REFERENCES_FILE_NAME)) || {}; + const taxonomyId = + (await readFile( + path.join(entriesSave, 'taxonomy'), + `${masterLocale}.json` + )) || {}; + + // Load taxonomy reference mappings for field transformation + const taxonomyReferenceLookup = await loadTaxonomyReferences( + referencesSave + ); + + // Process each content type from query config (like original) + const pageQuery = queryPageConfig.page; + const contentTypes = Object.keys(pageQuery); + // 🧪 Test migration: Process ALL content types but with limited data per content type + const typesToProcess = contentTypes; // Always process all content types + + for (const contentType of typesToProcess) { + await processContentType( + connection, + contentType, + queryPageConfig, + fieldConfigs, + assetId, + referenceId, + taxonomyId, + taxonomyFieldMapping, + referenceFieldMapping, + assetFieldMapping, + taxonomyReferenceLookup, + projectId, + destination_stack_id, + masterLocale, + contentTypeMapping, + isTest, + project + ); + } + + const successMessage = getLogMessage( + srcFunc, + `Successfully processed entries for ${typesToProcess.length} content types with multilingual support.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', successMessage); + + // Log multilingual structure summary + const structureSummary = getLogMessage( + srcFunc, + `Multilingual entries structure created at: ${DATA}/${destination_stack_id}/${ENTRIES_DIR_NAME}/[contentType]/[locale]/[locale].json`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + structureSummary + ); + } catch (err) { + const message = getLogMessage( + srcFunc, + `Error encountered while creating entries.`, + {}, + err + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw err; + } finally { + // Close database connection + if (connection) { + connection.end(); + } + } +}; diff --git a/api/src/services/drupal/field-analysis.service.ts b/api/src/services/drupal/field-analysis.service.ts new file mode 100644 index 000000000..fdfac7e90 --- /dev/null +++ b/api/src/services/drupal/field-analysis.service.ts @@ -0,0 +1,427 @@ +import mysql from "mysql2"; +import { getDbConnection } from "../../helper/index.js"; +import customLogger from "../../utils/custom-logger.utils.js"; +import { getLogMessage } from "../../utils/index.js"; +// Dynamic import for phpUnserialize will be used in the function + +interface FieldInfo { + field_name: string; + content_types: string; + field_type: string; + content_handler?: string; + target_type?: string; + handler_settings?: any; +} + +export interface TaxonomyFieldMapping { + [contentType: string]: { + [fieldName: string]: { + vocabulary?: string; + handler: string; + field_type: string; + }; + }; +} + +export interface ReferenceFieldMapping { + [contentType: string]: { + [fieldName: string]: { + target_type: string; + handler: string; + field_type: string; + }; + }; +} + +export interface AssetFieldMapping { + [contentType: string]: { + [fieldName: string]: { + field_type: string; + file_extensions?: string[]; + upload_location?: string; + max_filesize?: string; + }; + }; +} + +/** + * Execute SQL query with promise support + */ +const executeQuery = async (connection: mysql.Connection, query: string): Promise => { + return new Promise((resolve, reject) => { + connection.query(query, (error, results) => { + if (error) { + reject(error); + return; + } + resolve(results as any[]); + }); + }); +}; + +/** + * Analyze field configuration to identify taxonomy and reference fields + * Based on the original query.js logic that checks content_handler + */ +export const analyzeFieldTypes = async ( + dbConfig: any, + destination_stack_id: string, + projectId: string +): Promise<{ taxonomyFields: TaxonomyFieldMapping; referenceFields: ReferenceFieldMapping; assetFields: AssetFieldMapping }> => { + const srcFunc = 'analyzeFieldTypes'; + let connection: mysql.Connection | null = null; + + try { + const message = getLogMessage( + srcFunc, + `Analyzing field types to identify taxonomy fields...`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + // Create database connection + connection = await getDbConnection(dbConfig, projectId, destination_stack_id); + + // Query to get field configurations (same as original ct_mapped query) + const fieldConfigQuery = ` + SELECT *, CONVERT(data USING utf8) as data + FROM config + WHERE name LIKE '%field.field.node%' + `; + + const fieldConfigs = await executeQuery(connection, fieldConfigQuery); + + const taxonomyFieldMapping: TaxonomyFieldMapping = {}; + const referenceFieldMapping: ReferenceFieldMapping = {}; + const assetFieldMapping: AssetFieldMapping = {}; + let taxonomyFieldCount = 0; + let referenceFieldCount = 0; + let assetFieldCount = 0; + let totalFieldCount = 0; + + for (const fieldConfig of fieldConfigs) { + try { + // Unserialize the PHP data to get field details + const { unserialize } = await import('php-serialize'); + const fieldData = unserialize(fieldConfig.data); + + if (fieldData && fieldData.field_name && fieldData.bundle) { + totalFieldCount++; + + const fieldInfo: FieldInfo = { + field_name: fieldData.field_name, + content_types: fieldData.bundle, + field_type: fieldData.field_type || 'unknown', + content_handler: fieldData?.settings?.handler, + target_type: fieldData?.settings?.target_type, + handler_settings: fieldData?.settings?.handler_settings + }; + + // Initialize content type mappings if not exists + if (!taxonomyFieldMapping[fieldInfo.content_types]) { + taxonomyFieldMapping[fieldInfo.content_types] = {}; + } + if (!referenceFieldMapping[fieldInfo.content_types]) { + referenceFieldMapping[fieldInfo.content_types] = {}; + } + if (!assetFieldMapping[fieldInfo.content_types]) { + assetFieldMapping[fieldInfo.content_types] = {}; + } + + // Check if this is a taxonomy reference field + const isTaxonomyField = + // Check handler for taxonomy references + (fieldInfo.content_handler && fieldInfo.content_handler.includes('taxonomy_term')) || + // Check target_type for entity references to taxonomy terms + (fieldInfo.target_type === 'taxonomy_term') || + // Check field type for direct taxonomy reference fields + (fieldInfo.field_type === 'entity_reference' && fieldInfo.target_type === 'taxonomy_term') || + (fieldInfo.field_type === 'taxonomy_term_reference') || + // Check handler settings for vocabulary restrictions (taxonomy specific) + (fieldInfo.handler_settings?.target_bundles && + Object.keys(fieldInfo.handler_settings.target_bundles).some(bundle => + fieldInfo.target_type === 'taxonomy_term')); + + // Check if this is a node reference field (non-taxonomy entity reference) + const isReferenceField = + // Check for entity_reference field type + (fieldInfo.field_type === 'entity_reference') && + // Check handler for node references + (fieldInfo.content_handler && fieldInfo.content_handler.includes('node')) || + // Check target_type for entity references to nodes + (fieldInfo.target_type === 'node') && + // Make sure it's NOT a taxonomy field + !isTaxonomyField; + + if (isTaxonomyField) { + taxonomyFieldCount++; + + // Try to determine the vocabulary from handler settings + let vocabulary = 'unknown'; + if (fieldInfo.handler_settings?.target_bundles) { + const vocabularies = Object.keys(fieldInfo.handler_settings.target_bundles); + vocabulary = vocabularies.length === 1 ? vocabularies[0] : vocabularies.join(','); + } + + taxonomyFieldMapping[fieldInfo.content_types][fieldInfo.field_name] = { + vocabulary, + handler: fieldInfo.content_handler || 'default:taxonomy_term', + field_type: fieldInfo.field_type + }; + + const taxonomyMessage = getLogMessage( + srcFunc, + `Found taxonomy field: ${fieldInfo.content_types}.${fieldInfo.field_name} → vocabulary: ${vocabulary}`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', taxonomyMessage); + } else if (isReferenceField) { + referenceFieldCount++; + + referenceFieldMapping[fieldInfo.content_types][fieldInfo.field_name] = { + target_type: fieldInfo.target_type || 'node', + handler: fieldInfo.content_handler || 'default:node', + field_type: fieldInfo.field_type + }; + + const referenceMessage = getLogMessage( + srcFunc, + `Found reference field: ${fieldInfo.content_types}.${fieldInfo.field_name} → target_type: ${fieldInfo.target_type || 'node'}`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', referenceMessage); + } + + // Check if this is an asset/file field + const isAssetField = + // Check for file field type + (fieldInfo.field_type === 'file') || + // Check for image field type + (fieldInfo.field_type === 'image') || + // Check for managed_file field type + (fieldInfo.field_type === 'managed_file') || + // Check for entity_reference to file entities + (fieldInfo.field_type === 'entity_reference' && fieldInfo.target_type === 'file'); + + if (isAssetField) { + assetFieldCount++; + + // Extract file-related settings + const fileExtensions = fieldData?.settings?.file_extensions ? + fieldData.settings.file_extensions.split(' ') : []; + const uploadLocation = fieldData?.settings?.file_directory || + fieldData?.settings?.uri_scheme || 'public://'; + const maxFilesize = fieldData?.settings?.max_filesize || + fieldData?.settings?.file_size || ''; + + assetFieldMapping[fieldInfo.content_types][fieldInfo.field_name] = { + field_type: fieldInfo.field_type, + file_extensions: fileExtensions, + upload_location: uploadLocation, + max_filesize: maxFilesize + }; + + const assetMessage = getLogMessage( + srcFunc, + `Found asset field: ${fieldInfo.content_types}.${fieldInfo.field_name} → type: ${fieldInfo.field_type}, extensions: [${fileExtensions.join(', ')}]`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', assetMessage); + } + } + + } catch (parseError: any) { + // Log parsing error but continue with other fields + const parseMessage = getLogMessage( + srcFunc, + `Could not parse field config: ${parseError.message}`, + {}, + parseError + ); + await customLogger(projectId, destination_stack_id, 'warn', parseMessage); + } + } + + const summaryMessage = getLogMessage( + srcFunc, + `Field analysis complete: ${taxonomyFieldCount} taxonomy fields, ${referenceFieldCount} reference fields, and ${assetFieldCount} asset fields found out of ${totalFieldCount} total fields.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', summaryMessage); + + return { + taxonomyFields: taxonomyFieldMapping, + referenceFields: referenceFieldMapping, + assetFields: assetFieldMapping + }; + + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error analyzing field types: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } finally { + if (connection) { + connection.end(); + } + } +}; + +/** + * Check if a specific field is a taxonomy field + */ +export const isTaxonomyField = ( + fieldName: string, + contentType: string, + taxonomyMapping: TaxonomyFieldMapping +): boolean => { + return !!(taxonomyMapping[contentType] && taxonomyMapping[contentType][fieldName]); +}; + +/** + * Check if a specific field is a reference field + */ +export const isReferenceField = ( + fieldName: string, + contentType: string, + referenceMapping: ReferenceFieldMapping +): boolean => { + return !!(referenceMapping[contentType] && referenceMapping[contentType][fieldName]); +}; + +/** + * Check if a specific field is an asset field + */ +export const isAssetField = ( + fieldName: string, + contentType: string, + assetMapping: AssetFieldMapping +): boolean => { + return !!(assetMapping[contentType] && assetMapping[contentType][fieldName]); +}; + +/** + * Get taxonomy field information + */ +export const getTaxonomyFieldInfo = ( + fieldName: string, + contentType: string, + taxonomyMapping: TaxonomyFieldMapping +) => { + return taxonomyMapping[contentType]?.[fieldName] || null; +}; + +/** + * Get reference field information + */ +export const getReferenceFieldInfo = ( + fieldName: string, + contentType: string, + referenceMapping: ReferenceFieldMapping +) => { + return referenceMapping[contentType]?.[fieldName] || null; +}; + +/** + * Get asset field information + */ +export const getAssetFieldInfo = ( + fieldName: string, + contentType: string, + assetMapping: AssetFieldMapping +) => { + return assetMapping[contentType]?.[fieldName] || null; +}; + +/** + * Transform taxonomy field value to Contentstack format + * Converts tid to taxonomy term uid based on our taxonomy data + * + * The taxonomyData should contain individual vocabulary files: + * - taxonomies/list.json + * - taxonomies/news.json + * etc. + */ +export const transformTaxonomyValue = async ( + value: any, + fieldName: string, + contentType: string, + taxonomyMapping: TaxonomyFieldMapping, + taxonomyBasePath: string +): Promise => { + const fieldInfo = getTaxonomyFieldInfo(fieldName, contentType, taxonomyMapping); + + if (!fieldInfo || !value) { + return value; + } + + // If it's a taxonomy field with tid value, try to find the corresponding term + if (typeof value === 'number' || (typeof value === 'string' && /^\d+$/.test(value))) { + const tid = parseInt(value.toString()); + + try { + // Try to determine which vocabulary to look in based on field info + const vocabularies = fieldInfo.vocabulary ? fieldInfo.vocabulary.split(',') : ['unknown']; + + for (const vocabulary of vocabularies) { + try { + const fs = await import('fs'); + const path = await import('path'); + + const taxonomyFilePath = path.join(taxonomyBasePath, `${vocabulary}.json`); + + if (fs.existsSync(taxonomyFilePath)) { + const taxonomyContent = JSON.parse(fs.readFileSync(taxonomyFilePath, 'utf8')); + + if (taxonomyContent.terms && Array.isArray(taxonomyContent.terms)) { + for (const term of taxonomyContent.terms) { + if (term.drupal_term_id === tid) { + return term.uid; + } + } + } + } + } catch (vocabError) { + // Continue to next vocabulary if this one fails + continue; + } + } + + // If we couldn't find it in specific vocabularies, try all taxonomy files + const fs = await import('fs'); + const path = await import('path'); + + if (fs.existsSync(taxonomyBasePath)) { + const taxonomyFiles = fs.readdirSync(taxonomyBasePath) + .filter(file => file.endsWith('.json') && file !== 'taxonomies.json'); + + for (const file of taxonomyFiles) { + try { + const taxonomyContent = JSON.parse(fs.readFileSync(path.join(taxonomyBasePath, file), 'utf8')); + + if (taxonomyContent.terms && Array.isArray(taxonomyContent.terms)) { + for (const term of taxonomyContent.terms) { + if (term.drupal_term_id === tid) { + return term.uid; + } + } + } + } catch (fileError) { + // Continue to next file if this one fails + continue; + } + } + } + + } catch (error) { + // Return original value if transformation fails + return value; + } + } + + return value; +}; diff --git a/api/src/services/drupal/field-fetcher.service.ts b/api/src/services/drupal/field-fetcher.service.ts new file mode 100644 index 000000000..bb2c6ab6c --- /dev/null +++ b/api/src/services/drupal/field-fetcher.service.ts @@ -0,0 +1,228 @@ +import mysql from 'mysql2'; +import { getLogMessage } from '../../utils/index.js'; +import customLogger from '../../utils/custom-logger.utils.js'; + +/** + * Field Fetcher Service for Content Types with Many Fields + * Handles field data fetching for content types that exceed MySQL JOIN limits + */ + +interface DrupalFieldData { + field_name: string; + content_types: string; + type: string; + content_handler?: string; +} + +interface FieldDataResult { + [nid: number]: { + [fieldName: string]: any; + }; +} + +export class FieldFetcherService { + private connection: mysql.Connection; + private projectId: string; + private destinationStackId: string; + + constructor(connection: mysql.Connection, projectId: string, destinationStackId: string) { + this.connection = connection; + this.projectId = projectId; + this.destinationStackId = destinationStackId; + } + + /** + * Fetch field data for content types with many fields using individual queries + * This avoids the MySQL 61-table JOIN limit + */ + async fetchFieldDataForContentType( + contentType: string, + nodeIds: number[], + fieldsForType: DrupalFieldData[] + ): Promise { + const srcFunc = 'fetchFieldDataForContentType'; + const fieldData: FieldDataResult = {}; + + if (nodeIds.length === 0) { + return fieldData; + } + + // Initialize field data structure + nodeIds.forEach(nid => { + fieldData[nid] = {}; + }); + + const message = getLogMessage( + srcFunc, + `Fetching field data for ${contentType}: ${fieldsForType.length} fields, ${nodeIds.length} nodes`, + {} + ); + await customLogger(this.projectId, this.destinationStackId, 'info', message); + + // Process fields in batches to avoid overwhelming the database + const batchSize = 10; + for (let i = 0; i < fieldsForType.length; i += batchSize) { + const fieldBatch = fieldsForType.slice(i, i + batchSize); + + await Promise.all( + fieldBatch.map(field => this.fetchSingleFieldData(field, nodeIds, fieldData)) + ); + } + + const successMessage = getLogMessage( + srcFunc, + `Successfully fetched field data for ${contentType}: ${Object.keys(fieldData).length} nodes processed`, + {} + ); + await customLogger(this.projectId, this.destinationStackId, 'info', successMessage); + + return fieldData; + } + + /** + * Fetch data for a single field across multiple nodes + */ + private async fetchSingleFieldData( + field: DrupalFieldData, + nodeIds: number[], + fieldData: FieldDataResult + ): Promise { + const fieldTableName = `node__${field.field_name}`; + + try { + // Check if field table exists + const tableExistsQuery = ` + SELECT 1 FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + LIMIT 1 + `; + + const [tableExists] = await this.connection.promise().query(tableExistsQuery, [fieldTableName]) as any[]; + + if (tableExists.length === 0) { + console.warn(`Field table ${fieldTableName} does not exist`); + return; + } + + // Get field columns dynamically + const columnQuery = ` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + AND COLUMN_NAME LIKE ? + `; + + const [columns] = await this.connection.promise().query(columnQuery, [fieldTableName, `${field.field_name}_%`]) as any[]; + + if (columns.length === 0) { + console.warn(`No columns found for field ${field.field_name}`); + return; + } + + // Build field query with all relevant columns + const fieldColumns = columns.map((col: any) => col.COLUMN_NAME); + const selectColumns = fieldColumns.join(', '); + + const fieldQuery = ` + SELECT + entity_id, + ${selectColumns} + FROM ${fieldTableName} + WHERE entity_id IN (${nodeIds.map(() => '?').join(',')}) + `; + + const [fieldResults] = await this.connection.promise().query(fieldQuery, nodeIds) as any[]; + + // Merge field results into main data structure + fieldResults.forEach((row: any) => { + const nid = row.entity_id; + if (fieldData[nid]) { + // Add all field columns to the node data + fieldColumns.forEach((columnName: string) => { + if (row[columnName] !== null && row[columnName] !== undefined) { + fieldData[nid][columnName] = row[columnName]; + } + }); + } + }); + + } catch (error: any) { + console.warn(`Error fetching data for field ${field.field_name}:`, error.message); + + const errorMessage = getLogMessage( + 'fetchSingleFieldData', + `Failed to fetch data for field ${field.field_name}: ${error.message}`, + {}, + error + ); + await customLogger(this.projectId, this.destinationStackId, 'warn', errorMessage); + } + } + + /** + * Merge base node data with field data + */ + mergeNodeAndFieldData( + baseNodes: any[], + fieldData: FieldDataResult + ): any[] { + return baseNodes.map(node => { + const nid = node.nid; + const nodeFieldData = fieldData[nid] || {}; + + return { + ...node, + ...nodeFieldData + }; + }); + } + + /** + * Get field configuration for a content type + */ + async getFieldsForContentType(contentType: string): Promise { + const configQuery = ` + SELECT *, CONVERT(data USING utf8) as data + FROM config + WHERE name LIKE '%field.field.node%' + `; + + try { + const [rows] = await this.connection.promise().query(configQuery) as any[]; + const fields: DrupalFieldData[] = []; + + for (const row of rows) { + try { + const { unserialize } = await import('php-serialize'); + const configData = unserialize(row.data); + + if (configData && configData.bundle === contentType) { + fields.push({ + field_name: configData.field_name, + content_types: configData.bundle, + type: configData.field_type, + content_handler: configData?.settings?.handler + }); + } + } catch (parseError) { + console.warn(`Failed to parse field config for ${row.name}:`, parseError); + } + } + + return fields; + } catch (error: any) { + const errorMessage = getLogMessage( + 'getFieldsForContentType', + `Failed to get fields for content type ${contentType}: ${error.message}`, + {}, + error + ); + await customLogger(this.projectId, this.destinationStackId, 'error', errorMessage); + throw error; + } + } +} + +export default FieldFetcherService; diff --git a/api/src/services/drupal/locales.service.ts b/api/src/services/drupal/locales.service.ts new file mode 100644 index 000000000..c10029711 --- /dev/null +++ b/api/src/services/drupal/locales.service.ts @@ -0,0 +1,367 @@ +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; +import { MIGRATION_DATA_CONFIG } from '../../constants/index.js'; +import { Locale } from '../../models/types.js'; +import { getAllLocales, getLogMessage } from '../../utils/index.js'; +import customLogger from '../../utils/custom-logger.utils.js'; +import { createDbConnection } from '../../helper/index.js'; +import { v4 as uuidv4 } from 'uuid'; + +const { + DATA: MIGRATION_DATA_PATH, + LOCALE_DIR_NAME, + LOCALE_FILE_NAME, + LOCALE_MASTER_LOCALE, + LOCALE_CF_LANGUAGE, +} = MIGRATION_DATA_CONFIG; + +/** + * Writes data to a specified file, ensuring the target directory exists. + */ +async function writeFile(dirPath: string, filename: string, data: any) { + try { + await fs.promises.mkdir(dirPath, { recursive: true }); + const filePath = path.join(dirPath, filename); + await fs.promises.writeFile(filePath, JSON.stringify(data), 'utf8'); + } catch (err) { + console.error(`Error writing ${dirPath}/${filename}::`, err); + } +} + +/** + * Maps source locale to destination locale based on user-selected mapping from UI. + * Similar to WordPress/Contentful/Sitecore mapLocales function. + * + * @param masterLocale - The master locale code from the stack + * @param locale - The source locale code from Drupal + * @param locales - The locale mapping object from project (contains master_locale and locales) + * @param localeMapping - The direct locale mapping from UI (optional, takes precedence) + * @returns The mapped destination locale code + */ +export function mapDrupalLocales({ + masterLocale, + locale, + locales, + localeMapping, + sourceMasterLocale, + destinationMasterLocale, +}: { + masterLocale: string; + locale: string; + locales?: any; + localeMapping?: Record; + sourceMasterLocale?: string; + destinationMasterLocale?: string; +}): string { + // Priority 1: Check direct locale mapping from UI (format: { "en-master_locale": "fr-fr", "es": "es-es" }) + if (localeMapping) { + // Check if this is a master locale mapping + const masterKey = `${locale}-master_locale`; + if (localeMapping[masterKey]) { + return localeMapping[masterKey]; + } + + // Check direct mapping + if (localeMapping[locale]) { + return localeMapping[locale]; + } + } + + // Priority 2: Check if source locale matches master locale in mapping + if (locales?.masterLocale?.[masterLocale] === locale) { + return Object.keys(locales.masterLocale)?.[0] || masterLocale; + } + + // Priority 3: Check regular locales mapping + if (locales) { + for (const [key, value] of Object.entries(locales)) { + if (typeof value !== 'object' && value === locale) { + return key; + } + } + } + + // Priority 4: If this is the source master locale, map to destination master locale + // This handles the case where user selected a custom master locale (e.g., "div-mv") in UI + // but locale mapping wasn't saved in project.master_locale/locales + if ( + sourceMasterLocale && + destinationMasterLocale && + locale === sourceMasterLocale + ) { + return destinationMasterLocale?.toLowerCase?.(); + } + + // Priority 5: Check if locale matches the destination master locale (already in correct format) + if ( + destinationMasterLocale && + locale?.toLowerCase?.() === destinationMasterLocale?.toLowerCase?.() + ) { + return destinationMasterLocale?.toLowerCase?.(); + } + + // Priority 6: Return locale as-is (lowercase) + return locale?.toLowerCase?.() || locale; +} + +/** + * Applies special locale code transformations based on business rules + * - "und" alone → "en-us" + * - "und" + "en-us" → "und" become "en", "en-us" stays + * - "en" + "und" → "und" becomes "en-us", "en" stays + * - All three "en" + "und" + "en-us" → all three stays + * - Apart from these, all other locales stay as is + */ +function applyLocaleTransformations( + locales: string[], + masterLocale: string +): { code: string; name: string; isMaster: boolean }[] { + const hasUnd = locales.includes('und'); + const hasEn = locales.includes('en'); + const hasEnUs = locales.includes('en-us'); + + // First, apply the transformation rules to get the correct locale codes + const transformedCodes: string[] = []; + + // Start with all non-special locales (not und, en, en-us) + const nonSpecialLocales = locales.filter( + (locale) => !['und', 'en', 'en-us'].includes(locale) + ); + transformedCodes.push(...nonSpecialLocales); + + // Apply transformation rules based on combinations + if (hasEn && hasUnd && hasEnUs) { + // Rule 4: All three "en" + "und" + "en-us" → all three stays + transformedCodes.push('en', 'und', 'en-us'); + } else if (hasUnd && hasEnUs && !hasEn) { + // Rule 2: "und" + "en-us" → "und" become "en", "en-us" stays + transformedCodes.push('en', 'en-us'); + } else if (hasEn && hasUnd && !hasEnUs) { + // Rule 3: "en" + "und" → "und" becomes "en-us", "en" stays + transformedCodes.push('en', 'en-us'); + } else if (hasUnd && !hasEn && !hasEnUs) { + // Rule 1: "und" alone → "en-us" + transformedCodes.push('en-us'); + } else { + // For any other combinations, keep locales as they are + if (hasEn) transformedCodes.push('en'); + if (hasUnd) transformedCodes.push('und'); + if (hasEnUs) transformedCodes.push('en-us'); + } + + // Remove duplicates and sort + const uniqueTransformedCodes = Array.from(new Set(transformedCodes)).sort(); + + // Now map each transformed code to the proper format with names + return uniqueTransformedCodes.map((code) => { + let name = ''; + let isMaster = false; + + // Determine if this is the master locale (check against original and transformed) + isMaster = + code === masterLocale || + (masterLocale === 'und' && code === 'en-us') || // Rule 1 transformation + (masterLocale === 'und' && hasEnUs && code === 'en') || // Rule 2 transformation + (masterLocale === 'und' && hasEn && code === 'en-us'); // Rule 3 transformation + + // Set appropriate names + switch (code) { + case 'en': + name = 'English'; + break; + case 'en-us': + name = 'English - United States'; + break; + case 'und': + name = 'Language Neutral'; + break; + default: + name = ''; // Will be filled from Contentstack API later + break; + } + + return { code: code.toLowerCase(), name, isMaster }; + }); +} + +/** + * Processes and creates locale configurations from Drupal database for migration to Contentstack. + * + * This function: + * 1. Fetches master locale from Drupal system.site config + * 2. Fetches all locales from node_field_data + * 3. Uses user-selected locale mapping from UI (project.localeMapping) + * 4. Maps source locales to destination locales based on user selection + * 5. Sets master locale based on user selection + * 6. Creates 3 JSON files: master-locale.json, locales.json, language.json + */ +export const createLocale = async ( + dbConfig: any, + destination_stack_id: string, + projectId: string, + project: any +) => { + const srcFunc = 'createLocale'; + const localeSave = path.join( + MIGRATION_DATA_PATH, + destination_stack_id, + LOCALE_DIR_NAME + ); + + try { + const msLocale: Record = {}; + const allLocales: Record = {}; + const localeList: Record = {}; + + if (!dbConfig || !dbConfig.host || !dbConfig.user || !dbConfig.database) { + throw new Error( + 'Invalid database configuration provided to createLocale' + ); + } + + const connection = await createDbConnection(dbConfig); + + if (!connection) { + throw new Error('Failed to create database connection'); + } + + // Helper function to execute queries (same pattern as entries.service.ts) + const executeQuery = (query: string): Promise => { + return new Promise((resolve, reject) => { + connection.query(query, (error, results) => { + if (error) { + reject(error); + } else { + resolve(results as any[]); + } + }); + }); + }; + + // 1. Get master locale from Drupal system.site config + const masterLocaleQuery = ` + SELECT SUBSTRING_INDEX( + SUBSTRING_INDEX(CONVERT(data USING utf8), 'default_langcode";s:2:"', -1), + '"', 1 + ) as master_locale + FROM config + WHERE name = 'system.site' + `; + + const masterRows: any = await executeQuery(masterLocaleQuery); + const sourceMasterLocale = masterRows[0]?.master_locale || 'en'; + + // 2. Get all locales from node_field_data + const allLocalesQuery = ` + SELECT DISTINCT langcode + FROM node_field_data + WHERE langcode IS NOT NULL AND langcode != '' + ORDER BY langcode + `; + + const allLocaleRows: any = await executeQuery(allLocalesQuery); + const sourceLocaleCodes = allLocaleRows.map((row: any) => row.langcode); + + // Close database connection + connection.end(); + + // 3. Get user-selected locale mapping from UI (project.localeMapping or project.locales/master_locale) + // localeMapping format: { "en-master_locale": "fr-fr", "es": "es-es", ... } + const localeMapping = project?.localeMapping || {}; + // ✅ FIX: Use LOCALE_MAPPER as fallback like WordPress does (but stackDetails.master_locale takes precedence) + const masterLocaleFromProject = project?.master_locale || {}; + const localesFromProject = project?.locales || {}; + + // 4. Fetch locale names from Contentstack API + const [localesApiResponse] = await getAllLocales(); + const contentstackLocales = localesApiResponse || {}; // ✅ FIX: getAllLocales already returns the locales object + + // 5. Map source locales to destination locales using user selection + // Find the destination master locale based on source master locale + const masterLocaleKey = `${sourceMasterLocale}-master_locale`; + let destinationMasterLocale = + localeMapping[masterLocaleKey] || + Object.values(masterLocaleFromProject)?.[0] || // ✅ FIX: Use VALUES not KEYS! + project?.stackDetails?.master_locale || + 'en-us'; + + // Process transformed locales first (handle und, en, en-us) + const transformedLocales = applyLocaleTransformations( + sourceLocaleCodes, + sourceMasterLocale + ); + + // Map each transformed source locale to destination locale + transformedLocales.forEach((localeInfo) => { + const { code: sourceCode, isMaster } = localeInfo; + + // Find destination locale from mapping + let destinationCode: string; + + if (isMaster) { + // For master locale, use the mapped master locale + destinationCode = destinationMasterLocale; + } else { + // For non-master locales, check mapping or use as-is + destinationCode = + localeMapping[sourceCode] || + localesFromProject[sourceCode] || + sourceCode; + } + + // Create UID + const uid = uuidv4(); + + // Get name from Contentstack API + const localeName = + contentstackLocales[destinationCode] || + contentstackLocales[destinationCode.toLowerCase()] || + contentstackLocales[sourceCode] || + 'Unknown Language'; + + const newLocale: Locale = { + code: destinationCode.toLowerCase(), + name: localeName, + fallback_locale: isMaster + ? null + : destinationMasterLocale.toLowerCase(), + uid: uid, + }; + + // Add to appropriate collections using UID as key + if (isMaster) { + msLocale[uid] = newLocale; + } else { + allLocales[uid] = newLocale; + } + + localeList[uid] = newLocale; + }); + + // Handle case where no non-master locales exist + const finalAllLocales = + Object.keys(allLocales).length > 0 ? allLocales : {}; + + // 6. Write locale files (same structure as Contentful/WordPress) + await writeFile(localeSave, LOCALE_FILE_NAME, finalAllLocales); // locales.json (non-master only) + await writeFile(localeSave, LOCALE_MASTER_LOCALE, msLocale); // master-locale.json (master only) + await writeFile(localeSave, LOCALE_CF_LANGUAGE, localeList); // language.json (all locales) + + const message = getLogMessage( + srcFunc, + `Drupal locales have been successfully transformed. Source Master: ${sourceMasterLocale}, Destination Master: ${destinationMasterLocale}, Total: ${sourceLocaleCodes.length}`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + } catch (err) { + const message = getLogMessage( + srcFunc, + `Error while creating Drupal locales.`, + {}, + err + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw err; + } +}; diff --git a/api/src/services/drupal/query.service.ts b/api/src/services/drupal/query.service.ts new file mode 100644 index 000000000..b044cc8bc --- /dev/null +++ b/api/src/services/drupal/query.service.ts @@ -0,0 +1,444 @@ +import fs from 'fs'; +import path from 'path'; +import mysql from 'mysql2'; +import { getDbConnection } from '../../helper/index.js'; +import customLogger from '../../utils/custom-logger.utils.js'; +import { MIGRATION_DATA_CONFIG } from '../../constants/index.js'; + +const { DATA } = MIGRATION_DATA_CONFIG; + +// PHP unserialize functionality (simplified for Node.js) +// Dynamic import for phpUnserialize will be used in the function + +/** + * Interface for field data extracted from Drupal config + */ +interface DrupalFieldData { + field_name: string; + content_types: string; + type: string; + content_handler?: string; +} + +/** + * Interface for query configuration + */ +interface QueryConfig { + page: { [contentType: string]: string }; + count: { [contentType: string]: string }; +} + +/** + * Get field information by querying the database for a specific field + * Enhanced to handle link fields with both URI and TITLE columns + */ +const getQuery = ( + connection: mysql.Connection, + data: DrupalFieldData +): Promise => { + return new Promise((resolve, reject) => { + try { + const tableName = `node__${data.field_name}`; + + // Check if this is a link field first + if (data.type !== 'link') { + // For non-link fields, use existing logic + const value = data.field_name; + const handlerType = + data.content_handler === undefined ? 'invalid' : data.content_handler; + const query = `SELECT *, '${handlerType}' as handler, '${data.type}' as fieldType FROM ${tableName}`; + + connection.query(query, (error: any, rows: any, fields: any) => { + if (!error && fields) { + // Look for field patterns in the database columns + for (const field of fields) { + const fieldName = field.name; + + // Check for various Drupal field suffixes + if ( + fieldName === `${value}_value` || + fieldName === `${value}_fid` || + fieldName === `${value}_tid` || + fieldName === `${value}_status` || + fieldName === `${value}_target_id` || + fieldName === `${value}_uri` + ) { + const fieldTable = `node__${data.field_name}.${fieldName}`; + resolve(fieldTable); + return; + } + } + // If no matching field was found + resolve(''); + } else { + console.error(`Error executing query for field ${value}:`, error); + resolve(''); // Resolve with empty string on error to continue process + } + }); + return; + } + + // For LINK fields only - get both URI and TITLE columns + connection.query( + `SHOW COLUMNS FROM ${tableName}`, + (error: any, columns: any) => { + if (error) { + console.error( + `Error querying columns for link field ${data.field_name}:`, + error + ); + resolve(''); + return; + } + + // Filter for link-specific columns only + const linkColumns = columns + .map((col: any) => col.Field) + .filter( + (field: string) => + (field === `${data.field_name}_uri` || + field === `${data.field_name}_title`) && + field.startsWith(data.field_name) + ); + + if (linkColumns.length > 0) { + // Return both columns as MAX aggregations for link fields + const maxColumns = linkColumns.map( + (col: string) => `MAX(${tableName}.${col}) as ${col}` + ); + resolve(maxColumns.join(',')); + } else { + // Fallback to just URI if title doesn't exist + const uriColumn = `${data.field_name}_uri`; + resolve(`MAX(${tableName}.${uriColumn}) as ${uriColumn}`); + } + } + ); + } catch (error) { + console.error('Error in getQuery', error); + resolve(''); // Resolve with empty string on error to continue process + } + }); +}; + +/** + * Process field data and generate SQL queries for each content type + */ +const generateQueriesForFields = async ( + connection: mysql.Connection, + fieldData: DrupalFieldData[], + projectId: string, + destination_stack_id: string +): Promise => { + try { + const select: { [contentType: string]: string } = {}; + const countQuery: { [contentType: string]: string } = {}; + + // Group fields by content type and filter out profile + const contentTypes = [ + ...new Set(fieldData.map((field) => field.content_types)), + ].filter((contentType) => contentType !== 'profile'); + + const message = `Processing ${contentTypes.length} content types for query generation...`; + await customLogger(projectId, destination_stack_id, 'info', message); + + // Process each content type + for (const contentType of contentTypes) { + const fieldsForType = fieldData.filter( + (field) => field.content_types === contentType + ); + const fieldCount = fieldsForType.length; + const maxJoinLimit = 50; // Conservative limit to avoid MySQL's 61-table limit + + // Check if content type has too many fields for single query + if (fieldCount > maxJoinLimit) { + const warningMessage = `Content type '${contentType}' has ${fieldCount} fields (>${maxJoinLimit} limit). Using optimized base query only.`; + await customLogger( + projectId, + destination_stack_id, + 'warn', + warningMessage + ); + + // Generate simple base query without field JOINs to avoid MySQL limit + const baseQuery = ` + SELECT + node.nid, + node.title, + node.langcode, + node.type, + users.name as author_name + FROM node_field_data node + LEFT JOIN users ON users.uid = node.uid + WHERE node.type = '${contentType}' + GROUP BY node.nid + ` + .replace(/\s+/g, ' ') + .trim(); + + const baseCountQuery = ` + SELECT COUNT(DISTINCT node.nid) as countentry + FROM node_field_data node + WHERE node.type = '${contentType}' + ` + .replace(/\s+/g, ' ') + .trim(); + + select[contentType] = + baseQuery + ` /* OPTIMIZED_NO_JOINS:${fieldCount} */`; + countQuery[`${contentType}Count`] = baseCountQuery; + + const optimizedMessage = `Generated optimized base query for ${contentType} (avoiding ${fieldCount} JOINs)`; + await customLogger( + projectId, + destination_stack_id, + 'info', + optimizedMessage + ); + + continue; // Skip to next content type + } + + const tableJoins: string[] = []; + const queries: Promise[] = []; + + // Collect all field queries (only for content types with manageable field count) + fieldsForType.forEach((fieldData) => { + tableJoins.push(`node__${fieldData.field_name}`); + queries.push(getQuery(connection, fieldData)); + }); + + try { + // Wait for all field queries to complete + const results = await Promise.all(queries); + + // Filter out empty results + const validResults = results.filter((item) => item); + + if (validResults.length === 0) { + continue; + } + + // Build the SELECT clause with proper handling for link fields + const modifiedResults = validResults.map((item) => { + // Check if this is already a MAX aggregation (link fields) + if (item.includes('MAX(') && item.includes(' as ')) { + return item; // Link fields are already properly formatted + } + // For other fields, apply MAX aggregation + return `MAX(${item}) as ${item.split('.').pop()}`; + }); + + // Build LEFT JOIN clauses + const leftJoins = tableJoins.map( + (table) => `LEFT JOIN ${table} ON ${table}.entity_id = node.nid` + ); + leftJoins.push('LEFT JOIN users ON users.uid = node.uid'); + + // Construct the complete query + const selectClause = [ + 'SELECT node.nid, MAX(node.title) AS title, MAX(node.langcode) AS langcode, MAX(node.type) as type', + ...modifiedResults, + ].join(','); + + const fromClause = 'FROM node_field_data node'; + const joinClause = leftJoins.join(' '); + const whereClause = `WHERE node.type = '${contentType}'`; + const groupClause = 'GROUP BY node.nid'; + + // Final query construction + const finalQuery = `${selectClause} ${fromClause} ${joinClause} ${whereClause} ${groupClause}`; + + // Clean up any double commas + select[contentType] = finalQuery + .replace(/,,/g, ',') + .replace(/, ,/g, ','); + + // Build count query + const countQueryStr = `SELECT count(distinct(node.nid)) as countentry ${fromClause} ${joinClause} ${whereClause}`; + countQuery[`${contentType}Count`] = countQueryStr; + + const fieldMessage = `Generated queries for content type: ${contentType} with ${validResults.length} fields`; + await customLogger( + projectId, + destination_stack_id, + 'info', + fieldMessage + ); + } catch (error) { + const errorMessage = `Error processing queries for content type: ${contentType}`; + await customLogger( + projectId, + destination_stack_id, + 'error', + errorMessage + ); + console.error( + 'Error processing queries for content type:', + contentType, + error + ); + } + } + + return { + page: select, + count: countQuery, + }; + } catch (error: any) { + const errorMessage = `Error in generateQueriesForFields: ${error.message}`; + await customLogger(projectId, destination_stack_id, 'error', errorMessage); + throw error; + } +}; + +/** + * Extract field configuration from Drupal database and generate dynamic queries + * Based on upload-api/migration-drupal/libs/extractQueries.js + */ +/** + * Validates that query configuration file exists (legacy compatibility) + * + * NOTE: This function is for backward compatibility. + * The new dynamic query system uses createQuery() which generates queries + * based on actual database field analysis. + */ +export const createQueryConfig = async ( + destination_stack_id: string, + customQueries?: any +): Promise => { + const queryDir = path.join(DATA, destination_stack_id, 'query'); + const queryPath = path.join(queryDir, 'index.json'); + + try { + // Check if dynamic query file exists (should be created by createQuery service) + await fs.promises.access(queryPath); + } catch (error) { + // If no dynamic queries exist, this is an error since we removed hardcoded fallbacks + throw new Error( + `❌ No query configuration found at ${queryPath}. Dynamic queries must be generated first using createQuery() service.` + ); + } +}; + +export const createQuery = async ( + dbConfig: any, + destination_stack_id: string, + projectId: string +): Promise => { + let connection: mysql.Connection | null = null; + + try { + const queryDir = path.join(DATA, destination_stack_id, 'query'); + const queryPath = path.join(queryDir, 'index.json'); + + // Create query directory + await fs.promises.mkdir(queryDir, { recursive: true }); + + // 🔍 DEBUG: Log dbConfig received in query service + console.info(`🔍 query.service.ts createQuery - Received dbConfig:`, { + host: dbConfig?.host, + user: dbConfig?.user, + database: dbConfig?.database, + port: dbConfig?.port, + hasPassword: !!dbConfig?.password, + }); + + const message = `Generating dynamic queries from Drupal database...`; + await customLogger(projectId, destination_stack_id, 'info', message); + + // Create database connection + console.info( + `🔍 query.service.ts - About to call getDbConnection with config:`, + { + host: dbConfig?.host, + user: dbConfig?.user, + database: dbConfig?.database, + } + ); + + connection = await getDbConnection( + dbConfig, + projectId, + destination_stack_id + ); + + console.info( + `🔍 query.service.ts - Database connection established successfully` + ); + + // SQL query to extract field configuration from Drupal + const configQuery = + "SELECT *, CONVERT(data USING utf8) as data FROM config WHERE name LIKE '%field.field.node%'"; + + // Execute query using promise-based approach + const [rows] = (await connection.promise().query(configQuery)) as any[]; + + let fieldData: DrupalFieldData[] = []; + + // Process results and extract field information + for (let i = 0; i < rows.length; i++) { + try { + const { unserialize } = await import('php-serialize'); + const convDetails = unserialize(rows[i].data); + if ( + convDetails && + typeof convDetails === 'object' && + 'field_name' in convDetails && + convDetails.bundle !== 'profile' // Filter out profile fields + ) { + fieldData.push({ + field_name: convDetails.field_name, + content_types: convDetails.bundle, + type: convDetails.field_type, + content_handler: convDetails?.settings?.handler, + }); + } + } catch (err: any) { + console.warn(`Couldn't parse row ${i}:`, err.message); + } + } + + if (fieldData.length === 0) { + throw new Error('No field configuration found in Drupal database'); + } + + const fieldMessage = `Found ${fieldData.length} field configurations in database (profile fields filtered out)`; + await customLogger(projectId, destination_stack_id, 'info', fieldMessage); + + // Generate queries based on field data + const queryConfig = await generateQueriesForFields( + connection, + fieldData, + projectId, + destination_stack_id + ); + + // Write query configuration to file + await fs.promises.writeFile( + queryPath, + JSON.stringify(queryConfig, null, 4), + 'utf8' + ); + + const successMessage = `Successfully generated and saved dynamic queries to: ${queryPath}`; + await customLogger(projectId, destination_stack_id, 'info', successMessage); + } catch (error: any) { + const errorMessage = `Failed to generate dynamic queries: ${error.message}`; + await customLogger(projectId, destination_stack_id, 'error', errorMessage); + + console.error('❌ Error generating dynamic queries:', error); + throw new Error( + `Failed to connect to database or generate queries: ${error.message}` + ); + } finally { + // Always close the connection when done + if (connection) { + try { + connection.end(); + } catch (err: any) { + console.warn('Connection was already closed:', err.message); + } + } + } +}; diff --git a/api/src/services/drupal/references.service.ts b/api/src/services/drupal/references.service.ts new file mode 100644 index 000000000..9f70e16ff --- /dev/null +++ b/api/src/services/drupal/references.service.ts @@ -0,0 +1,421 @@ +import fs from "fs"; +import path from "path"; +import mysql from 'mysql2'; +import { MIGRATION_DATA_CONFIG } from "../../constants/index.js"; +import { getLogMessage } from "../../utils/index.js"; +import customLogger from "../../utils/custom-logger.utils.js"; +import { getDbConnection } from "../../helper/index.js"; + +const { + DATA, + REFERENCES_DIR_NAME, + REFERENCES_FILE_NAME, +} = MIGRATION_DATA_CONFIG; + +interface QueryConfig { + page: { + [contentType: string]: string; + }; + count: { + [contentTypeCount: string]: string; + }; +} + +interface DrupalEntry { + nid: number; + title: string; + langcode: string; + created: number; + type: string; + [key: string]: any; +} + +interface TaxonomyReference { + drupal_term_id: number; + taxonomy_uid: string; + term_uid: string; +} + +interface DrupalTaxonomyTerm { + taxonomy_uid: string; // vid (vocabulary id) + drupal_term_id: number; // term id + term_name: string; // term name + term_description: string | null; // term description +} + +const LIMIT = 100; // Pagination limit for references + +// NOTE: Hardcoded queries have been REMOVED. All queries are now generated dynamically +// by the query.service.ts based on actual database field analysis. + +/** + * Executes SQL query and returns results as Promise + */ +const executeQuery = (connection: mysql.Connection, query: string): Promise => { + return new Promise((resolve, reject) => { + connection.query(query, (error, results) => { + if (error) { + reject(error); + } else { + resolve(results as any[]); + } + }); + }); +}; + +/** + * Writes data to a specified file, ensuring the target directory exists. + */ +async function writeFile(dirPath: string, filename: string, data: any) { + try { + await fs.promises.mkdir(dirPath, { recursive: true }); + const filePath = path.join(dirPath, filename); + await fs.promises.writeFile(filePath, JSON.stringify(data, null, 4), 'utf8'); + } catch (err) { + console.error(`Error writing ${dirPath}/${filename}::`, err); + } +} + +/** + * Reads existing references file or returns empty object + */ +async function readReferencesFile(referencesPath: string): Promise { + try { + const data = await fs.promises.readFile(referencesPath, "utf8"); + return JSON.parse(data); + } catch (err) { + return {}; + } +} + +/** + * Processes entries for a specific content type and creates reference mappings + * Following the original putPosts logic from references.js + */ +const putPosts = async ( + entries: DrupalEntry[], + contentType: string, + referencesPath: string, + projectId: string, + destination_stack_id: string +): Promise => { + const srcFunc = 'putPosts'; + + try { + // Read existing references data + const referenceData = await readReferencesFile(referencesPath); + + // Process each entry and create reference mapping + entries.forEach((entry) => { + const referenceKey = `content_type_entries_title_${entry.nid}`; + referenceData[referenceKey] = { + uid: referenceKey, + _content_type_uid: contentType, + }; + }); + + // Write updated references back to file + await fs.promises.writeFile(referencesPath, JSON.stringify(referenceData, null, 4), 'utf8'); + + const message = getLogMessage( + srcFunc, + `Created ${entries.length} reference mappings for content type ${contentType}.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error creating references for ${contentType}: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Processes entries for a specific content type with pagination + * Following the original getQuery logic from references.js + */ +const getQuery = async ( + connection: mysql.Connection, + contentType: string, + skip: number, + queryPageConfig: QueryConfig, + referencesPath: string, + projectId: string, + destination_stack_id: string +): Promise => { + const srcFunc = 'getQuery'; + + try { + // Following original pattern: queryPageConfig['page']['' + pagename + ''] + const baseQuery = queryPageConfig['page'][contentType]; + if (!baseQuery) { + throw new Error(`No query found for content type: ${contentType}`); + } + + const query = baseQuery + ` LIMIT ${skip}, ${LIMIT}`; + const entries = await executeQuery(connection, query); + + if (entries.length === 0) { + return false; // No more entries + } + + await putPosts(entries, contentType, referencesPath, projectId, destination_stack_id); + return true; // More entries might exist + + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error querying references for ${contentType}: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Processes all entries for a specific content type + * Following the original getPageCount logic from references.js + */ +const getPageCount = async ( + connection: mysql.Connection, + contentType: string, + queryPageConfig: QueryConfig, + referencesPath: string, + projectId: string, + destination_stack_id: string +): Promise => { + const srcFunc = 'getPageCount'; + + try { + // Process entries in batches + let skip = 0; + let hasMoreEntries = true; + + while (hasMoreEntries) { + hasMoreEntries = await getQuery( + connection, + contentType, + skip, + queryPageConfig, + referencesPath, + projectId, + destination_stack_id + ); + skip += LIMIT; + } + + const message = getLogMessage( + srcFunc, + `Completed reference extraction for content type ${contentType}.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error processing content type ${contentType}: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Reads dynamic query configuration file generated by query.service.ts + * Following original pattern: helper.readFile(path.join(process.cwd(), config.data, 'query', 'index.json')) + * + * NOTE: No fallback to hardcoded queries - dynamic queries MUST be generated first + */ +async function readQueryConfig(destination_stack_id: string): Promise { + try { + const queryPath = path.join(DATA, destination_stack_id, 'query', 'index.json'); + const data = await fs.promises.readFile(queryPath, "utf8"); + return JSON.parse(data); + } catch (err) { + // No fallback - dynamic queries must be generated first by createQuery() service + throw new Error(`❌ No dynamic query configuration found at query/index.json. Dynamic queries must be generated first using createQuery() service. Original error: ${err}`); + } +} + +/** + * Creates taxonomy reference mappings from Drupal database + * Using the taxonomy query to create a flat mapping file: taxonomyReference.json + */ +const createTaxonomyReferences = async ( + connection: mysql.Connection, + referencesSave: string, + projectId: string, + destination_stack_id: string +): Promise => { + const srcFunc = 'createTaxonomyReferences'; + + try { + const message = getLogMessage( + srcFunc, + `Creating taxonomy reference mappings...`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + // Use the same SQL query as taxonomy.service.ts + const taxonomyQuery = ` + SELECT + f.vid AS taxonomy_uid, + f.tid AS drupal_term_id, + f.name AS term_name, + f.description__value AS term_description + FROM taxonomy_term_field_data f + ORDER BY f.vid, f.tid + `; + + const taxonomyTerms = await executeQuery(connection, taxonomyQuery); + + if (taxonomyTerms.length === 0) { + const noDataMessage = getLogMessage( + srcFunc, + `No taxonomy terms found in database.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', noDataMessage); + return; + } + + // Transform to taxonomy reference format + const taxonomyReferences: TaxonomyReference[] = []; + + for (const term of taxonomyTerms as DrupalTaxonomyTerm[]) { + const termUid = `${term.taxonomy_uid}_${term.drupal_term_id}`; + + taxonomyReferences.push({ + drupal_term_id: term.drupal_term_id, + taxonomy_uid: term.taxonomy_uid, + term_uid: termUid + }); + } + + // Save taxonomy references to taxonomyReference.json + await writeFile(referencesSave, 'taxonomyReference.json', taxonomyReferences); + + const successMessage = getLogMessage( + srcFunc, + `Created ${taxonomyReferences.length} taxonomy reference mappings.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', successMessage); + + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error creating taxonomy references: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Creates reference mappings from Drupal database for migration to Contentstack. + * Based on the original Drupal v8 references.js logic with direct SQL queries. + * + * This creates a references.json file that maps node IDs to content types, + * which is then used by the entries service to resolve entity references. + * + * Supports dynamic SQL queries from query/index.json file following original pattern: + * var queryPageConfig = helper.readFile(path.join(process.cwd(), config.data, 'query', 'index.json')); + * var query = queryPageConfig['page']['' + pagename + '']; + */ +export const createRefrence = async ( + dbConfig: any, + destination_stack_id: string, + projectId: string, + isTest = false +): Promise => { + const srcFunc = 'createRefrence'; + let connection: mysql.Connection | null = null; + + try { + const referencesSave = path.join(DATA, destination_stack_id, REFERENCES_DIR_NAME); + const referencesPath = path.join(referencesSave, REFERENCES_FILE_NAME); + + // Initialize directories and files + await fs.promises.mkdir(referencesSave, { recursive: true }); + + // Initialize empty references file if it doesn't exist + if (!await fs.promises.access(referencesPath).then(() => true).catch(() => false)) { + await fs.promises.writeFile(referencesPath, JSON.stringify({}, null, 4), 'utf8'); + } + + const message = getLogMessage( + srcFunc, + `Exporting references...`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + // Read query configuration (following original pattern) + const queryPageConfig = await readQueryConfig(destination_stack_id); + + // Create database connection + connection = await getDbConnection(dbConfig, projectId, destination_stack_id); + + // Process each content type from query config (like original) + const pageQuery = queryPageConfig.page; + const contentTypes = Object.keys(pageQuery); + const typesToProcess = isTest ? contentTypes.slice(0, 2) : contentTypes; + + // Process content types sequentially (like original sequence logic) + for (const contentType of typesToProcess) { + await getPageCount( + connection, + contentType, + queryPageConfig, + referencesPath, + projectId, + destination_stack_id + ); + } + + // Create taxonomy reference mappings + await createTaxonomyReferences( + connection, + referencesSave, + projectId, + destination_stack_id + ); + + const successMessage = getLogMessage( + srcFunc, + `Successfully created reference mappings for ${typesToProcess.length} content types and taxonomy references.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', successMessage); + + } catch (err) { + const message = getLogMessage( + srcFunc, + `Error encountered while creating references.`, + {}, + err + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw err; + } finally { + // Close database connection + if (connection) { + connection.end(); + } + } +}; \ No newline at end of file diff --git a/api/src/services/drupal/taxonomy.service.ts b/api/src/services/drupal/taxonomy.service.ts new file mode 100644 index 000000000..58321b99d --- /dev/null +++ b/api/src/services/drupal/taxonomy.service.ts @@ -0,0 +1,465 @@ +import fs from 'fs'; +import path from 'path'; +import mysql from 'mysql2'; +import { getDbConnection } from '../../helper/index.js'; +import customLogger from '../../utils/custom-logger.utils.js'; +import { getLogMessage } from '../../utils/index.js'; +import { MIGRATION_DATA_CONFIG } from '../../constants/index.js'; + +const { DATA, TAXONOMIES_DIR_NAME } = MIGRATION_DATA_CONFIG; + +interface DrupalTaxonomyTerm { + taxonomy_uid: string; // vid (vocabulary id) + term_tid: number; // term id + term_name: string; // term name + term_description: string | null; // term description +} + +interface TaxonomyTerm { + uid: string; + name: string; + parent_uid: string | null; + description?: string; +} + +interface TaxonomyStructure { + taxonomy: { + uid: string; + name: string; + description: string; + }; + terms: TaxonomyTerm[]; +} + +/** + * Execute SQL query with promise support + */ +const executeQuery = async ( + connection: mysql.Connection, + query: string +): Promise => { + return new Promise((resolve, reject) => { + connection.query(query, (error, results) => { + if (error) { + reject(error); + return; + } + resolve(results as any[]); + }); + }); +}; + +/** + * Get vocabulary names from Drupal database + * Note: In Drupal 8+, vocabulary names are in the config table + */ +const getVocabularyNames = async ( + connection: mysql.Connection, + projectId: string, + destination_stack_id: string +): Promise> => { + const srcFunc = 'getVocabularyNames'; + + try { + // Try to get vocabulary names from config table (Drupal 8+) + const configQuery = ` + SELECT + SUBSTRING_INDEX(SUBSTRING_INDEX(name, '.', 3), '.', -1) as vid, + JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) as name + FROM config + WHERE name LIKE 'taxonomy.vocabulary.%' + AND data IS NOT NULL + `; + + const vocabularies = await executeQuery(connection, configQuery); + + const vocabNames: Record = {}; + + for (const vocab of vocabularies) { + if (vocab.vid && vocab.name) { + vocabNames[vocab.vid] = vocab.name; + } + } + + const message = getLogMessage( + srcFunc, + `Found ${Object.keys(vocabNames).length} vocabularies in config.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + return vocabNames; + } catch (error: any) { + // Fallback: use vid as name if config method fails + const message = getLogMessage( + srcFunc, + `Could not fetch vocabulary names from config, will use vid as name: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'warn', message); + + return {}; + } +}; + +/** + * Fetch taxonomy hierarchy information + * Note: Drupal uses taxonomy_term__parent or taxonomy_term_hierarchy table for hierarchy + */ +const getTermHierarchy = async ( + connection: mysql.Connection, + projectId: string, + destination_stack_id: string +): Promise> => { + const srcFunc = 'getTermHierarchy'; + + try { + // Try different possible hierarchy table structures + const hierarchyQueries = [ + // Drupal 8+ field-based hierarchy + `SELECT entity_id as tid, parent_target_id as parent_tid + FROM taxonomy_term__parent + WHERE parent_target_id IS NOT NULL AND parent_target_id != 0`, + + // Drupal 7 style hierarchy + `SELECT tid, parent + FROM taxonomy_term_hierarchy + WHERE parent IS NOT NULL AND parent != 0`, + ]; + + let hierarchyData: any[] = []; + + for (const query of hierarchyQueries) { + try { + hierarchyData = await executeQuery(connection, query); + if (hierarchyData.length > 0) { + break; // Use the first successful query + } + } catch (queryError) { + // Continue to next query if this one fails + continue; + } + } + + const hierarchy: Record = {}; + + for (const item of hierarchyData) { + const childTid = item.tid || item.entity_id; + const parentTid = item.parent || item.parent_tid || item.parent_target_id; + + if (childTid && parentTid) { + if (!hierarchy[parentTid]) { + hierarchy[parentTid] = []; + } + hierarchy[parentTid].push(childTid); + } + } + + const message = getLogMessage( + srcFunc, + `Found ${Object.keys(hierarchy).length} parent-child relationships.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + return hierarchy; + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Could not fetch term hierarchy: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'warn', message); + + return {}; + } +}; + +/** + * Process taxonomy terms and organize by vocabulary + */ +const processTaxonomyData = async ( + terms: DrupalTaxonomyTerm[], + vocabularyNames: Record, + hierarchy: Record, + projectId: string, + destination_stack_id: string +): Promise> => { + const srcFunc = 'processTaxonomyData'; + + try { + const taxonomies: Record = {}; + + // Group terms by vocabulary + const termsByVocabulary: Record = {}; + + for (const term of terms) { + if (!termsByVocabulary[term.taxonomy_uid]) { + termsByVocabulary[term.taxonomy_uid] = []; + } + termsByVocabulary[term.taxonomy_uid].push(term); + } + + // Create taxonomy structure for each vocabulary + for (const [vid, vocabTerms] of Object.entries(termsByVocabulary)) { + const vocabularyName = vocabularyNames[vid] || vid; + + const taxonomyStructure: TaxonomyStructure = { + taxonomy: { + uid: vid, + name: vocabularyName, + description: '', + }, + terms: [], + }; + + // Convert terms to Contentstack format + for (const term of vocabTerms) { + // 🏷️ Generate term UID using vocabulary prefix + term ID format + const vocabularyPrefix = vid.toLowerCase(); + const termUid = `${vocabularyPrefix}_${term.term_tid}`; + + // Find parent if exists + let parentUid: string | null = null; + for (const [parentTid, childTids] of Object.entries(hierarchy)) { + if (childTids.includes(term.term_tid)) { + // Find parent term in the same vocabulary + const parentTerm = vocabTerms.find( + (t) => t.term_tid === parseInt(parentTid) + ); + if (parentTerm) { + // 🏷️ Generate parent UID using same vocabulary prefix + term ID format + parentUid = `${vocabularyPrefix}_${parentTerm.term_tid}`; + } + break; + } + } + + taxonomyStructure.terms.push({ + uid: termUid, + name: term.term_name, + parent_uid: parentUid, + description: term.term_description || '', + }); + } + + taxonomies[vid] = taxonomyStructure; + } + + const message = getLogMessage( + srcFunc, + `Processed ${Object.keys(taxonomies).length} vocabularies with ${ + terms.length + } total terms.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + return taxonomies; + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error processing taxonomy data: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Save taxonomy files to disk + */ +const saveTaxonomyFiles = async ( + taxonomies: Record, + taxonomiesPath: string, + projectId: string, + destination_stack_id: string +): Promise => { + const srcFunc = 'saveTaxonomyFiles'; + + try { + let filesSaved = 0; + + // Save individual taxonomy files (existing functionality) + for (const [vid, taxonomy] of Object.entries(taxonomies)) { + const filePath = path.join(taxonomiesPath, `${vid}.json`); + await fs.promises.writeFile( + filePath, + JSON.stringify(taxonomy, null, 2), + 'utf8' + ); + filesSaved++; + + const message = getLogMessage( + srcFunc, + `Saved taxonomy file: ${vid}.json with ${taxonomy.terms.length} terms.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + } + + // Create consolidated taxonomies.json file with just vocabulary metadata + const taxonomiesDataObject: Record = {}; + + for (const [vid, taxonomy] of Object.entries(taxonomies)) { + taxonomiesDataObject[vid] = { + uid: taxonomy.taxonomy.uid, + name: taxonomy.taxonomy.name, + description: taxonomy.taxonomy.description, + }; + } + + const taxonomiesFilePath = path.join(taxonomiesPath, 'taxonomies.json'); + await fs.promises.writeFile( + taxonomiesFilePath, + JSON.stringify(taxonomiesDataObject, null, 2), + 'utf8' + ); + + const consolidatedMessage = getLogMessage( + srcFunc, + `Saved consolidated taxonomies.json with ${ + Object.keys(taxonomiesDataObject).length + } vocabularies.`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + consolidatedMessage + ); + + const summaryMessage = getLogMessage( + srcFunc, + `Successfully saved ${filesSaved} individual taxonomy files + 1 consolidated taxonomies.json file.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', summaryMessage); + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Error saving taxonomy files: ${error.message}`, + {}, + error + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw error; + } +}; + +/** + * Creates taxonomy files from Drupal database for migration to Contentstack. + * + * Extracts taxonomy vocabularies and terms from Drupal database, + * organizes them by vocabulary, and saves individual JSON files + * for each vocabulary in the format expected by Contentstack. + */ +export const createTaxonomy = async ( + dbConfig: any, + destination_stack_id: string, + projectId: string +): Promise => { + const srcFunc = 'createTaxonomy'; + let connection: mysql.Connection | null = null; + + try { + const taxonomiesPath = path.join( + DATA, + destination_stack_id, + TAXONOMIES_DIR_NAME + ); + + // Create taxonomies directory + await fs.promises.mkdir(taxonomiesPath, { recursive: true }); + + const message = getLogMessage(srcFunc, `Exporting taxonomies...`, {}); + await customLogger(projectId, destination_stack_id, 'info', message); + + // Create database connection + connection = await getDbConnection( + dbConfig, + projectId, + destination_stack_id + ); + + // Main SQL query to fetch taxonomy terms + const taxonomyQuery = ` + SELECT + f.vid AS taxonomy_uid, + f.tid AS term_tid, + f.name AS term_name, + f.description__value AS term_description + FROM taxonomy_term_field_data f + ORDER BY f.vid, f.tid + `; + + // Fetch taxonomy data + const taxonomyTerms = await executeQuery(connection, taxonomyQuery); + + if (taxonomyTerms.length === 0) { + const noDataMessage = getLogMessage( + srcFunc, + `No taxonomy terms found in database.`, + {} + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + noDataMessage + ); + return; + } + + // Get vocabulary names and hierarchy + const [vocabularyNames, hierarchy] = await Promise.all([ + getVocabularyNames(connection, projectId, destination_stack_id), + getTermHierarchy(connection, projectId, destination_stack_id), + ]); + + // Process taxonomy data + const taxonomies = await processTaxonomyData( + taxonomyTerms, + vocabularyNames, + hierarchy, + projectId, + destination_stack_id + ); + + // Save taxonomy files + await saveTaxonomyFiles( + taxonomies, + taxonomiesPath, + projectId, + destination_stack_id + ); + + const successMessage = getLogMessage( + srcFunc, + `Successfully exported ${ + Object.keys(taxonomies).length + } taxonomies with ${taxonomyTerms.length} total terms.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', successMessage); + } catch (err) { + const message = getLogMessage( + srcFunc, + `Error encountered while creating taxonomies.`, + {}, + err + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw err; + } finally { + // Close database connection + if (connection) { + connection.end(); + } + } +}; diff --git a/api/src/services/drupal/version.service.ts b/api/src/services/drupal/version.service.ts new file mode 100644 index 000000000..20e6ff6be --- /dev/null +++ b/api/src/services/drupal/version.service.ts @@ -0,0 +1,61 @@ +import fs from "fs"; +import path from "path"; +import { MIGRATION_DATA_CONFIG } from "../../constants/index.js"; +import { getLogMessage } from "../../utils/index.js"; +import customLogger from "../../utils/custom-logger.utils.js"; + +const { DATA, EXPORT_INFO_FILE } = MIGRATION_DATA_CONFIG; + +/** + * Writes data to a specified file, ensuring the target directory exists. + */ +async function writeFile(dirPath: string, filename: string, data: any) { + try { + await fs.promises.mkdir(dirPath, { recursive: true }); + const filePath = path.join(dirPath, filename); + await fs.promises.writeFile(filePath, JSON.stringify(data), 'utf8'); + } catch (err) { + console.error(`Error writing ${dirPath}/${filename}::`, err); + } +} + +/** + * Creates a version file for the given destination stack for Drupal migration. + */ +export const createVersionFile = async ( + destination_stack_id: string, + projectId: string +): Promise => { + const srcFunc = 'createVersionFile'; + + try { + const versionData = { + contentVersion: 2, + logsPath: "", + migrationSource: "drupal", + migrationTimestamp: new Date().toISOString(), + }; + + await writeFile( + path.join(DATA, destination_stack_id), + EXPORT_INFO_FILE, + versionData + ); + + const message = getLogMessage( + srcFunc, + `Version file has been successfully created.`, + {} + ); + await customLogger(projectId, destination_stack_id, 'info', message); + + } catch (err) { + const message = getLogMessage( + srcFunc, + `Error writing version file: ${err}`, + {}, + err + ); + await customLogger(projectId, destination_stack_id, 'error', message); + } +}; diff --git a/api/src/services/marketplace.service.ts b/api/src/services/marketplace.service.ts index 06a010483..277cec757 100644 --- a/api/src/services/marketplace.service.ts +++ b/api/src/services/marketplace.service.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-var-requires, operator-linebreak */ + import path from 'path'; import fs from 'fs'; import getAuthtoken from '../utils/auth.utils.js'; diff --git a/api/src/services/migration.service.ts b/api/src/services/migration.service.ts index 6bfcd5f7b..279a08717 100644 --- a/api/src/services/migration.service.ts +++ b/api/src/services/migration.service.ts @@ -13,7 +13,7 @@ import { LOCALE_MAPPER, STEPPER_STEPS, CMS, - GET_AUDIT_DATA + GET_AUDIT_DATA, } from '../constants/index.js'; import { BadRequestError, @@ -22,6 +22,7 @@ import { import { fieldAttacher } from '../utils/field-attacher.utils.js'; import { siteCoreService } from './sitecore.service.js'; import { wordpressService } from './wordpress.service.js'; +import { drupalService } from './drupal.service.js'; import { testFolderCreator } from '../utils/test-folder-creator.utils.js'; import { utilsCli } from './runCli.service.js'; import customLogger from '../utils/custom-logger.utils.js'; @@ -110,6 +111,66 @@ const createTestStack = async (req: Request): Promise => { .findIndex({ id: projectId }) .value(); if (index > -1) { + // ✅ NEW: Generate queries for new test stack (Drupal only) + const project = ProjectModelLowdb.data.projects[index]; + if (project?.legacy_cms?.cms === CMS.DRUPAL) { + try { + const startMessage = getLogMessage( + srcFun, + `Generating dynamic queries for new test stack (${res?.data?.stack?.api_key})...`, + token_payload + ); + await customLogger( + projectId, + res?.data?.stack?.api_key, + 'info', + startMessage + ); + + // Get database configuration from project + const dbConfig = { + host: project?.legacy_cms?.mySQLDetails?.host, + user: project?.legacy_cms?.mySQLDetails?.user, + password: project?.legacy_cms?.mySQLDetails?.password || '', + database: project?.legacy_cms?.mySQLDetails?.database, + port: project?.legacy_cms?.mySQLDetails?.port || 3306, + }; + + // Generate dynamic queries for the new test stack + await drupalService.createQuery( + dbConfig, + res?.data?.stack?.api_key, + projectId + ); + + const successMessage = getLogMessage( + srcFun, + `Successfully generated queries for test stack (${res?.data?.stack?.api_key})`, + token_payload + ); + await customLogger( + projectId, + res?.data?.stack?.api_key, + 'info', + successMessage + ); + } catch (error: any) { + const errorMessage = getLogMessage( + srcFun, + `Failed to generate queries for test stack: ${error.message}. Test migration may fail.`, + token_payload, + error + ); + await customLogger( + projectId, + res?.data?.stack?.api_key, + 'error', + errorMessage + ); + // Don't throw error - let test stack creation succeed even if query generation fails + } + } + ProjectModelLowdb.update((data: any) => { data.projects[index].current_step = STEPPER_STEPS['TESTING']; data.projects[index].current_test_stack_id = res?.data?.stack?.api_key; @@ -123,8 +184,9 @@ const createTestStack = async (req: Request): Promise => { return { data: { data: res.data, - url: `${config.CS_URL[token_payload?.region as keyof typeof config.CS_URL] - }/stack/${res.data.stack.api_key}/dashboard`, + url: `${ + config.CS_URL[token_payload?.region as keyof typeof config.CS_URL] + }/stack/${res.data.stack.api_key}/dashboard`, }, status: res.status, }; @@ -354,7 +416,7 @@ const startTestMigration = async (req: Request): Promise => { destinationStackId: project?.current_test_stack_id, projectId, keyMapper: project?.mapperKeys, - project + project, }); await siteCoreService?.createLocale( req, @@ -364,7 +426,7 @@ const startTestMigration = async (req: Request): Promise => { ); await siteCoreService?.createEnvironment( project?.current_test_stack_id - ) + ); await siteCoreService?.createVersionFile( project?.current_test_stack_id ); @@ -373,20 +435,102 @@ const startTestMigration = async (req: Request): Promise => { } case CMS.WORDPRESS: { if (packagePath) { - await wordpressService?.createLocale(req, project?.current_test_stack_id, projectId, project); - await wordpressService?.getAllAssets(file_path, packagePath, project?.current_test_stack_id, projectId) - await wordpressService?.createAssetFolderFile(file_path, project?.current_test_stack_id, projectId) - await wordpressService?.getAllreference(file_path, packagePath, project?.current_test_stack_id, projectId) - await wordpressService?.extractChunks(file_path, packagePath, project?.current_test_stack_id, projectId) - await wordpressService?.getAllAuthors(file_path, packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) + await wordpressService?.createLocale( + req, + project?.current_test_stack_id, + projectId, + project + ); + await wordpressService?.getAllAssets( + file_path, + packagePath, + project?.current_test_stack_id, + projectId + ); + await wordpressService?.createAssetFolderFile( + file_path, + project?.current_test_stack_id, + projectId + ); + await wordpressService?.getAllreference( + file_path, + packagePath, + project?.current_test_stack_id, + projectId + ); + await wordpressService?.extractChunks( + file_path, + packagePath, + project?.current_test_stack_id, + projectId + ); + await wordpressService?.getAllAuthors( + file_path, + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); //await wordpressService?.extractContentTypes(projectId, project?.current_test_stack_id, contentTypes) - await wordpressService?.getAllTerms(file_path, packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.getAllTags(file_path, packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.getAllCategories(file_path, packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractPosts(packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractPages(packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractGlobalFields(project?.current_test_stack_id, projectId) - await wordpressService?.createVersionFile(project?.current_test_stack_id, projectId); + await wordpressService?.getAllTerms( + file_path, + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.getAllTags( + file_path, + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.getAllCategories( + file_path, + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractPosts( + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractPages( + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractGlobalFields( + project?.current_test_stack_id, + projectId + ); + await wordpressService?.createVersionFile( + project?.current_test_stack_id, + projectId + ); } break; } @@ -436,7 +580,11 @@ const startTestMigration = async (req: Request): Promise => { } case CMS.AEM: { - await aemService.createAssets({ projectId, packagePath, destinationStackId: project?.current_test_stack_id }); + await aemService.createAssets({ + projectId, + packagePath, + destinationStackId: project?.current_test_stack_id, + }); await aemService.createEntry({ packagePath, contentTypes, @@ -444,20 +592,96 @@ const startTestMigration = async (req: Request): Promise => { destinationStackId: project?.current_test_stack_id, projectId, keyMapper: project?.mapperKeys, - project - }) + project, + }); await aemService?.createLocale( req, project?.current_test_stack_id, projectId, project ); - await aemService?.createVersionFile( - project?.current_test_stack_id - ); + await aemService?.createVersionFile(project?.current_test_stack_id); break; } + case CMS.DRUPAL: { + // Get database configuration from project + const dbConfig = { + host: project?.legacy_cms?.mySQLDetails?.host, + user: project?.legacy_cms?.mySQLDetails?.user, + password: project?.legacy_cms?.mySQLDetails?.password || '', + database: project?.legacy_cms?.mySQLDetails?.database, + port: project?.legacy_cms?.mySQLDetails?.port || 3306, + }; + + // Get Drupal assets URL configuration from project, request body, or environment variables + // Priority: project config > request body > environment variables > empty (auto-detection) + const drupalAssetsConfig = { + base_url: + project?.legacy_cms?.assetsConfig?.base_url || + req.body?.assetsConfig?.base_url || + process.env.DRUPAL_ASSETS_BASE_URL || + '', + public_path: + project?.legacy_cms?.assetsConfig?.public_path || + req.body?.assetsConfig?.public_path || + process.env.DRUPAL_ASSETS_PUBLIC_PATH || + '', + }; + + // Run Drupal migration services in proper order (following test-drupal-services sequence) + // Step 1: Generate dynamic queries from database analysis (MUST RUN FIRST) + await drupalService?.createQuery( + dbConfig, + project?.current_test_stack_id, + projectId + ); + + // Step 2: Generate content type schemas from upload-api (CRITICAL: Must run after upload-api generates schema) + await drupalService?.generateContentTypeSchemas( + project?.current_test_stack_id, + projectId + ); + + await drupalService?.createAssets( + dbConfig, + project?.current_test_stack_id, + projectId, + true, + drupalAssetsConfig + ); + await drupalService?.createRefrence( + dbConfig, + project?.current_test_stack_id, + projectId, + true + ); + await drupalService?.createTaxonomy( + dbConfig, + project?.current_test_stack_id, + projectId + ); + await drupalService?.createEntry( + dbConfig, + project?.current_test_stack_id, + projectId, + true, + project?.stackDetails?.master_locale, + project?.content_mapper || [], + project + ); + await drupalService?.createLocale( + dbConfig, + project?.current_test_stack_id, + projectId, + project + ); + await drupalService?.createVersionFile( + project?.current_test_stack_id, + projectId + ); + break; + } default: break; } @@ -618,7 +842,7 @@ const startMigration = async (req: Request): Promise => { destinationStackId: project?.destination_stack_id, projectId, keyMapper: project?.mapperKeys, - project + project, }); await siteCoreService?.createLocale( req, @@ -634,20 +858,102 @@ const startMigration = async (req: Request): Promise => { } case CMS.WORDPRESS: { if (packagePath) { - await wordpressService?.createLocale(req, project?.current_test_stack_id, projectId, project); - await wordpressService?.getAllAssets(file_path, packagePath, project?.destination_stack_id, projectId,) - await wordpressService?.createAssetFolderFile(file_path, project?.destination_stack_id, projectId) - await wordpressService?.getAllreference(file_path, packagePath, project?.destination_stack_id, projectId) - await wordpressService?.extractChunks(file_path, packagePath, project?.destination_stack_id, projectId) - await wordpressService?.getAllAuthors(file_path, packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) + await wordpressService?.createLocale( + req, + project?.current_test_stack_id, + projectId, + project + ); + await wordpressService?.getAllAssets( + file_path, + packagePath, + project?.destination_stack_id, + projectId + ); + await wordpressService?.createAssetFolderFile( + file_path, + project?.destination_stack_id, + projectId + ); + await wordpressService?.getAllreference( + file_path, + packagePath, + project?.destination_stack_id, + projectId + ); + await wordpressService?.extractChunks( + file_path, + packagePath, + project?.destination_stack_id, + projectId + ); + await wordpressService?.getAllAuthors( + file_path, + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); //await wordpressService?.extractContentTypes(projectId, project?.destination_stack_id) - await wordpressService?.getAllTerms(file_path, packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.getAllTags(file_path, packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.getAllCategories(file_path, packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractPosts(packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractPages(packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractGlobalFields(project?.destination_stack_id, projectId) - await wordpressService?.createVersionFile(project?.destination_stack_id, projectId); + await wordpressService?.getAllTerms( + file_path, + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.getAllTags( + file_path, + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.getAllCategories( + file_path, + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractPosts( + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractPages( + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractGlobalFields( + project?.destination_stack_id, + projectId + ); + await wordpressService?.createVersionFile( + project?.destination_stack_id, + projectId + ); } break; } @@ -679,13 +985,29 @@ const startMigration = async (req: Request): Promise => { project?.destination_stack_id, projectId ); + // 🔍 DEBUG: Log master_locale before passing to createEntry + const masterLocaleForContentful = project?.stackDetails?.master_locale; + console.info( + '🔍 Contentful startMigration - master_locale before createEntry:', + { + master_locale: masterLocaleForContentful, + master_locale_type: typeof masterLocaleForContentful, + master_locale_isLowercase: + masterLocaleForContentful === + masterLocaleForContentful?.toLowerCase?.(), + master_locale_toLowerCase: + masterLocaleForContentful?.toLowerCase?.(), + project_stackDetails: project?.stackDetails, + } + ); + await contentfulService?.createEntry( cleanLocalPath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, - project?.stackDetails?.master_locale, + masterLocaleForContentful, project ); await contentfulService?.createVersionFile( @@ -695,7 +1017,11 @@ const startMigration = async (req: Request): Promise => { break; } case CMS.AEM: { - await aemService.createAssets({ projectId, packagePath, destinationStackId: project?.destination_stack_id }); + await aemService.createAssets({ + projectId, + packagePath, + destinationStackId: project?.destination_stack_id, + }); await aemService.createEntry({ packagePath, contentTypes, @@ -703,20 +1029,96 @@ const startMigration = async (req: Request): Promise => { destinationStackId: project?.destination_stack_id, projectId, keyMapper: project?.mapperKeys, - project - }) + project, + }); await aemService?.createLocale( req, project?.destination_stack_id, projectId, project ); - await aemService?.createVersionFile( - project?.destination_stack_id + await aemService?.createVersionFile(project?.destination_stack_id); + break; + } + + case CMS.DRUPAL: { + // Get database configuration from project + const dbConfig = { + host: project?.legacy_cms?.mySQLDetails?.host, + user: project?.legacy_cms?.mySQLDetails?.user, + password: project?.legacy_cms?.mySQLDetails?.password || '', + database: project?.legacy_cms?.mySQLDetails?.database, + port: project?.legacy_cms?.mySQLDetails?.port || 3306, + }; + + // Get Drupal assets URL configuration from project, request body, or environment variables + // Priority: project config > request body > environment variables > empty (auto-detection) + const drupalAssetsConfig = { + base_url: + project?.legacy_cms?.assetsConfig?.base_url || + req.body?.assetsConfig?.base_url || + process.env.DRUPAL_ASSETS_BASE_URL || + '', + public_path: + project?.legacy_cms?.assetsConfig?.public_path || + req.body?.assetsConfig?.public_path || + process.env.DRUPAL_ASSETS_PUBLIC_PATH || + '', + }; + + // Run Drupal migration services in proper order (following test-drupal-services sequence) + // Step 1: Generate dynamic queries from database analysis (MUST RUN FIRST) + await drupalService?.createQuery( + dbConfig, + project?.destination_stack_id, + projectId + ); + + // Step 2: Generate content type schemas from upload-api (CRITICAL: Must run after upload-api generates schema) + await drupalService?.generateContentTypeSchemas( + project?.destination_stack_id, + projectId + ); + + await drupalService?.createAssets( + dbConfig, + project?.destination_stack_id, + projectId, + false, + drupalAssetsConfig + ); + await drupalService?.createRefrence( + dbConfig, + project?.destination_stack_id, + projectId, + false + ); + await drupalService?.createTaxonomy( + dbConfig, + project?.destination_stack_id, + projectId + ); + await drupalService?.createLocale( + dbConfig, + project?.destination_stack_id, + projectId, + project + ); + await drupalService?.createEntry( + dbConfig, + project?.destination_stack_id, + projectId, + false, + project?.stackDetails?.master_locale, + project?.content_mapper || [], + project + ); + await drupalService?.createVersionFile( + project?.destination_stack_id, + projectId ); break; } - default: break; } @@ -739,24 +1141,36 @@ const getAuditData = async (req: Request): Promise => { const stopIndex = startIndex + limit; const searchText = req?.params?.searchText; const filter = req?.params?.filter; - const srcFunc = "getAuditData"; - if (projectId?.includes('..') || stackId?.includes('..') || moduleName?.includes('..')) { - throw new BadRequestError("Invalid projectId, stackId, or moduleName"); + const srcFunc = 'getAuditData'; + if ( + projectId?.includes('..') || + stackId?.includes('..') || + moduleName?.includes('..') + ) { + throw new BadRequestError('Invalid projectId, stackId, or moduleName'); } try { - const mainPath = process?.cwd() + const mainPath = process?.cwd(); const logsDir = path.join(mainPath, GET_AUDIT_DATA?.MIGRATION_DATA_DIR); const stackFolders = fs.readdirSync(logsDir); - const stackFolder = stackFolders?.find(folder => folder?.startsWith?.(stackId)); + const stackFolder = stackFolders?.find((folder) => + folder?.startsWith?.(stackId) + ); if (!stackFolder) { - throw new BadRequestError("Migration data not found for this stack"); + throw new BadRequestError('Migration data not found for this stack'); } - const auditLogPath = path?.resolve(logsDir, stackFolder, GET_AUDIT_DATA?.LOGS_DIR, GET_AUDIT_DATA?.AUDIT_DIR, GET_AUDIT_DATA?.AUDIT_REPORT); + const auditLogPath = path?.resolve( + logsDir, + stackFolder, + GET_AUDIT_DATA?.LOGS_DIR, + GET_AUDIT_DATA?.AUDIT_DIR, + GET_AUDIT_DATA?.AUDIT_REPORT + ); if (!fs.existsSync(auditLogPath)) { - throw new BadRequestError("Audit log path not found"); + throw new BadRequestError('Audit log path not found'); } const filePath = path?.resolve(auditLogPath, `${moduleName}.json`); let fileData; @@ -770,7 +1184,7 @@ const getAuditData = async (req: Request): Promise => { if (Array.isArray(parsed)) { combinedData = combinedData.concat(parsed); } else if (parsed && typeof parsed === 'object') { - Object.values(parsed).forEach(val => { + Object.values(parsed).forEach((val) => { if (Array.isArray(val)) { combinedData = combinedData.concat(val); } else if (val && typeof val === 'object') { @@ -786,14 +1200,20 @@ const getAuditData = async (req: Request): Promise => { throw new BadRequestError('Access to this file is not allowed.'); } - const fileContent = await fsPromises?.readFile(safeEntriesSelectFieldPath, 'utf8'); + const fileContent = await fsPromises?.readFile( + safeEntriesSelectFieldPath, + 'utf8' + ); try { if (typeof fileContent === 'string') { const parsed = JSON?.parse(fileContent); addToCombined(parsed); } } catch (error) { - logger.error(`Error parsing JSON from file ${entriesSelectFieldPath}:`, error); + logger.error( + `Error parsing JSON from file ${entriesSelectFieldPath}:`, + error + ); throw new BadRequestError('Invalid JSON format in audit file'); } } @@ -827,7 +1247,9 @@ const getAuditData = async (req: Request): Promise => { safeFilePath.includes('..') || !safeFilePath.startsWith(auditLogPath) ) { - throw new BadRequestError('Path traversal detected or access to this file is not allowed.'); + throw new BadRequestError( + 'Path traversal detected or access to this file is not allowed.' + ); } const fileContent = await fsPromises?.readFile(safeFilePath, 'utf8'); try { @@ -842,27 +1264,32 @@ const getAuditData = async (req: Request): Promise => { } if (!fileData) { - throw new BadRequestError(`No audit data found for module: ${moduleName}`); + throw new BadRequestError( + `No audit data found for module: ${moduleName}` + ); } let transformedData = transformAndFlattenData(fileData); if (moduleName === 'Entries_Select_feild') { if (filter != GET_AUDIT_DATA?.FILTERALL) { - const filters = filter?.split("-"); + const filters = filter?.split('-'); transformedData = transformedData?.filter((log) => { return filters?.some((filter) => { return ( - log?.display_type?.toLowerCase()?.includes(filter?.toLowerCase()) || + log?.display_type + ?.toLowerCase() + ?.includes(filter?.toLowerCase()) || log?.data_type?.toLowerCase()?.includes(filter?.toLowerCase()) ); }); }); } - if (searchText && searchText !== null && searchText !== "null") { + if (searchText && searchText !== 'null') { transformedData = transformedData?.filter((item) => { - return Object?.values(item)?.some(value => - value && - typeof value === 'string' && - value?.toLowerCase?.()?.includes(searchText?.toLowerCase()) + return Object?.values(item)?.some( + (value) => + value && + typeof value === 'string' && + value?.toLowerCase?.()?.includes(searchText?.toLowerCase()) ); }); } @@ -870,25 +1297,24 @@ const getAuditData = async (req: Request): Promise => { return { data: finalData, totalCount: transformedData?.length, - status: HTTP_CODES?.OK + status: HTTP_CODES?.OK, }; } if (filter != GET_AUDIT_DATA?.FILTERALL) { - const filters = filter?.split("-"); + const filters = filter?.split('-'); transformedData = transformedData?.filter((log) => { return filters?.some((filter) => { - return ( - log?.data_type?.toLowerCase()?.includes(filter?.toLowerCase()) - ); + return log?.data_type?.toLowerCase()?.includes(filter?.toLowerCase()); }); }); } - if (searchText && searchText !== null && searchText !== "null") { + if (searchText && searchText !== null) { transformedData = transformedData?.filter((item: any) => { - return Object?.values(item)?.some(value => - value && - typeof value === 'string' && - value?.toLowerCase?.()?.includes(searchText?.toLowerCase()) + return Object?.values(item)?.some( + (value) => + value && + typeof value === 'string' && + value?.toLowerCase?.()?.includes(searchText?.toLowerCase()) ); }); } @@ -897,9 +1323,8 @@ const getAuditData = async (req: Request): Promise => { return { data: paginatedData, totalCount: transformedData?.length, - status: HTTP_CODES?.OK + status: HTTP_CODES?.OK, }; - } catch (error: any) { logger.error( getLogMessage( @@ -918,14 +1343,16 @@ const getAuditData = async (req: Request): Promise => { * Transforms and flattens nested data structure into an array of items * with sequential tuid values */ -const transformAndFlattenData = (data: any): Array<{ [key: string]: any, id: number }> => { +const transformAndFlattenData = ( + data: any +): Array<{ [key: string]: any; id: number }> => { try { const flattenedItems: Array<{ [key: string]: any }> = []; if (Array.isArray(data)) { data?.forEach((item, index) => { flattenedItems?.push({ - ...item ?? {}, - uid: item?.uid || `item-${index}` + ...(item ?? {}), + uid: item?.uid || `item-${index}`, }); }); } else if (typeof data === 'object' && data !== null) { @@ -933,24 +1360,24 @@ const transformAndFlattenData = (data: any): Array<{ [key: string]: any, id: num if (Array.isArray(value)) { value?.forEach((item, index) => { flattenedItems?.push({ - ...item ?? {}, + ...(item ?? {}), parentKey: key, - uid: item?.uid || `${key}-${index}` + uid: item?.uid || `${key}-${index}`, }); }); } else if (typeof value === 'object' && value !== null) { flattenedItems?.push({ ...value, key, - uid: (value as any)?.uid || key + uid: (value as any)?.uid || key, }); } }); } return flattenedItems?.map((item, index) => ({ - ...item ?? {}, - id: index + 1 + ...(item ?? {}), + id: index + 1, })); } catch (error) { console.error('Error transforming data:', error); @@ -958,41 +1385,47 @@ const transformAndFlattenData = (data: any): Array<{ [key: string]: any, id: num } }; const getLogs = async (req: Request): Promise => { - const projectId = req?.params?.projectId ? path?.basename(req.params.projectId) : ""; - const stackId = req?.params?.stackId ? path?.basename(req.params.stackId) : ""; + const projectId = req?.params?.projectId + ? path?.basename(req.params.projectId) + : ''; + const stackId = req?.params?.stackId + ? path?.basename(req.params.stackId) + : ''; const limit = req?.params?.limit ? parseInt(req.params.limit) : 10; - const startIndex = req?.params?.startIndex ? parseInt(req.params.startIndex) : 0; + const startIndex = req?.params?.startIndex + ? parseInt(req.params.startIndex) + : 0; const stopIndex = startIndex + limit; const searchText = req?.params?.searchText ?? null; - const filter = req?.params?.filter ?? "all"; - const srcFunc = "getLogs"; + const filter = req?.params?.filter ?? 'all'; + const srcFunc = 'getLogs'; if ( !projectId || !stackId || - projectId?.includes("..") || - stackId?.includes("..") + projectId?.includes('..') || + stackId?.includes('..') ) { - throw new BadRequestError("Invalid projectId or stackId"); + throw new BadRequestError('Invalid projectId or stackId'); } try { const mainPath = process?.cwd(); if (!mainPath) { - throw new BadRequestError("Invalid application path"); + throw new BadRequestError('Invalid application path'); } - const logsDir = path?.join(mainPath, "logs"); + const logsDir = path?.join(mainPath, 'logs'); const loggerPath = path?.join(logsDir, projectId, `${stackId}.log`); const absolutePath = path?.resolve(loggerPath); if (!absolutePath?.startsWith(logsDir)) { - throw new BadRequestError("Access to this file is not allowed."); + throw new BadRequestError('Access to this file is not allowed.'); } if (fs.existsSync(absolutePath)) { let index = 0; - const logs = await fs?.promises?.readFile?.(absolutePath, "utf8"); + const logs = await fs?.promises?.readFile?.(absolutePath, 'utf8'); let logEntries = logs - ?.split("\n") + ?.split('\n') ?.map((line) => { try { - const parsedLine = JSON?.parse(line) + const parsedLine = JSON?.parse(line); parsedLine && (parsedLine['id'] = index); ++index; @@ -1003,23 +1436,28 @@ const getLogs = async (req: Request): Promise => { }) ?.filter?.((entry) => entry !== null); if (!logEntries?.length) { - return { logs: [], total: 0, filterOptions: [], status: HTTP_CODES?.OK }; + return { + logs: [], + total: 0, + filterOptions: [], + status: HTTP_CODES?.OK, + }; } - const filterOptions = Array?.from(new Set(logEntries?.map((log) => log?.level))); - const auditStartIndex = logEntries?.findIndex?.(log => log?.message?.includes("Starting audit process")); - const auditEndIndex = logEntries?.findIndex?.(log => log?.message?.includes("Audit process completed")); + const filterOptions = Array?.from( + new Set(logEntries?.map((log) => log?.level)) + ); logEntries = logEntries?.slice?.(1, logEntries?.length - 2); - if (filter !== "all") { - const filters = filter?.split("-") ?? []; + if (filter !== 'all') { + const filters = filter?.split('-') ?? []; logEntries = logEntries?.filter((log) => { return filters?.some((filter) => { return log?.level ?.toLowerCase() - ?.includes?.(filter?.toLowerCase() ?? ""); + ?.includes?.(filter?.toLowerCase() ?? ''); }); }); } - if (searchText && searchText !== "null") { + if (searchText && searchText !== 'null') { logEntries = logEntries?.filter?.((log) => matchesSearchText(log, searchText) ); @@ -1029,7 +1467,7 @@ const getLogs = async (req: Request): Promise => { logs: paginatedLogs, total: logEntries?.length ?? 0, filterOptions: filterOptions, - status: HTTP_CODES?.OK + status: HTTP_CODES?.OK, }; } else { logger.error(getLogMessage(srcFunc, HTTP_TEXTS?.LOGS_NOT_FOUND)); @@ -1053,7 +1491,45 @@ const getLogs = async (req: Request): Promise => { */ export const createSourceLocales = async (req: Request) => { const projectId = req?.params?.projectId; - const locales = req?.body?.locale; + const rawLocales = req?.body?.locale; + + console.info('🔍 [createSourceLocales] Received locales from upload-api:', { + rawLocales, + rawLocales_type: typeof rawLocales, + rawLocales_isArray: Array.isArray(rawLocales), + rawLocales_length: Array.isArray(rawLocales) ? rawLocales.length : 'N/A', + firstLocale_is_master: + Array.isArray(rawLocales) && rawLocales.length > 0 + ? rawLocales[0] + : 'N/A', + projectId, + }); + + // 🔧 CRITICAL: Always normalize to lowercase before saving to database + // This is a final safety check - even if upload-api sends uppercase, we normalize here + // Master locale is already FIRST element in the array from upload-api + const locales = Array.isArray(rawLocales) + ? rawLocales + .map((locale: any, index: number) => { + const localeValue = + typeof locale === 'string' + ? locale + : locale?.code || locale?.value || locale; + const normalized = (localeValue || '').toLowerCase(); + const isMaster = index === 0 ? ' (MASTER - first element)' : ''; + console.info( + `🔍 [createSourceLocales] Normalizing locale [${index}]: "${localeValue}" -> "${normalized}"${isMaster}` + ); + return normalized; + }) + .filter((locale: string) => locale && locale.length > 0) + : []; + + console.info('🔍 [createSourceLocales] Final normalized locales to save:', { + locales, + master_locale_first: locales[0] || 'NONE', + total_count: locales.length, + }); try { // Find the project with the specified projectId @@ -1065,6 +1541,15 @@ export const createSourceLocales = async (req: Request) => { if (index > -1) { ProjectModelLowdb?.update?.((data: any) => { data.projects[index].source_locales = locales; + + console.info( + '✅ [createSourceLocales] Saved source_locales to project:', + { + projectId, + saved_source_locales: locales, + first_element_master: locales[0] || 'NONE', + } + ); }); } else { logger.error(`Project with ID: ${projectId} not found`, { @@ -1107,12 +1592,68 @@ export const updateLocaleMapper = async (req: Request) => { ?.get?.('projects') ?.findIndex?.({ id: projectId }) ?.value?.(); + if (index > -1) { + // 🔧 Reconstruct localeMapping from master_locale and locales + // 🔧 CRITICAL: Always convert to lowercase for consistent mapping across all CMS types + const localeMapping: Record = {}; + + // Add master locale mappings with "-master_locale" suffix + Object.entries(mapperObject?.master_locale || {}).forEach( + ([source, dest]) => { + const normalizedSource = (source || '').toLowerCase(); + const normalizedDest = ((dest as string) || '').toLowerCase(); + localeMapping[`${normalizedSource}-master_locale`] = normalizedDest; + } + ); + + // Add regular locale mappings + Object.entries(mapperObject?.locales || {}).forEach(([source, dest]) => { + const normalizedSource = (source || '').toLowerCase(); + const normalizedDest = ((dest as string) || '').toLowerCase(); + localeMapping[normalizedSource] = normalizedDest; + }); + ProjectModelLowdb?.update?.((data: any) => { data.projects[index].master_locale = mapperObject?.master_locale; data.projects[index].locales = mapperObject?.locales; + data.projects[index].localeMapping = localeMapping; // ✅ SAVE localeMapping! }); + // Write back the updated projects + await ProjectModelLowdb.write(); + + // 🔍 DEBUG: Log what was saved + await ProjectModelLowdb?.read?.(); + const updatedProject = ProjectModelLowdb.chain + .get('projects') + .find({ id: projectId }) + .value(); + console.info( + '================================================================================' + ); + console.info( + '🔍 [API updateLocaleMapper] Saved locale data to database:' + ); + console.info(' Project ID:', projectId); + console.info(' master_locale:', updatedProject?.master_locale); + console.info(' locales:', updatedProject?.locales); + console.info(' localeMapping:', updatedProject?.localeMapping); + console.info( + ' localeMapping keys:', + Object.keys(updatedProject?.localeMapping || {}) + ); + console.info( + '================================================================================' + ); + + // 🔍 LOGGING: Log after update + await ProjectModelLowdb?.read?.(); + logger.info('Locale mapping updated successfully', { + projectId, + masterLocaleKeys: Object.keys(mapperObject?.master_locale || {}), + localesKeys: Object.keys(mapperObject?.locales || {}), + }); } else { logger.error(`Project with ID: ${projectId} not found`, { status: HTTP_CODES?.NOT_FOUND, @@ -1120,11 +1661,7 @@ export const updateLocaleMapper = async (req: Request) => { }); } } catch (err: any) { - console.error( - '🚀 ~ updateLocaleMapper ~ err:', - err?.response?.data ?? err, - err - ); + console.error('Error details:', err?.response?.data ?? err); logger.warn('Bad Request', { status: HTTP_CODES?.BAD_REQUEST, message: HTTP_TEXTS?.INTERNAL_ERROR, @@ -1144,5 +1681,5 @@ export const migrationService = { getLogs, createSourceLocales, updateLocaleMapper, - getAuditData + getAuditData, }; diff --git a/api/src/services/org.service.ts b/api/src/services/org.service.ts index 72fa72eaf..b4ae2828b 100644 --- a/api/src/services/org.service.ts +++ b/api/src/services/org.service.ts @@ -108,6 +108,16 @@ const createStack = async (req: Request): Promise => { const orgId = req?.params?.orgId; const { token_payload, name, description, master_locale } = req.body; + // 🔍 DEBUG: Log master_locale before creating stack + console.info('🔍 createStack - master_locale before stack creation:', { + master_locale, + master_locale_type: typeof master_locale, + master_locale_isLowercase: master_locale === master_locale?.toLowerCase?.(), + master_locale_toLowerCase: master_locale?.toLowerCase?.(), + name, + description + }); + try { const authtoken = await getAuthtoken( token_payload?.region, @@ -133,6 +143,16 @@ const createStack = async (req: Request): Promise => { }, }) ); + + // 🔍 DEBUG: Log response + if (res && res.data) { + console.info('🔍 createStack - Stack created successfully:', { + stack_response: res.data, + stack_master_locale: res.data?.stack?.master_locale, + stack_master_locale_type: typeof res.data?.stack?.master_locale, + stack_master_locale_isLowercase: res.data?.stack?.master_locale === res.data?.stack?.master_locale?.toLowerCase?.(), + }); + } if (err) { logger.error( diff --git a/api/src/services/projects.service.ts b/api/src/services/projects.service.ts index f2e6f7de1..b1e7bbb15 100644 --- a/api/src/services/projects.service.ts +++ b/api/src/services/projects.service.ts @@ -4,6 +4,7 @@ import { Request } from 'express'; import ProjectModelLowdb from '../models/project-lowdb.js'; import ContentTypesMapperModelLowdb from '../models/contentTypesMapper-lowdb.js'; import FieldMapperModel from '../models/FieldMapper.js'; +import { drupalService } from './drupal.service.js'; import { BadRequestError, @@ -15,6 +16,7 @@ import { HTTP_CODES, STEPPER_STEPS, NEW_PROJECT_STATUS, + CMS, } from '../constants/index.js'; import { config } from '../config/index.js'; import { getLogMessage, isEmpty, safePromise } from '../utils/index.js'; @@ -22,6 +24,7 @@ import getAuthtoken from '../utils/auth.utils.js'; import https from '../utils/https.utils.js'; import getProjectUtil from '../utils/get-project.utils.js'; import logger from '../utils/logger.js'; +import customLogger from '../utils/custom-logger.utils.js'; // import { contentMapperService } from "./contentMapper.service.js"; import { v4 as uuidv4 } from 'uuid'; @@ -92,6 +95,25 @@ const getProject = async (req: Request) => { 'getProject' ); + // Type guard: ensure project is not a number (it should be a Project object) + if (typeof project === 'number') { + throw new BadRequestError('Invalid project data received'); + } + + // 🔍 DEBUG: Log locale data being sent to frontend + console.info( + '================================================================================' + ); + console.info('🔍 [API getProject] Sending locale data to frontend:'); + console.info(' Project ID:', projectId); + console.info(' source_locales:', project?.source_locales); + console.info(' localeMapping:', project?.localeMapping); + console.info(' locales:', project?.locales); + console.info(' master_locale:', project?.master_locale); + console.info( + '================================================================================' + ); + return project; }; @@ -138,6 +160,16 @@ const createProject = async (req: Request) => { bucketName: '', buketKey: '', }, + is_sql: false, + mySQLDetails: { + host: '', + user: '', + database: '', + }, + assetsConfig: { + base_url: '', + public_path: '', + }, }, content_mapper: [], execution_log: [], @@ -550,6 +582,9 @@ const updateFileFormat = async (req: Request) => { is_localPath, is_fileValid, awsDetails, + is_sql, + mySQLDetails, + assetsConfig, } = req?.body || {}; if (!token_payload) { @@ -628,11 +663,56 @@ const updateFileFormat = async (req: Request) => { data.projects[projectIndex].legacy_cms.awsDetails = {}; } data.projects[projectIndex].legacy_cms.awsDetails.awsRegion = - awsDetails.awsRegion || ''; + awsDetails.awsRegion; data.projects[projectIndex].legacy_cms.awsDetails.bucketName = - awsDetails.bucketName || ''; + awsDetails.bucketName; data.projects[projectIndex].legacy_cms.awsDetails.buketKey = - awsDetails.buketKey || ''; + awsDetails.buketKey; + } + + // Update SQL fields if provided + if (is_sql !== undefined) { + data.projects[projectIndex].legacy_cms.is_sql = is_sql; + } + + // Update MySQL details if provided + if (mySQLDetails && typeof mySQLDetails === 'object') { + if (!data.projects[projectIndex].legacy_cms.mySQLDetails) { + data.projects[projectIndex].legacy_cms.mySQLDetails = {}; + } + data.projects[projectIndex].legacy_cms.mySQLDetails.host = + mySQLDetails.host; + data.projects[projectIndex].legacy_cms.mySQLDetails.user = + mySQLDetails.user; + data.projects[projectIndex].legacy_cms.mySQLDetails.database = + mySQLDetails.database; + // Also save password and port if provided + if (mySQLDetails.password !== undefined) { + data.projects[projectIndex].legacy_cms.mySQLDetails.password = + mySQLDetails.password; + } + if (mySQLDetails.port !== undefined) { + data.projects[projectIndex].legacy_cms.mySQLDetails.port = + mySQLDetails.port; + } + } + + // Only update assetsConfig if it's provided and has values + // Don't overwrite existing config with empty strings + if (assetsConfig && typeof assetsConfig === 'object') { + if (!data.projects[projectIndex].legacy_cms.assetsConfig) { + data.projects[projectIndex].legacy_cms.assetsConfig = {}; + } + if (assetsConfig.base_url || assetsConfig.public_path) { + data.projects[projectIndex].legacy_cms.assetsConfig.base_url = + assetsConfig.base_url || + data.projects[projectIndex].legacy_cms.assetsConfig.base_url || + ''; + data.projects[projectIndex].legacy_cms.assetsConfig.public_path = + assetsConfig.public_path || + data.projects[projectIndex].legacy_cms.assetsConfig.public_path || + ''; + } } }); @@ -874,6 +954,160 @@ const updateDestinationStack = async (req: Request) => { } }; +/** + * Generates dynamic queries for Drupal projects with retry logic + * @param project - The project object + * @param stackId - The stack ID to generate queries for + * @param projectId - The project ID + * @param stackType - Type of stack ('destination' or 'test') + * @param token_payload - Token payload for logging + * @returns Promise - Success/failure of query generation + */ +const generateQueriesWithRetry = async ( + project: any, + stackId: string, + projectId: string, + stackType: string, + token_payload: any, + shouldLogAsWarning: boolean = false // New parameter to control log level +): Promise => { + const srcFunc = 'generateQueriesWithRetry'; + const maxRetries = 3; + + // Only generate queries for Drupal projects + if (project?.legacy_cms?.cms !== CMS.DRUPAL) { + return true; // Skip for non-Drupal projects + } + + if (!stackId) { + const message = getLogMessage( + srcFunc, + `No ${stackType} stack ID found, skipping query generation`, + token_payload + ); + await customLogger(projectId, stackId, 'warn', message); + return true; // Skip if no stack ID + } + + // Get database configuration from project + const dbConfig = { + host: project?.legacy_cms?.mySQLDetails?.host, + user: project?.legacy_cms?.mySQLDetails?.user, + password: project?.legacy_cms?.mySQLDetails?.password || '', + database: project?.legacy_cms?.mySQLDetails?.database, + port: project?.legacy_cms?.mySQLDetails?.port || 3306, + }; + + // 🔍 DEBUG: Log dbConfig being passed to query service + logger.info( + getLogMessage( + srcFunc, + `🔍 dbConfig for ${stackType} stack query generation: ${JSON.stringify({ + host: dbConfig.host, + user: dbConfig.user, + database: dbConfig.database, + port: dbConfig.port, + hasPassword: !!dbConfig.password, + })}`, + token_payload + ) + ); + + // Validate database configuration + if (!dbConfig.host || !dbConfig.user || !dbConfig.database) { + const message = getLogMessage( + srcFunc, + `MySQL details incomplete for ${stackType} stack. Skipping query generation. (host: ${!!dbConfig.host}, user: ${!!dbConfig.user}, database: ${!!dbConfig.database})`, + token_payload + ); + await customLogger(projectId, stackId, 'warn', message); + return true; // Skip query generation if MySQL details are incomplete + } + + const logMessage = getLogMessage( + srcFunc, + `Starting query generation for ${stackType} stack (${stackId})...`, + token_payload + ); + await customLogger(projectId, stackId, 'info', logMessage); + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const attemptMessage = getLogMessage( + srcFunc, + `Query generation attempt ${attempt}/${maxRetries} for ${stackType} stack`, + token_payload + ); + await customLogger(projectId, stackId, 'info', attemptMessage); + + // Generate dynamic queries using the query service + await drupalService.createQuery(dbConfig, stackId, projectId); + + const successMessage = getLogMessage( + srcFunc, + `Successfully generated queries for ${stackType} stack (${stackId}) on attempt ${attempt}`, + token_payload + ); + await customLogger(projectId, stackId, 'info', successMessage); + + return true; // Success + } catch (error: any) { + // Log the actual error details for debugging + const actualError = + error?.message || error?.toString() || 'Unknown error'; + const errorStack = error?.stack || ''; + logger.error( + `Query generation error details: ${actualError}`, + errorStack + ); + + // Use warning level if MySQL details are present (non-blocking), error if missing (blocking) + const logLevel = shouldLogAsWarning ? 'warn' : 'error'; + const errorMessage = getLogMessage( + srcFunc, + `Query generation attempt ${attempt}/${maxRetries} failed for ${stackType} stack: ${actualError}`, + token_payload, + error + ); + await customLogger(projectId, stackId, logLevel, errorMessage); + + if (attempt === maxRetries) { + // Final attempt failed - include actual error details + const actualError = + error?.message || error?.toString() || 'Unknown error'; + const finalErrorMessage = getLogMessage( + srcFunc, + `Query generation failed after ${maxRetries} attempts for ${stackType} stack. Error: ${actualError}. Please check MySQL connection details and try again.`, + token_payload, + error + ); + // Use warning level if MySQL details are present (non-blocking), error if missing (blocking) + if (shouldLogAsWarning) { + logger.warn(finalErrorMessage); + await customLogger(projectId, stackId, 'warn', finalErrorMessage); + } else { + logger.error(finalErrorMessage); + await customLogger(projectId, stackId, 'error', finalErrorMessage); + } + return false; // All attempts failed + } + + // Wait before retry (exponential backoff) + const retryDelay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s + const retryMessage = getLogMessage( + srcFunc, + `Retrying query generation in ${retryDelay / 1000} seconds...`, + token_payload + ); + await customLogger(projectId, stackId, 'info', retryMessage); + + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + } + } + + return false; // Should never reach here +}; + /** * Updates the current step of a project based on the provided request. * @param req - The request object containing the parameters and body. @@ -965,6 +1199,92 @@ const updateCurrentStep = async (req: Request) => { ); } + // ✅ NEW: Generate dynamic queries for both destination and test stacks (Drupal only) + if (project?.legacy_cms?.cms === CMS.DRUPAL) { + const startMessage = getLogMessage( + srcFunc, + `Generating dynamic queries for Drupal project before proceeding to Content Mapping...`, + token_payload + ); + await customLogger( + projectId, + project?.destination_stack_id, + 'info', + startMessage + ); + + // Check if MySQL details are present - if yes, log as warning (non-blocking), if no, log as error (blocking) + const hasMySQLDetails = + project?.legacy_cms?.mySQLDetails?.host && + project?.legacy_cms?.mySQLDetails?.user && + project?.legacy_cms?.mySQLDetails?.database; + const shouldLogAsWarning: boolean = Boolean(hasMySQLDetails); + + // Generate queries for destination stack + const destinationSuccess = await generateQueriesWithRetry( + project, + project?.destination_stack_id, + projectId, + 'destination', + token_payload, + shouldLogAsWarning + ); + + // Generate queries for test stack (if exists) + let testSuccess = true; + if (project?.current_test_stack_id) { + testSuccess = await generateQueriesWithRetry( + project, + project?.current_test_stack_id, + projectId, + 'test', + token_payload, + shouldLogAsWarning + ); + } + + // Check if query generation failed + if (!destinationSuccess || !testSuccess) { + const failedStacks = []; + if (!destinationSuccess) failedStacks.push('destination'); + if (!testSuccess) failedStacks.push('test'); + + // Check if MySQL details are missing + const hasMySQLDetails = + project?.legacy_cms?.mySQLDetails?.host && + project?.legacy_cms?.mySQLDetails?.user && + project?.legacy_cms?.mySQLDetails?.database; + + let errorMessage = `Query generation failed for ${failedStacks.join( + ' and ' + )} stack(s).`; + + if (!hasMySQLDetails) { + errorMessage += + ' MySQL connection details (host, user, database) are required but missing. Please update your file format configuration.'; + logger.error(getLogMessage(srcFunc, errorMessage, token_payload)); + throw new BadRequestError(errorMessage); + } else { + // MySQL details present but connection failed - already logged as warning in generateQueriesWithRetry + // Just continue without throwing error - allow user to proceed + // Query generation can be retried later when MySQL connection is fixed + // No need to log again - generateQueriesWithRetry already logged the warning + } + } + + const completeMessage = getLogMessage( + srcFunc, + `Dynamic queries successfully generated for all stacks. Proceeding to Content Mapping step.`, + token_payload + ); + await customLogger( + projectId, + project?.destination_stack_id, + 'info', + completeMessage + ); + } + await ProjectModelLowdb.update((data: any) => { if ( !data?.projects || @@ -1302,6 +1622,17 @@ const updateStackDetails = async (req: Request) => { const srcFunc = 'updateStackDetails'; + // 🔍 DEBUG: Log stack_details before saving + console.info('🔍 updateStackDetails - stack_details received:', { + stack_details, + master_locale: stack_details?.master_locale, + master_locale_type: typeof stack_details?.master_locale, + master_locale_isLowercase: + stack_details?.master_locale === + stack_details?.master_locale?.toLowerCase?.(), + master_locale_toLowerCase: stack_details?.master_locale?.toLowerCase?.(), + }); + await ProjectModelLowdb.read(); const projectIndex = (await getProjectUtil( projectId, @@ -1326,6 +1657,19 @@ const updateStackDetails = async (req: Request) => { } data.projects[projectIndex].stackDetails = stack_details; data.projects[projectIndex].updated_at = new Date().toISOString(); + + // 🔍 DEBUG: Log what was saved + console.info('🔍 updateStackDetails - Saved stackDetails:', { + saved_master_locale: + data.projects[projectIndex].stackDetails?.master_locale, + saved_master_locale_type: + typeof data.projects[projectIndex].stackDetails?.master_locale, + saved_master_locale_isLowercase: + data.projects[projectIndex].stackDetails?.master_locale === + data.projects[ + projectIndex + ].stackDetails?.master_locale?.toLowerCase?.(), + }); }); logger.info( @@ -1452,7 +1796,6 @@ const updateMigrationExecution = async (req: Request) => { // Ensure the `ProjectModelLowdb` database is ready to be read await ProjectModelLowdb.read(); - // Retrieve the project index using the `getProjectUtil` helper const projectIndex = (await getProjectUtil( projectId, diff --git a/api/src/services/runCli.service.ts b/api/src/services/runCli.service.ts index 956e01834..52ae3863f 100644 --- a/api/src/services/runCli.service.ts +++ b/api/src/services/runCli.service.ts @@ -203,9 +203,6 @@ export const runCli = async ( ); await createDirectoryAndFile(loggerPath, transformePath); - // Debug which log path is being used - console.info(`Log path for CLI commands: ${transformePath}`); - // Make sure to set the global.currentLogFile to the project log file // This is the key part - setting the log file path to the migration service log file await setLogFilePath(transformePath); @@ -229,9 +226,6 @@ export const runCli = async ( transformePath ); // Pass the log file path here - // After the import command completes - console.info('Import command completed successfully'); - // Write the completion message ONCE in the format the UI expects if (isTest) { const directLogEntry = { @@ -250,8 +244,6 @@ export const runCli = async ( if (loggerPath && loggerPath !== transformePath) { fs.appendFileSync(loggerPath, JSON.stringify(directLogEntry) + '\n'); } - - console.info('Added test completion message to logs'); } else { const directLogEntry = { level: 'info', @@ -269,21 +261,8 @@ export const runCli = async ( if (loggerPath && loggerPath !== transformePath) { fs.appendFileSync(loggerPath, JSON.stringify(directLogEntry) + '\n'); } - - console.info('Added migration completion message to logs'); } - // Keep the project status update code: - console.info( - `Updating project status: projectId=${projectId}, isTest=${isTest}` - ); - // ... rest of the code ... - - // Add debug logs to track project index and test flag - console.info( - `Updating project status: projectId=${projectId}, isTest=${isTest}` - ); - // Make sure we have the latest data await ProjectModelLowdb.read(); const projectIndex = ProjectModelLowdb.chain @@ -291,27 +270,18 @@ export const runCli = async ( .findIndex({ id: projectId }) .value(); - console.info(`Found project index: ${projectIndex}`); - // Debug: Log the full project data to verify it exists try { const project = ProjectModelLowdb.chain .get('projects') .find({ id: projectId }) .value(); - console.info(`Project found: ${project ? 'Yes' : 'No'}`); - if (project) { - console.info( - `Current migration status: started=${project.isMigrationStarted}, completed=${project.isMigrationCompleted}` - ); - } } catch (err) { console.error('Error reading project data:', err); } // Handle test migration updates if (projectIndex > -1 && isTest) { - console.info('Updating test migration status'); const project = ProjectModelLowdb.data.projects[projectIndex]; // Initialize test_stacks if needed @@ -332,17 +302,15 @@ export const runCli = async ( // Update project status for non-test migrations if (projectIndex > -1 && !isTest) { // Direct modification might be more reliable - ProjectModelLowdb.data.projects[projectIndex].isMigrationCompleted = true; - ProjectModelLowdb.data.projects[projectIndex].isMigrationStarted = false; + ProjectModelLowdb.data.projects[projectIndex].isMigrationCompleted = + true; + ProjectModelLowdb.data.projects[projectIndex].isMigrationStarted = + false; ProjectModelLowdb.data.projects[projectIndex].current_step = 5; ProjectModelLowdb.data.projects[projectIndex].status = 5; await ProjectModelLowdb.write(); - console.info( - `Project ${projectId} status updated: migration completed` - ); } } else { - console.info('User not found.'); } } catch (error) { console.error('🚀 ~ runCli ~ error:', error); diff --git a/api/src/utils/batch-processor.utils.ts b/api/src/utils/batch-processor.utils.ts new file mode 100644 index 000000000..2f2e25488 --- /dev/null +++ b/api/src/utils/batch-processor.utils.ts @@ -0,0 +1,108 @@ +/** + * Batch Processor Utility + * Handles large datasets by processing them in smaller batches to prevent resource exhaustion + */ + +export interface BatchProcessorOptions { + batchSize: number; + concurrency: number; + delayBetweenBatches?: number; +} + +export class BatchProcessor { + private options: BatchProcessorOptions; + + constructor(options: BatchProcessorOptions) { + this.options = { + delayBetweenBatches: 100, // Default 100ms delay + ...options + }; + } + + /** + * Process large array in batches to prevent resource exhaustion + */ + async processBatches( + items: T[], + processor: (item: T) => Promise, + onBatchComplete?: (batchIndex: number, totalBatches: number, results: R[]) => void + ): Promise { + const { batchSize, concurrency, delayBetweenBatches } = this.options; + const totalBatches = Math.ceil(items.length / batchSize); + const allResults: R[] = []; + + console.log(`📦 Processing ${items.length} items in ${totalBatches} batches (${batchSize} items per batch, concurrency: ${concurrency})`); + + for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) { + const startIndex = batchIndex * batchSize; + const endIndex = Math.min(startIndex + batchSize, items.length); + const batch = items.slice(startIndex, endIndex); + + console.log(`📋 Processing batch ${batchIndex + 1}/${totalBatches} (${batch.length} items)`); + + // Process batch with controlled concurrency + const batchResults = await this.processBatchWithConcurrency(batch, processor, concurrency); + allResults.push(...batchResults); + + // Callback for batch completion + if (onBatchComplete) { + onBatchComplete(batchIndex + 1, totalBatches, batchResults); + } + + // Delay between batches to allow file handles to close + if (batchIndex < totalBatches - 1 && delayBetweenBatches && delayBetweenBatches > 0) { + await this.delay(delayBetweenBatches); + } + + // Force garbage collection if available + if (global.gc) { + global.gc(); + } + } + + console.log(`✅ Completed processing ${items.length} items in ${totalBatches} batches`); + return allResults; + } + + /** + * Process a single batch with controlled concurrency + */ + private async processBatchWithConcurrency( + batch: T[], + processor: (item: T) => Promise, + concurrency: number + ): Promise { + const results: R[] = []; + + for (let i = 0; i < batch.length; i += concurrency) { + const chunk = batch.slice(i, i + concurrency); + const chunkPromises = chunk.map(processor); + const chunkResults = await Promise.all(chunkPromises); + results.push(...chunkResults); + } + + return results; + } + + /** + * Delay utility + */ + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} + +/** + * Quick utility function for batch processing + */ +export async function processBatches( + items: T[], + processor: (item: T) => Promise, + options: BatchProcessorOptions, + onBatchComplete?: (batchIndex: number, totalBatches: number, results: R[]) => void +): Promise { + const batchProcessor = new BatchProcessor(options); + return batchProcessor.processBatches(items, processor, onBatchComplete); +} + +export default BatchProcessor; diff --git a/api/src/utils/content-type-creator.utils.ts b/api/src/utils/content-type-creator.utils.ts index 9bc51d83a..00cc8f01b 100644 --- a/api/src/utils/content-type-creator.utils.ts +++ b/api/src/utils/content-type-creator.utils.ts @@ -348,8 +348,58 @@ const saveAppMapper = async ({ marketPlacePath, data, fileName }: any) => { } } -const convertToSchemaFormate = ({ field, advanced = false, marketPlacePath, keyMapper }: any) => { +// Field type conversion validation to prevent data loss +export const validateFieldTypeConversion = (currentType: string, newType: string): { allowed: boolean; reason?: string } => { + // Define field type hierarchy (simple → complex) + const typeHierarchy = { + 'single_line_text': 1, + 'multi_line_text': 2, + 'html': 3, + 'json': 4 + }; + + const currentLevel = typeHierarchy[currentType as keyof typeof typeHierarchy] || 0; + const newLevel = typeHierarchy[newType as keyof typeof typeHierarchy] || 0; + + // Allow same type or upgrades (simple → complex) + if (currentLevel <= newLevel) { + return { allowed: true }; + } + + // Special case: Allow JSON RTE → HTML RTE (both are rich content types) + if (currentType === 'json' && newType === 'html') { + return { allowed: true }; + } + + // Block downgrades (complex → simple) to prevent data loss + const reasons = { + 'multi_line_text_to_single': 'Converting multi-line to single-line may lose line breaks and formatting', + 'html_to_text': 'Converting HTML RTE to text fields will lose rich content formatting', + 'json_to_text': 'Converting JSON RTE to text fields will lose structured data and assets' + }; + + let reason = 'This conversion may result in data loss'; + if (currentType === 'multi_line_text' && newType === 'single_line_text') { + reason = reasons.multi_line_text_to_single; + } else if (currentType === 'html' && ['single_line_text', 'multi_line_text'].includes(newType)) { + reason = reasons.html_to_text; + } else if (currentType === 'json' && ['single_line_text', 'multi_line_text'].includes(newType)) { + reason = reasons.json_to_text; + } + + return { allowed: false, reason }; +}; + +export const convertToSchemaFormate = ({ field, advanced = true, marketPlacePath, currentFieldType, keyMapper }: any) => { // Clean up field UID by removing ALL leading underscores + + // Validate field type conversion if currentFieldType is provided + if (currentFieldType && field?.contentstackFieldType) { + const validation = validateFieldTypeConversion(currentFieldType, field.contentstackFieldType); + if (!validation.allowed) { + throw new Error(`Field type conversion blocked: ${validation.reason}`); + } + } const rawUid = field?.uid; const cleanedUid = sanitizeUid(rawUid); switch (field?.contentstackFieldType) { @@ -632,6 +682,55 @@ const convertToSchemaFormate = ({ field, advanced = false, marketPlacePath, keyM "non_localizable": field.advanced?.nonLocalizable ?? false } } + + case "text": { + // Handle generic text field - determine if it should be single line or multi line based on metadata + const isMultiline = field?.advanced?.multiline === true; + return { + "data_type": "text", + "display_name": field?.title, + uid: field?.uid, + "field_metadata": { + description: "", + default_value: field?.advanced?.default_value ?? '', + ...(isMultiline && { "multiline": true }) + }, + "format": field?.advanced?.validationRegex ?? '', + "error_messages": { + "format": field?.advanced?.validationErrorMessage ?? '', + }, + "multiple": field?.advanced?.multiple ?? false, + "mandatory": field?.advanced?.mandatory ?? false, + "unique": field?.advanced?.unique ?? false, + "non_localizable": field.advanced?.nonLocalizable ?? false + } + } + + case "html": { + return { + "data_type": "text", + "display_name": field?.title, + uid: field?.uid, + "field_metadata": { + "allow_rich_text": true, + "description": "", + default_value: field?.advanced?.default_value ?? '', + "multiline": false, + "rich_text_type": "advanced", + "options": [], + "ref_multiple_content_types": true, + "embed_entry": field?.advanced?.embedObjects?.length ? true : false + }, + "format": field?.advanced?.validationRegex ?? '', + "error_messages": { + "format": field?.advanced?.validationErrorMessage ?? '', + }, + "multiple": field?.advanced?.multiple ?? false, + "mandatory": field?.advanced?.mandatory ?? false, + "unique": field?.advanced?.unique ?? false, + "non_localizable": field.advanced?.nonLocalizable ?? false + } + } case 'markdown': { return { "data_type": "text", @@ -710,10 +809,13 @@ const convertToSchemaFormate = ({ field, advanced = false, marketPlacePath, keyM } case "reference": { + // Get reference fields from multiple possible sources + const referenceFields = field?.referenceTo || field?.refrenceTo || field?.advanced?.embedObjects || []; + return { data_type: "reference", display_name: field?.title, - reference_to: field?.refrenceTo ?? [], + reference_to: referenceFields ?? [], field_metadata: { ref_multiple: true, ref_multiple_content_types: true @@ -730,6 +832,27 @@ const convertToSchemaFormate = ({ field, advanced = false, marketPlacePath, keyM }; } + case "taxonomy": { + return { + data_type: "taxonomy", + display_name: field?.title, + uid: field?.uid, + taxonomies: field?.advanced?.taxonomies || [], + field_metadata: { + description: field?.advanced?.field_metadata?.description || "", + default_value: field?.advanced?.field_metadata?.default_value || "" + }, + format: field?.advanced?.validationRegex ?? '', + error_messages: { + format: field?.advanced?.validationErrorMessage ?? '', + }, + mandatory: field?.advanced?.mandatory ?? false, + multiple: field?.advanced?.multiple ?? true, + non_localizable: field?.advanced?.non_localizable ?? false, + unique: field?.advanced?.unique ?? false + }; + } + case 'html': { const htmlField: any = { "data_type": "text", @@ -740,7 +863,6 @@ const convertToSchemaFormate = ({ field, advanced = false, marketPlacePath, keyM "description": "", "multiline": false, "rich_text_type": "advanced", - "version": 3, "options": [], "ref_multiple_content_types": true, "embed_entry": field?.advanced?.embedObjects?.length ? true : false, diff --git a/api/src/utils/entries-field-creator.utils.ts b/api/src/utils/entries-field-creator.utils.ts index a90800df8..0792725ae 100644 --- a/api/src/utils/entries-field-creator.utils.ts +++ b/api/src/utils/entries-field-creator.utils.ts @@ -201,35 +201,84 @@ export const entriesFieldCreator = async ({ } case 'dropdown': { - const isOptionPresent = field?.advanced?.options?.find( - (ops: any) => ops?.key === content || ops?.value === content + // Handle dropdown, radio, and checkbox fields following CSV scenarios + const choices = + field?.advanced?.enum?.choices || field?.advanced?.options || []; + const isMultiple = + field?.advanced?.multiple || field?.advanced?.Multiple || false; + + // Handle multiple values (arrays) - for checkboxes + if (Array.isArray(content)) { + const matchedValues = []; + for (const item of content) { + const match = choices.find( + (choice: any) => + choice?.key === item || + choice?.value === item || + String(choice?.key) === String(item) || + String(choice?.value) === String(item) + ); + if (match) { + // For checkboxes (multiple=true), return the value + // For single fields, return just the value + matchedValues.push(match.value); + } + } + return matchedValues.length > 0 + ? isMultiple + ? matchedValues + : matchedValues[0] + : null; + } + + // Handle single values - for dropdown and radio + const match = choices.find( + (choice: any) => + choice?.key === content || + choice?.value === content || + String(choice?.key) === String(content) || + String(choice?.value) === String(content) ); - if (isOptionPresent) { - if (field?.advanced?.Multiple) { - if (!isOptionPresent?.key) { - return isOptionPresent; + + if (match) { + // Handle Multiple flag from incoming branch + if (isMultiple) { + // For multiple fields, check if we should return the whole object or just value + if (!match.key) { + return match; } - return isOptionPresent; + return match; } - return isOptionPresent?.value ?? null; - } else { - if (field?.advanced?.default_value) { - const isOptionDefaultValue = field?.advanced?.options?.find( - (ops: any) => - ops?.key === field?.advanced?.default_value || - ops?.value === field?.advanced?.default_value - ); - if (field?.advanced?.Multiple) { - if (!isOptionDefaultValue?.key) { - return isOptionDefaultValue; + // Always return the value, not the whole choice object + return match.value ?? null; + } + + // Fallback to default value + const defaultValue = + field?.advanced?.default_value || + field?.advanced?.field_metadata?.default_value; + if (defaultValue) { + const defaultMatch = choices.find( + (choice: any) => + choice?.key === defaultValue || + choice?.value === defaultValue || + String(choice?.key) === String(defaultValue) || + String(choice?.value) === String(defaultValue) + ); + if (defaultMatch) { + if (isMultiple) { + if (!defaultMatch.key) { + return defaultMatch; } - return isOptionDefaultValue; + return defaultMatch; } - return isOptionDefaultValue?.value ?? null; - } else { - return field?.advanced?.default_value; + return defaultMatch.value ?? null; } + // If no match found, return the default value as-is (from incoming branch) + return defaultValue; } + + return null; } case 'number': { @@ -276,26 +325,32 @@ export const entriesFieldCreator = async ({ case 'reference': { const refs: any = []; - if (field?.refrenceTo?.length) { + if (field?.refrenceTo?.length && content) { field?.refrenceTo?.forEach((entry: any) => { const templatePresent = entriesData?.find( (tel: any) => uidCorrector({ uid: tel?.template }) === entry ); content?.split('|')?.forEach((id: string) => { - const entryid = - templatePresent?.locale?.[locale]?.[idCorrector({ id })]; - if (entryid) { - refs?.push({ - uid: idCorrector({ id }), - _content_type_uid: entry, - }); - } else { - // console.info("no entry for following id", id) + if (id && id.trim()) { + const trimmedId = id.trim(); + const correctedId = idCorrector({ id: trimmedId }); + + // Check if entry exists in entriesData + const entryExists = + templatePresent?.locale?.[locale]?.[correctedId]; + + // For Drupal, we create references even if the entry doesn't exist yet + // as entries might be created in a different order + // For other CMSs, only create reference if entry exists + if (entryExists || !templatePresent) { + refs?.push({ + uid: correctedId, + _content_type_uid: entry, + }); + } } }); }); - } else { - console.info('test ====>'); } return refs; } @@ -350,6 +405,38 @@ export const entriesFieldCreator = async ({ break; } + case 'isodate': { + // Handle Unix timestamp conversion to ISO date string + if ( + typeof content === 'number' || + (typeof content === 'string' && !isNaN(Number(content))) + ) { + const timestamp = + typeof content === 'string' ? parseInt(content) : content; + return dayjs.unix(timestamp).toISOString(); + } + // If it's already a date string, try to parse and convert + if (typeof content === 'string') { + const parsed = dayjs(content); + if (parsed.isValid()) { + return parsed.toISOString(); + } + } + return content; + } + + case 'taxonomy': { + // Handle taxonomy field - return as-is if it's already in the expected format + if (Array.isArray(content)) { + return content; + } + // If it's a single taxonomy object, wrap in array + if (content && typeof content === 'object' && content.taxonomy_uid) { + return [content]; + } + return content; + } + case 'boolean': { return typeof content === 'string' && content === '1' ? true : false; } diff --git a/api/src/utils/optimized-query-builder.utils.ts b/api/src/utils/optimized-query-builder.utils.ts new file mode 100644 index 000000000..0dfd70c9d --- /dev/null +++ b/api/src/utils/optimized-query-builder.utils.ts @@ -0,0 +1,452 @@ +import mysql from 'mysql2'; +import { getLogMessage } from './index.js'; +import customLogger from './custom-logger.utils.js'; + +/** + * Optimized Query Builder for Drupal Field Data + * Eliminates the 61-table JOIN limit by using sequential queries + */ + +interface DrupalFieldData { + field_name: string; + content_types: string; + type: string; + content_handler?: string; +} + +interface FieldResult { + entity_id: number; + [key: string]: any; +} + +interface OptimizedQueryResult { + baseQuery: string; + countQuery: string; + fieldQueries: string[]; +} + +export class OptimizedQueryBuilder { + private connection: mysql.Connection; + private projectId: string; + private destinationStackId: string; + + constructor(connection: mysql.Connection, projectId: string, destinationStackId: string) { + this.connection = connection; + this.projectId = projectId; + this.destinationStackId = destinationStackId; + } + + /** + * Strategy 1: Sequential Field Queries (No JOINs) + * Fetch base node data first, then field data separately + */ + async generateSequentialQueries( + contentType: string, + fieldsForType: DrupalFieldData[] + ): Promise { + const srcFunc = 'generateSequentialQueries'; + + // 1. Base query for node data (no JOINs) + const baseQuery = ` + SELECT + node.nid, + node.title, + node.langcode, + node.created, + node.type, + users.name as author_name + FROM node_field_data node + LEFT JOIN users ON users.uid = node.uid + WHERE node.type = '${contentType}' + ORDER BY node.nid + `; + + // 2. Count query (simple, no JOINs) + const countQuery = ` + SELECT COUNT(DISTINCT node.nid) as countentry + FROM node_field_data node + WHERE node.type = '${contentType}' + `; + + // 3. Individual field queries (one per field table) + const fieldQueries: string[] = []; + + for (const field of fieldsForType) { + // Check if field table exists and get column structure + const fieldTableName = `node__${field.field_name}`; + + try { + // Get field columns dynamically + const columnQuery = ` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '${fieldTableName}' + AND COLUMN_NAME LIKE '${field.field_name}_%' + `; + + const [columns] = await this.connection.promise().query(columnQuery) as any[]; + + if (columns.length > 0) { + // Build field-specific query + const fieldColumns = columns.map((col: any) => col.COLUMN_NAME).join(', '); + + const fieldQuery = ` + SELECT + entity_id, + ${fieldColumns} + FROM ${fieldTableName} + WHERE entity_id IN ( + SELECT nid FROM node_field_data WHERE type = '${contentType}' + ) + `; + + fieldQueries.push(fieldQuery); + } + } catch (error) { + console.warn(`Field table ${fieldTableName} not found or inaccessible:`, error); + } + } + + const message = getLogMessage( + srcFunc, + `Generated optimized queries for ${contentType}: 1 base + ${fieldQueries.length} field queries (0 JOINs)`, + {} + ); + await customLogger(this.projectId, this.destinationStackId, 'info', message); + + return { + baseQuery, + countQuery, + fieldQueries + }; + } + + /** + * Strategy 2: Batch Field Queries (Limited JOINs) + * Group fields into batches with max 15 JOINs each + */ + async generateBatchedQueries( + contentType: string, + fieldsForType: DrupalFieldData[], + batchSize: number = 15 + ): Promise<{ baseQuery: string; batchQueries: string[]; countQuery: string }> { + const srcFunc = 'generateBatchedQueries'; + + // Base query (always the same) + const baseQuery = ` + SELECT + node.nid, + node.title, + node.langcode, + node.created, + node.type + FROM node_field_data node + WHERE node.type = '${contentType}' + ORDER BY node.nid + `; + + // Count query + const countQuery = ` + SELECT COUNT(DISTINCT node.nid) as countentry + FROM node_field_data node + WHERE node.type = '${contentType}' + `; + + // Create batches of fields + const fieldBatches = this.createFieldBatches(fieldsForType, batchSize); + const batchQueries: string[] = []; + + for (let i = 0; i < fieldBatches.length; i++) { + const batch = fieldBatches[i]; + const validFields: string[] = []; + const joinClauses: string[] = []; + + // Validate each field in the batch + for (const field of batch) { + try { + const fieldTableName = `node__${field.field_name}`; + + // Check if table exists + const tableExistsQuery = ` + SELECT 1 FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '${fieldTableName}' + `; + + const [tableExists] = await this.connection.promise().query(tableExistsQuery) as any[]; + + if (tableExists.length > 0) { + // Get field columns + const columnQuery = ` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '${fieldTableName}' + AND COLUMN_NAME LIKE '${field.field_name}_%' + LIMIT 1 + `; + + const [columns] = await this.connection.promise().query(columnQuery) as any[]; + + if (columns.length > 0) { + const columnName = columns[0].COLUMN_NAME; + validFields.push(`MAX(${fieldTableName}.${columnName}) as ${columnName}`); + joinClauses.push(`LEFT JOIN ${fieldTableName} ON ${fieldTableName}.entity_id = node.nid`); + } + } + } catch (error) { + console.warn(`Skipping field ${field.field_name}:`, error); + } + } + + if (validFields.length > 0) { + const batchQuery = ` + SELECT + node.nid, + ${validFields.join(',\n ')} + FROM node_field_data node + ${joinClauses.join('\n ')} + WHERE node.type = '${contentType}' + GROUP BY node.nid + ORDER BY node.nid + `; + + batchQueries.push(batchQuery); + } + } + + const message = getLogMessage( + srcFunc, + `Generated ${batchQueries.length} batched queries for ${contentType} (max ${batchSize} JOINs each)`, + {} + ); + await customLogger(this.projectId, this.destinationStackId, 'info', message); + + return { + baseQuery, + batchQueries, + countQuery + }; + } + + /** + * Strategy 3: Union-Based Field Queries + * Use UNION to combine field data without JOINs + */ + async generateUnionQueries( + contentType: string, + fieldsForType: DrupalFieldData[] + ): Promise<{ baseQuery: string; unionQuery: string; countQuery: string }> { + const srcFunc = 'generateUnionQueries'; + + // Base query + const baseQuery = ` + SELECT + node.nid, + node.title, + node.langcode, + node.created, + node.type + FROM node_field_data node + WHERE node.type = '${contentType}' + ORDER BY node.nid + `; + + // Count query + const countQuery = ` + SELECT COUNT(DISTINCT node.nid) as countentry + FROM node_field_data node + WHERE node.type = '${contentType}' + `; + + // Union query for all field data + const unionParts: string[] = []; + + for (const field of fieldsForType) { + const fieldTableName = `node__${field.field_name}`; + + try { + // Get field columns + const columnQuery = ` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '${fieldTableName}' + AND COLUMN_NAME LIKE '${field.field_name}_%' + LIMIT 1 + `; + + const [columns] = await this.connection.promise().query(columnQuery) as any[]; + + if (columns.length > 0) { + const columnName = columns[0].COLUMN_NAME; + + unionParts.push(` + SELECT + entity_id as nid, + '${field.field_name}' as field_name, + ${columnName} as field_value + FROM ${fieldTableName} + WHERE entity_id IN ( + SELECT nid FROM node_field_data WHERE type = '${contentType}' + ) + `); + } + } catch (error) { + console.warn(`Skipping field ${field.field_name} in union:`, error); + } + } + + const unionQuery = unionParts.length > 0 ? unionParts.join('\nUNION ALL\n') : ''; + + const message = getLogMessage( + srcFunc, + `Generated union query for ${contentType} with ${unionParts.length} field parts`, + {} + ); + await customLogger(this.projectId, this.destinationStackId, 'info', message); + + return { + baseQuery, + unionQuery, + countQuery + }; + } + + /** + * Execute optimized queries and merge results + */ + async executeOptimizedQueries( + strategy: 'sequential' | 'batched' | 'union', + contentType: string, + fieldsForType: DrupalFieldData[], + batchSize: number = 15 + ): Promise { + const srcFunc = 'executeOptimizedQueries'; + + try { + switch (strategy) { + case 'sequential': + return await this.executeSequentialQueries(contentType, fieldsForType); + + case 'batched': + return await this.executeBatchedQueries(contentType, fieldsForType, batchSize); + + case 'union': + return await this.executeUnionQueries(contentType, fieldsForType); + + default: + throw new Error(`Unknown strategy: ${strategy}`); + } + } catch (error: any) { + const message = getLogMessage( + srcFunc, + `Failed to execute optimized queries for ${contentType}: ${error.message}`, + {}, + error + ); + await customLogger(this.projectId, this.destinationStackId, 'error', message); + throw error; + } + } + + private async executeSequentialQueries(contentType: string, fieldsForType: DrupalFieldData[]): Promise { + const { baseQuery, fieldQueries } = await this.generateSequentialQueries(contentType, fieldsForType); + + // Execute base query + const [baseResults] = await this.connection.promise().query(baseQuery) as any[]; + + // Create result map + const resultMap = new Map(); + baseResults.forEach((row: any) => { + resultMap.set(row.nid, { ...row }); + }); + + // Execute field queries and merge results + for (const fieldQuery of fieldQueries) { + const [fieldResults] = await this.connection.promise().query(fieldQuery) as any[]; + + fieldResults.forEach((fieldRow: any) => { + const nid = fieldRow.entity_id; + if (resultMap.has(nid)) { + const existingRow = resultMap.get(nid); + // Merge field data (exclude entity_id) + const { entity_id, ...fieldData } = fieldRow; + Object.assign(existingRow, fieldData); + } + }); + } + + return Array.from(resultMap.values()); + } + + private async executeBatchedQueries(contentType: string, fieldsForType: DrupalFieldData[], batchSize: number): Promise { + const { baseQuery, batchQueries } = await this.generateBatchedQueries(contentType, fieldsForType, batchSize); + + // Execute base query + const [baseResults] = await this.connection.promise().query(baseQuery) as any[]; + + // Create result map + const resultMap = new Map(); + baseResults.forEach((row: any) => { + resultMap.set(row.nid, { ...row }); + }); + + // Execute batch queries and merge results + for (const batchQuery of batchQueries) { + const [batchResults] = await this.connection.promise().query(batchQuery) as any[]; + + batchResults.forEach((batchRow: any) => { + const nid = batchRow.nid; + if (resultMap.has(nid)) { + const existingRow = resultMap.get(nid); + // Merge batch data (exclude nid) + const { nid: _, ...batchData } = batchRow; + Object.assign(existingRow, batchData); + } + }); + } + + return Array.from(resultMap.values()); + } + + private async executeUnionQueries(contentType: string, fieldsForType: DrupalFieldData[]): Promise { + const { baseQuery, unionQuery } = await this.generateUnionQueries(contentType, fieldsForType); + + // Execute base query + const [baseResults] = await this.connection.promise().query(baseQuery) as any[]; + + // Create result map + const resultMap = new Map(); + baseResults.forEach((row: any) => { + resultMap.set(row.nid, { ...row }); + }); + + // Execute union query if it exists + if (unionQuery) { + const [unionResults] = await this.connection.promise().query(unionQuery) as any[]; + + // Group union results by nid + unionResults.forEach((unionRow: any) => { + const nid = unionRow.nid; + if (resultMap.has(nid)) { + const existingRow = resultMap.get(nid); + existingRow[unionRow.field_name] = unionRow.field_value; + } + }); + } + + return Array.from(resultMap.values()); + } + + private createFieldBatches(fields: DrupalFieldData[], batchSize: number): DrupalFieldData[][] { + const batches: DrupalFieldData[][] = []; + for (let i = 0; i < fields.length; i += batchSize) { + batches.push(fields.slice(i, i + batchSize)); + } + return batches; + } +} + +export default OptimizedQueryBuilder; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index c513dec99..000000000 --- a/package-lock.json +++ /dev/null @@ -1,4694 +0,0 @@ -{ - "name": "migration-v2", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "migration-v2", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@contentstack/cli-utilities": "^1.0.3", - "express-validator": "^7.3.0" - }, - "devDependencies": { - "@types/estree": "^1.0.7", - "@types/express": "^5.0.1", - "husky": "^4.3.8", - "prettier": "^2.4.1", - "rimraf": "^3.0.2", - "validate-branch-name": "^1.3.0", - "xml2js": "^0.5.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@contentstack/cli-utilities": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-utilities/-/cli-utilities-1.15.0.tgz", - "integrity": "sha512-Q3csEjZk7rdEvbhRyq41jMT9nFduxR7zVpyGAkYdialh4KjMHxJvzVUdmYuA3PA92xoTlFdcY97yXPQJpmptTw==", - "dependencies": { - "@contentstack/management": "~1.22.0", - "@contentstack/marketplace-sdk": "^1.2.8", - "@oclif/core": "^4.3.0", - "axios": "^1.9.0", - "chalk": "^4.1.2", - "cli-cursor": "^3.1.0", - "cli-progress": "^3.12.0", - "cli-table": "^0.3.11", - "conf": "^10.2.0", - "dotenv": "^16.5.0", - "figures": "^3.2.0", - "inquirer": "8.2.6", - "inquirer-search-checkbox": "^1.0.0", - "inquirer-search-list": "^1.2.6", - "js-yaml": "^4.1.0", - "klona": "^2.0.6", - "lodash": "^4.17.21", - "mkdirp": "^1.0.4", - "open": "^8.4.2", - "ora": "^5.4.1", - "papaparse": "^5.5.3", - "recheck": "~4.4.5", - "rxjs": "^6.6.7", - "traverse": "^0.6.11", - "tty-table": "^4.2.3", - "unique-string": "^2.0.0", - "uuid": "^9.0.1", - "winston": "^3.17.0", - "xdg-basedir": "^4.0.0" - } - }, - "node_modules/@contentstack/management": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@contentstack/management/-/management-1.22.0.tgz", - "integrity": "sha512-TmwCKhdZnmGpcTuXn5JWbvMqbu0PqEn8Z/oEUlCelAxpo9vSC2qS4aejJtLTqC3Gii/7cJwjqF1BoFpwSO5J9A==", - "dependencies": { - "assert": "^2.1.0", - "axios": "^1.9.0", - "buffer": "^6.0.3", - "form-data": "^4.0.2", - "husky": "^9.1.7", - "lodash": "^4.17.21", - "qs": "^6.14.0", - "stream-browserify": "^3.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@contentstack/management/node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/@contentstack/marketplace-sdk": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@contentstack/marketplace-sdk/-/marketplace-sdk-1.3.0.tgz", - "integrity": "sha512-zEpAxDeSSFxcE409IqDjepEzROe8zk/sqHhh+KkcwwYra1h5NVdbboQQXZrwjQEDVy4UsW0+Y1Ttnl8avu4w3A==", - "dependencies": { - "axios": "^1.11.0" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@oclif/core": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.5.2.tgz", - "integrity": "sha512-eQcKyrEcDYeZJKu4vUWiu0ii/1Gfev6GF4FsLSgNez5/+aQyAUCjg3ZWlurf491WiYZTXCWyKAxyPWk8DKv2MA==", - "dependencies": { - "ansi-escapes": "^4.3.2", - "ansis": "^3.17.0", - "clean-stack": "^3.0.1", - "cli-spinners": "^2.9.2", - "debug": "^4.4.0", - "ejs": "^3.1.10", - "get-package-type": "^0.1.0", - "indent-string": "^4.0.0", - "is-wsl": "^2.2.0", - "lilconfig": "^3.1.3", - "minimatch": "^9.0.5", - "semver": "^7.6.3", - "string-width": "^4.2.3", - "supports-color": "^8", - "tinyglobby": "^0.2.14", - "widest-line": "^3.1.0", - "wordwrap": "^1.0.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true - }, - "node_modules/@types/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", - "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/node": { - "version": "22.15.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.14.tgz", - "integrity": "sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==", - "dev": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true - }, - "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansis": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", - "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", - "engines": { - "node": ">=14" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/atomically": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", - "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/breakword": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.6.tgz", - "integrity": "sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==", - "dependencies": { - "wcwidth": "^1.0.1" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, - "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "node_modules/clean-stack": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", - "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", - "dependencies": { - "escape-string-regexp": "4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "dependencies": { - "string-width": "^4.2.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", - "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", - "dependencies": { - "colors": "1.0.3" - }, - "engines": { - "node": ">= 0.2.0" - } - }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/conf": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", - "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", - "dependencies": { - "ajv": "^8.6.3", - "ajv-formats": "^2.1.1", - "atomically": "^1.7.0", - "debounce-fn": "^4.0.0", - "dot-prop": "^6.0.1", - "env-paths": "^2.2.1", - "json-schema-typed": "^7.0.3", - "onetime": "^5.1.2", - "pkg-up": "^3.1.0", - "semver": "^7.3.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/csv": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/csv/-/csv-5.5.3.tgz", - "integrity": "sha512-QTaY0XjjhTQOdguARF0lGKm5/mEq9PD9/VhZZegHDIBq2tQwgNpHc3dneD4mGo2iJs+fTKv5Bp0fZ+BRuY3Z0g==", - "dependencies": { - "csv-generate": "^3.4.3", - "csv-parse": "^4.16.3", - "csv-stringify": "^5.6.5", - "stream-transform": "^2.1.3" - }, - "engines": { - "node": ">= 0.1.90" - } - }, - "node_modules/csv-generate": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-3.4.3.tgz", - "integrity": "sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==" - }, - "node_modules/csv-parse": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", - "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==" - }, - "node_modules/csv-stringify": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", - "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==" - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debounce-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", - "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", - "dependencies": { - "mimic-fn": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/express-validator": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.0.tgz", - "integrity": "sha512-ujK2BX5JUun5NR4JuBo83YSXoDDIpoGz3QxgHTzQcHFevkKnwV1in4K7YNuuXQ1W3a2ObXB/P4OTnTZpUyGWiw==", - "dependencies": { - "lodash": "^4.17.21", - "validator": "~13.15.15" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "dependencies": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/external-editor/node_modules/chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha512-j/Toj7f1z98Hh2cYo2BVr85EpIRWqUi7rtRSGxh/cqUjqrnJe9l9UE7IUGd2vQ2p+kSHLkSzObQPZPLUC6TQwg==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] - }, - "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-versions": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz", - "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==", - "dev": true, - "dependencies": { - "semver-regex": "^3.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fuzzy": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", - "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/git-local-info": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/git-local-info/-/git-local-info-1.0.1.tgz", - "integrity": "sha512-QdCZytNlj9xHoyCXgqOOVYuxuq8Vo8fm3sDobpDrp59D4fa0wZ5f1huR49/qbxkzIdnJAA1tUIO68DkrvVa6Sg==", - "dev": true, - "dependencies": { - "ini": "^1.3.5" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/husky": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz", - "integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "compare-versions": "^3.6.0", - "cosmiconfig": "^7.0.0", - "find-versions": "^4.0.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^5.0.0", - "please-upgrade-node": "^3.2.0", - "slash": "^3.0.0", - "which-pm-runs": "^1.0.0" - }, - "bin": { - "husky-run": "bin/run.js", - "husky-upgrade": "lib/upgrader/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/husky" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/inquirer": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", - "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^6.0.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/inquirer-search-checkbox": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/inquirer-search-checkbox/-/inquirer-search-checkbox-1.0.0.tgz", - "integrity": "sha512-KR6kfe0+h7Zgyrj6GCBVgS4ZmmBhsXofcJoQv6EXZWxK+bpJZV9kOb2AaQ2fbjnH91G0tZWQaS5WteWygzXcmA==", - "dependencies": { - "chalk": "^2.3.0", - "figures": "^2.0.0", - "fuzzy": "^0.1.3", - "inquirer": "^3.3.0" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==" - }, - "node_modules/inquirer-search-checkbox/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/inquirer-search-checkbox/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "dependencies": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.0.4", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==" - }, - "node_modules/inquirer-search-checkbox/node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-checkbox/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/inquirer-search-list/-/inquirer-search-list-1.2.6.tgz", - "integrity": "sha512-C4pKSW7FOYnkAloH8rB4FiM91H1v08QFZZJh6KRt//bMfdDBIhgdX8wjHvrVH2bu5oIo6wYqGpzSBxkeClPxew==", - "dependencies": { - "chalk": "^2.3.0", - "figures": "^2.0.0", - "fuzzy": "^0.1.3", - "inquirer": "^3.3.0" - } - }, - "node_modules/inquirer-search-list/node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==" - }, - "node_modules/inquirer-search-list/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/inquirer-search-list/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/inquirer-search-list/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/inquirer-search-list/node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "dependencies": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.0.4", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, - "node_modules/inquirer-search-list/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==" - }, - "node_modules/inquirer-search-list/node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer-search-list/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/inquirer/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/inquirer/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/json-schema-typed": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", - "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", - "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mixme": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.10.tgz", - "integrity": "sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q==", - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/onetime/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "dev": true, - "bin": { - "opencollective-postinstall": "index.js" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/papaparse": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", - "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", - "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", - "dev": true, - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, - "dependencies": { - "semver-compare": "^1.0.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/recheck": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/recheck/-/recheck-4.4.5.tgz", - "integrity": "sha512-J80Ykhr+xxWtvWrfZfPpOR/iw2ijvb4WY8d9AVoN8oHsPP07JT1rCAalUSACMGxM1cvSocb6jppWFjVS6eTTrA==", - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "recheck-jar": "4.4.5", - "recheck-linux-x64": "4.4.5", - "recheck-macos-x64": "4.4.5", - "recheck-windows-x64": "4.4.5" - } - }, - "node_modules/recheck-jar": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/recheck-jar/-/recheck-jar-4.4.5.tgz", - "integrity": "sha512-a2kMzcfr+ntT0bObNLY22EUNV6Z6WeZ+DybRmPOUXVWzGcqhRcrK74tpgrYt3FdzTlSh85pqoryAPmrNkwLc0g==", - "optional": true - }, - "node_modules/recheck-linux-x64": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/recheck-linux-x64/-/recheck-linux-x64-4.4.5.tgz", - "integrity": "sha512-s8OVPCpiSGw+tLCxH3eei7Zp2AoL22kXqLmEtWXi0AnYNwfuTjZmeLn2aQjW8qhs8ZPSkxS7uRIRTeZqR5Fv/Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/recheck-macos-x64": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/recheck-macos-x64/-/recheck-macos-x64-4.4.5.tgz", - "integrity": "sha512-Ouup9JwwoKCDclt3Na8+/W2pVbt8FRpzjkDuyM32qTR2TOid1NI+P1GA6/VQAKEOjvaxgGjxhcP/WqAjN+EULA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/recheck-windows-x64": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/recheck-windows-x64/-/recheck-windows-x64-4.4.5.tgz", - "integrity": "sha512-mkpzLHu9G9Ztjx8HssJh9k/Xm1d1d/4OoT7etHqFk+k1NGzISCRXBD22DqYF9w8+J4QEzTAoDf8icFt0IGhOEQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha512-Cun9QucwK6MIrp3mry/Y7hqD1oFqTYLQ4pGxaHTjIdaFDWRGGLikqp6u8LcWJnzpoALg9hap+JGk8sFIUuEGNA==" - }, - "node_modules/rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha512-3xPNZGW93oCjiO7PtKxRK6iOVYBWBvtf9QHDfU23Oc+dLIQmAV//UnyXV/yihv81VS/UqoQPk4NegS8EFi55Hg==", - "dependencies": { - "rx-lite": "*" - } - }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true - }, - "node_modules/semver-regex": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz", - "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/smartwrap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/smartwrap/-/smartwrap-2.0.2.tgz", - "integrity": "sha512-vCsKNQxb7PnCNd2wY1WClWifAc2lwqsG8OaswpJkVJsvMGcnEntdTCDajZCkk93Ay1U3t/9puJmb525Rg5MZBA==", - "dependencies": { - "array.prototype.flat": "^1.2.3", - "breakword": "^1.0.5", - "grapheme-splitter": "^1.0.4", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1", - "yargs": "^15.1.0" - }, - "bin": { - "smartwrap": "src/terminal-adapter.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/smartwrap/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/smartwrap/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smartwrap/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smartwrap/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/smartwrap/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smartwrap/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smartwrap/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "node_modules/smartwrap/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smartwrap/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "engines": { - "node": "*" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dependencies": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "node_modules/stream-transform": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-2.1.3.tgz", - "integrity": "sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==", - "dependencies": { - "mixme": "^0.5.1" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/traverse": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.11.tgz", - "integrity": "sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==", - "dependencies": { - "gopd": "^1.2.0", - "typedarray.prototype.slice": "^1.0.5", - "which-typed-array": "^1.1.18" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/tty-table": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.2.3.tgz", - "integrity": "sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==", - "dependencies": { - "chalk": "^4.1.2", - "csv": "^5.5.3", - "kleur": "^4.1.5", - "smartwrap": "^2.0.2", - "strip-ansi": "^6.0.1", - "wcwidth": "^1.0.1", - "yargs": "^17.7.1" - }, - "bin": { - "tty-table": "adapters/terminal-adapter.js" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typedarray.prototype.slice": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.5.tgz", - "integrity": "sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "math-intrinsics": "^1.1.0", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-offset": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true - }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-branch-name": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/validate-branch-name/-/validate-branch-name-1.3.2.tgz", - "integrity": "sha512-oBh69R6LGw/uwkieCVOnDNYi4NEmFSXBeVSPAhyvcGExrS1vUWkkEju30DeEIknVgx0UjOK0WmJC9u6ex4aPRg==", - "dev": true, - "dependencies": { - "commander": "^8.3.0", - "cosmiconfig": "^8.1.3", - "git-local-info": "^1.0.1" - }, - "bin": { - "validate-branch-name": "cli.js" - } - }, - "node_modules/validate-branch-name/node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/validator": { - "version": "13.15.20", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", - "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, - "node_modules/which-pm-runs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", - "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "dependencies": { - "string-width": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json index ad8f2546a..914d7f68f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@types/estree": "^1.0.7", "@types/express": "^5.0.1", "husky": "^4.3.8", + "mysql2": "^3.15.1", "prettier": "^2.4.1", "rimraf": "^3.0.2", "validate-branch-name": "^1.3.0", diff --git a/ui/package.json b/ui/package.json index 00494abf7..2d2858552 100644 --- a/ui/package.json +++ b/ui/package.json @@ -36,6 +36,8 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", + "test:language-mapper": "node test-language-mapper.js", + "test:language-mapper-unit": "react-scripts test --testNamePattern='LoadLanguageMapper' --watchAll=false", "eject": "react-scripts eject", "prettify": "prettier --write .", "lint": "eslint .", diff --git a/ui/src/cmsData/legacyCms.json b/ui/src/cmsData/legacyCms.json index c5c4c327e..88db540e4 100644 --- a/ui/src/cmsData/legacyCms.json +++ b/ui/src/cmsData/legacyCms.json @@ -94,9 +94,6 @@ }, { "cms_id": "drupal", - "_metadata": { - "uid": "csb96887a2cfee9e8c" - }, "title": "Drupal", "description": "", "group_name": "lightning", @@ -106,61 +103,10 @@ }, "parent": "Drupal", "isactive": true, - "allowed_file_formats": [ - { - "fileformat_id": "zip", - "_metadata": { - "uid": "cs5d9c8914dc21ea80" - }, - "title": "Zip", - "description": "", - "group_name": "zip", - "isactive": true - } - ] - }, - { - "cms_id": "drupal v7", - "title": "Drupal v7", - "description": "", - "group_name": "lightning", - "doc_url": { - "title": "https://www.drupal.org/", - "href": "https://www.drupal.org/" - }, - "parent": "Drupal", - "isactive": true, - "allowed_file_formats": [ - { - "fileformat_id": "sql", - "title": "Sql", - "description": "", - "group_name": "sql", - "isactive": true, - "_metadata": { - "uid": "csceba83e388748bf1" - } - } - ], - "_metadata": { - "uid": "cs88cc83ef30625782" - } - }, - { - "cms_id": "drupal v8+", - "title": "Drupal v8+", - "description": "", - "group_name": "lightning", - "doc_url": { - "title": "https://www.drupal.org/", - "href": "https://www.drupal.org/" - }, - "parent": "Drupal", - "isactive": true, "allowed_file_formats": [ { "fileformat_id": "sql", - "title": "Sql", + "title": "ApiTokens", "description": "", "group_name": "sql", "isactive": true, @@ -377,4 +323,4 @@ "restricted_keyword_checkbox_text": "Please acknowledge that you have referred to the Contentstack restricted keywords", "affix_cta": "Continue", "file_format_cta": "Continue" -} \ No newline at end of file +} diff --git a/ui/src/components/AdvancePropertise/index.tsx b/ui/src/components/AdvancePropertise/index.tsx index 2988dbd41..e1fab940a 100644 --- a/ui/src/components/AdvancePropertise/index.tsx +++ b/ui/src/components/AdvancePropertise/index.tsx @@ -11,11 +11,12 @@ import { Icon, Select, Radio, - Button + Button, + InstructionText, } from '@contentstack/venus-components'; // Service -import { getContentTypes } from '../../services/api/migration.service'; +import { getContentTypes, getExistingTaxonomies } from '../../services/api/migration.service'; // Utilities import { validateArray } from '../../utilities/functions'; @@ -32,6 +33,13 @@ interface ContentTypeOption { value: string; } +interface Taxonomy { + uid: string; + name: string; + description?: string; + source?: string; +} + /** * Component for displaying advanced properties. * @param props - The schema properties. @@ -67,7 +75,7 @@ const AdvancePropertise = (props: SchemaProps) => { value: item })); - const referencedItems = props?.data?.refrenceTo?.map((item: string) => ({ + const referencedItems = props?.data?.referenceTo?.map((item: string) => ({ label: item, value: item })); @@ -78,9 +86,11 @@ const AdvancePropertise = (props: SchemaProps) => { const [embedObjectsLabels, setEmbedObjectsLabels] = useState( props?.value?.embedObjects ); - const [referencedCT, setReferencedCT] = useState( - referencedItems || null - ); + const [referencedCT, setReferencedCT] = useState(referencedItems || null); + const [sourceTaxonomies, setSourceTaxonomies] = useState([]); + const [destinationTaxonomies, setDestinationTaxonomies] = useState([]); + const [referencedTaxonomies, setReferencedTaxonomies] = useState(referencedItems || null); + const [isTaxonomiesLoading, setIsTaxonomiesLoading] = useState(false); const [showOptions, setShowOptions] = useState>({}); const [showIcon, setShowIcon] = useState(); const filterRef = useRef(null); @@ -91,18 +101,18 @@ const AdvancePropertise = (props: SchemaProps) => { const [errorMessage, setErrorMessage] = useState(''); useEffect(() => { - if (props?.data?.refrenceTo && Array.isArray(props?.data?.refrenceTo)) { - const updatedReferencedItems = props?.data?.refrenceTo.map((item: string) => ({ + if (props?.data?.referenceTo && Array.isArray(props?.data?.referenceTo)) { + const updatedReferencedItems = props?.data?.referenceTo.map((item: string) => ({ label: item, value: item })); setReferencedCT(updatedReferencedItems); setToggleStates((prevStates) => ({ ...prevStates, - referenedItems: props?.data?.refrenceTo + referenedItems: props?.data?.referenceTo })); } - }, [props?.data?.refrenceTo]); + }, [props?.data?.referenceTo]); useEffect(() => { const defaultIndex = toggleStates?.option?.findIndex( (item: optionsType) => toggleStates?.default_value === item?.key @@ -112,9 +122,68 @@ const AdvancePropertise = (props: SchemaProps) => { setShowIcon(defaultIndex); } }, []); + useEffect(() => { fetchContentTypes(''); + + // Only fetch taxonomies if this is a Taxonomy field + if (props?.fieldtype === 'Taxonomy') { + fetchTaxonomies(); + } }, []); + + // Update referenced CT when content types are fetched (only for Reference fields) + useEffect(() => { + if (props?.fieldtype === 'Reference' && contentTypes.length > 0) { + // Merge old (upload-api) and new (UI) selections + // Reference fields can use embedObjects OR reference_to + const oldReferences = props?.data?.advanced?.embedObjects || props?.data?.advanced?.reference_to || []; + const newReferences = props?.data?.referenceTo || []; + const allReferenceUIDs = Array.from(new Set([...oldReferences, ...newReferences])); + + if (allReferenceUIDs.length > 0) { + const matchedCTs = allReferenceUIDs + .map((uid: string) => { + const ct = contentTypes.find((c: ContentType) => c.contentstackUid === uid); + return ct ? { label: ct.contentstackTitle, value: ct.contentstackUid } : null; + }) + .filter(Boolean) as ContentTypeOption[]; + + if (matchedCTs.length > 0) { + setReferencedCT(matchedCTs); + } + } + } + }, [contentTypes, props?.data?.referenceTo, props?.data?.advanced, props?.fieldtype]); + + // Update referenced taxonomies when taxonomies are fetched (only for Taxonomy fields) + useEffect(() => { + if (props?.fieldtype === 'Taxonomy' && (sourceTaxonomies.length > 0 || destinationTaxonomies.length > 0)) { + const allTaxonomies = [...sourceTaxonomies, ...destinationTaxonomies]; + + // Merge old (upload-api) and new (UI) selections + const oldTaxonomies = (props?.data?.advanced?.taxonomies || []).map((t: { taxonomy_uid?: string } | string) => (typeof t === 'string' ? t : t.taxonomy_uid || '')); + const newTaxonomies = props?.data?.referenceTo || []; + const allTaxonomyUIDs = Array.from(new Set([...oldTaxonomies, ...newTaxonomies])); + + + + if (allTaxonomyUIDs.length > 0) { + const matchedTaxonomies = allTaxonomyUIDs + .map((uid: string) => { + const taxonomy = allTaxonomies.find((t: Taxonomy) => t.uid === uid); + return taxonomy ? { label: taxonomy.name || taxonomy.uid, value: taxonomy.uid } : null; + }) + .filter(Boolean) as ContentTypeOption[]; + + + + if (matchedTaxonomies.length > 0) { + setReferencedTaxonomies(matchedTaxonomies); + } + } + } + }, [sourceTaxonomies, destinationTaxonomies, props?.data?.referenceTo, props?.data?.advanced, props?.fieldtype]); /** * Fetches the content types list. * @param searchText - The search text. @@ -129,6 +198,29 @@ const AdvancePropertise = (props: SchemaProps) => { } }; + /** + * Fetches taxonomies from both source CMS and destination stack. + */ + const fetchTaxonomies = async () => { + setIsTaxonomiesLoading(true); + try { + + const { data } = await getExistingTaxonomies(props?.projectId ?? ''); + + + + setSourceTaxonomies(data?.sourceTaxonomies || []); + setDestinationTaxonomies(data?.destinationTaxonomies || []); + + + } catch (error) { + console.error('❌ Error fetching taxonomies:', error); + return error; + } finally { + setIsTaxonomiesLoading(false); + } + }; + /** * Handles the change event for input fields. * @param field - The field name. @@ -618,6 +710,74 @@ const AdvancePropertise = (props: SchemaProps) => { )} + {props?.fieldtype === 'Taxonomy' && ( + + + Referenced Taxonomies + +