diff --git a/MIGRATION.md b/MIGRATION.md index 2eebeba746..98c4621478 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -130,6 +130,31 @@ csdx cm:stacks:migration -k b*******9ca0 --file-path "../contentstack-migration/ **Migration Action:** use the import/export commands instead. +### 5. 🔌 HTML RTE to JSON RTE Migration (Separate Plugin) + +**What Changed:** +- HTML RTE to JSON RTE migration has been extracted into a separate plugin +- Now requires separate installation as `@contentstack/cli-cm-migrate-rte` + +**Installation:** + +```bash +npm install -g @contentstack/cli-cm-migrate-rte +``` + +**Commands:** + +```bash +# Migrate HTML RTE to JSON RTE +csdx cm:entries:migrate-html-rte --config-path path/to/config.json + +# Or using alias +csdx cm:migrate-rte --config-path path/to/config.json +``` + +**Migration Action:** +- Install `@contentstack/cli-cm-migrate-rte` separately if you use RTE migration + ## Troubleshooting ### Common Issues diff --git a/package-lock.json b/package-lock.json index 3a6f68dfa4..14242203ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -280,25 +280,25 @@ } }, "node_modules/@aws-sdk/client-cloudfront": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudfront/-/client-cloudfront-3.956.0.tgz", - "integrity": "sha512-7eozK4+yBs2Ig0pTBkoc9WS1+qWPRKdhzuscLLX9XsKjPVcIqOWhzNy7aROHsYC471CBNKgp4rU/rmL5WU8qEg==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudfront/-/client-cloudfront-3.958.0.tgz", + "integrity": "sha512-PktCfsiamxT22H3zWS+Re0di6vbSiiOdS0gir/hSA7RQPcq72GDSnCEI3EXNRxRbS2I6k6dN+gIgjvrwofpTKA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/credential-provider-node": "3.956.0", - "@aws-sdk/middleware-host-header": "3.956.0", - "@aws-sdk/middleware-logger": "3.956.0", - "@aws-sdk/middleware-recursion-detection": "3.956.0", - "@aws-sdk/middleware-user-agent": "3.956.0", - "@aws-sdk/region-config-resolver": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-endpoints": "3.956.0", - "@aws-sdk/util-user-agent-browser": "3.956.0", - "@aws-sdk/util-user-agent-node": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-node": "3.958.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", @@ -333,33 +333,33 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.956.0.tgz", - "integrity": "sha512-O+Z7PSY9TjaqJcZSDMvVmXBuV/jmFRJIu7ga+9XgWv4+qfjhAX2N2s4kgsRnIdjIO4xgkN3O/BugTCyjIRrIDQ==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.958.0.tgz", + "integrity": "sha512-ol8Sw37AToBWb6PjRuT/Wu40SrrZSA0N4F7U3yTkjUNX0lirfO1VFLZ0hZtZplVJv8GNPITbiczxQ8VjxESXxg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/credential-provider-node": "3.956.0", - "@aws-sdk/middleware-bucket-endpoint": "3.956.0", - "@aws-sdk/middleware-expect-continue": "3.956.0", - "@aws-sdk/middleware-flexible-checksums": "3.956.0", - "@aws-sdk/middleware-host-header": "3.956.0", - "@aws-sdk/middleware-location-constraint": "3.956.0", - "@aws-sdk/middleware-logger": "3.956.0", - "@aws-sdk/middleware-recursion-detection": "3.956.0", - "@aws-sdk/middleware-sdk-s3": "3.956.0", - "@aws-sdk/middleware-ssec": "3.956.0", - "@aws-sdk/middleware-user-agent": "3.956.0", - "@aws-sdk/region-config-resolver": "3.956.0", - "@aws-sdk/signature-v4-multi-region": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-endpoints": "3.956.0", - "@aws-sdk/util-user-agent-browser": "3.956.0", - "@aws-sdk/util-user-agent-node": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-node": "3.958.0", + "@aws-sdk/middleware-bucket-endpoint": "3.957.0", + "@aws-sdk/middleware-expect-continue": "3.957.0", + "@aws-sdk/middleware-flexible-checksums": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-location-constraint": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-sdk-s3": "3.957.0", + "@aws-sdk/middleware-ssec": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/signature-v4-multi-region": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/eventstream-serde-browser": "^4.2.7", @@ -400,24 +400,24 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.956.0.tgz", - "integrity": "sha512-TCxCa9B1IMILvk/7sig0fRQzff+M2zBQVZGWOJL8SAZq/gfElIMAf/nYjQwMhXjyq8PFDRGm4GN8ZhNKPeNleQ==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.958.0.tgz", + "integrity": "sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/middleware-host-header": "3.956.0", - "@aws-sdk/middleware-logger": "3.956.0", - "@aws-sdk/middleware-recursion-detection": "3.956.0", - "@aws-sdk/middleware-user-agent": "3.956.0", - "@aws-sdk/region-config-resolver": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-endpoints": "3.956.0", - "@aws-sdk/util-user-agent-browser": "3.956.0", - "@aws-sdk/util-user-agent-node": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", @@ -450,14 +450,14 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.956.0.tgz", - "integrity": "sha512-BMOCXZNz5z4cR3/SaNHUfeoZQUG/y39bLscdLUgg3RL6mDOhuINIqMc0qc6G3kpwDTLVdXikF4nmx2UrRK9y5A==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.957.0.tgz", + "integrity": "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", - "@aws-sdk/xml-builder": "3.956.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/xml-builder": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", @@ -474,15 +474,29 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.957.0.tgz", + "integrity": "sha512-qSwSfI+qBU9HDsd6/4fM9faCxYJx2yDuHtj+NVOQ6XYDWQzFab/hUdwuKZ77Pi6goLF1pBZhJ2azaC2w7LbnTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.956.0.tgz", - "integrity": "sha512-aLJavJMPVTvhmggJ0pcdCKEWJk3sL9QkJkUIEoTzOou7HnxWS66N4sC5e8y27AF2nlnYfIxq3hkEiZlGi/vlfA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.957.0.tgz", + "integrity": "sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" @@ -492,14 +506,14 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.956.0.tgz", - "integrity": "sha512-VsKzBNhwT6XJdW3HQX6o4KOHj1MAzSwA8/zCsT9mOGecozw1yeCcQPtlWDSlfsfygKVCXz7fiJzU03yl11NKMA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.957.0.tgz", + "integrity": "sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", @@ -514,21 +528,21 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.956.0.tgz", - "integrity": "sha512-TlDy+IGr0JIRBwnPdV31J1kWXEcfsR3OzcNVWQrguQdHeTw2lU5eft16kdizo6OruqcZRF/LvHBDwAWx4u51ww==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.958.0.tgz", + "integrity": "sha512-u7twvZa1/6GWmPBZs6DbjlegCoNzNjBsMS/6fvh5quByYrcJr/uLd8YEr7S3UIq4kR/gSnHqcae7y2nL2bqZdg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/credential-provider-env": "3.956.0", - "@aws-sdk/credential-provider-http": "3.956.0", - "@aws-sdk/credential-provider-login": "3.956.0", - "@aws-sdk/credential-provider-process": "3.956.0", - "@aws-sdk/credential-provider-sso": "3.956.0", - "@aws-sdk/credential-provider-web-identity": "3.956.0", - "@aws-sdk/nested-clients": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-env": "3.957.0", + "@aws-sdk/credential-provider-http": "3.957.0", + "@aws-sdk/credential-provider-login": "3.958.0", + "@aws-sdk/credential-provider-process": "3.957.0", + "@aws-sdk/credential-provider-sso": "3.958.0", + "@aws-sdk/credential-provider-web-identity": "3.958.0", + "@aws-sdk/nested-clients": "3.958.0", + "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", @@ -540,15 +554,15 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.956.0.tgz", - "integrity": "sha512-p2Y62mdIlUpiyi5tvn8cKTja5kq1e3Rm5gm4wpNQ9caTayfkIEXyKrbP07iepTv60Coaylq9Fx6b5En/siAeGA==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.958.0.tgz", + "integrity": "sha512-sDwtDnBSszUIbzbOORGh5gmXGl9aK25+BHb4gb1aVlqB+nNL2+IUEJA62+CE55lXSH8qXF90paivjK8tOHTwPA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/nested-clients": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.958.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", @@ -560,19 +574,19 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.956.0.tgz", - "integrity": "sha512-ITjp7uAQh17ljUsCWkPRmLjyFfupGlJVUfTLHnZJ+c7G0P0PDRquaM+fBSh0y33tauHsBa5fGnCCLRo5hy9sGQ==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.958.0.tgz", + "integrity": "sha512-vdoZbNG2dt66I7EpN3fKCzi6fp9xjIiwEA/vVVgqO4wXCGw8rKPIdDUus4e13VvTr330uQs2W0UNg/7AgtquEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.956.0", - "@aws-sdk/credential-provider-http": "3.956.0", - "@aws-sdk/credential-provider-ini": "3.956.0", - "@aws-sdk/credential-provider-process": "3.956.0", - "@aws-sdk/credential-provider-sso": "3.956.0", - "@aws-sdk/credential-provider-web-identity": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/credential-provider-env": "3.957.0", + "@aws-sdk/credential-provider-http": "3.957.0", + "@aws-sdk/credential-provider-ini": "3.958.0", + "@aws-sdk/credential-provider-process": "3.957.0", + "@aws-sdk/credential-provider-sso": "3.958.0", + "@aws-sdk/credential-provider-web-identity": "3.958.0", + "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", @@ -584,14 +598,14 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.956.0.tgz", - "integrity": "sha512-wpAex+/LGVWkHPchsn9FWy1ahFualIeSYq3ADFc262ljJjrltOWGh3+cu3OK3gTMkX6VEsl+lFvy1P7Bk7cgXA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.957.0.tgz", + "integrity": "sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", @@ -602,16 +616,16 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.956.0.tgz", - "integrity": "sha512-IRFSDF32x8TpOEYSGMcGQVJUiYuJaFkek0aCjW0klNIZHBF1YpflVpUarK9DJe4v4ryfVq3c0bqR/JFui8QFmw==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.958.0.tgz", + "integrity": "sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.956.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/token-providers": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/client-sso": "3.958.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/token-providers": "3.958.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", @@ -622,15 +636,15 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.956.0.tgz", - "integrity": "sha512-4YkmjwZC+qoUKlVOY9xNx7BTKRdJ1R1/Zjk2QSW5aWtwkk2e07ZUQvUpbW4vGpAxGm1K4EgRcowuSpOsDTh44Q==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.958.0.tgz", + "integrity": "sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/nested-clients": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.958.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", @@ -641,14 +655,14 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.956.0.tgz", - "integrity": "sha512-+iHH9cnkNZgKkTBnPP9rbapHliKDrOuj7MDz6+wL0NV4N/XGB5tbrd+uDP608FXVeMHcWIIZtWkANADUmAI49w==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.957.0.tgz", + "integrity": "sha512-iczcn/QRIBSpvsdAS/rbzmoBpleX1JBjXvCynMbDceVLBIcVrwT1hXECrhtIC2cjh4HaLo9ClAbiOiWuqt+6MA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-arn-parser": "3.953.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-arn-parser": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", @@ -660,13 +674,13 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.956.0.tgz", - "integrity": "sha512-97rmalK9x09Darcl6AbShZRXYxWiyCeO8ll1C9rx1xyZMs2DeIKAZ/xuAJ/bywB3l25ls6VqXO4/EuDFJHL8eA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.957.0.tgz", + "integrity": "sha512-AlbK3OeVNwZZil0wlClgeI/ISlOt/SPUxBsIns876IFaVu/Pj3DgImnYhpcJuFRek4r4XM51xzIaGQXM6GDHGg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" @@ -676,17 +690,18 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.956.0.tgz", - "integrity": "sha512-Rd/VeVKuw+lQ1oJmJOyXV0flIkp9ouMGAS9QT28ogdQVxXriaheNo754N4z0+8+R6uTcmeojN7dN4jzt51WV2g==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.957.0.tgz", + "integrity": "sha512-iJpeVR5V8se1hl2pt+k8bF/e9JO4KWgPCMjg8BtRspNtKIUGy7j6msYvbDixaKZaF2Veg9+HoYcOhwnZumjXSA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/crc64-nvme": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", @@ -701,13 +716,13 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.956.0.tgz", - "integrity": "sha512-JujNJDp/dj1DbsI0ntzhrz2uJ4jpumcKtr743eMpEhdboYjuu/UzY8/7n1h5JbgU9TNXgqE9lgQNa5QPG0Tvsg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.957.0.tgz", + "integrity": "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" @@ -717,13 +732,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.956.0.tgz", - "integrity": "sha512-eANhYRFcVO/lI9tliitSW0DK5H1d9J7BK/9RrRz86bd5zPWteVqqzQRbMUdErVi1nwSbSIAa6YGv/ItYPswe0w==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.957.0.tgz", + "integrity": "sha512-y8/W7TOQpmDJg/fPYlqAhwA4+I15LrS7TwgUEoxogtkD8gfur9wFMRLT8LCyc9o4NMEcAnK50hSb4+wB0qv6tQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, @@ -732,13 +747,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.956.0.tgz", - "integrity": "sha512-Qff39yEOPYgRsm4SrkHOvS0nSoxXILYnC8Akp0uMRi2lOcZVyXL3WCWqIOtI830qVI4GPa796sleKguxx50RHg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.957.0.tgz", + "integrity": "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, @@ -747,13 +762,13 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.956.0.tgz", - "integrity": "sha512-/f4JxL2kSCYhy63wovqts6SJkpalSLvuFe78ozt3ClrGoHGyr69o7tPRYx5U7azLgvrIGjsWUyTayeAk3YHIVQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.957.0.tgz", + "integrity": "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", @@ -764,15 +779,15 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.956.0.tgz", - "integrity": "sha512-U/+jYb4iowqqpLjB6cSYan0goAMOlh2xg2CPIdSy550o8mYnJtuajBUQ20A9AA9PYKLlEAoCNEysNZkn4o/63g==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.957.0.tgz", + "integrity": "sha512-5B2qY2nR2LYpxoQP0xUum5A1UNvH2JQpLHDH1nWFNF/XetV7ipFHksMxPNhtJJ6ARaWhQIDXfOUj0jcnkJxXUg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-arn-parser": "3.953.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-arn-parser": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", @@ -790,13 +805,13 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.956.0.tgz", - "integrity": "sha512-1Et0vPoIzfhkUAdNRzu0pC25ZawFqXo5T8xpvbwkfDgfIkeVj+sm9t01iXO3pCOK52OSuLRAy7fiAo/AoHjOYg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.957.0.tgz", + "integrity": "sha512-qwkmrK0lizdjNt5qxl4tHYfASh8DFpHXM1iDVo+qHe+zuslfMqQEGRkzxS8tJq/I+8F0c6v3IKOveKJAfIvfqQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, @@ -805,15 +820,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.956.0.tgz", - "integrity": "sha512-azH8OJ0AIe3NafaTNvJorG/ALaLNTYwVKtyaSeQKOvaL8TNuBVuDnM5iHCiWryIaRgZotomqycwyfNKLw2D3JQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.957.0.tgz", + "integrity": "sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-endpoints": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", @@ -824,24 +839,24 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.956.0.tgz", - "integrity": "sha512-GHDQMkxoWpi3eTrhWGmghw0gsZJ5rM1ERHfBFhlhduCdtV3TyhKVmDgFG84KhU8v18dcVpSp3Pu3KwH7j1tgIg==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.958.0.tgz", + "integrity": "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/middleware-host-header": "3.956.0", - "@aws-sdk/middleware-logger": "3.956.0", - "@aws-sdk/middleware-recursion-detection": "3.956.0", - "@aws-sdk/middleware-user-agent": "3.956.0", - "@aws-sdk/region-config-resolver": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-endpoints": "3.956.0", - "@aws-sdk/util-user-agent-browser": "3.956.0", - "@aws-sdk/util-user-agent-node": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", @@ -874,13 +889,13 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.956.0.tgz", - "integrity": "sha512-byU5XYekW7+rZ3e067y038wlrpnPkdI4fMxcHCHrv+TAfzl8CCk5xLyzerQtXZR8cVPVOXuaYWe1zKW0uCnXUA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.957.0.tgz", + "integrity": "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", @@ -891,14 +906,14 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.956.0.tgz", - "integrity": "sha512-gejlXPmor08VydGC8bx0Bv4/tPT92eK0WLe2pUPR0AaMXL+5ycDpThAi1vLWjWr0aUjCA7lXx0pMENWlJlYK3A==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.957.0.tgz", + "integrity": "sha512-t6UfP1xMUigMMzHcb7vaZcjv7dA2DQkk9C/OAP1dKyrE0vb4lFGDaTApi17GN6Km9zFxJthEMUbBc7DL0hq1Bg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/middleware-sdk-s3": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/types": "^4.11.0", @@ -909,15 +924,15 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.956.0.tgz", - "integrity": "sha512-I01Q9yDeG9oXge14u/bubtSdBpok/rTsPp2AQwy5xj/5PatRTHPbUTP6tef3AH/lFCAqkI0nncIcgx6zikDdUQ==", + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.958.0.tgz", + "integrity": "sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/nested-clients": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.958.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", @@ -928,9 +943,9 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.956.0.tgz", - "integrity": "sha512-DMRU/p9wAlAJxEjegnLwduCA8YP2pcT/sIJ+17KSF38c5cC6CbBhykwbZLECTo+zYzoFrOqeLbqE6paH8Gx3ug==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.957.0.tgz", + "integrity": "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -942,9 +957,9 @@ } }, "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.953.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.953.0.tgz", - "integrity": "sha512-9hqdKkn4OvYzzaLryq2xnwcrPc8ziY34i9szUdgBfSqEC6pBxbY9/lLXmrgzfwMSL2Z7/v2go4Od0p5eukKLMQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.957.0.tgz", + "integrity": "sha512-Aj6m+AyrhWyg8YQ4LDPg2/gIfGHCEcoQdBt5DeSFogN5k9mmJPOJ+IAmNSWmWRjpOxEy6eY813RNDI6qS97M0g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -955,13 +970,13 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.956.0.tgz", - "integrity": "sha512-xZ5CBoubS4rs9JkFniKNShDtfqxaMUnwaebYMoybZm070q9+omFkQkJYXl7kopTViEgZgQl1sAsAkrawBM8qEQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.957.0.tgz", + "integrity": "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-endpoints": "^3.2.7", @@ -972,9 +987,9 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.953.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.953.0.tgz", - "integrity": "sha512-mPxK+I1LcrgC/RSa3G5AMAn8eN2Ay0VOgw8lSRmV1jCtO+iYvNeCqOdxoJUjOW6I5BA4niIRWqVORuRP07776Q==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.957.0.tgz", + "integrity": "sha512-nhmgKHnNV9K+i9daumaIz8JTLsIIML9PE/HUks5liyrjUzenjW/aHoc7WJ9/Td/gPZtayxFnXQSJRb/fDlBuJw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -985,27 +1000,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.956.0.tgz", - "integrity": "sha512-s8KwYR3HqiGNni7a1DN2P3RUog64QoBQ6VCSzJkHBWb6++8KSOpqeeDkfmEz+22y1LOne+bRrpDGKa0aqOc3rQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.957.0.tgz", + "integrity": "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.956.0.tgz", - "integrity": "sha512-H0r6ol3Rr63/3xvrUsLqHps+cA7VkM7uCU5NtuTHnMbv3uYYTKf9M2XFHAdVewmmRgssTzvqemrARc8Ji3SNvg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.957.0.tgz", + "integrity": "sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" @@ -1023,9 +1038,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.956.0.tgz", - "integrity": "sha512-x/IvXUeQYNUEQojpRIQpFt4X7XGxqzjUlXFRdwaTCtTz3q1droXVJvYOhnX3KiMgzeHGlBJfY4Nmq3oZNEUGFw==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.957.0.tgz", + "integrity": "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1897,9 +1912,9 @@ } }, "node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -2353,9 +2368,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5631,14 +5646,14 @@ } }, "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", - "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0" + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5649,9 +5664,9 @@ } }, "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -5663,21 +5678,21 @@ } }, "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", - "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.0", - "@typescript-eslint/tsconfig-utils": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5691,16 +5706,16 @@ } }, "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", - "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0" + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5715,13 +5730,13 @@ } }, "node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", - "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -6382,14 +6397,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", - "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", + "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.50.0", - "@typescript-eslint/types": "^8.50.0", + "@typescript-eslint/tsconfig-utils": "^8.51.0", + "@typescript-eslint/types": "^8.51.0", "debug": "^4.3.4" }, "engines": { @@ -6404,9 +6419,9 @@ } }, "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -6436,9 +6451,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", - "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", + "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", "dev": true, "license": "MIT", "engines": { @@ -8085,9 +8100,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", "dev": true, "funding": [ { @@ -9960,9 +9975,9 @@ } }, "node_modules/eslint-config-oclif": { - "version": "6.0.127", - "resolved": "https://registry.npmjs.org/eslint-config-oclif/-/eslint-config-oclif-6.0.127.tgz", - "integrity": "sha512-ObIh5+hteGpRVXQCWl8AqjTVNrdKdFmBP3fSlcn06ScxLdsIZWAXznLvA5U3/F8WlCSSjwMHyfugu1JL2NViPw==", + "version": "6.0.128", + "resolved": "https://registry.npmjs.org/eslint-config-oclif/-/eslint-config-oclif-6.0.128.tgz", + "integrity": "sha512-ktbct5MAQMSEe2Jxqz0f4CdsDAVcLU1K5B9I1Vm4FfMiJhVxDtiqrm/6cByIFq9n1xoLgkoIiV25Rlbj+7u13w==", "dev": true, "license": "MIT", "dependencies": { @@ -9982,7 +9997,7 @@ "eslint-plugin-n": "^17.22.0", "eslint-plugin-perfectionist": "^4", "eslint-plugin-unicorn": "^56.0.1", - "typescript-eslint": "^8.50.0" + "typescript-eslint": "^8.50.1" }, "engines": { "node": ">=18.18.0" @@ -10413,20 +10428,20 @@ } }, "node_modules/eslint-config-oclif/node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", - "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", + "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/type-utils": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/type-utils": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10436,7 +10451,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.50.0", + "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -10452,16 +10467,16 @@ } }, "node_modules/eslint-config-oclif/node_modules/@typescript-eslint/parser": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", - "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", + "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "engines": { @@ -10477,14 +10492,14 @@ } }, "node_modules/eslint-config-oclif/node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", - "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0" + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10495,17 +10510,17 @@ } }, "node_modules/eslint-config-oclif/node_modules/@typescript-eslint/type-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", - "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", + "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10520,9 +10535,9 @@ } }, "node_modules/eslint-config-oclif/node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -10534,21 +10549,21 @@ } }, "node_modules/eslint-config-oclif/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", - "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.0", - "@typescript-eslint/tsconfig-utils": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10578,16 +10593,16 @@ } }, "node_modules/eslint-config-oclif/node_modules/@typescript-eslint/utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", - "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0" + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10602,13 +10617,13 @@ } }, "node_modules/eslint-config-oclif/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", - "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -11382,14 +11397,14 @@ } }, "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", - "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0" + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -11400,9 +11415,9 @@ } }, "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -11414,21 +11429,21 @@ } }, "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", - "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.0", - "@typescript-eslint/tsconfig-utils": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -11442,16 +11457,16 @@ } }, "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", - "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0" + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -11466,13 +11481,13 @@ } }, "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", - "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -11706,9 +11721,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12054,9 +12069,9 @@ } }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -17633,9 +17648,9 @@ } }, "node_modules/normalize-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", - "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", "dev": true, "license": "MIT", "engines": { @@ -20561,14 +20576,14 @@ } }, "node_modules/oclif": { - "version": "4.22.59", - "resolved": "https://registry.npmjs.org/oclif/-/oclif-4.22.59.tgz", - "integrity": "sha512-Yq9R5mSSdDJAcCewEi+gipPNEvx0+0CyVmPAmamYRydc87I40o5YezVMWTjJ/0VzkdZZdmXl+EBlsXuNDqyhxg==", + "version": "4.22.61", + "resolved": "https://registry.npmjs.org/oclif/-/oclif-4.22.61.tgz", + "integrity": "sha512-fxjZTwg+4NLk6rshf7aI2J8W8Vl/MUePxzOHv3GEhmHlZgXfDD7dswWJ/Dqm/dRk+9oNu7PWH/h48r//RzALuQ==", "dev": true, "license": "MIT", "dependencies": { - "@aws-sdk/client-cloudfront": "^3.956.0", - "@aws-sdk/client-s3": "^3.956.0", + "@aws-sdk/client-cloudfront": "^3.958.0", + "@aws-sdk/client-s3": "^3.958.0", "@inquirer/confirm": "^3.1.22", "@inquirer/input": "^2.2.4", "@inquirer/select": "^2.5.0", @@ -21591,9 +21606,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -24483,9 +24498,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -24962,16 +24977,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", - "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz", + "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.50.0", - "@typescript-eslint/parser": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0" + "@typescript-eslint/eslint-plugin": "8.51.0", + "@typescript-eslint/parser": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -24986,20 +25001,20 @@ } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", - "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", + "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/type-utils": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/type-utils": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -25009,22 +25024,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.50.0", + "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", - "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", + "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "engines": { @@ -25040,14 +25055,14 @@ } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", - "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0" + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -25058,17 +25073,17 @@ } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", - "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", + "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -25083,9 +25098,9 @@ } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -25097,21 +25112,21 @@ } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", - "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.0", - "@typescript-eslint/tsconfig-utils": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -25125,16 +25140,16 @@ } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", - "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0" + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -25149,13 +25164,13 @@ } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", - "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { diff --git a/packages/contentstack-audit/src/audit-base-command.ts b/packages/contentstack-audit/src/audit-base-command.ts index 1eb63cfd49..2a9a989997 100644 --- a/packages/contentstack-audit/src/audit-base-command.ts +++ b/packages/contentstack-audit/src/audit-base-command.ts @@ -301,11 +301,11 @@ export abstract class AuditBaseCommand extends BaseCommand { log.debug('LoginCommand run method started', this.contextDetails); try { - log.debug('Initializing management API client', this.contextDetails); + log.debug('Initializing the Management API client.', this.contextDetails); const managementAPIClient = await managementSDKClient({ host: this.cmaHost, skipTokenValidity: true }); - log.debug('Management API client initialized successfully', this.contextDetails); + log.debug('Management API client initialized successfully.', this.contextDetails); const { flags: loginFlags } = await this.parse(LoginCommand); - log.debug('Token add flags parsed', { ...this.contextDetails, flags: loginFlags }); + log.debug('Token add flags parsed.', { ...this.contextDetails, flags: loginFlags }); authHandler.client = managementAPIClient; log.debug('Auth handler client set', this.contextDetails); @@ -86,7 +86,7 @@ export default class LoginCommand extends BaseCommand { await this.login(username, password); } } catch (error) { - log.debug('Login command failed', { + log.debug('Login failed.', { ...this.contextDetails, error, }); @@ -116,7 +116,7 @@ export default class LoginCommand extends BaseCommand { } const user: User = await authHandler.login(username, password, tfaToken); - log.debug('Auth handler login completed', { + log.debug('Auth handler login completed.', { ...this.contextDetails, hasUser: !!user, hasAuthToken: !!user?.authtoken, @@ -124,18 +124,18 @@ export default class LoginCommand extends BaseCommand { }); if (typeof user !== 'object' || !user.authtoken || !user.email) { - log.debug('Login failed - invalid user response', { ...this.contextDetails, user }); - throw new CLIError('Failed to login - invalid response'); + log.debug('Login failed: Invalid user response', { ...this.contextDetails, user }); + throw new CLIError('Login failed: Invalid response.'); } - log.debug('Setting config data for basic auth', this.contextDetails); + log.debug('Setting configuration data for basic authentication.', this.contextDetails); await oauthHandler.setConfigData('basicAuth', user); - log.debug('Config data set successfully', this.contextDetails); + log.debug('Configuration data set successfully.', this.contextDetails); log.success(messageHandler.parse('CLI_AUTH_LOGIN_SUCCESS'), this.contextDetails); - log.debug('Login process completed successfully', this.contextDetails); + log.debug('Login completed successfully.', this.contextDetails); } catch (error) { - log.debug('Login process failed', { ...this.contextDetails, error }); + log.debug('Login failed.', { ...this.contextDetails, error }); throw error; } } diff --git a/packages/contentstack-auth/src/commands/auth/logout.ts b/packages/contentstack-auth/src/commands/auth/logout.ts index 3140750a63..6f0a016ef2 100644 --- a/packages/contentstack-auth/src/commands/auth/logout.ts +++ b/packages/contentstack-auth/src/commands/auth/logout.ts @@ -61,7 +61,7 @@ export default class LogoutCommand extends BaseCommand { } try { - log.debug('Initializing management API client for logout', this.contextDetails); + log.debug('Initializing the Management API client for logout.', this.contextDetails); const managementAPIClient = await managementSDKClient({ host: this.cmaHost, skipTokenValidity: true }); log.debug('Management API client initialized successfully', this.contextDetails); @@ -75,9 +75,9 @@ export default class LogoutCommand extends BaseCommand { if (await oauthHandler.isAuthorisationTypeBasic()) { log.debug('Using basic authentication for logout', this.contextDetails); const authToken = configHandler.get('authtoken'); - log.debug('Retrieved auth token for logout', { ...this.contextDetails, hasAuthToken: !!authToken }); + log.debug('Authentication token retrieved for logout.', { ...this.contextDetails, hasAuthToken: !!authToken }); await authHandler.logout(authToken); - log.debug('Basic auth logout completed', this.contextDetails); + log.debug('Basic authentication logout completed.', this.contextDetails); } else if (await oauthHandler.isAuthorisationTypeOAuth()) { log.debug('Using OAuth authentication for logout', this.contextDetails); await oauthHandler.oauthLogout(); @@ -86,7 +86,7 @@ export default class LogoutCommand extends BaseCommand { cliux.loader(''); log.success(messageHandler.parse('CLI_AUTH_LOGOUT_SUCCESS'), this.contextDetails); - log.debug('Logout process completed successfully', this.contextDetails); + log.debug('Logout completed successfully.', this.contextDetails); } else { log.debug('User not confirmed or not authenticated, skipping logout', { ...this.contextDetails, @@ -96,14 +96,14 @@ export default class LogoutCommand extends BaseCommand { log.success(messageHandler.parse('CLI_AUTH_LOGOUT_ALREADY'), this.contextDetails); } } catch (error) { - log.debug('Logout command failed', { ...this.contextDetails, error: error.message }); + log.debug('Logout failed.', { ...this.contextDetails, error: error.message }); cliux.print('CLI_AUTH_LOGOUT_FAILED', { color: 'yellow' }); handleAndLogError(error, { ...this.contextDetails }); } finally { if (confirm === true) { - log.debug('Setting config data for logout', this.contextDetails); + log.debug('Setting configuration data for logout.', this.contextDetails); await oauthHandler.setConfigData('logout'); - log.debug('Config data set for logout', this.contextDetails); + log.debug('Configuration data set for logout.', this.contextDetails); } } } diff --git a/packages/contentstack-auth/src/commands/auth/tokens/add.ts b/packages/contentstack-auth/src/commands/auth/tokens/add.ts index 8ca4a46cbb..b95cb11b70 100644 --- a/packages/contentstack-auth/src/commands/auth/tokens/add.ts +++ b/packages/contentstack-auth/src/commands/auth/tokens/add.ts @@ -82,11 +82,11 @@ export default class TokensAddCommand extends BaseCommand] [--delivery] [--management] [-e ] [-k ] [-y] [--token ]'; async run(): Promise { - log.debug('TokensAddCommand run method started', this.contextDetails); + log.debug('TokensAddCommand run method started.', this.contextDetails); this.contextDetails.module = 'tokens-add'; const { flags: addTokenFlags } = await this.parse(TokensAddCommand); - log.debug('Token add flags parsed', { ...this.contextDetails, flags: addTokenFlags }); + log.debug('Token add flags parsed.', { ...this.contextDetails, flags: addTokenFlags }); let isAliasExist = false; const skipAliasReplaceConfirmation = addTokenFlags.force || addTokenFlags.yes; @@ -141,7 +141,7 @@ export default class TokensAddCommand extends BaseCommand { - log.debug('TokensListCommand run method started', this.contextDetails); + log.debug('TokensListCommand run method started.', this.contextDetails); this.contextDetails.module = 'tokens-list'; try { - log.debug('Retrieving tokens from configuration', this.contextDetails); + log.debug('Retrieving tokens from configuration.', this.contextDetails); const managementTokens = configHandler.get('tokens'); - log.debug('Tokens retrieved from configuration', {...this.contextDetails, tokenCount: managementTokens ? Object.keys(managementTokens).length : 0 }); + log.debug('Tokens retrieved from configuration.', {...this.contextDetails, tokenCount: managementTokens ? Object.keys(managementTokens).length : 0 }); const tokens: Record[] = []; if (managementTokens && Object.keys(managementTokens).length > 0) { - log.debug('Processing tokens for display', this.contextDetails); + log.debug('Processing tokens for display.', this.contextDetails); Object.keys(managementTokens).forEach(function (item) { tokens.push({ alias: item, @@ -46,7 +46,7 @@ export default class TokensListCommand extends BaseCommand = []; if (token || ignore) { - log.debug('Token found or ignore flag set, proceeding with removal', {...this.contextDetails, hasToken: !!token, ignore }); + log.debug('Token found, or ignore flag set.', {...this.contextDetails, hasToken: !!token, ignore }); configHandler.delete(`tokens.${alias}`); - log.debug('Token removed from configuration', {...this.contextDetails, alias }); + log.debug('Token removed from configuration.', {...this.contextDetails, alias }); return cliux.success(`CLI_AUTH_TOKENS_REMOVE_SUCCESS`); } if (tokens && Object.keys(tokens).length > 0) { - log.debug('Building token options for user selection', this.contextDetails); + log.debug('Building token options for user selection.', this.contextDetails); Object.keys(tokens).forEach(function (item) { const tokenOption = `${item}: ${tokens[item].token} : ${tokens[item].apiKey}${ tokens[item].environment ? ' : ' + tokens[item].environment + ' ' : '' @@ -48,11 +48,11 @@ export default class TokensRemoveCommand extends BaseCommand = await cliux.inquire({ name: 'selectedTokens', message: 'CLI_AUTH_TOKENS_REMOVE_SELECT_TOKEN', @@ -62,7 +62,7 @@ export default class TokensRemoveCommand extends BaseCommand { const selectedToken = element.split(':')[0]; log.debug(`Removing token: ${selectedToken}`, this.contextDetails); @@ -79,9 +79,9 @@ export default class TokensRemoveCommand extends BaseCommand { log.debug('WhoamiCommand run method started', this.contextDetails); try { - log.debug('Checking user email from context', { ...this.contextDetails, hasEmail: !!this.email }); + log.debug('Checking user email from context.', { ...this.contextDetails, hasEmail: !!this.email }); if (this.email) { log.debug('User email found, displaying user information', { ...this.contextDetails, email: this.email }); cliux.print('CLI_AUTH_WHOAMI_LOGGED_IN_AS', { color: 'white' }); cliux.print(this.email, { color: 'green' }); - log.debug('Whoami command completed successfully', this.contextDetails); + log.debug('whoami command completed successfully.', this.contextDetails); } else { - log.debug('No user email found in context', this.contextDetails); + log.debug('No user email found in context.', this.contextDetails); log.error(messageHandler.parse('CLI_AUTH_WHOAMI_FAILED'), this.contextDetails); } } catch (error) { - log.debug('Whoami command failed', { ...this.contextDetails, error }); + log.debug('whoami command failed.', { ...this.contextDetails, error }); cliux.print('CLI_AUTH_WHOAMI_FAILED', { color: 'yellow' }); handleAndLogError(error, { ...this.contextDetails }); } diff --git a/packages/contentstack-auth/src/utils/auth-handler.ts b/packages/contentstack-auth/src/utils/auth-handler.ts index 7ecdc26cd7..7d263ebaac 100644 --- a/packages/contentstack-auth/src/utils/auth-handler.ts +++ b/packages/contentstack-auth/src/utils/auth-handler.ts @@ -10,11 +10,11 @@ class AuthHandler { private _client; private _host; set client(contentStackClient) { - log.debug('Setting ContentStack client', { module: 'auth-handler' }); + log.debug('Setting Contentstack client.', { module: 'auth-handler' }); this._client = contentStackClient; } set host(contentStackHost) { - log.debug(`Setting ContentStack host: ${contentStackHost}`, { module: 'auth-handler' }); + log.debug(`Setting Contentstack host: ${contentStackHost}`, { module: 'auth-handler' }); this._host = contentStackHost; } @@ -68,13 +68,13 @@ class AuthHandler { * @throws CLIError if SMS request fails */ private async requestSMSOTP(loginPayload: any): Promise { - log.debug('Sending SMS OTP request', { module: 'auth-handler' }); + log.debug('Sending SMS OTP request.', { module: 'auth-handler' }); try { await this._client.axiosInstance.post('/user/request_token_sms', { user: loginPayload }); - log.debug('SMS OTP request successful', { module: 'auth-handler' }); + log.debug('SMS OTP request successful.', { module: 'auth-handler' }); cliux.print('CLI_AUTH_LOGIN_SECURITY_CODE_SEND_SUCCESS'); } catch (error) { - log.debug('SMS OTP request failed', { module: 'auth-handler', error }); + log.debug('SMS OTP request failed.', { module: 'auth-handler', error }); throw error; } } @@ -98,7 +98,7 @@ class AuthHandler { } = { email, password }; if (tfaToken) { loginPayload.tfa_token = tfaToken; - log.debug('Adding TFA token to login payload', { module: 'auth-handler' }); + log.debug('Adding TFA token to login payload.', { module: 'auth-handler' }); } log.debug('Making login API call', { @@ -124,17 +124,17 @@ class AuthHandler { try { resolve(await this.login(email, password, tfToken)); } catch (error) { - log.debug('Login with TFA token failed', { module: 'auth-handler', error }); + log.debug('Login with TFA token failed.', { module: 'auth-handler', error }); cliux.print('CLI_AUTH_2FA_FAILED', { color: 'red' }); reject(error); } } else { - log.debug('Login failed - no user found', { module: 'auth-handler', result }); + log.debug('Login failed: no user found.', { module: 'auth-handler', result }); reject(new Error(messageHandler.parse('CLI_AUTH_LOGIN_NO_USER'))); } }) .catch((error: any) => { - log.debug('Login API call failed', { module: 'auth-handler', error: error?.errorMessage || error }); + log.debug('Login API call failed.', { module: 'auth-handler', error: error?.errorMessage || error }); cliux.print('CLI_AUTH_LOGIN_FAILED', { color: 'yellow' }); handleAndLogError(error, { module: 'auth-handler' }); }); @@ -158,25 +158,25 @@ class AuthHandler { * @returns {Promise} Promise object returns response object from Contentstack */ async logout(authtoken: string): Promise { - log.debug('Starting logout process', { module: 'auth-handler', hasAuthToken: !!authtoken }); + log.debug('Starting logout process.', { module: 'auth-handler', hasAuthToken: !!authtoken }); return new Promise((resolve, reject) => { if (authtoken) { - log.debug('Making logout API call', { module: 'auth-handler' }); + log.debug('Making logout API call.', { module: 'auth-handler' }); this._client .logout(authtoken) .then(function (response: object) { - log.debug('Logout API call successful', { module: 'auth-handler', response }); + log.debug('Logout API call successful.', { module: 'auth-handler', response }); return resolve(response); }) .catch((error: Error) => { - log.debug('Logout API call failed', { module: 'auth-handler', error: error.message }); + log.debug('Logout API call failed.', { module: 'auth-handler', error: error.message }); cliux.print('CLI_AUTH_LOGOUT_FAILED', { color: 'yellow' }); reject(error); }); } else { - log.debug('Logout failed - no auth token provided', { module: 'auth-handler' }); + log.debug('Logout failed: no auth token provided.', { module: 'auth-handler' }); reject(new Error(messageHandler.parse('CLI_AUTH_LOGOUT_NO_TOKEN'))); } }); @@ -188,25 +188,25 @@ class AuthHandler { * @returns {Promise} Promise object returns response object from Contentstack */ async validateAuthtoken(authtoken: string): Promise { - log.debug('Starting token validation', { module: 'auth-handler', hasAuthToken: !!authtoken }); + log.debug('Starting token validation.', { module: 'auth-handler', hasAuthToken: !!authtoken }); return new Promise((resolve, reject) => { if (authtoken) { - log.debug('Making token validation API call', { module: 'auth-handler' }); + log.debug('Making token validation API call.', { module: 'auth-handler' }); this._client .getUser() .then((user: object) => { - log.debug('Token validation successful', { module: 'auth-handler', user }); + log.debug('Token validation successful.', { module: 'auth-handler', user }); resolve(user); }) .catch((error: Error) => { - log.debug('Token validation failed', { module: 'auth-handler', error: error.message }); + log.debug('Token validation failed.', { module: 'auth-handler', error: error.message }); cliux.print('CLI_AUTH_TOKEN_VALIDATION_FAILED', { color: 'yellow' }); handleAndLogError(error, { module: 'auth-handler' }); }); } else { - log.debug('Token validation failed - no auth token provided', { module: 'auth-handler' }); + log.debug('Token validation failed: no auth token provided.', { module: 'auth-handler' }); reject(new Error(messageHandler.parse('CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN'))); } }); diff --git a/packages/contentstack-auth/src/utils/tokens-validation.ts b/packages/contentstack-auth/src/utils/tokens-validation.ts index 94ae7a7855..ab230dbf07 100644 --- a/packages/contentstack-auth/src/utils/tokens-validation.ts +++ b/packages/contentstack-auth/src/utils/tokens-validation.ts @@ -11,28 +11,28 @@ export const validateEnvironment = async ( apiKey: string, environment: string, ): Promise => { - log.debug('Starting environment validation', { module: 'tokens-validation', apiKeyStatus: apiKey ? 'provided' : 'not-provided', environment }); + log.debug('Starting environment validation.', { module: 'tokens-validation', apiKeyStatus: apiKey ? 'provided' : 'not-provided', environment }); let result: { valid: boolean; message: string }; try { - log.debug('Making environment validation API call', { module: 'tokens-validation', environment }); + log.debug('Making environment validation API call.', { module: 'tokens-validation', environment }); const validationResult = await contentStackClient.Stack({ api_key: apiKey }).environment(environment).fetch(); - log.debug('Environment validation API response received', { module: 'tokens-validation', validationResult }); + log.debug('Environment validation API response received.', { module: 'tokens-validation', validationResult }); if (validationResult.name === environment) { - log.debug('Environment validation successful', { module: 'tokens-validation', environment, validationResult }); + log.debug('Environment validation successful.', { module: 'tokens-validation', environment, validationResult }); result = { valid: true, message: validationResult }; } else { - log.debug('Environment validation failed - name mismatch', { module: 'tokens-validation', expected: environment, actual: validationResult.name }); + log.debug('Environment validation failed: name mismatch.', { module: 'tokens-validation', expected: environment, actual: validationResult.name }); result = { valid: false, message: messageHandler.parse('CLI_AUTH_TOKENS_VALIDATION_INVALID_ENVIRONMENT_NAME') }; } } catch (error) { - log.debug('Environment validation API call failed', { module: 'tokens-validation', error: error.message, environment }); + log.debug('Environment validation API call failed.', { module: 'tokens-validation', error: error.message, environment }); handleAndLogError(error, { apiKey, environment }, ); result = { valid: false, message: 'CLI_AUTH_TOKENS_VALIDATION_INVALID_ENVIRONMENT_NAME' }; } - log.debug('Environment validation completed', { module: 'tokens-validation', result }); + log.debug('Environment validation completed.', { module: 'tokens-validation', result }); return result; }; @@ -43,27 +43,27 @@ export const validateEnvironment = async ( * @returns */ export const validateAPIKey = async (contentStackClient: any, apiKey: string): Promise => { - log.debug('Starting API key validation', { module: 'tokens-validation', apiKeyStatus: apiKey ? 'provided' : 'not-provided' }); + log.debug('Starting API key validation.', { module: 'tokens-validation', apiKeyStatus: apiKey ? 'provided' : 'not-provided' }); let result: { valid: boolean; message: string }; try { - log.debug('Making API key validation API call', { module: 'tokens-validation' }); + log.debug('Making API key validation API call.', { module: 'tokens-validation' }); const validateAPIKeyResult = await contentStackClient.stack({ api_key: apiKey }).fetch(); - log.debug('API key validation API response received', { module: 'tokens-validation', validateAPIKeyResult }); + log.debug('API key validation API response received.', { module: 'tokens-validation', validateAPIKeyResult }); if (validateAPIKeyResult.api_key === apiKey) { - log.debug('API key validation successful', { module: 'tokens-validation', apiKey: validateAPIKeyResult.api_key }); + log.debug('API key validation successful.', { module: 'tokens-validation', apiKey: validateAPIKeyResult.api_key }); result = { valid: true, message: validateAPIKeyResult }; } else { - log.debug('API key validation failed - key mismatch', { module: 'tokens-validation', expected: apiKey, actual: validateAPIKeyResult.api_key }); + log.debug('API key validation failed: key mismatch.', { module: 'tokens-validation', expected: apiKey, actual: validateAPIKeyResult.api_key }); result = { valid: false, message: messageHandler.parse('CLI_AUTH_TOKENS_VALIDATION_INVALID_API_KEY') }; } } catch (error) { - log.debug('API key validation API call failed', { module: 'tokens-validation', error: error.message }); + log.debug('API key validation API call failed.', { module: 'tokens-validation', error: error.message }); handleAndLogError(error, { apiKey }, ); result = { valid: false, message: messageHandler.parse('CLI_AUTH_TOKENS_VALIDATION_INVALID_API_KEY') }; } - log.debug('API key validation completed', { module: 'tokens-validation', result }); + log.debug('API key validation completed.', { module: 'tokens-validation', result }); return result; }; diff --git a/packages/contentstack-bootstrap/src/bootstrap/utils.ts b/packages/contentstack-bootstrap/src/bootstrap/utils.ts index 91575eb1d6..1266a3fd13 100644 --- a/packages/contentstack-bootstrap/src/bootstrap/utils.ts +++ b/packages/contentstack-bootstrap/src/bootstrap/utils.ts @@ -138,7 +138,7 @@ export const setupEnvironments = async ( cliux.print(messageHandler.parse('CLI_BOOTSTRAP_APP_FAILED_TO_CREATE_ENV_FILE_FOR_ENV', environment.name)); } } else { - cliux.print('No environments name found for the environment'); + cliux.print('No environment name found for the selected environment.'); } } } else { diff --git a/packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts b/packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts index 2a353d1d5d..8cf129cf30 100644 --- a/packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts +++ b/packages/contentstack-bootstrap/src/commands/cm/bootstrap.ts @@ -147,7 +147,7 @@ export default class BootstrapCommand extends Command { } else if (appType === 'starterapp') { selectedApp = await inquireApp(config.starterApps); } else { - this.error('Invalid app type provided ' + appType, { exit: 1 }); + this.error('Invalid app type provided: ' + appType, { exit: 1 }); } } diff --git a/packages/contentstack-branches/src/branch/merge-handler.ts b/packages/contentstack-branches/src/branch/merge-handler.ts index 67d0f0369d..0d27552152 100644 --- a/packages/contentstack-branches/src/branch/merge-handler.ts +++ b/packages/contentstack-branches/src/branch/merge-handler.ts @@ -280,7 +280,7 @@ export default class MergeHandler { mergeContent[module].deleted = moduleBranchCompareData.deleted; break; default: - cliux.error(`error: Invalid strategy ${strategy}`); + cliux.error(`Error: Invalid strategy '${strategy}'`); process.exit(1); } } diff --git a/packages/contentstack-branches/src/commands/cm/branches/delete.ts b/packages/contentstack-branches/src/commands/cm/branches/delete.ts index b6787b13cf..19eab092a7 100644 --- a/packages/contentstack-branches/src/commands/cm/branches/delete.ts +++ b/packages/contentstack-branches/src/commands/cm/branches/delete.ts @@ -47,7 +47,7 @@ export default class BranchDeleteCommand extends Command { if (!branchDeleteFlags.yes) { const confirmBranch = await interactive.askBranchNameConfirmation(); if (confirmBranch !== branchDeleteFlags.uid) { - cliux.error(`error: To delete the branch, enter a valid branch name '${branchDeleteFlags.uid}'`); + cliux.error(`Error: To delete the branch, enter a valid branch name '${branchDeleteFlags.uid}'`); process.exit(1); } } diff --git a/packages/contentstack-branches/src/utils/asset-folder-create-script.ts b/packages/contentstack-branches/src/utils/asset-folder-create-script.ts index a358c9e286..0a41c0a092 100644 --- a/packages/contentstack-branches/src/utils/asset-folder-create-script.ts +++ b/packages/contentstack-branches/src/utils/asset-folder-create-script.ts @@ -143,13 +143,13 @@ export function assetFolderCreateScript(contentType) { migration.addTask(createAssetTask()); } else { if (apiKey.length === 0) { - console.error('Please provide api key using --stack-api-key flag'); + console.error('Provide the API key using the --stack-api-key flag.'); } if (!compareBranch) { - console.error('Please provide compare branch through --config compare-branch: flag'); + console.error('Specify the compare branch using the --config compare-branch: flag.'); } if (branch.length === 0) { - console.error('Please provide branch name through --branch flag'); + console.error('Specify the branch name using the --branch flag.'); } } } diff --git a/packages/contentstack-branches/src/utils/entry-create-script.ts b/packages/contentstack-branches/src/utils/entry-create-script.ts index bfda436afc..dabd71ff9b 100644 --- a/packages/contentstack-branches/src/utils/entry-create-script.ts +++ b/packages/contentstack-branches/src/utils/entry-create-script.ts @@ -599,13 +599,13 @@ export function entryCreateScript(contentType) { migration.addTask(createEntryTask()); } else { if (apiKey.length === 0) { - console.error('Please provide api key using --stack-api-key flag'); + console.error('Provide the API key using the --stack-api-key flag.'); } if (!compareBranch) { - console.error('Please provide compare branch through --config compare-branch: flag'); + console.error('Specify the compare branch using the --config compare-branch: flag.'); } if (branch.length === 0) { - console.error('Please provide branch name through --branch flag'); + console.error('Specify the branch name using the --branch flag.'); } } }; diff --git a/packages/contentstack-branches/src/utils/entry-create-update-script.ts b/packages/contentstack-branches/src/utils/entry-create-update-script.ts index a47fcdb9b1..ac4ea205c1 100644 --- a/packages/contentstack-branches/src/utils/entry-create-update-script.ts +++ b/packages/contentstack-branches/src/utils/entry-create-update-script.ts @@ -669,13 +669,13 @@ export function entryCreateUpdateScript(contentType) { migration.addTask(updateEntryTask()); } else { if (apiKey.length === 0) { - console.error('Please provide api key using --stack-api-key flag'); + console.error('Provide the API key using the --stack-api-key flag.'); } if (!compareBranch) { - console.error('Please provide compare branch through --config compare-branch: flag'); + console.error('Specify the compare branch using the --config compare-branch: flag.'); } if (branch.length === 0) { - console.error('Please provide branch name through --branch flag'); + console.error('Specify the branch name using the --branch flag.'); } } };`; diff --git a/packages/contentstack-branches/src/utils/entry-update-script.ts b/packages/contentstack-branches/src/utils/entry-update-script.ts index 88ea7eb77c..8c1fa73f32 100644 --- a/packages/contentstack-branches/src/utils/entry-update-script.ts +++ b/packages/contentstack-branches/src/utils/entry-update-script.ts @@ -666,13 +666,13 @@ export function entryUpdateScript(contentType) { migration.addTask(updateEntryTask()); } else { if (apiKey.length === 0) { - console.error('Please provide api key using --stack-api-key flag'); + console.error('Provide the API key using the --stack-api-key flag.'); } if (!compareBranch) { - console.error('Please provide compare branch through --config compare-branch: flag'); + console.error('Specify the compare branch using the --config compare-branch: flag.'); } if (branch.length === 0) { - console.error('Please provide branch name through --branch flag'); + console.error('Specify the branch name using the --branch flag.'); } } };`; diff --git a/packages/contentstack-branches/src/utils/interactive.ts b/packages/contentstack-branches/src/utils/interactive.ts index 50b6bef4f8..dc8730fd6f 100644 --- a/packages/contentstack-branches/src/utils/interactive.ts +++ b/packages/contentstack-branches/src/utils/interactive.ts @@ -97,7 +97,7 @@ export async function selectMergeStrategy(): Promise { }) .then((name) => name as string) .catch((err) => { - cliux.error('Failed to collect the merge strategy'); + cliux.error('Failed to retrieve the merge strategy.'); process.exit(1); }); @@ -120,7 +120,7 @@ export async function selectMergeStrategySubOptions(): Promise { }) .then((name) => name as string) .catch((err) => { - cliux.error('Failed to collect the merge strategy'); + cliux.error('Failed to retrieve the merge strategy.'); process.exit(1); }); @@ -166,7 +166,7 @@ export async function selectContentMergePreference(): Promise { }) .then((name) => name as string) .catch((err) => { - cliux.error('Failed to collect the preference'); + cliux.error('Failed to retrieve the preference.'); process.exit(1); }); diff --git a/packages/contentstack-bulk-publish/src/commands/cm/assets/publish.js b/packages/contentstack-bulk-publish/src/commands/cm/assets/publish.js index f5baa17886..a032678c03 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/assets/publish.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/assets/publish.js @@ -50,7 +50,7 @@ class AssetsPublishCommand extends Command { } else if (updatedFlags['stack-api-key']) { config.stackApiKey = updatedFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } updatedFlags.bulkPublish = updatedFlags.bulkPublish === 'false' ? false : true; if (updatedFlags.folderUid === undefined) { @@ -104,10 +104,10 @@ class AssetsPublishCommand extends Command { this.error(message, { exit: 2 }); } } else { - this.error('Confirmation failed'); + this.error('Confirmation failed.'); } } else { - this.error('Validation failed'); + this.error('Validation failed.'); } } @@ -118,7 +118,7 @@ class AssetsPublishCommand extends Command { } if (sourceEnv && !deliveryToken) { - this.error('Specify source environment delivery token. Please check --help for more details', { exit: 2 }); + this.error('Specify the source environment delivery token. Run --help for more details.', { exit: 2 }); } if (!environments || environments.length === 0) { diff --git a/packages/contentstack-bulk-publish/src/commands/cm/assets/unpublish.js b/packages/contentstack-bulk-publish/src/commands/cm/assets/unpublish.js index d127464f34..75aa85d142 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/assets/unpublish.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/assets/unpublish.js @@ -53,7 +53,7 @@ class UnpublishCommand extends Command { } else if (updatedFlags['stack-api-key']) { config.stackApiKey = updatedFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } if (!updatedFlags.deliveryToken) { updatedFlags.deliveryToken = await cliux.prompt('Enter delivery token of your source environment'); @@ -62,7 +62,7 @@ class UnpublishCommand extends Command { stack = await getStack(config); } if (!updatedFlags.deliveryToken && updatedFlags.deliveryToken.length === 0) { - this.error('Delivery Token is required for executing this command', { exit: 2 }); + this.error('A delivery token is required to execute this command.', { exit: 2 }); } if (await this.confirmFlags(updatedFlags)) { diff --git a/packages/contentstack-bulk-publish/src/commands/cm/bulk-publish/cross-publish.js b/packages/contentstack-bulk-publish/src/commands/cm/bulk-publish/cross-publish.js index 81f6ddb338..5ca4af0315 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/bulk-publish/cross-publish.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/bulk-publish/cross-publish.js @@ -41,7 +41,7 @@ class CrossPublishCommand extends Command { } else if (updatedFlags['stack-api-key']) { config.stackApiKey = updatedFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } if (!updatedFlags.deliveryToken) { updatedFlags.deliveryToken = await cliux.prompt('Enter delivery token of your source environment'); @@ -52,7 +52,7 @@ class CrossPublishCommand extends Command { } if (!updatedFlags.deliveryToken && updatedFlags.deliveryToken.length === 0) { - this.error('Delivery Token is required for executing this command', { exit: 2 }); + this.error('A delivery token is required to execute this command.', { exit: 2 }); } if (await this.confirmFlags(updatedFlags)) { diff --git a/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-modified.js b/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-modified.js index 30c2decc63..7d93ffbd55 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-modified.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-modified.js @@ -51,7 +51,7 @@ class PublishModifiedCommand extends Command { } else if (updatedFlags['stack-api-key']) { config.stackApiKey = updatedFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } updatedFlags.bulkPublish = updatedFlags.bulkPublish !== 'false'; stack = await getStack(config); diff --git a/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-non-localized-fields.js b/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-non-localized-fields.js index 834abdeb4f..2551f75514 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-non-localized-fields.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-non-localized-fields.js @@ -60,7 +60,7 @@ class NonlocalizedFieldChangesCommand extends Command { } else if (updatedFlags['stack-api-key']) { config.stackApiKey = updatedFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } stack = await getStack(config); } diff --git a/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-only-unpublished.js b/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-only-unpublished.js index febfa9f96c..57577a68a2 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-only-unpublished.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/entries/publish-only-unpublished.js @@ -8,7 +8,7 @@ class PublishOnlyUnpublished extends Command { try { await publishOnlyUnpublishedService.apply(this, [PublishOnlyUnpublished]); } catch (error) { - this.error(error, { exit: 2 }); + this.error(error?.message || error, { exit: 2 }); } } } diff --git a/packages/contentstack-bulk-publish/src/commands/cm/entries/publish.js b/packages/contentstack-bulk-publish/src/commands/cm/entries/publish.js index 92ec008a82..fc32da49ce 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/entries/publish.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/entries/publish.js @@ -64,7 +64,7 @@ class PublishEntriesCommand extends Command { } else if (updatedFlags['stack-api-key']) { config.stackApiKey = updatedFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } updatedFlags.bulkPublish = updatedFlags.bulkPublish !== 'false'; stack = await getStack(config); @@ -131,7 +131,7 @@ class PublishEntriesCommand extends Command { } if (sourceEnv && !deliveryToken) { - this.error('Specify source environment delivery token. Please check --help for more details', { exit: 2 }); + this.error('Specify the source environment delivery token. Run --help for more details.', { exit: 2 }); } if (publishAllContentTypes && contentTypes && contentTypes.length > 0) { diff --git a/packages/contentstack-bulk-publish/src/commands/cm/entries/unpublish.js b/packages/contentstack-bulk-publish/src/commands/cm/entries/unpublish.js index 8aa29a7d87..2402a4c193 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/entries/unpublish.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/entries/unpublish.js @@ -56,7 +56,7 @@ class UnpublishCommand extends Command { } else if (updatedFlags['stack-api-key']) { config.stackApiKey = updatedFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } if (!updatedFlags.deliveryToken) { updatedFlags.deliveryToken = await cliux.prompt('Enter delivery token of your source environment'); @@ -65,7 +65,7 @@ class UnpublishCommand extends Command { stack = await getStack(config); } if (!updatedFlags.deliveryToken && updatedFlags.deliveryToken.length === 0) { - this.error('Delivery Token is required for executing this command', { exit: 2 }); + this.error('A delivery token is required to execute this command.', { exit: 2 }); } if (await this.confirmFlags(updatedFlags)) { diff --git a/packages/contentstack-bulk-publish/src/commands/cm/entries/update-and-publish.js b/packages/contentstack-bulk-publish/src/commands/cm/entries/update-and-publish.js index b60a99a021..ec36a1768b 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/entries/update-and-publish.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/entries/update-and-publish.js @@ -50,7 +50,7 @@ class UpdateAndPublishCommand extends Command { } else if (updatedFlags['stack-api-key']) { config.stackApiKey = updatedFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } updatedFlags.bulkPublish = updatedFlags.bulkPublish === 'false' ? false : true; diff --git a/packages/contentstack-bulk-publish/src/commands/cm/stacks/publish-clear-logs.js b/packages/contentstack-bulk-publish/src/commands/cm/stacks/publish-clear-logs.js index 018f308d22..7f36ef17ee 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/stacks/publish-clear-logs.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/stacks/publish-clear-logs.js @@ -37,7 +37,7 @@ class ClearCommand extends Command { } this.log('Log files have been cleared'); } else { - this.error(`The log directory doesn't exist.`); + this.error(`The log directory does not exist.`); } } catch (e) { return; @@ -50,7 +50,7 @@ class ClearCommand extends Command { this.log('Total number of log files - ', files.length); }); } else { - this.error(`The log directory doesn't exist.`); + this.error(`The log directory does not exist.`); } } } diff --git a/packages/contentstack-bulk-publish/src/commands/cm/stacks/publish-configure.js b/packages/contentstack-bulk-publish/src/commands/cm/stacks/publish-configure.js index 1a59d10674..e403e3d7e1 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/stacks/publish-configure.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/stacks/publish-configure.js @@ -13,12 +13,12 @@ class ConfigureCommand extends Command { try { this.getToken(configureFlags.alias); } catch (error) { - this.error(`The configured management token alias ${configureFlags.alias} has not been added yet. Add it using 'csdx auth:tokens:add -a ${configureFlags.alias}'`, { exit: 2 }) + this.error(`The configured management token alias '${configureFlags.alias}' has not been added yet. Add it using 'csdx auth:tokens:add -a ${configureFlags.alias}'.`, { exit: 2 }) } } else if (configureFlags['stack-api-key']) { configureFlags.stackApiKey = configureFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } this.setConfig(configureFlags); diff --git a/packages/contentstack-bulk-publish/src/commands/cm/stacks/unpublish.js b/packages/contentstack-bulk-publish/src/commands/cm/stacks/unpublish.js index b2c867996f..2b24cea396 100644 --- a/packages/contentstack-bulk-publish/src/commands/cm/stacks/unpublish.js +++ b/packages/contentstack-bulk-publish/src/commands/cm/stacks/unpublish.js @@ -57,7 +57,7 @@ class UnpublishCommand extends Command { } else if (updatedFlags['stack-api-key']) { config.stackApiKey = updatedFlags['stack-api-key']; } else { - this.error('Please use `--alias` or `--stack-api-key` to proceed.', { exit: 2 }); + this.error('Use the `--alias` or `--stack-api-key` flag to proceed.', { exit: 2 }); } if (!updatedFlags.deliveryToken) { updatedFlags.deliveryToken = await cliux.prompt('Enter delivery token of your source environment'); @@ -66,7 +66,7 @@ class UnpublishCommand extends Command { stack = await getStack(config); } if (!updatedFlags.deliveryToken && updatedFlags.deliveryToken.length === 0) { - this.error('Delivery Token is required for executing this command', { exit: 2 }); + this.error('A delivery token is required to execute this command.', { exit: 2 }); } if (await this.confirmFlags(updatedFlags)) { diff --git a/packages/contentstack-bulk-publish/src/consumer/publish.js b/packages/contentstack-bulk-publish/src/consumer/publish.js index e7b95649a5..2138c48bab 100644 --- a/packages/contentstack-bulk-publish/src/consumer/publish.js +++ b/packages/contentstack-bulk-publish/src/consumer/publish.js @@ -35,22 +35,25 @@ function removePublishDetails(elements) { function displayEntriesDetails(sanitizedData, action, mapping = []) { if (action === 'bulk_publish') { sanitizedData.forEach((entry) => { - entry?.publish_details.forEach((pd) => { - if (Object.keys(mapping).includes(pd.environment)) { + if (Array.isArray(entry?.publish_details) && entry.publish_details.length > 0) { + const matchingPublishDetails = entry.publish_details.filter((pd) => + Object.keys(mapping).includes(pd.environment) + ); + if (matchingPublishDetails.length > 0) { + const pd = matchingPublishDetails[0]; console.log( chalk.green( - `Entry UID '${entry.uid}' of CT '${entry.content_type}' in locale '${entry.locale}' version '${pd.version}' in environment '${pd.environment}'`, + `Entry UID: '${entry.uid}', Content Type: '${entry.content_type}', Locale: '${entry.locale}', Version: '${pd.version}', Environment: '${pd.environment}'`, ), - ) + ); } - }); - if(!Array.isArray(entry.publish_details)){ - console.log(chalk.green(`Entry UID '${entry.uid}' of CT '${entry.content_type}' in locale '${entry.locale}'`)); + } else if (!Array.isArray(entry.publish_details)) { + console.log(chalk.green(`Entry UID: '${entry.uid}', Content Type: '${entry.content_type}', Locale: '${entry.locale}'`)); } }); } else if (action === 'bulk_unpublish') { sanitizedData.forEach((entry) => { - console.log(chalk.green(`Entry UID '${entry.uid}' of CT '${entry.content_type}' in locale '${entry.locale}'`)); + console.log(chalk.green(`Entry UID: '${entry.uid}', Content Type: '${entry.content_type}', Locale: '${entry.locale}'`)); }); } } @@ -62,9 +65,9 @@ function displayAssetsDetails(sanitizedData, action, mapping) { if (Object.keys(mapping).includes(pd.environment)) { console.log( chalk.green( - `Asset UID '${asset.uid}' ${pd.version ? `and version '${pd.version}'` : ''} ${ - asset.locale ? `in locale '${asset.locale}'` : '' - } in environment ${pd.environment}`, + `Asset UID: '${asset.uid}'${pd.version ? `, Version: '${pd.version}'` : ''}${ + asset.locale ? `, Locale: '${asset.locale}'` : '' + }, Environment: ${pd.environment}`, ), ); } @@ -74,8 +77,8 @@ function displayAssetsDetails(sanitizedData, action, mapping) { sanitizedData.forEach((asset) => { console.log( chalk.green( - `Asset UID '${asset.uid}' ${asset.version ? `and version '${asset.version}'` : ''} ${ - asset.locale ? `in locale '${asset.locale}'` : '' + `Asset UID: '${asset.uid}'${asset.version ? `, Version: '${asset.version}'` : ''}${ + asset.locale ? `, Locale: '${asset.locale}'` : '' }`, ), ); @@ -98,7 +101,7 @@ async function publishEntry(data, _config, queue) { if (!publishEntryResponse.error_message) { console.log( chalk.green( - `entry published with ContentType uid=${entryObj.content_type} Entry uid=${entryObj.entryUid} locale=${entryObj.locale}`, + `Entry published. Content Type UID: ${entryObj.content_type}, Entry UID: ${entryObj.entryUid}, Locale: ${entryObj.locale}`, ), ); delete entryObj.stack; @@ -119,9 +122,9 @@ async function publishEntry(data, _config, queue) { delete entryObj.stack; console.log( chalk.red( - `entry could not be published with ContentType uid=${entryObj.content_type} entry uid=${ + `Entry could not be published. Content Type UID: ${entryObj.content_type}, Entry UID: ${ entryObj.entryUid - } locale=${entryObj.locale} error=${formatError(error)}`, + }, Locale: ${entryObj.locale}, Error: ${formatError(error)}`, ), ); addLogs( @@ -147,7 +150,7 @@ async function publishAsset(data, _config, queue) { .publish({ publishDetails: { environments: assetobj.environments, locales: [assetobj.locale || 'en-us'] } }) .then((publishAssetResponse) => { if (!publishAssetResponse.error_message) { - console.log(chalk.green(`asset published with Asset uid=${assetobj.assetUid}, locale=${assetobj.locale}`)); + console.log(chalk.green(`Asset published. Asset UID: ${assetobj.assetUid}, Locale: ${assetobj.locale}`)); delete assetobj.stack; addLogs( logger, @@ -164,7 +167,7 @@ async function publishAsset(data, _config, queue) { queue.Enqueue(data); } else { delete assetobj.stack; - console.log(chalk.red(`Could not publish because of Error=${formatError(error)}`)); + console.log(chalk.red(`Could not publish. Error: ${formatError(error)}`)); addLogs( logger, { @@ -193,7 +196,7 @@ async function UnpublishEntry(data, _config, queue) { delete entryObj.stack; console.log( chalk.green( - `Entry unpublished with ContentType uid=${entryObj.content_type} Entry uid=${entryObj.entryUid} locale=${entryObj.locale}`, + `Entry unpublished. Content Type UID: ${entryObj.content_type}, Entry UID: ${entryObj.entryUid}, Locale: ${entryObj.locale}`, ), ); addLogs( @@ -213,9 +216,9 @@ async function UnpublishEntry(data, _config, queue) { delete entryObj.stack; console.log( chalk.red( - `Entry could not be unpublished with ContentType uid=${entryObj.content_type} Entry uid=${ + `Entry could not be unpublished. Content Type UID: ${entryObj.content_type}, Entry UID: ${ entryObj.entryUid - } locale=${entryObj.locale} error=${formatError(error)}`, + }, Locale: ${entryObj.locale}, Error: ${formatError(error)}`, ), ); addLogs( @@ -237,7 +240,7 @@ async function UnpublishAsset(data, _config, queue) { .then((unpublishAssetResponse) => { if (!unpublishAssetResponse.error_message) { delete assetobj.stack; - console.log(`Asset unpublished with Asset uid=${assetobj.assetUid}`); + console.log(`The asset with UID '${assetobj.assetUid}' has been unpublished.`); addLogs( logger, { options: assetobj, api_key: stack.stackHeaders.api_key, alias: stack.alias, host: stack.host }, @@ -253,7 +256,7 @@ async function UnpublishAsset(data, _config, queue) { queue.Enqueue(data); } else { delete assetobj.stack; - console.log(chalk.red(`Could not Unpublish because of error=${formatError(error)}`)); + console.log(chalk.red(`Could not unpublish. Error: ${formatError(error)}`)); addLogs( logger, { options: assetobj, api_key: stack.stackHeaders.api_key, alias: stack.alias, host: stack.host }, @@ -330,7 +333,7 @@ async function performBulkPublish(data, _config, queue) { queue.Enqueue(data); } else { delete bulkPublishObj.stack; - console.log(chalk.red(`Bulk entries failed to publish with error ${formatError(error)}`)); + console.log(chalk.red(`Bulk entries failed to publish. Error: ${formatError(error)}`)); displayEntriesDetails(bulkPublishObj.entries, 'bulk_publish', mapping); addLogs( logger, @@ -385,7 +388,7 @@ async function performBulkPublish(data, _config, queue) { queue.Enqueue(data); } else { delete bulkPublishObj.stack; - console.log(chalk.red(`Bulk assets failed to publish with error ${formatError(error)}`)); + console.log(chalk.red(`Bulk assets failed to publish. Error: ${formatError(error)}`)); displayAssetsDetails(sanitizedData, 'bulk_publish', mapping); addLogs( @@ -397,7 +400,7 @@ async function performBulkPublish(data, _config, queue) { }); break; default: - console.log('No such type'); + console.log('No such type found. If it is for a content type, use "No such content type found."'); } } @@ -455,7 +458,7 @@ async function performBulkUnPublish(data, _config, queue) { queue.Enqueue(data); } else { delete bulkUnPublishObj.stack; - console.log(chalk.red(`Bulk entries failed to Unpublish with error ${formatError(error)}`)); + console.log(chalk.red(`Bulk entries failed to unpublish. Error: ${formatError(error)}`)); displayEntriesDetails(bulkUnPublishObj.entries, 'bulk_unpublish'); addLogs( logger, @@ -510,7 +513,7 @@ async function performBulkUnPublish(data, _config, queue) { queue.Enqueue(data); } else { delete bulkUnPublishObj.stack; - console.log(chalk.red(`Bulk assets failed to Unpublish with error ${formatError(error)}`)); + console.log(chalk.red(`Bulk assets failed to unpublish. Error: ${formatError(error)}`)); displayAssetsDetails(bulkUnPublishObj.assets, 'bulk_unpublish'); addLogs( logger, @@ -643,7 +646,7 @@ async function publishUsingVersion(data, _config, queue) { } } - console.log(chalk.red(`Entry=${entry.uid} failed to publish with error ${formatError(error)}`)); + console.log(chalk.red(`Entry '${entry.uid}' failed to publish. Error: ${formatError(error)}`)); } }); }); diff --git a/packages/contentstack-bulk-publish/src/producer/add-fields.js b/packages/contentstack-bulk-publish/src/producer/add-fields.js index e3853dfb57..2f83d46768 100644 --- a/packages/contentstack-bulk-publish/src/producer/add-fields.js +++ b/packages/contentstack-bulk-publish/src/producer/add-fields.js @@ -62,6 +62,124 @@ function removeUnwanted(entry, unwantedkeys) { return entry; } +function isLinkObject(obj, keyName) { + if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) { + return false; + } + + const linkKeyNames = ['link', 'card_link']; + if (linkKeyNames.includes(keyName)) { + return true; + } + + const hasTitle = 'title' in obj && obj.title !== undefined; + const hasUrl = 'url' in obj && obj.url !== undefined; + const hasHref = 'href' in obj && obj.href !== undefined; + + return hasTitle && (hasUrl || hasHref); +} + +function ensureHrefIsString(linkObj) { + if (linkObj.href === undefined || linkObj.href === null) { + linkObj.href = ''; + } else if (typeof linkObj.href !== 'string') { + linkObj.href = String(linkObj.href); + } +} + +function isValidJsonRte(obj) { + return obj !== null && + typeof obj === 'object' && + !Array.isArray(obj) && + typeof obj.type === 'string' && + obj.type !== ''; +} + +function cleanJsonFields(obj) { + if (obj === null || obj === undefined || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => cleanJsonFields(item)); + } + + const cleaned = {}; + for (const key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + let value = obj[key]; + const isJsonField = key.endsWith('_rte') || key === 'json_rte'; + const isAccessibilityField = key.endsWith('_accessibility') || key === 'image_preset_accessibility'; + + if (isJsonField) { + if (value === '' || value === null || value === undefined) { + continue; + } + if (typeof value === 'object' && !Array.isArray(value)) { + const keyCount = Object.keys(value).length; + if (keyCount === 0) { + continue; + } + if (!isValidJsonRte(value)) { + continue; + } + cleaned[key] = value; + } else { + continue; + } + } else if (isAccessibilityField && value === '') { + cleaned[key] = {}; + } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + value = cleanJsonFields(value); + if (value !== null && typeof value === 'object') { + cleaned[key] = value; + } + } else { + cleaned[key] = value; + } + } + return cleaned; +} + +function convertUrlToHref(obj) { + if (obj === null || obj === undefined) { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => convertUrlToHref(item)); + } + + if (typeof obj === 'object') { + const converted = {}; + for (const key in obj) { + const value = obj[key]; + + if (isLinkObject(value, key)) { + converted[key] = { ...value }; + if (converted[key].url !== undefined && converted[key].href === undefined) { + if (typeof converted[key].url === 'string') { + converted[key].href = converted[key].url; + } else if (converted[key].url === null || converted[key].url === undefined) { + converted[key].href = ''; + } else { + converted[key].href = String(converted[key].url); + } + delete converted[key].url; + } + ensureHrefIsString(converted[key]); + } else { + converted[key] = convertUrlToHref(value); + } + } + return converted; + } + + return obj; +} + function fileFields(entry, uid, multiple) { if (entry[uid]) { if (typeof entry[uid] === 'object' || Array.isArray(entry[uid])) { @@ -106,6 +224,11 @@ function addFields(contentType, entry) { } } else if (schema.enum) { entry[schema.uid] = null; + } else if (schema.data_type === 'json') { + const isJsonRteField = schema.uid && (schema.uid.endsWith('_rte') || schema.uid === 'json_rte'); + if (!isJsonRteField) { + entry[schema.uid] = {}; + } } else if (Object.prototype.hasOwnProperty.call(defaults, schema.data_type)) { entry[schema.uid] = defaults[schema.data_type]; } else { @@ -126,19 +249,31 @@ function addFields(contentType, entry) { if (schema.data_type === 'group' && !schema.multiple) { addFields(schema.schema, entry[schema.uid]); + if (entry[schema.uid]) { + entry[schema.uid] = convertUrlToHref(entry[schema.uid]); + } } if (schema.data_type === 'group' && schema.multiple) { entry[schema.uid].forEach((field) => { addFields(schema.schema, field); }); + if (entry[schema.uid]) { + entry[schema.uid] = convertUrlToHref(entry[schema.uid]); + } } if (schema.data_type === 'global_field' && !schema.multiple) { addFields(schema.schema, entry[schema.uid]); + if (entry[schema.uid]) { + entry[schema.uid] = convertUrlToHref(entry[schema.uid]); + } } if (schema.data_type === 'global_field' && schema.multiple) { entry[schema.uid].forEach((field) => { addFields(schema.schema, field); }); + if (entry[schema.uid]) { + entry[schema.uid] = convertUrlToHref(entry[schema.uid]); + } } if (schema.data_type === 'blocks') { if (!entry[schema.uid] && !Array.isArray(entry[schema.uid])) { @@ -156,6 +291,9 @@ function addFields(contentType, entry) { if (filterBlockFields.length > 0) { filterBlockFields.forEach((bfield) => { addFields(block.schema, bfield[block.uid]); + if (bfield[block.uid]) { + bfield[block.uid] = convertUrlToHref(bfield[block.uid]); + } }); } else { entry[schema.uid].push({ [block.uid]: {} }); @@ -169,6 +307,9 @@ function addFields(contentType, entry) { if (filterBlockFields.length > 0) { filterBlockFields.forEach((bfield) => { addFields(block.schema, bfield[block.uid]); + if (bfield[block.uid]) { + bfield[block.uid] = convertUrlToHref(bfield[block.uid]); + } }); } } @@ -221,8 +362,14 @@ async function getEntries( for (let index = 0; index < entriesResponse.items.length; index++) { let updatedEntry = addFields(schema, entries[index]); if (updatedEntry.changedFlag || forceUpdate) { - updatedEntry = removeUnwanted(entries[index], deleteFields); - const flag = await updateEntry(updatedEntry, locale); + let entryData = JSON.parse(JSON.stringify(updatedEntry.entry)); + entryData = removeUnwanted(entryData, deleteFields); + entryData = cleanJsonFields(entryData); + entryData = convertUrlToHref(entryData); + entryData = cleanJsonFields(entryData); + const entry = stack.contentType(contentType).entry(entries[index].uid); + Object.assign(entry, entryData); + const flag = await updateEntry(entry, locale); if (flag) { if (bulkPublish) { if (bulkPublishSet.length < bulkPublishLimit) { @@ -256,10 +403,10 @@ async function getEntries( }); } } else { - console.log(`Update Failed for entryUid ${entries[index].uid} with contentType ${contentType}`); + console.log(`Update failed for entry UID '${entries[index].uid}' of content type '${contentType}'.`); } } else { - console.log(`No change Observed for contentType ${contentType} with entry ${entries[index].uid}`); + console.log(`No changes detected for content type '${contentType}' and entry UID '${entries[index].uid}'.`); } if (index === entriesResponse.items.length - 1 && bulkPublishSet.length > 0 && bulkPublishSet.length < bulkPublishLimit) { @@ -342,19 +489,17 @@ async function start( bulkPublishLimit ); } catch (err) { - console.log(`Failed to get Entries with contentType ${contentTypes[i]} and locale ${locales[j]}`); + console.log(`Failed to retrieve entries for content type '${contentTypes[i]}' and locale '${locales[j]}'.`); } } }) .catch((err) => { - console.log(`Failed to fetch schema${JSON.stringify(err)}`); + console.log(`Failed to fetch schema: ${JSON.stringify(err)}`); }); } } } -// start() - module.exports = { start, getContentTypeSchema, diff --git a/packages/contentstack-bulk-publish/src/producer/cross-publish.js b/packages/contentstack-bulk-publish/src/producer/cross-publish.js index 3a13f35ee4..f77f34b7f9 100644 --- a/packages/contentstack-bulk-publish/src/producer/cross-publish.js +++ b/packages/contentstack-bulk-publish/src/producer/cross-publish.js @@ -225,7 +225,7 @@ async function getSyncEntries( await bulkAction(stack, entriesResponse.items, bulkPublish, filter, destEnv, apiVersion, bulkPublishLimit, variantsFlag); } if (!entriesResponse.pagination_token) { - if (!changedFlag) console.log('No Entries/Assets Found published on specified environment'); + if (!changedFlag) console.log('No entries or assets found published in the specified environment.'); return resolve(); } setTimeout(async () => { diff --git a/packages/contentstack-bulk-publish/src/producer/publish-edits.js b/packages/contentstack-bulk-publish/src/producer/publish-edits.js index 83f31695fb..dfc57a15d5 100644 --- a/packages/contentstack-bulk-publish/src/producer/publish-edits.js +++ b/packages/contentstack-bulk-publish/src/producer/publish-edits.js @@ -107,7 +107,7 @@ async function getEntries(stack, contentType, environmentUid, locale, bulkPublis } if (responseEntries.count === skipCount) { if (!changedFlag) - console.log(`No Edits Were observed on specified Environment for contentType ${contentType}`); + console.log(`No edits were detected in the specified environment for content type ${contentType}`); bulkPublishSet = []; return resolve(); } diff --git a/packages/contentstack-bulk-publish/src/producer/publish-unpublished-env.js b/packages/contentstack-bulk-publish/src/producer/publish-unpublished-env.js index 0bcfcc89ff..7e2c4c0b62 100644 --- a/packages/contentstack-bulk-publish/src/producer/publish-unpublished-env.js +++ b/packages/contentstack-bulk-publish/src/producer/publish-unpublished-env.js @@ -122,7 +122,7 @@ async function getEntries(stack, contentType, environmentUid, locale, bulkPublis } } if (responseEntries.count === skipCount) { - if (!changedFlag) console.log(`No Draft Entries of contentType ${contentType} was found`); + if (!changedFlag) console.log(`No draft entries found for content type ${contentType}`); bulkPublishSet = []; return resolve(); } diff --git a/packages/contentstack-bulk-publish/src/producer/unpublish.js b/packages/contentstack-bulk-publish/src/producer/unpublish.js index e774a85cac..95b45b9786 100644 --- a/packages/contentstack-bulk-publish/src/producer/unpublish.js +++ b/packages/contentstack-bulk-publish/src/producer/unpublish.js @@ -229,7 +229,7 @@ async function getSyncEntries( await bulkAction(stack, entriesResponse.items, bulkUnpublish, environment, locale, apiVersion, bulkPublishLimit, false); } if (entriesResponse.items.length === 0 && !entriesResponse.pagination_token) { - if (!changedFlag) console.log('No Entries/Assets Found published on specified environment'); + if (!changedFlag) console.log('No entries or assets found published in the specified environment.'); return resolve(); } diff --git a/packages/contentstack-bulk-publish/src/util/index.js b/packages/contentstack-bulk-publish/src/util/index.js index 9651b8761d..ad067baaaf 100644 --- a/packages/contentstack-bulk-publish/src/util/index.js +++ b/packages/contentstack-bulk-publish/src/util/index.js @@ -2,7 +2,7 @@ const chalk = require('chalk'); const fs = require('fs'); function prettyPrint(data) { - console.log(chalk.yellow('Configuration to be used for executing this command:')); + console.log(chalk.yellow('Configuration to use for executing this command:')); Object.keys(data).forEach((key, _index) => { console.log(chalk.grey(`${key}: ${data[key]}`)); }); diff --git a/packages/contentstack-bulk-publish/src/util/logger.js b/packages/contentstack-bulk-publish/src/util/logger.js index 16434dd504..400f5ed1ae 100644 --- a/packages/contentstack-bulk-publish/src/util/logger.js +++ b/packages/contentstack-bulk-publish/src/util/logger.js @@ -47,7 +47,7 @@ module.exports.addLogs = (logger, data, Type) => { logger.info(data); break; default: - console.log('Unknown logging level'); + console.log('Unknown log level.'); } }; diff --git a/packages/contentstack-bulk-publish/src/util/store.js b/packages/contentstack-bulk-publish/src/util/store.js index cf745f6cf4..ad98bfc8f4 100644 --- a/packages/contentstack-bulk-publish/src/util/store.js +++ b/packages/contentstack-bulk-publish/src/util/store.js @@ -12,7 +12,7 @@ function save(key, data) { console.log(chalk.red(error)); return; } - console.log(chalk.green(`Configuration file has been successfully created at ${filePath}`)); + console.log(chalk.green(`Configuration file successfully created at '${filePath}'.`)); }); } @@ -53,7 +53,7 @@ function updateMissing(key, flags) { savedConfig = get(key, pathValidator(flags.config)); Object.keys(savedConfig).forEach((element) => { if (flags[element] === undefined) { - console.log(`Using ${element} from config file`); + console.log(`Using '${element}' from the configuration file.`); flags[element] = savedConfig[element]; } }); diff --git a/packages/contentstack-bulk-publish/test/unit/commands/assets/publish.test.js b/packages/contentstack-bulk-publish/test/unit/commands/assets/publish.test.js index 3201988118..fb42fdd299 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/assets/publish.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/assets/publish.test.js @@ -57,7 +57,7 @@ describe('AssetsPublish', () => { it('Should fail when alias and stack api key flags are not passed', async () => { const args = ['--environments', environments[0], '--locales', locales[0], '--yes']; - const expectedError = 'Please use `--alias` or `--stack-api-key` to proceed.'; + const expectedError = 'Use the `--alias` or `--stack-api-key` flag to proceed.'; try { await AssetsPublish.run(args); } catch (error) { diff --git a/packages/contentstack-bulk-publish/test/unit/commands/assets/unpublish.test.js b/packages/contentstack-bulk-publish/test/unit/commands/assets/unpublish.test.js index 42c8b62a50..157a768d91 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/assets/unpublish.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/assets/unpublish.test.js @@ -90,7 +90,7 @@ describe('AssetsUnpublish Command', () => { try { await UnpublishCommand.run(['--environment', 'env', '--locale', 'en-us', '--yes']); } catch (error) { - expect(error.message).to.equal('Please use `--alias` or `--stack-api-key` to proceed.'); + expect(error.message).to.equal('Use the `--alias` or `--stack-api-key` flag to proceed.'); expect(runStub.called).to.be.false; } }); diff --git a/packages/contentstack-bulk-publish/test/unit/commands/bulk-publish/cross-publish.test.js b/packages/contentstack-bulk-publish/test/unit/commands/bulk-publish/cross-publish.test.js index 69f3b7a55e..f6337514b1 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/bulk-publish/cross-publish.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/bulk-publish/cross-publish.test.js @@ -102,7 +102,7 @@ describe('CrossPublish', () => { 'token123', ]); } catch (error) { - expect(error.message).to.include('Please use `--alias` or `--stack-api-key` to proceed.'); + expect(error.message).to.include('Use the `--alias` or `--stack-api-key` flag to proceed.'); } }); diff --git a/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-modified.test.js b/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-modified.test.js index 5ede06e909..c8a24b1a9e 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-modified.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-modified.test.js @@ -53,7 +53,7 @@ describe('EntriesPublishModified Command', () => { await EntriesPublishModified.run(args); } catch (error) { expect(error).to.be.an('error'); - expect(error.message).to.equal('Please use `--alias` or `--stack-api-key` to proceed.'); + expect(error.message).to.equal('Use the `--alias` or `--stack-api-key` flag to proceed.'); expect(runStub.calledOnce).to.be.true; } }); diff --git a/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-non-localized-fields.test.js b/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-non-localized-fields.test.js index 83544a0fcb..c189faaa8f 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-non-localized-fields.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-non-localized-fields.test.js @@ -50,7 +50,7 @@ describe('EntriesPublishNonLocalizedFields', () => { environments[1], '--yes', ]; - const expectedError = 'Please use `--alias` or `--stack-api-key` to proceed.'; + const expectedError = 'Use the `--alias` or `--stack-api-key` flag to proceed.'; try { await EntriesPublishNonLocalizedFields.run(args); diff --git a/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-only-unpublished.test.js b/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-only-unpublished.test.js index 1c88160cf2..eacbc7fcd9 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-only-unpublished.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/entries/publish-only-unpublished.test.js @@ -50,7 +50,7 @@ describe('EntriesPublishOnlyUnpublished', () => { '--yes', ]; - const expectedError = 'Please use `--alias` or `--stack-api-key` to proceed.'; + const expectedError = 'Use the `--alias` or `--stack-api-key` flag to proceed.'; runStub = sinon.stub(EntriesPublishOnlyUnpublished.prototype, 'run').callsFake(function () { throw new Error(expectedError); diff --git a/packages/contentstack-bulk-publish/test/unit/commands/entries/publish.test.js b/packages/contentstack-bulk-publish/test/unit/commands/entries/publish.test.js index 212fd125db..e1fa2018d4 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/entries/publish.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/entries/publish.test.js @@ -59,7 +59,7 @@ describe('EntriesPublish Command', () => { '--yes', ]; - const expectedError = 'Please use `--alias` or `--stack-api-key` to proceed.'; + const expectedError = 'Use the `--alias` or `--stack-api-key` flag to proceed.'; try { await EntriesPublish.run(args); diff --git a/packages/contentstack-bulk-publish/test/unit/commands/entries/unpublish.test.js b/packages/contentstack-bulk-publish/test/unit/commands/entries/unpublish.test.js index f17cb02817..8c29666cd0 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/entries/unpublish.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/entries/unpublish.test.js @@ -90,7 +90,7 @@ describe('EntriesUnpublish Command', () => { try { await EntriesUnpublish.run(['--environment', 'env', '--locale', 'en-us', '--yes']); } catch (error) { - expect(error.message).to.equal('Please use `--alias` or `--stack-api-key` to proceed.'); + expect(error.message).to.equal('Use the `--alias` or `--stack-api-key` flag to proceed.'); expect(runStub.called).to.be.false; } }); diff --git a/packages/contentstack-bulk-publish/test/unit/commands/entries/update-and-publish.test.js b/packages/contentstack-bulk-publish/test/unit/commands/entries/update-and-publish.test.js index df7db33689..4af1e01c82 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/entries/update-and-publish.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/entries/update-and-publish.test.js @@ -51,7 +51,7 @@ describe('EntriesUpdateAndPublish', () => { it('Should fail when alias and stack api key flags are not passed', async () => { const args = ['--content-types', contentTypes[0], '-e', environments[0], '--locales', locales[0], '--yes']; const entriesUpdateAndPublishSpy = sinon.spy(EntriesUpdateAndPublish.prototype, 'run'); - const expectedError = 'Please use `--alias` or `--stack-api-key` to proceed.'; + const expectedError = 'Use the `--alias` or `--stack-api-key` flag to proceed.'; try { await EntriesUpdateAndPublish.run(args); } catch (error) { diff --git a/packages/contentstack-bulk-publish/test/unit/commands/stacks/publish.test.js b/packages/contentstack-bulk-publish/test/unit/commands/stacks/publish.test.js index 36237f663b..5ebdd2a86d 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/stacks/publish.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/stacks/publish.test.js @@ -59,7 +59,7 @@ describe('StackPublish', () => { '--yes', ]; - const expectedError = 'Please use `--alias` or `--stack-api-key` to proceed.'; + const expectedError = 'Use the `--alias` or `--stack-api-key` flag to proceed.'; runStub = sinon.stub(StackPublish.prototype, 'run').callsFake(function () { throw new Error(expectedError); diff --git a/packages/contentstack-bulk-publish/test/unit/commands/stacks/unpublish.test.js b/packages/contentstack-bulk-publish/test/unit/commands/stacks/unpublish.test.js index ed25eb69b8..a26cf70487 100644 --- a/packages/contentstack-bulk-publish/test/unit/commands/stacks/unpublish.test.js +++ b/packages/contentstack-bulk-publish/test/unit/commands/stacks/unpublish.test.js @@ -76,7 +76,7 @@ describe('StackUnpublish', () => { '--yes', ]; - const expectedError = 'Please use `--alias` or `--stack-api-key` to proceed.'; + const expectedError = 'Use the `--alias` or `--stack-api-key` flag to proceed.'; runStub = sinon.stub(StackUnpublish.prototype, 'run').callsFake(function () { throw new Error(expectedError); diff --git a/packages/contentstack-clone/src/commands/cm/stacks/clone.js b/packages/contentstack-clone/src/commands/cm/stacks/clone.js index a1b5a44bcc..5089ddce9e 100644 --- a/packages/contentstack-clone/src/commands/cm/stacks/clone.js +++ b/packages/contentstack-clone/src/commands/cm/stacks/clone.js @@ -1,5 +1,6 @@ const { Command } = require('@contentstack/cli-command'); -const { configHandler, flags, isAuthenticated, managementSDKClient } = require('@contentstack/cli-utilities'); +const { configHandler, flags, isAuthenticated, managementSDKClient, log, handleAndLogError } = require('@contentstack/cli-utilities'); +const { configHandler, flags, isAuthenticated, managementSDKClient, log, handleAndLogError } = require('@contentstack/cli-utilities'); const { CloneHandler } = require('../../../lib/util/clone-handler'); const path = require('path'); const { rimraf } = require('rimraf'); @@ -9,6 +10,82 @@ const { readdirSync, readFileSync } = require('fs'); let config = {}; class StackCloneCommand extends Command { + /** + * Determine authentication method based on user preference + */ + determineAuthenticationMethod(sourceManagementTokenAlias, destinationManagementTokenAlias) { + // Track authentication method + let authenticationMethod = 'unknown'; + + // Determine authentication method based on user preference + if (sourceManagementTokenAlias || destinationManagementTokenAlias) { + authenticationMethod = 'Management Token'; + } else if (isAuthenticated()) { + // Check if user is authenticated via OAuth + const isOAuthUser = configHandler.get('authorisationType') === 'OAUTH' || false; + if (isOAuthUser) { + authenticationMethod = 'OAuth'; + } else { + authenticationMethod = 'Basic Auth'; + } + } else { + authenticationMethod = 'Basic Auth'; + } + + return authenticationMethod; + } + + /** + * Create clone context object for logging + */ + createCloneContext(authenticationMethod) { + return { + command: this.context?.info?.command || 'cm:stacks:clone', + module: 'clone', + email: configHandler.get('email') || '', + sessionId: this.context?.sessionId || '', + authenticationMethod: authenticationMethod || 'Basic Auth', + }; + } + + /** + * Determine authentication method based on user preference + */ + determineAuthenticationMethod(sourceManagementTokenAlias, destinationManagementTokenAlias) { + // Track authentication method + let authenticationMethod = 'unknown'; + + // Determine authentication method based on user preference + if (sourceManagementTokenAlias || destinationManagementTokenAlias) { + authenticationMethod = 'Management Token'; + } else if (isAuthenticated()) { + // Check if user is authenticated via OAuth + const isOAuthUser = configHandler.get('authorisationType') === 'OAUTH' || false; + if (isOAuthUser) { + authenticationMethod = 'OAuth'; + } else { + authenticationMethod = 'Basic Auth'; + } + } else { + authenticationMethod = 'Basic Auth'; + } + + return authenticationMethod; + } + + /** + * Create clone context object for logging + */ + createCloneContext(authenticationMethod) { + return { + command: this.context?.info?.command || 'cm:stacks:clone', + module: 'clone', + email: configHandler.get('email') || '', + sessionId: this.context?.sessionId || '', + authenticationMethod: authenticationMethod || 'Basic Auth', + }; + } + async run() { try { let self = this; @@ -31,14 +108,40 @@ class StackCloneCommand extends Command { const handleClone = async () => { const listOfTokens = configHandler.get('tokens'); + const authenticationMethod = this.determineAuthenticationMethod( + sourceManagementTokenAlias, + destinationManagementTokenAlias, + ); + const cloneContext = this.createCloneContext(authenticationMethod); + log.debug('Starting clone operation setup', cloneContext); + const authenticationMethod = this.determineAuthenticationMethod( + sourceManagementTokenAlias, + destinationManagementTokenAlias, + ); + const cloneContext = this.createCloneContext(authenticationMethod); + log.debug('Starting clone operation setup', cloneContext); if (externalConfigPath) { + log.debug(`Loading external configuration from: ${externalConfigPath}`, cloneContext); + log.debug(`Loading external configuration from: ${externalConfigPath}`, cloneContext); let externalConfig = readFileSync(externalConfigPath, 'utf-8'); externalConfig = JSON.parse(externalConfig); config = merge.recursive(config, externalConfig); } config.forceStopMarketplaceAppsPrompt = yes; config.skipAudit = cloneCommandFlags['skip-audit']; + log.debug('Clone configuration prepared', { + ...cloneContext, + cloneType: config.cloneType, + skipAudit: config.skipAudit, + forceStopMarketplaceAppsPrompt: config.forceStopMarketplaceAppsPrompt + }); + log.debug('Clone configuration prepared', { + ...cloneContext, + cloneType: config.cloneType, + skipAudit: config.skipAudit, + forceStopMarketplaceAppsPrompt: config.forceStopMarketplaceAppsPrompt + }); if (cloneType) { config.cloneType = cloneType; @@ -67,15 +170,23 @@ class StackCloneCommand extends Command { if (sourceManagementTokenAlias && listOfTokens[sourceManagementTokenAlias]) { config.source_alias = sourceManagementTokenAlias; config.source_stack = listOfTokens[sourceManagementTokenAlias].apiKey; + log.debug(`Using source token alias: ${sourceManagementTokenAlias}`, cloneContext); + log.debug(`Using source token alias: ${sourceManagementTokenAlias}`, cloneContext); } else if (sourceManagementTokenAlias) { - console.log(`Provided source token alias (${sourceManagementTokenAlias}) not found in your config.!`); + log.warn(`Provided source token alias (${sourceManagementTokenAlias}) not found in your config.!`, cloneContext); + log.warn(`Provided source token alias (${sourceManagementTokenAlias}) not found in your config.!`, cloneContext); } if (destinationManagementTokenAlias && listOfTokens[destinationManagementTokenAlias]) { config.destination_alias = destinationManagementTokenAlias; config.target_stack = listOfTokens[destinationManagementTokenAlias].apiKey; + log.debug(`Using destination token alias: ${destinationManagementTokenAlias}`, cloneContext); + log.debug(`Using destination token alias: ${destinationManagementTokenAlias}`, cloneContext); } else if (destinationManagementTokenAlias) { - console.log( + log.warn( + log.warn( `Provided destination token alias (${destinationManagementTokenAlias}) not found in your config.!`, + cloneContext, + cloneContext, ); } if (importWebhookStatus) { @@ -83,18 +194,31 @@ class StackCloneCommand extends Command { } const managementAPIClient = await managementSDKClient(config); + log.debug('Management API client initialized successfully', cloneContext); + log.debug('Management API client initialized successfully', cloneContext); - await this.removeContentDirIfNotEmptyBeforeClone(pathdir); // NOTE remove if folder not empty before clone - this.registerCleanupOnInterrupt(pathdir); + log.debug(`Content directory path: ${pathdir}`, cloneContext); + await this.removeContentDirIfNotEmptyBeforeClone(pathdir, cloneContext); // NOTE remove if folder not empty before clone + this.registerCleanupOnInterrupt(pathdir, cloneContext); + log.debug(`Content directory path: ${pathdir}`, cloneContext); + await this.removeContentDirIfNotEmptyBeforeClone(pathdir, cloneContext); // NOTE remove if folder not empty before clone + this.registerCleanupOnInterrupt(pathdir, cloneContext); config.auth_token = configHandler.get('authtoken'); config.host = this.cmaHost; config.cdn = this.cdaHost; config.pathDir = pathdir; + config.cloneContext = cloneContext; + log.debug('Clone configuration finalized', cloneContext); + config.cloneContext = cloneContext; + log.debug('Clone configuration finalized', cloneContext); const cloneHandler = new CloneHandler(config); cloneHandler.setClient(managementAPIClient); + log.debug('Starting clone operation', cloneContext); + log.debug('Starting clone operation', cloneContext); cloneHandler.execute().catch((error) => { - console.log(error); + handleAndLogError(error, cloneContext); + handleAndLogError(error, cloneContext); }); }; @@ -103,7 +227,7 @@ class StackCloneCommand extends Command { if (isAuthenticated()) { handleClone(); } else { - console.log('Please login to execute this command, csdx auth:login'); + log.error('Please login to execute this command, csdx auth:login', cloneContext); this.exit(1); } } else { @@ -112,76 +236,97 @@ class StackCloneCommand extends Command { } else if (isAuthenticated()) { handleClone(); } else { - console.log('Please login to execute this command, csdx auth:login'); + log.error('Please login to execute this command, csdx auth:login', cloneContext); + log.error('Please login to execute this command, csdx auth:login', cloneContext); this.exit(1); } } catch (error) { if (error) { - await this.cleanUp(pathdir); - // eslint-disable-next-line no-console - console.log(error.message || error); + await this.cleanUp(pathdir, null, cloneContext); + log.error('Stack clone command failed', { ...cloneContext, error: error?.message || error }); + await this.cleanUp(pathdir, null, cloneContext); + log.error('Stack clone command failed', { ...cloneContext, error: error?.message || error }); } } } - async removeContentDirIfNotEmptyBeforeClone(dir) { + async removeContentDirIfNotEmptyBeforeClone(dir, cloneContext) { + async removeContentDirIfNotEmptyBeforeClone(dir, cloneContext) { try { + log.debug('Checking if content directory is empty', { ...cloneContext, dir }); + log.debug('Checking if content directory is empty', { ...cloneContext, dir }); const dirNotEmpty = readdirSync(dir).length; if (dirNotEmpty) { - await this.cleanUp(dir); + log.debug('Content directory is not empty, cleaning up', { ...cloneContext, dir }); + await this.cleanUp(dir, null, cloneContext); + log.debug('Content directory is not empty, cleaning up', { ...cloneContext, dir }); + await this.cleanUp(dir, null, cloneContext); } } catch (error) { const omit = ['ENOENT']; // NOTE add emittable error codes in the array if (!omit.includes(error.code)) { - console.log(error.message); + log.error('Error checking content directory', { ...cloneContext, error: error?.message, code: error.code }); + log.error('Error checking content directory', { ...cloneContext, error: error?.message, code: error.code }); } } } - async cleanUp(pathDir, message) { + async cleanUp(pathDir, message, cloneContext) { + async cleanUp(pathDir, message, cloneContext) { try { + log.debug('Starting cleanup', { ...cloneContext, pathDir }); + log.debug('Starting cleanup', { ...cloneContext, pathDir }); await rimraf(pathDir); if (message) { - // eslint-disable-next-line no-console - console.log(message); + log.info(message, cloneContext); + log.info(message, cloneContext); } + log.debug('Cleanup completed', { ...cloneContext, pathDir }); + log.debug('Cleanup completed', { ...cloneContext, pathDir }); } catch (err) { if (err) { - console.log('\nCleaning up'); + log.debug('Cleaning up', cloneContext); + log.debug('Cleaning up', cloneContext); const skipCodeArr = ['ENOENT', 'EBUSY', 'EPERM', 'EMFILE', 'ENOTEMPTY']; if (skipCodeArr.includes(err.code)) { + log.debug('Cleanup error code is in skip list, exiting', { ...cloneContext, code: err?.code }); + log.debug('Cleanup error code is in skip list, exiting', { ...cloneContext, code: err?.code }); process.exit(); } } } } - registerCleanupOnInterrupt(pathDir) { + registerCleanupOnInterrupt(pathDir, cloneContext) { + registerCleanupOnInterrupt(pathDir, cloneContext) { const interrupt = ['SIGINT', 'SIGQUIT', 'SIGTERM']; const exceptions = ['unhandledRejection', 'uncaughtException']; const cleanUp = async (exitOrError) => { if (exitOrError) { - // eslint-disable-next-line no-console - console.log('\nCleaning up'); - await this.cleanUp(pathDir); - // eslint-disable-next-line no-console - console.log('done'); - // eslint-disable-next-line no-process-exit + log.debug('Cleaning up on interrupt', cloneContext); + await this.cleanUp(pathDir, null, cloneContext); + log.info('Cleanup done', cloneContext); + log.debug('Cleaning up on interrupt', cloneContext); + await this.cleanUp(pathDir, null, cloneContext); + log.info('Cleanup done', cloneContext); if (exitOrError instanceof Promise) { exitOrError.catch((error) => { - console.log((error && error.message) || ''); + log.error('Error during cleanup', { ...cloneContext, error: (error && error?.message) || '' }); + log.error('Error during cleanup', { ...cloneContext, error: (error && error?.message) || '' }); }); } else if (exitOrError.message) { - console.log(exitOrError.message); + log.error('Cleanup error', { ...cloneContext, error: exitOrError?.message }); + log.error('Cleanup error', { ...cloneContext, error: exitOrError?.message }); } else if (exitOrError.errorMessage) { - console.log(exitOrError.message); + log.error('Cleanup error', { ...cloneContext, error: exitOrError?.errorMessage }); + log.error('Cleanup error', { ...cloneContext, error: exitOrError?.errorMessage }); } if (exitOrError === true) process.exit(); diff --git a/packages/contentstack-clone/src/lib/util/clone-handler.js b/packages/contentstack-clone/src/lib/util/clone-handler.js index 1449bd0cbf..0bd4aab725 100644 --- a/packages/contentstack-clone/src/lib/util/clone-handler.js +++ b/packages/contentstack-clone/src/lib/util/clone-handler.js @@ -21,7 +21,7 @@ const { Clone, HandleBranchCommand, } = require('../helpers/command-helpers'); -const { configHandler, getBranchFromAlias } = require('@contentstack/cli-utilities'); +const { configHandler, getBranchFromAlias, log } = require('@contentstack/cli-utilities'); let client = {}; let config; @@ -76,6 +76,7 @@ class CloneHandler { cloneCommand = new Clone(); this.pathDir = opt.pathDir; process.stdin.setMaxListeners(50); + log.debug('Initializing CloneHandler', config.cloneContext, { pathDir: opt.pathDir, cloneType: opt.cloneType }); } setClient(managementSDKClient) { client = managementSDKClient; @@ -84,19 +85,24 @@ class CloneHandler { handleOrgSelection(options = {}) { return new Promise(async (resolve, reject) => { const { msg = '', isSource = true } = options || {}; + log.debug('Handling organization selection', config.cloneContext); const orgList = await this.getOrganizationChoices(msg).catch(reject); - if (orgList) { - const orgSelected = await inquirer.prompt(orgList); + if (orgList) { + log.debug(`Found ${orgList.choices?.length || 0} organization(s) to choose from`, config.cloneContext); + const orgSelected = await inquirer.prompt(orgList); + log.debug(`Organization selected: ${orgSelected.Organization}`, config.cloneContext); - if (isSource) { - config.sourceOrg = orgUidList[orgSelected.Organization]; - } else { - config.targetOrg = orgUidList[orgSelected.Organization]; - } + if (isSource) { + config.sourceOrg = orgUidList[orgSelected.Organization]; + log.debug(`Source organization UID: ${config.sourceOrg}`, config.cloneContext); + } else { + config.targetOrg = orgUidList[orgSelected.Organization]; + log.debug(`Target organization UID: ${config.targetOrg}`, config.cloneContext); + } - resolve(orgSelected); - } + resolve(orgSelected); + } }); } @@ -104,13 +110,16 @@ class CloneHandler { return new Promise(async (resolve, reject) => { try { const { org = {}, msg = '', isSource = true } = options || {}; + log.debug('Handling stack selection', config.cloneContext, { isSource, orgName: org.Organization, msg }); const stackList = await this.getStack(org, msg, isSource).catch(reject); if (stackList) { this.displayBackOptionMessage(); + log.debug(`Found ${stackList.choices?.length || 0} stack(s) to choose from`, config.cloneContext); const selectedStack = await inquirer.prompt(stackList); + log.debug(`Stack selected: ${selectedStack.stack}`, config.cloneContext); if (this.executingCommand != 1) { return reject(); } @@ -118,9 +127,11 @@ class CloneHandler { config.sourceStackName = selectedStack.stack; master_locale = masterLocaleList[selectedStack.stack]; config.source_stack = stackUidList[selectedStack.stack]; + log.debug(`Source stack configured`, config.cloneContext); } else { config.target_stack = stackUidList[selectedStack.stack]; config.destinationStackName = selectedStack.stack; + log.debug(`Target stack configured`, config.cloneContext); } resolve(selectedStack); @@ -136,6 +147,7 @@ class CloneHandler { return new Promise(async (resolve, reject) => { let spinner; try { + log.debug('Handling branch selection', config.cloneContext, { isSource, returnBranch, stackApiKey: isSource ? config.source_stack : config.target_stack }); const stackAPIClient = client.stack({ api_key: isSource ? config.source_stack : config.target_stack, management_token: config.management_token, @@ -143,22 +155,27 @@ class CloneHandler { // NOTE validate if source branch is exist if (isSource && config.sourceStackBranch) { + log.debug('Validating source branch exists', { ...config.cloneContext, branch: config.sourceStackBranch }); await this.validateIfBranchExist(stackAPIClient, true); return resolve(); } else if(isSource && config.sourceStackBranchAlias) { + log.debug('Resolving source branch alias', { ...config.cloneContext, alias: config.sourceStackBranchAlias }); await this.resolveBranchAliases(true); return resolve(); } // NOTE Validate target branch is exist if (!isSource && config.targetStackBranch) { + log.debug('Validating target branch exists', { ...config.cloneContext, branch: config.targetStackBranch }); await this.validateIfBranchExist(stackAPIClient, false); return resolve(); } else if (!isSource && config.targetStackBranchAlias) { + log.debug('Resolving target branch alias', { ...config.cloneContext, alias: config.targetStackBranchAlias }); await this.resolveBranchAliases(); return resolve(); } spinner = ora('Fetching Branches').start(); + log.debug(`Querying branches for stack: ${isSource ? config.source_stack : config.target_stack}`, config.cloneContext); const result = await stackAPIClient .branch() .query() @@ -167,6 +184,7 @@ class CloneHandler { .catch((_err) => {}); const condition = result && Array.isArray(result) && result.length > 0; + log.debug(`Found ${result?.length || 0} branch(es)`, config.cloneContext); // NOTE if want to get only list of branches (Pass param -> returnBranch = true ) if (returnBranch) { @@ -185,8 +203,10 @@ class CloneHandler { } if (isSource) { config.sourceStackBranch = branch; + log.debug(`Source branch selected: ${branch}`, config.cloneContext); } else { config.targetStackBranch = branch; + log.debug(`Target branch selected: ${branch}`, config.cloneContext); } } else { spinner.succeed('No branches found.!'); @@ -196,7 +216,6 @@ class CloneHandler { } } catch (e) { if (spinner) spinner.fail(); - console.error(e && e.message); return reject(e); } }); @@ -210,6 +229,7 @@ class CloneHandler { }; try { const branch = isSource ? config.sourceStackBranch : config.targetStackBranch; + log.debug('Validating branch existence', config.cloneContext); spinner = ora(`Validation if ${isSource ? 'source' : 'target'} branch exist.!`).start(); const isBranchExist = await stackAPIClient .branch(branch) @@ -217,8 +237,10 @@ class CloneHandler { .then((data) => data); if (isBranchExist && typeof isBranchExist === 'object') { + log.debug('Branch validation successful', config.cloneContext); completeSpinner(`${isSource ? 'Source' : 'Target'} branch verified.!`); } else { + log.error('Branch not found', config.cloneContext); completeSpinner(`${isSource ? 'Source' : 'Target'} branch not found.!`, 'fail'); process.exit(); } @@ -247,8 +269,10 @@ class CloneHandler { return new Promise(async (resolve, reject) => { let keyPressHandler; try { + log.debug('Starting clone execution', { ...config.cloneContext, sourceStack: config.source_stack, targetStack: config.target_stack }); if (!config.source_stack) { const orgMsg = 'Choose an organization where your source stack exists:'; + log.debug('Source stack not provided, prompting for organization', config.cloneContext); this.setExectingCommand(0); this.removeBackKeyPressHandler(); const org = await cloneCommand.execute(new HandleOrgCommand({ msg: orgMsg, isSource: true }, this)); @@ -278,17 +302,21 @@ class CloneHandler { return reject('Org not found.'); } } else { + log.debug('Source stack provided, proceeding with branch selection and export', config.cloneContext); this.setExectingCommand(2); await this.handleBranchSelection({ api_key: config.sourceStack }); + log.debug('Starting export operation', config.cloneContext); const exportRes = await cloneCommand.execute(new HandleExportCommand(null, this)); await cloneCommand.execute(new SetBranchCommand(null, this)); if (exportRes) { + log.debug('Export completed, proceeding with destination setup', config.cloneContext); this.executeDestination().catch((error) => { return reject(error); }); } } + log.debug('Clone execution completed successfully', config.cloneContext); return resolve(); } catch (error) { return reject(error); @@ -327,10 +355,12 @@ class CloneHandler { async executeExport() { try { + log.debug('Executing export operation', config.cloneContext); const exportRes = await cloneCommand.execute(new HandleExportCommand(null, this)); await cloneCommand.execute(new SetBranchCommand(null, this)); if (exportRes) { + log.debug('Export operation completed, proceeding with destination', config.cloneContext); this.executeDestination().catch(() => { throw ''; }); @@ -346,8 +376,10 @@ class CloneHandler { return new Promise(async (resolve, reject) => { let keyPressHandler; try { + log.debug('Executing destination setup', config.cloneContext); let canCreateStack = false; if (!config.target_stack) { + log.debug('Target stack not provided, prompting for stack creation', config.cloneContext); canCreateStack = await inquirer.prompt(stackCreationConfirmation); } @@ -397,6 +429,7 @@ class CloneHandler { await this.executeBranchDestinationPrompt(params); } + log.debug('Destination setup completed successfully', config.cloneContext); return resolve(); } catch (error) { reject(error); @@ -469,10 +502,12 @@ class CloneHandler { choices: [], }; return new Promise(async (resolve, reject) => { + log.debug('Fetching organization choices', config.cloneContext); const spinner = ora('Fetching Organization').start(); try { let organizations; const configOrgUid = configHandler.get('oauthOrgUid'); + log.debug('Getting organizations', config.cloneContext, { hasConfigOrgUid: !!configOrgUid }); if (configOrgUid) { organizations = await client.organization(configOrgUid).fetch(); @@ -481,6 +516,7 @@ class CloneHandler { } spinner.succeed('Fetched Organization'); + log.debug('Fetched organizations', config.cloneContext); for (const element of organizations.items || [organizations]) { orgUidList[element.name] = element.uid; orgChoice.choices.push(element.name); @@ -501,12 +537,15 @@ class CloneHandler { message: stkMessage !== undefined ? stkMessage : 'Select the stack', choices: [], }; + log.debug('Fetching stacks', config.cloneContext); const spinner = ora('Fetching stacks').start(); try { const organization_uid = orgUidList[answer.Organization]; + log.debug('Querying stacks for organization', config.cloneContext, { organizationUid: organization_uid }); const stackList = client.stack().query({ organization_uid }).find(); stackList .then((stacklist) => { + log.debug('Fetched stacks', config.cloneContext, { count: stacklist.items ? stacklist.items.length : 0 }); for (const element of stacklist.items) { stackUidList[element.name] = element.api_key; masterLocaleList[element.name] = element.master_locale; @@ -530,9 +569,11 @@ class CloneHandler { return new Promise(async (resolve, reject) => { try { const { orgUid } = options; + log.debug('Creating new stack', config.cloneContext, { orgUid, masterLocale: master_locale, stackName: config.stackName }); this.displayBackOptionMessage(); let inputvalue; if (!config.stackName) { + log.debug('Stack name not provided, prompting user', config.cloneContext); prompt.start(); prompt.message = ''; this.setCreateNewStackPrompt(prompt); @@ -542,17 +583,24 @@ class CloneHandler { inputvalue = { stack: config.stackName }; } if (this.executingCommand === 0 || !inputvalue) { + log.debug('Stack creation cancelled or invalid input', config.cloneContext); return reject(); } let stack = { name: inputvalue.stack, master_locale: master_locale }; + log.debug('Creating stack with configuration', config.cloneContext); const spinner = ora('Creating New stack').start(); + log.debug('Sending stack creation API request', config.cloneContext); let newStack = client.stack().create({ stack }, { organization_uid: orgUid }); newStack .then((result) => { + log.debug('Stack created successfully', config.cloneContext, { + stackName: result.name, + }); spinner.succeed('New Stack created Successfully name as ' + result.name); config.target_stack = result.api_key; config.destinationStackName = result.name; + log.debug('Target stack configuration updated', config.cloneContext); return resolve(result); }) .catch((error) => { @@ -589,12 +637,15 @@ class CloneHandler { async resolveBranchAliases(isSource = false) { try { + log.debug('Resolving branch aliases', { ...config.cloneContext, isSource, alias: isSource ? config.sourceStackBranchAlias : config.targetStackBranchAlias }); if (isSource) { const sourceStack = client.stack({ api_key: config.source_stack }); config.sourceStackBranch = await getBranchFromAlias(sourceStack, config.sourceStackBranchAlias); + log.debug('Source branch alias resolved', { ...config.cloneContext, alias: config.sourceStackBranchAlias, branch: config.sourceStackBranch }); } else { const targetStack = client.stack({ api_key: config.target_stack }); config.targetStackBranch = await getBranchFromAlias(targetStack, config.targetStackBranchAlias); + log.debug('Target branch alias resolved', { ...config.cloneContext, alias: config.targetStackBranchAlias, branch: config.targetStackBranch }); } } catch (error) { throw error; @@ -604,6 +655,7 @@ class CloneHandler { async cloneTypeSelection() { console.clear(); return new Promise(async (resolve, reject) => { + log.debug('Starting clone type selection', config.cloneContext); const choices = [ 'Structure (all modules except entries & assets)', 'Structure with content (all modules including entries & assets)', @@ -619,83 +671,139 @@ class CloneHandler { let successMsg; let selectedValue = {}; config['data'] = path.join(__dirname.split('src')[0], 'contents', config.sourceStackBranch || ''); + log.debug(`Clone data directory: ${config['data']}`, config.cloneContext); if (!config.cloneType) { + log.debug('Clone type not specified, prompting user for selection', config.cloneContext); selectedValue = await inquirer.prompt(cloneTypeSelection); + } else { + log.debug(`Using pre-configured clone type: ${config.cloneType}`, config.cloneContext); } if (config.cloneType === 'a' || selectedValue.type === 'Structure (all modules except entries & assets)') { config['modules'] = structureList; successMsg = 'Stack clone Structure completed'; + log.debug(`Clone type: Structure only. Modules to clone: ${structureList.join(', ')}`, config.cloneContext); } else { successMsg = 'Stack clone completed with structure and content'; + log.debug('Clone type: Structure with content (all modules)', config.cloneContext); } this.cmdImport() - .then(() => resolve(successMsg)) + .then(() => { + log.debug('Clone type selection and import completed successfully', config.cloneContext); + resolve(successMsg); + }) .catch(reject); }); } async cmdExport() { return new Promise((resolve, reject) => { + log.debug('Preparing export command', { ...config.cloneContext, sourceStack: config.source_stack, cloneType: config.cloneType }); // Creating export specific config by merging external configurations let exportConfig = Object.assign({}, cloneDeep(config), { ...config?.export }); delete exportConfig.import; delete exportConfig.export; - const cmd = ['-k', exportConfig.source_stack, '-d', __dirname.split('src')[0] + 'contents']; + const exportDir = __dirname.split('src')[0] + 'contents'; + log.debug(`Export directory: ${exportDir}`, config.cloneContext); + const cmd = ['-k', exportConfig.source_stack, '-d', exportDir]; + if (exportConfig.cloneType === 'a') { exportConfig.filteredModules = ['stack'].concat(structureList); + log.debug(`Filtered modules for structure-only export: ${exportConfig.filteredModules.join(', ')}`, config.cloneContext); } if (exportConfig.source_alias) { cmd.push('-a', exportConfig.source_alias); + log.debug(`Using source alias: ${exportConfig.source_alias}`, config.cloneContext); } if (exportConfig.sourceStackBranch) { cmd.push('--branch', exportConfig.sourceStackBranch); + log.debug(`Using source branch: ${exportConfig.sourceStackBranch}`, config.cloneContext); } - if (exportConfig.forceStopMarketplaceAppsPrompt) cmd.push('-y'); + if (exportConfig.forceStopMarketplaceAppsPrompt) { + cmd.push('-y'); + log.debug('Force stop marketplace apps prompt enabled', config.cloneContext); + } + const configFilePath = path.join(__dirname, 'dummyConfig.json'); cmd.push('-c'); - cmd.push(path.join(__dirname, 'dummyConfig.json')); - - fs.writeFileSync(path.join(__dirname, 'dummyConfig.json'), JSON.stringify(exportConfig)); + cmd.push(configFilePath); + log.debug(`Writing export config to: ${configFilePath}`, config.cloneContext); + + fs.writeFileSync(configFilePath, JSON.stringify(exportConfig)); + log.debug('Export command prepared', config.cloneContext, { + cmd: cmd.join(' '), + exportDir, + sourceStack: exportConfig.source_stack, + branch: exportConfig.sourceStackBranch + }); + log.debug('Running export command', config.cloneContext, { cmd }); let exportData = exportCmd.run(cmd); - exportData.then(() => resolve(true)).catch(reject); + exportData.then(() => { + log.debug('Export command completed successfully', config.cloneContext); + resolve(true); + }).catch((error) => { + reject(error); + }); }); } async cmdImport() { return new Promise(async (resolve, _reject) => { + log.debug('Preparing import command', { ...config.cloneContext, targetStack: config.target_stack, targetBranch: config.targetStackBranch }); // Creating export specific config by merging external configurations let importConfig = Object.assign({}, cloneDeep(config), { ...config?.import }); delete importConfig.import; delete importConfig.export; - const cmd = ['-c', path.join(__dirname, 'dummyConfig.json')]; + const configFilePath = path.join(__dirname, 'dummyConfig.json'); + const cmd = ['-c', configFilePath]; if (importConfig.destination_alias) { cmd.push('-a', importConfig.destination_alias); + log.debug(`Using destination alias: ${importConfig.destination_alias}`, config.cloneContext); } if (!importConfig.data && importConfig.sourceStackBranch) { - cmd.push('-d', path.join(importConfig.pathDir, importConfig.sourceStackBranch)); + const dataPath = path.join(importConfig.pathDir, importConfig.sourceStackBranch); + cmd.push('-d', dataPath); + log.debug(`Import data path: ${dataPath}`, config.cloneContext); } if (importConfig.targetStackBranch) { cmd.push('--branch', importConfig.targetStackBranch); + log.debug(`Using target branch: ${importConfig.targetStackBranch}`, config.cloneContext); } if (importConfig.importWebhookStatus) { cmd.push('--import-webhook-status', importConfig.importWebhookStatus); + log.debug(`Import webhook status: ${importConfig.importWebhookStatus}`, config.cloneContext); } - if (importConfig.skipAudit) cmd.push('--skip-audit'); + if (importConfig.skipAudit) { + cmd.push('--skip-audit'); + log.debug('Skip audit flag enabled', config.cloneContext); + } - if (importConfig.forceStopMarketplaceAppsPrompt) cmd.push('-y'); + if (importConfig.forceStopMarketplaceAppsPrompt) { + cmd.push('-y'); + log.debug('Force stop marketplace apps prompt enabled', config.cloneContext); + } - fs.writeFileSync(path.join(__dirname, 'dummyConfig.json'), JSON.stringify(importConfig)); + log.debug(`Writing import config to: ${configFilePath}`, config.cloneContext); + fs.writeFileSync(configFilePath, JSON.stringify(importConfig)); + log.debug('Import command prepared', config.cloneContext, { + cmd: cmd.join(' '), + targetStack: importConfig.target_stack, + targetBranch: importConfig.targetStackBranch, + dataPath: importConfig.data || path.join(importConfig.pathDir, importConfig.sourceStackBranch) + }); + log.debug('Running import command', config.cloneContext, { cmd }); await importCmd.run(cmd); - fs.writeFileSync(path.join(__dirname, 'dummyConfig.json'), JSON.stringify({})); + log.debug('Import command completed successfully', config.cloneContext); + log.debug('Clearing import config file', config.cloneContext); + fs.writeFileSync(configFilePath, JSON.stringify({})); return resolve(); }); } diff --git a/packages/contentstack-clone/src/lib/util/log.js b/packages/contentstack-clone/src/lib/util/log.js deleted file mode 100644 index 7d806cd9a8..0000000000 --- a/packages/contentstack-clone/src/lib/util/log.js +++ /dev/null @@ -1,105 +0,0 @@ -/*! - * Contentstack Import - * Copyright (c) 2024 Contentstack LLC - * MIT Licensed - */ - -var winston = require('winston'); -var path = require('path'); -var mkdirp = require('mkdirp'); -const { pathValidator, sanitizePath } = require('@contentstack/cli-utilities'); -var slice = Array.prototype.slice; - -function returnString(args) { - var returnStr = ''; - if (args && args.length) { - returnStr = args - .map(function (item) { - if (item && typeof item === 'object') { - return JSON.stringify(item); - } - return item; - }) - .join(' ') - .trim(); - } - return returnStr; -} - -var myCustomLevels = { - levels: { - error: 0, - warn: 1, - info: 2, - debug: 3, - }, - colors: { - info: 'blue', - debug: 'green', - warn: 'yellow', - error: 'red', - }, -}; - -function init(_logPath, logfileName) { - var logsDir = pathValidator(path.resolve(sanitizePath(_logPath), 'logs', 'import')); - // Create dir if doesn't already exist - mkdirp.sync(logsDir); - var logPath = path.join(sanitizePath(logsDir), pathValidator(sanitizePath(logfileName)) + '.log'); - - var transports = [ - new winston.transports.File({ - filename: logPath, - maxFiles: 20, - maxsize: 1000000, - tailable: true, - json: true, - }), - ]; - - transports.push(new winston.transports.Console()); - - var logger = winston.createLogger({ - transports: transports, - levels: myCustomLevels.levels, - }); - - return { - log: function () { - var args = slice.call(arguments); - var logString = returnString(args); - if (logString) { - logger.log('info', logString); - } - }, - warn: function () { - var args = slice.call(arguments); - var logString = returnString(args); - if (logString) { - logger.log('warn', logString); - } - }, - error: function () { - var args = slice.call(arguments); - var logString = returnString(args); - if (logString) { - logger.log('error', logString); - } - }, - debug: function () { - var args = slice.call(arguments); - var logString = returnString(args); - if (logString) { - logger.log('debug', logString); - } - }, - }; -} - -exports.addlogs = async (config, message, type) => { - if (type !== 'error') { - init(config.oldPath, type).log(message); - } else { - init(config.oldPath, type).error(message); - } -}; diff --git a/packages/contentstack-command/src/index.ts b/packages/contentstack-command/src/index.ts index 77051b4283..e82bee1075 100644 --- a/packages/contentstack-command/src/index.ts +++ b/packages/contentstack-command/src/index.ts @@ -21,7 +21,7 @@ abstract class ContentstackCommand extends Command { if (this._email) return this._email; this._email = configHandler.get('email'); if (this._email) return this._email; - throw new CLIError('You are not logged in. Please login with command $ csdx auth:login'); + throw new CLIError('You are not logged in. Run the command: $ csdx auth:login'); } get deliveryAPIClient() { @@ -105,6 +105,9 @@ abstract class ContentstackCommand extends Command { get personalizeUrl() { return this.region.personalizeUrl; } + get composableStudioUrl() { + return this.region.composableStudioUrl; + } } module.exports = { diff --git a/packages/contentstack-command/src/interfaces/index.ts b/packages/contentstack-command/src/interfaces/index.ts index 4538fef85b..262acce498 100644 --- a/packages/contentstack-command/src/interfaces/index.ts +++ b/packages/contentstack-command/src/interfaces/index.ts @@ -5,5 +5,6 @@ export interface Region { developerHubUrl: string; personalizeUrl: string; launchHubUrl: string; + composableStudioUrl: string; uiHost: string; } diff --git a/packages/contentstack-config/src/commands/config/get/base-branch.ts b/packages/contentstack-config/src/commands/config/get/base-branch.ts index cf8dbdfe0d..71c4b05f4b 100644 --- a/packages/contentstack-config/src/commands/config/get/base-branch.ts +++ b/packages/contentstack-config/src/commands/config/get/base-branch.ts @@ -25,7 +25,7 @@ export default class BranchGetCommand extends Command { cliux.print(`error: ${messageHandler.parse('CLI_CONFIG_BRANCH_LIST_NO_BRANCHES')}`, { color: 'red' }); } } catch (error) { - cliux.error('error', error); + cliux.error('Error', error); } } } diff --git a/packages/contentstack-config/src/commands/config/get/early-access-header.ts b/packages/contentstack-config/src/commands/config/get/early-access-header.ts index 883cae1220..93c2bff299 100644 --- a/packages/contentstack-config/src/commands/config/get/early-access-header.ts +++ b/packages/contentstack-config/src/commands/config/get/early-access-header.ts @@ -24,7 +24,7 @@ export default class GetEarlyAccessHeaderCommand extends Command { ]; cliux.table(tableHeaders, tableData); } else { - cliux.print(`No Early Access header found!`, { color: 'red' }); + cliux.print(`Early Access header not found.`, { color: 'red' }); } } catch (error) { this.log('Unable to retrieve the Early Access header config', error instanceof Error ? error.message : error); diff --git a/packages/contentstack-config/src/commands/config/get/log.ts b/packages/contentstack-config/src/commands/config/get/log.ts index 8b2307a12c..6195bc3850 100644 --- a/packages/contentstack-config/src/commands/config/get/log.ts +++ b/packages/contentstack-config/src/commands/config/get/log.ts @@ -34,7 +34,7 @@ export default class LogGetCommand extends Command { color: 'dim', }); } catch (error) { - cliux.error('error', error); + cliux.error('Error', error); } } } diff --git a/packages/contentstack-config/src/commands/config/get/region.ts b/packages/contentstack-config/src/commands/config/get/region.ts index 5763ae9ac9..d49295d5bc 100644 --- a/packages/contentstack-config/src/commands/config/get/region.ts +++ b/packages/contentstack-config/src/commands/config/get/region.ts @@ -10,18 +10,17 @@ export default class RegionGetCommand extends BaseCommand Number(u.trim())); if (utilizeValues.some((u: number) => isNaN(u) || u < 0 || u > 100)) { - cliux.error('Utilize percentages must be numbers between 0 and 100.'); + cliux.error('Utilization percentages must be numbers between 0 and 100.'); return; } if (limitName?.length > 0 && limitName[0]?.split(',')?.length !== utilizeValues.length) { - cliux.error('The number of utilization percentages must match the number of limit names provided.'); + cliux.error('The number of utilization percentages must match the number of limit names.'); return; } else { config.utilize = utilize.split(',').map((v: string) => v.trim()); @@ -85,7 +85,7 @@ export default class SetRateLimitCommand extends BaseCommand --cda --ui-host --name "India" --developer-hub ', '$ csdx config:set:region --cma --cda --ui-host --name "India" --personalize ', '$ csdx config:set:region --cma --cda --ui-host --name "India" --launch ', - '$ csdx config:set:region --cda --cma --ui-host --name "India" --developer-hub --launch --personalize ', + '$ csdx config:set:region --cma --cda --ui-host --name "India" --studio ', + '$ csdx config:set:region --cda --cma --ui-host --name "India" --developer-hub --launch --personalize --studio ', ]; static args: ArgInput = { @@ -78,6 +82,7 @@ export default class RegionSetCommand extends BaseCommand { it("Should execute 'config:set:region --AZURE-NA'", () => { const result = spawnSync('csdx', ['config:set:region', 'AZURE-NA'], { encoding: 'utf-8' }); const output = result.stdout + result.stderr; - expect(output).to.include('Region has been set to AZURE-NA'); - expect(output).to.include('CDA HOST: https://azure-na-cdn.contentstack.com'); - expect(output).to.include('CMA HOST: https://azure-na-api.contentstack.com'); + expect(output).to.include('CDA host: https://azure-na-cdn.contentstack.com'); + expect(output).to.include('CMA host: https://azure-na-api.contentstack.com'); }); it("Should execute 'config:get:region' and return the current region", () => { @@ -16,16 +15,15 @@ describe('ContentStack-Config Plugin Tests', () => { const output = result.stdout + result.stderr; expect(output).to.include('Currently using'); - expect(output).to.include('CDA HOST:'); - expect(output).to.include('CMA HOST:'); + expect(output).to.include('CDA host:'); + expect(output).to.include('CMA host:'); }); it("Should execute 'config:set:region AWS-NA' and set AWS-NA region", () => { const result = spawnSync('csdx', ['config:set:region', 'AWS-NA'], { encoding: 'utf-8' }); const output = result.stdout + result.stderr; - expect(output).to.include('Region has been set to AWS-NA'); - expect(output).to.include('CDA HOST: https://cdn.contentstack.io'); - expect(output).to.include('CMA HOST: https://api.contentstack.io'); + expect(output).to.include('CDA host: https://cdn.contentstack.io'); + expect(output).to.include('CMA host: https://api.contentstack.io'); }); }); diff --git a/packages/contentstack-config/test/unit/commands/rate-limit.test.ts b/packages/contentstack-config/test/unit/commands/rate-limit.test.ts index 429848964a..c1585b9213 100644 --- a/packages/contentstack-config/test/unit/commands/rate-limit.test.ts +++ b/packages/contentstack-config/test/unit/commands/rate-limit.test.ts @@ -59,7 +59,7 @@ describe('Rate Limit Commands', () => { const args = ['--org', 'test-org-id', '--utilize', '150', '--limit-name', 'getLimit']; await SetRateLimitCommand.run(args); - expect(errorMessage).to.equal('Utilize percentages must be numbers between 0 and 100.'); + expect(errorMessage).to.equal('Utilization percentages must be numbers between 0 and 100.'); expect(exitStub.calledWith(1)).to.be.true; @@ -74,7 +74,7 @@ describe('Rate Limit Commands', () => { await SetRateLimitCommand.run(args); expect(errorMessage).to.equal( - 'The number of utilization percentages must match the number of limit names provided.', + 'The number of utilization percentages must match the number of limit names.', ); expect(exitStub.calledWith(1)).to.be.true; @@ -90,7 +90,7 @@ describe('Rate Limit Commands', () => { await SetRateLimitCommand.run(args); expect(errorMessage).to.equal( - 'The number of utilization percentages must match the number of limit names provided.', + 'The number of utilization percentages must match the number of limit names.', ); expect(exitStub.calledWith(1)).to.be.true; diff --git a/packages/contentstack-config/test/unit/commands/region.test.ts b/packages/contentstack-config/test/unit/commands/region.test.ts index 7063bb954e..8802287981 100644 --- a/packages/contentstack-config/test/unit/commands/region.test.ts +++ b/packages/contentstack-config/test/unit/commands/region.test.ts @@ -17,6 +17,7 @@ describe('Region command', function () { developerHubUrl: 'https://developerhub-api.contentstack.com', launchHubUrl: 'https://launch-api.contentstack.com', personalizeUrl: 'https://personalization-api.contentstack.com', + composableStudioUrl: 'https://composable-studio-api.contentstack.com', }; let cliuxPrintStub: sinon.SinonStub; let configGetStub: sinon.SinonStub; @@ -69,6 +70,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://composable-studio-api.contentstack.com'); }); it('should set EU region', function () { @@ -80,6 +82,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://eu-developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://eu-personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://eu-launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://eu-composable-studio-api.contentstack.com'); }); it('should set AU region', function () { @@ -91,6 +94,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://au-developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://au-personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://au-launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://au-composable-studio-api.contentstack.com'); }); it('should set AWS-NA region', function () { @@ -102,6 +106,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://composable-studio-api.contentstack.com'); }); it('should set AWS-EU region', function () { @@ -113,6 +118,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://eu-developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://eu-personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://eu-launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://eu-composable-studio-api.contentstack.com'); }); it('should set AWS-AU region', function () { @@ -124,6 +130,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://au-developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://au-personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://au-launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://au-composable-studio-api.contentstack.com'); }); it('should set AZURE-NA region', function () { @@ -135,6 +142,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://azure-na-developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://azure-na-personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://azure-na-launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://azure-na-composable-studio-api.contentstack.com'); }); it('should set AZURE-EU region', function () { @@ -146,6 +154,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://azure-eu-developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://azure-eu-personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://azure-eu-launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://azure-eu-composable-studio-api.contentstack.com'); }); it('should set GCP-NA region', function () { @@ -157,6 +166,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://gcp-na-developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://gcp-na-personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://gcp-na-launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://gcp-na-composable-studio-api.contentstack.com'); }); it('should set GCP-EU region', function () { @@ -168,6 +178,7 @@ describe('Region command', function () { expect(result.developerHubUrl).to.equal('https://gcp-eu-developerhub-api.contentstack.com'); expect(result.personalizeUrl).to.equal('https://gcp-eu-personalize-api.contentstack.com'); expect(result.launchHubUrl).to.equal('https://gcp-eu-launch-api.contentstack.com'); + expect(result.composableStudioUrl).to.equal('https://gcp-eu-composable-studio-api.contentstack.com'); }); it('should return undefined for invalid region', function () { @@ -263,6 +274,18 @@ describe('Region command', function () { expect(result.launchHubUrl).to.equal(customRegion.launchHubUrl); }); + it('should set a custom region with studio URL', function () { + const customRegion = { + cma: 'https://custom-cma.com', + cda: 'https://custom-cda.com', + uiHost: 'https://custom-ui.com', + name: 'Custom Region', + composableStudioUrl: 'https://custom-composable-studio.com', + }; + const result = UserConfig.setCustomRegion(customRegion); + expect(result.composableStudioUrl).to.equal(customRegion.composableStudioUrl); + }); + it('should set a custom region with all optional URLs', function () { const customRegion = { cma: 'https://custom-cma.com', @@ -272,6 +295,7 @@ describe('Region command', function () { developerHubUrl: 'https://custom-developer-hub.com', personalizeUrl: 'https://custom-personalize.com', launchHubUrl: 'https://custom-launch.com', + composableStudioUrl: 'https://custom-composable-studio.com', }; const result = UserConfig.setCustomRegion(customRegion); expect(result).to.deep.equal(customRegion); @@ -286,6 +310,7 @@ describe('Region command', function () { developerHubUrl: 'https://custom-developer-hub.com', personalizeUrl: 'https://custom-personalize.com', launchHubUrl: 'https://custom-launch.com', + composableStudioUrl: 'https://custom-composable-studio.com', invalidProperty: 'should be removed', }; const result = UserConfig.setCustomRegion(customRegion); diff --git a/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js b/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js index 2c98a30506..96461c8320 100644 --- a/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js +++ b/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js @@ -395,7 +395,7 @@ class ExportToCsvCommand extends Command { token: listOfTokens[managementTokenAlias].token, }; } else if (managementTokenAlias) { - this.error('Provided management token alias not found in your config.!'); + this.error('The provided management token alias was not found in your config.'); } return { apiClient, diff --git a/packages/contentstack-export-to-csv/src/util/index.js b/packages/contentstack-export-to-csv/src/util/index.js index e1b2cfc7c9..e2b8923605 100644 --- a/packages/contentstack-export-to-csv/src/util/index.js +++ b/packages/contentstack-export-to-csv/src/util/index.js @@ -378,7 +378,7 @@ function getContentTypeCount(stackAPIClient) { } function exitProgram() { - debug('Exiting'); + debug('Exiting...'); // eslint-disable-next-line no-undef process.exit(); } diff --git a/packages/contentstack-export/messages/index.json b/packages/contentstack-export/messages/index.json index 16fda7957d..95095be474 100644 --- a/packages/contentstack-export/messages/index.json +++ b/packages/contentstack-export/messages/index.json @@ -11,58 +11,110 @@ "ASSET_VERSIONED_QUERY_FAILED": "Failed to query versioned asset data from the API", "ASSET_COUNT_QUERY_FAILED": "Failed to retrieve total asset count", -"CONTENT_TYPE_EXPORT_COMPLETE": "Content types exported successfully", -"CONTENT_TYPE_NO_TYPES": "No content types found", -"CONTENT_TYPE_EXPORT_FAILED": "Failed to export content types", -"CONTENT_TYPE_NO_TYPES_RETURNED": "API returned no content types for the given query", - -"ENVIRONMENT_EXPORT_COMPLETE": "Successfully exported %s environment(s)", -"ENVIRONMENT_EXPORT_SUCCESS": "Environment '%s' exported successfully", -"ENVIRONMENT_NOT_FOUND": "No environments found in the current stack", - -"EXTENSION_EXPORT_COMPLETE": "Successfully exported %s extension(s)", -"EXTENSION_EXPORT_SUCCESS": "Extension '%s' exported successfully", -"EXTENSION_NOT_FOUND": "No extensions found in the current stack", - -"GLOBAL_FIELDS_EXPORT_COMPLETE": "Successfully exported %s global field(s)", - -"LABELS_EXPORT_COMPLETE": "Successfully exported %s label(s)", -"LABEL_EXPORT_SUCCESS": "Label '%s' exported successfully", -"LABELS_NOT_FOUND": "No labels found in the current stack", - -"LOCALES_EXPORT_COMPLETE": "Successfully exported %s locale(s) including %s master locale(s)", - -"TAXONOMY_EXPORT_COMPLETE": "Successfully exported %s taxonomy entries", -"TAXONOMY_EXPORT_SUCCESS": "Taxonomy '%s' exported successfully", -"TAXONOMY_NOT_FOUND": "No taxonomies found in the current stack", - -"WEBHOOK_EXPORT_COMPLETE": "Successfully exported %s webhook(s)", -"WEBHOOK_EXPORT_SUCCESS": "Webhook '%s' exported successfully", -"WEBHOOK_NOT_FOUND": "No webhooks found in the current stack", - -"WORKFLOW_EXPORT_COMPLETE": "Successfully exported %s workflow(s)", -"WORKFLOW_EXPORT_SUCCESS": "Workflow '%s' exported successfully", -"WORKFLOW_NOT_FOUND": "No workflows found in the current stack", - -"PERSONALIZE_URL_NOT_SET": "Cannot export Personalize project: URL not configured", -"PERSONALIZE_SKIPPING_WITH_MANAGEMENT_TOKEN": "Skipping Personalize project export: Management token not supported", -"PERSONALIZE_MODULE_NOT_IMPLEMENTED": "Module '%s' implementation not found", -"PERSONALIZE_NOT_ENABLED": "Personalize feature is not enabled for this organization", - -"MARKETPLACE_APPS_EXPORT_COMPLETE": "Successfully exported %s marketplace app(s)", -"MARKETPLACE_APP_CONFIG_EXPORT": "Exporting configuration for app '%s'", -"MARKETPLACE_APP_CONFIG_SUCCESS": "Successfully exported configuration for app '%s'", -"MARKETPLACE_APP_EXPORT_SUCCESS": "Successfully exported app '%s'", -"MARKETPLACE_APPS_NOT_FOUND": "No marketplace apps found in the current stack", -"MARKETPLACE_APP_CONFIG_EXPORT_FAILED": "Failed to export configuration for app '%s'", -"MARKETPLACE_APP_MANIFEST_EXPORT_FAILED": "Failed to export manifest for app '%s'", - -"ENTRIES_EXPORT_COMPLETE": "Successfully exported entries (Content Type: %s, Locale: %s)", -"ENTRIES_EXPORT_SUCCESS": "All entries exported successfully", -"ENTRIES_VERSIONED_EXPORT_SUCCESS": "Successfully exported versioned entry (Content Type: %s, UID: %s, Locale: %s)", -"ENTRIES_EXPORT_VERSIONS_FAILED": "Failed to export versions for content type '%s' (UID: %s)", - -"BRANCH_EXPORT_FAILED": "Failed to export contents from branch (UID: %s)", + "CONTENT_TYPE_EXPORT_COMPLETE": "Content types exported successfully", + "CONTENT_TYPE_NO_TYPES": "No content types found", + "CONTENT_TYPE_EXPORT_FAILED": "Failed to export content types", + "CONTENT_TYPE_NO_TYPES_RETURNED": "API returned no content types for the given query", + "CONTENT_TYPE_EXPORT_COMPLETE": "Content types exported successfully", + "CONTENT_TYPE_NO_TYPES": "No content types found", + "CONTENT_TYPE_EXPORT_FAILED": "Failed to export content types", + "CONTENT_TYPE_NO_TYPES_RETURNED": "API returned no content types for the given query", + + "ENVIRONMENT_EXPORT_COMPLETE": "Successfully exported %s environment(s)", + "ENVIRONMENT_EXPORT_SUCCESS": "Environment '%s' exported successfully", + "ENVIRONMENT_NOT_FOUND": "No environments found in the current stack", + "ENVIRONMENT_EXPORT_COMPLETE": "Successfully exported %s environment(s)", + "ENVIRONMENT_EXPORT_SUCCESS": "Environment '%s' exported successfully", + "ENVIRONMENT_NOT_FOUND": "No environments found in the current stack", + + "EXTENSION_EXPORT_COMPLETE": "Successfully exported %s extension(s)", + "EXTENSION_EXPORT_SUCCESS": "Extension '%s' exported successfully", + "EXTENSION_NOT_FOUND": "No extensions found in the current stack", + "EXTENSION_EXPORT_COMPLETE": "Successfully exported %s extension(s)", + "EXTENSION_EXPORT_SUCCESS": "Extension '%s' exported successfully", + "EXTENSION_NOT_FOUND": "No extensions found in the current stack", + + "GLOBAL_FIELDS_EXPORT_COMPLETE": "Successfully exported %s global field(s)", + "GLOBAL_FIELDS_EXPORT_COMPLETE": "Successfully exported %s global field(s)", + + "LABELS_EXPORT_COMPLETE": "Successfully exported %s label(s)", + "LABEL_EXPORT_SUCCESS": "Label '%s' exported successfully", + "LABELS_NOT_FOUND": "No labels found in the current stack", + "LABELS_EXPORT_COMPLETE": "Successfully exported %s label(s)", + "LABEL_EXPORT_SUCCESS": "Label '%s' exported successfully", + "LABELS_NOT_FOUND": "No labels found in the current stack", + + "LOCALES_EXPORT_COMPLETE": "Successfully exported %s locale(s) including %s master locale(s)", + "LOCALES_EXPORT_COMPLETE": "Successfully exported %s locale(s) including %s master locale(s)", + + "TAXONOMY_EXPORT_COMPLETE": "Successfully exported %s taxonomy entries", + "TAXONOMY_EXPORT_SUCCESS": "Taxonomy '%s' exported successfully", + "TAXONOMY_NOT_FOUND": "No taxonomies found in the current stack", + "TAXONOMY_EXPORT_COMPLETE": "Successfully exported %s taxonomy entries", + "TAXONOMY_EXPORT_SUCCESS": "Taxonomy '%s' exported successfully", + "TAXONOMY_NOT_FOUND": "No taxonomies found in the current stack", + + "WEBHOOK_EXPORT_COMPLETE": "Successfully exported %s webhook(s)", + "WEBHOOK_EXPORT_SUCCESS": "Webhook '%s' exported successfully", + "WEBHOOK_NOT_FOUND": "No webhooks found in the current stack", + "WEBHOOK_EXPORT_COMPLETE": "Successfully exported %s webhook(s)", + "WEBHOOK_EXPORT_SUCCESS": "Webhook '%s' exported successfully", + "WEBHOOK_NOT_FOUND": "No webhooks found in the current stack", + + "WORKFLOW_EXPORT_COMPLETE": "Successfully exported %s workflow(s)", + "WORKFLOW_EXPORT_SUCCESS": "Workflow '%s' exported successfully", + "WORKFLOW_NOT_FOUND": "No workflows found in the current stack", + "WORKFLOW_EXPORT_COMPLETE": "Successfully exported %s workflow(s)", + "WORKFLOW_EXPORT_SUCCESS": "Workflow '%s' exported successfully", + "WORKFLOW_NOT_FOUND": "No workflows found in the current stack", + + "PERSONALIZE_URL_NOT_SET": "Cannot export Personalize project: URL not configured", + "PERSONALIZE_SKIPPING_WITH_MANAGEMENT_TOKEN": "Skipping Personalize project export: Management token not supported", + "PERSONALIZE_MODULE_NOT_IMPLEMENTED": "Module '%s' implementation not found", + "PERSONALIZE_NOT_ENABLED": "Personalize feature is not enabled for this organization", + "PERSONALIZE_URL_NOT_SET": "Cannot export Personalize project: URL not configured", + "PERSONALIZE_SKIPPING_WITH_MANAGEMENT_TOKEN": "Skipping Personalize project export: Management token not supported", + "PERSONALIZE_MODULE_NOT_IMPLEMENTED": "Module '%s' implementation not found", + "PERSONALIZE_NOT_ENABLED": "Personalize feature is not enabled for this organization", + + "MARKETPLACE_APPS_EXPORT_COMPLETE": "Successfully exported %s marketplace app(s)", + "MARKETPLACE_APP_CONFIG_EXPORT": "Exporting configuration for app '%s'", + "MARKETPLACE_APP_CONFIG_SUCCESS": "Successfully exported configuration for app '%s'", + "MARKETPLACE_APP_EXPORT_SUCCESS": "Successfully exported app '%s'", + "MARKETPLACE_APPS_NOT_FOUND": "No marketplace apps found in the current stack", + "MARKETPLACE_APP_CONFIG_EXPORT_FAILED": "Failed to export configuration for app '%s'", + "MARKETPLACE_APP_MANIFEST_EXPORT_FAILED": "Failed to export manifest for app '%s'", + + "COMPOSABLE_STUDIO_EXPORT_START": "Starting Studio project export...", + "COMPOSABLE_STUDIO_NOT_FOUND": "No Studio project found for this stack", + "COMPOSABLE_STUDIO_EXPORT_COMPLETE": "Successfully exported Studio project '%s'", + "COMPOSABLE_STUDIO_EXPORT_FAILED": "Failed to export Studio project: %s", + "COMPOSABLE_STUDIO_AUTH_REQUIRED": "To export Studio projects, you must be logged in", + "MARKETPLACE_APPS_EXPORT_COMPLETE": "Successfully exported %s marketplace app(s)", + "MARKETPLACE_APP_CONFIG_EXPORT": "Exporting configuration for app '%s'", + "MARKETPLACE_APP_CONFIG_SUCCESS": "Successfully exported configuration for app '%s'", + "MARKETPLACE_APP_EXPORT_SUCCESS": "Successfully exported app '%s'", + "MARKETPLACE_APPS_NOT_FOUND": "No marketplace apps found in the current stack", + "MARKETPLACE_APP_CONFIG_EXPORT_FAILED": "Failed to export configuration for app '%s'", + "MARKETPLACE_APP_MANIFEST_EXPORT_FAILED": "Failed to export manifest for app '%s'", + + "COMPOSABLE_STUDIO_EXPORT_START": "Starting Studio project export...", + "COMPOSABLE_STUDIO_NOT_FOUND": "No Studio project found for this stack", + "COMPOSABLE_STUDIO_EXPORT_COMPLETE": "Successfully exported Studio project '%s'", + "COMPOSABLE_STUDIO_EXPORT_FAILED": "Failed to export Studio project: %s", + "COMPOSABLE_STUDIO_AUTH_REQUIRED": "To export Studio projects, you must be logged in", + + "ENTRIES_EXPORT_COMPLETE": "Successfully exported entries (Content Type: %s, Locale: %s)", + "ENTRIES_EXPORT_SUCCESS": "All entries exported successfully", + "ENTRIES_VERSIONED_EXPORT_SUCCESS": "Successfully exported versioned entry (Content Type: %s, UID: %s, Locale: %s)", + "ENTRIES_EXPORT_VERSIONS_FAILED": "Failed to export versions for content type '%s' (UID: %s)", + "ENTRIES_EXPORT_COMPLETE": "Successfully exported entries (Content Type: %s, Locale: %s)", + "ENTRIES_EXPORT_SUCCESS": "All entries exported successfully", + "ENTRIES_VERSIONED_EXPORT_SUCCESS": "Successfully exported versioned entry (Content Type: %s, UID: %s, Locale: %s)", + "ENTRIES_EXPORT_VERSIONS_FAILED": "Failed to export versions for content type '%s' (UID: %s)", + + "BRANCH_EXPORT_FAILED": "Failed to export contents from branch (UID: %s)", + "BRANCH_EXPORT_FAILED": "Failed to export contents from branch (UID: %s)", "ROLES_NO_CUSTOM_ROLES": "No custom roles found in the current stack", "ROLES_EXPORTING_ROLE": "Exporting role '%s'", diff --git a/packages/contentstack-export/src/commands/cm/stacks/export.ts b/packages/contentstack-export/src/commands/cm/stacks/export.ts index 5304f4d059..87e0d8bea8 100644 --- a/packages/contentstack-export/src/commands/cm/stacks/export.ts +++ b/packages/contentstack-export/src/commands/cm/stacks/export.ts @@ -79,7 +79,8 @@ export default class ExportCommand extends Command { module: flags.string({ char: 'm', description: - '[optional] Specific module name. If not specified, the export command will export all the modules to the stack. The available modules are assets, content-types, entries, environments, extensions, marketplace-apps, global-fields, labels, locales, webhooks, workflows, custom-roles, and taxonomies.', + '[optional] Specific module name. If not specified, the export command will export all the modules to the stack. The available modules are assets, content-types, entries, environments, extensions, marketplace-apps, global-fields, labels, locales, webhooks, workflows, custom-roles, taxonomies, and studio.', + '[optional] Specific module name. If not specified, the export command will export all the modules to the stack. The available modules are assets, content-types, entries, environments, extensions, marketplace-apps, global-fields, labels, locales, webhooks, workflows, custom-roles, taxonomies, and studio.', parse: printFlagDeprecation(['-m'], ['--module']), }), 'content-types': flags.string({ @@ -138,8 +139,8 @@ export default class ExportCommand extends Command { `The content of the stack ${exportConfig.apiKey} has been exported successfully!`, exportConfig.context, ); - log.info(`The exported content has been stored at '${exportDir}'`, exportConfig.context); - log.success(`The log has been stored at '${getLogPath()}'`, exportConfig.context); + log.info(`The exported content has been stored at '${exportDir}'.`, exportConfig.context); + log.success(`The log has been stored at '${getLogPath()}'.`, exportConfig.context); // Print comprehensive summary at the end if (!exportConfig.branches) CLIProgressManager.printGlobalSummary(); @@ -154,7 +155,7 @@ export default class ExportCommand extends Command { handleAndLogError(error); if (!configHandler.get('log')?.showConsoleLogs) { cliux.print(`Error: ${error}`, { color: 'red' }); - cliux.print(`The log has been stored at '${getLogPath()}'`, { color: 'green' }); + cliux.print(`The log has been stored at '${getLogPath()}'.`, { color: 'green' }); } } } @@ -185,5 +186,13 @@ export default class ExportCommand extends Command { if (this.personalizeUrl) { exportConfig.modules.personalize.baseURL[exportConfig.region.name] = this.personalizeUrl; } + + if (this.composableStudioUrl) { + exportConfig.modules['composable-studio'].apiBaseUrl = this.composableStudioUrl; + } + + if (this.composableStudioUrl) { + exportConfig.modules['composable-studio'].apiBaseUrl = this.composableStudioUrl; + } } } diff --git a/packages/contentstack-export/src/config/index.ts b/packages/contentstack-export/src/config/index.ts index dee5213920..14fe590b32 100644 --- a/packages/contentstack-export/src/config/index.ts +++ b/packages/contentstack-export/src/config/index.ts @@ -39,6 +39,7 @@ const config: DefaultConfig = { 'entries', 'labels', 'marketplace-apps', + 'composable-studio', ], locales: { dirName: 'locales', @@ -212,6 +213,12 @@ const config: DefaultConfig = { dirName: 'marketplace_apps', fileName: 'marketplace_apps.json', }, + 'composable-studio': { + dirName: 'composable_studio', + fileName: 'composable_studio.json', + apiBaseUrl: 'https://composable-studio-api.contentstack.com', + apiVersion: 'v1', + }, taxonomies: { dirName: 'taxonomies', fileName: 'taxonomies.json', diff --git a/packages/contentstack-export/src/export/module-exporter.ts b/packages/contentstack-export/src/export/module-exporter.ts index 90bc73f854..56a39bbd0c 100644 --- a/packages/contentstack-export/src/export/module-exporter.ts +++ b/packages/contentstack-export/src/export/module-exporter.ts @@ -104,7 +104,8 @@ class ModuleExporter { } async exportByModuleByName(moduleName: Modules) { - log.info(`Exporting module: ${moduleName}`, this.exportConfig.context); + log.info(`Exporting module: '${moduleName}'...`, this.exportConfig.context); + log.info(`Exporting module: '${moduleName}'...`, this.exportConfig.context); // export the modules by name // calls the module runner which inturn calls the module itself await startModuleExport({ @@ -122,9 +123,10 @@ class ModuleExporter { } if (!this.exportConfig.skipDependencies) { - const { - modules: { [moduleName]: { dependencies = [] } = {} }, - } = this.exportConfig; + const moduleConfig = this.exportConfig.modules[moduleName as keyof typeof this.exportConfig.modules]; + const dependencies = (moduleConfig as any)?.dependencies || []; + const moduleConfig = this.exportConfig.modules[moduleName as keyof typeof this.exportConfig.modules]; + const dependencies = (moduleConfig as any)?.dependencies || []; if (dependencies.length > 0) { exportModules = exportModules.concat(dependencies); diff --git a/packages/contentstack-export/src/export/modules/composable-studio.ts b/packages/contentstack-export/src/export/modules/composable-studio.ts new file mode 100644 index 0000000000..8faff8c2b5 --- /dev/null +++ b/packages/contentstack-export/src/export/modules/composable-studio.ts @@ -0,0 +1,138 @@ +import { resolve as pResolve } from 'node:path'; +import { + cliux, + isAuthenticated, + log, + messageHandler, + handleAndLogError, + HttpClient, + authenticationHandler, +} from '@contentstack/cli-utilities'; + +import { fsUtil, getOrgUid } from '../../utils'; +import { ModuleClassParams, ComposableStudioConfig, ExportConfig, ComposableStudioProject } from '../../types'; + +export default class ExportComposableStudio { + protected composableStudioConfig: ComposableStudioConfig; + protected composableStudioProject: ComposableStudioProject | null = null; + protected apiClient: HttpClient; + public composableStudioPath: string; + public exportConfig: ExportConfig; + + constructor({ exportConfig }: Omit) { + this.exportConfig = exportConfig; + this.composableStudioConfig = exportConfig.modules['composable-studio']; + this.exportConfig.context.module = 'composable-studio'; + + // Initialize HttpClient with Studio API base URL + this.apiClient = new HttpClient(); + this.apiClient.baseUrl(`${this.composableStudioConfig.apiBaseUrl}/${this.composableStudioConfig.apiVersion}`); + } + + async start(): Promise { + log.debug('Starting Studio project export process...', this.exportConfig.context); + + if (!isAuthenticated()) { + cliux.print( + 'WARNING!!! To export Studio projects, you must be logged in. Please check csdx auth:login --help to log in', + { color: 'yellow' }, + ); + return Promise.resolve(); + } + + this.composableStudioPath = pResolve( + this.exportConfig.data, + this.exportConfig.branchName || '', + this.composableStudioConfig.dirName, + ); + log.debug(`Studio folder path: ${this.composableStudioPath}`, this.exportConfig.context); + + await fsUtil.makeDirectory(this.composableStudioPath); + log.debug('Created Studio directory', this.exportConfig.context); + + this.exportConfig.org_uid = this.exportConfig.org_uid || (await getOrgUid(this.exportConfig)); + log.debug(`Organization UID: ${this.exportConfig.org_uid}`, this.exportConfig.context); + + await this.exportProjects(); + log.debug('Studio project export process completed', this.exportConfig.context); + } + + /** + * Export Studio projects connected to the current stack + */ + async exportProjects(): Promise { + log.debug('Starting Studio project export...', this.exportConfig.context); + + try { + // Get authentication details - following personalization-api-adapter pattern + log.debug('Initializing Studio API authentication...', this.exportConfig.context); + await authenticationHandler.getAuthDetails(); + const token = authenticationHandler.accessToken; + log.debug( + `Authentication type: ${authenticationHandler.isOauthEnabled ? 'OAuth' : 'Token'}`, + this.exportConfig.context, + ); + + // Set authentication headers based on auth type + if (authenticationHandler.isOauthEnabled) { + log.debug('Setting OAuth authorization header', this.exportConfig.context); + this.apiClient.headers({ authorization: token }); + } else { + log.debug('Setting authtoken header', this.exportConfig.context); + this.apiClient.headers({ authtoken: token }); + } + + // Set organization_uid header + this.apiClient.headers({ + organization_uid: this.exportConfig.org_uid, + Accept: 'application/json', + }); + + const apiUrl = '/projects'; + log.debug( + `Fetching projects from: ${this.composableStudioConfig.apiBaseUrl}${apiUrl}`, + this.exportConfig.context, + ); + + // Make API call to fetch projects using HttpClient + const response = await this.apiClient.get(apiUrl); + + if (response.status < 200 || response.status >= 300) { + throw new Error(`API call failed with status ${response.status}: ${JSON.stringify(response.data)}`); + } + + const data = response.data; + log.debug(`Fetched ${data.projects?.length || 0} total projects`, this.exportConfig.context); + + // Filter projects connected to this stack + const connectedProject = data.projects?.filter( + (project: ComposableStudioProject) => project.connectedStackApiKey === this.exportConfig.apiKey, + ); + + if (!connectedProject || connectedProject.length === 0) { + log.info(messageHandler.parse('COMPOSABLE_STUDIO_NOT_FOUND'), this.exportConfig.context); + return; + } + + // Use the first connected project (stacks should have only one project) + this.composableStudioProject = connectedProject[0]; + log.debug(`Found Studio project: ${this.composableStudioProject.name}`, this.exportConfig.context); + + // Write the project to file + const composableStudioFilePath = pResolve(this.composableStudioPath, this.composableStudioConfig.fileName); + log.debug(`Writing Studio project to: ${composableStudioFilePath}`, this.exportConfig.context); + + fsUtil.writeFile(composableStudioFilePath, this.composableStudioProject as unknown as Record); + + log.success( + messageHandler.parse('COMPOSABLE_STUDIO_EXPORT_COMPLETE', this.composableStudioProject.name), + this.exportConfig.context, + ); + } catch (error: any) { + log.debug('Error occurred while exporting Studio project', this.exportConfig.context); + handleAndLogError(error, { + ...this.exportConfig.context, + }); + } + } +} diff --git a/packages/contentstack-export/src/export/modules/content-types.ts b/packages/contentstack-export/src/export/modules/content-types.ts index 28c6f1cbdd..26ad70726d 100644 --- a/packages/contentstack-export/src/export/modules/content-types.ts +++ b/packages/contentstack-export/src/export/modules/content-types.ts @@ -123,7 +123,8 @@ export default class ContentTypesExport extends BaseClass { } sanitizeAttribs(contentTypes: Record[]): Record[] { - log.debug(`Sanitizing ${contentTypes.length} content types`, this.exportConfig.context); + log.debug(`Sanitizing ${contentTypes?.length} content types...`, this.exportConfig.context); + log.debug(`Sanitizing ${contentTypes?.length} content types...`, this.exportConfig.context); const updatedContentTypes: Record[] = []; @@ -140,7 +141,8 @@ export default class ContentTypesExport extends BaseClass { } async writeContentTypes(contentTypes: Record[]) { - log.debug(`Writing ${contentTypes.length} content types to disk`, this.exportConfig.context); + log.debug(`Writing ${contentTypes?.length} content types to disk...`, this.exportConfig.context); + log.debug(`Writing ${contentTypes?.length} content types to disk...`, this.exportConfig.context); const writeWithProgress = (contentType: Record) => { const filename = `${contentType.uid === 'schema' ? 'schema|1' : contentType.uid}.json`; diff --git a/packages/contentstack-export/src/export/modules/custom-roles.ts b/packages/contentstack-export/src/export/modules/custom-roles.ts index 48bb96fdcc..aec20a17b4 100644 --- a/packages/contentstack-export/src/export/modules/custom-roles.ts +++ b/packages/contentstack-export/src/export/modules/custom-roles.ts @@ -37,7 +37,7 @@ export default class ExportCustomRoles extends BaseClass { async start(): Promise { try { - log.debug('Starting custom roles export process...', this.exportConfig.context); + log.debug('Starting export process for custom roles...', this.exportConfig.context); const [totalRoles, totalLocales] = await this.withLoadingSpinner( 'CUSTOM-ROLES: Analyzing roles and locales...', @@ -121,17 +121,19 @@ export default class ExportCustomRoles extends BaseClass { .role() .fetchAll({ include_rules: true, include_permissions: true }) .then((data: any) => { - log.debug(`Fetched ${data.items?.length || 0} total roles`, this.exportConfig.context); + log.debug(`Fetched ${data.items?.length || 0} roles from the stack.`, this.exportConfig.context); + log.debug(`Fetched ${data.items?.length || 0} roles from the stack.`, this.exportConfig.context); return data; }) .catch((err: any) => { - log.debug('Error occurred while fetching roles', this.exportConfig.context); + log.debug('An error occurred while fetching roles.', this.exportConfig.context); + log.debug('An error occurred while fetching roles.', this.exportConfig.context); return handleAndLogError(err, { ...this.exportConfig.context }); }); const customRoles = roles.items.filter((role: any) => !this.existingRoles[role.name]); log.debug( - `Found ${customRoles.length} custom roles out of ${roles.items?.length || 0} total roles`, + `Found ${customRoles.length} custom roles from ${roles.items?.length || 0} total roles.`, this.exportConfig.context, ); @@ -149,7 +151,8 @@ export default class ExportCustomRoles extends BaseClass { }); const customRolesFilePath = pResolve(this.rolesFolderPath, this.customRolesConfig.fileName); - log.debug(`Writing custom roles to: ${customRolesFilePath}`, this.exportConfig.context); + log.debug(`Writing custom roles to: ${customRolesFilePath}.`, this.exportConfig.context); + log.debug(`Writing custom roles to: ${customRolesFilePath}.`, this.exportConfig.context); fsUtil.writeFile(customRolesFilePath, this.customRoles); } @@ -161,11 +164,13 @@ export default class ExportCustomRoles extends BaseClass { .query({}) .find() .then((data: any) => { - log.debug(`Fetched ${data?.items?.length || 0} locales`, this.exportConfig.context); + log.debug(`Fetched ${data?.items?.length || 0} locales.`, this.exportConfig.context); + log.debug(`Fetched ${data?.items?.length || 0} locales.`, this.exportConfig.context); return data; }) .catch((err: any) => { - log.debug('Error occurred while fetching locales', this.exportConfig.context); + log.debug('An error occurred while fetching locales.', this.exportConfig.context); + log.debug('An error occurred while fetching locales.', this.exportConfig.context); return handleAndLogError(err, { ...this.exportConfig.context }); }); @@ -176,8 +181,8 @@ export default class ExportCustomRoles extends BaseClass { // Track progress for each locale this.progressManager?.tick(true, `locale: ${locale.name}`, null, PROCESS_NAMES.FETCH_LOCALES); } - - log.debug(`Mapped ${Object.keys(this.sourceLocalesMap || {}).length} locales`, this.exportConfig.context); + + log.debug(`Mapped ${Object.keys(this.sourceLocalesMap || {}).length} source locales.`, this.exportConfig.context); } async getCustomRolesLocales() { @@ -190,11 +195,12 @@ export default class ExportCustomRoles extends BaseClass { const rulesLocales = find(customRole.rules, (rule: any) => rule.module === 'locale'); if (rulesLocales?.locales?.length) { log.debug( - `Found ${rulesLocales.locales.length} locales for role: ${customRole.name}`, + `Found ${rulesLocales.locales.length} locales for the role: ${customRole.name}.`, this.exportConfig.context, ); forEach(rulesLocales.locales, (locale: any) => { - log.debug(`Adding locale ${locale} to custom roles mapping`, this.exportConfig.context); + log.debug(`Adding locale ${locale} to the custom roles mapping.`, this.exportConfig.context); + log.debug(`Adding locale ${locale} to the custom roles mapping.`, this.exportConfig.context); this.localesMap[locale] = 1; }); } @@ -215,7 +221,8 @@ export default class ExportCustomRoles extends BaseClass { log.debug(`Writing custom roles locales to: ${this.customRolesLocalesFilepath}`, this.exportConfig.context); fsUtil.writeFile(this.customRolesLocalesFilepath, this.localesMap); } else { - log.debug('No custom role locales found to process', this.exportConfig.context); + log.debug('No custom role locales found to process.', this.exportConfig.context); + log.debug('No custom role locales found to process.', this.exportConfig.context); } // Track progress for mapping completion diff --git a/packages/contentstack-export/src/export/modules/entries.ts b/packages/contentstack-export/src/export/modules/entries.ts index 616a069f55..e4187af9ba 100644 --- a/packages/contentstack-export/src/export/modules/entries.ts +++ b/packages/contentstack-export/src/export/modules/entries.ts @@ -322,7 +322,7 @@ export default class EntriesExport extends BaseClass { log.debug('Initialized FsUtility for writing entries', this.exportConfig.context); } - log.debug(`Writing ${entriesSearchResponse.items.length} entries to file`, this.exportConfig.context); + log.debug(`Writing ${entriesSearchResponse.items.length} entries to file...`, this.exportConfig.context); this.entriesFileHelper.writeIntoFile(entriesSearchResponse.items, { mapKeyVal: true }); // Track progress for individual entries @@ -331,14 +331,14 @@ export default class EntriesExport extends BaseClass { }); if (this.entriesConfig.exportVersions) { - log.debug('Exporting entry versions is enabled', this.exportConfig.context); + log.debug('Exporting entry versions is enabled.', this.exportConfig.context); let versionedEntryPath = path.join( sanitizePath(this.entriesDirPath), sanitizePath(options.contentType), sanitizePath(options.locale), 'versions', ); - log.debug(`Creating versioned entries directory at: ${versionedEntryPath}`, this.exportConfig.context); + log.debug(`Creating versioned entries directory at: ${versionedEntryPath}.`, this.exportConfig.context); fsUtil.makeDirectory(versionedEntryPath); await this.fetchEntriesVersions(entriesSearchResponse.items, { locale: options.locale, @@ -393,7 +393,7 @@ export default class EntriesExport extends BaseClass { entries: any, options: { locale: string; contentType: string; versionedEntryPath: string }, ): Promise { - log.debug(`Fetching versions for ${entries.length} entries`, this.exportConfig.context); + log.debug(`Fetching versions for ${entries.length} entries...`, this.exportConfig.context); const onSuccess = ({ response, apiData: entry }: any) => { const versionFilePath = path.join(sanitizePath(options.versionedEntryPath), sanitizePath(`${entry.uid}.json`)); @@ -459,7 +459,7 @@ export default class EntriesExport extends BaseClass { return new Promise(async (resolve, reject) => { return await this.getEntryByVersion(apiParams.queryParam, entry) .then((response) => { - log.debug(`Successfully fetched versions for entry: ${entry.uid}`, this.exportConfig.context); + log.debug(`Successfully fetched versions for entry UID: ${entry.uid}`, this.exportConfig.context); apiParams.resolve({ response, apiData: entry, @@ -467,7 +467,7 @@ export default class EntriesExport extends BaseClass { resolve(true); }) .catch((error) => { - log.debug(`Failed to fetch versions for entry: ${entry.uid}`, this.exportConfig.context); + log.debug(`Failed to fetch versions for entry UID: ${entry.uid}`, this.exportConfig.context); apiParams.reject({ error, apiData: entry, @@ -490,7 +490,7 @@ export default class EntriesExport extends BaseClass { version: entry._version, }; - log.debug(`Fetching entry version ${entry._version} for uid: ${entry.uid}`, this.exportConfig.context); + log.debug(`Fetching entry version ${entry._version} for entry UID: '${entry.uid}'.`, this.exportConfig.context); const entryResponse = await this.stackAPIClient .contentType(options.contentType) diff --git a/packages/contentstack-export/src/export/modules/environments.ts b/packages/contentstack-export/src/export/modules/environments.ts index 68961f3e17..35c8c51339 100644 --- a/packages/contentstack-export/src/export/modules/environments.ts +++ b/packages/contentstack-export/src/export/modules/environments.ts @@ -80,9 +80,11 @@ export default class ExportEnvironments extends BaseClass { async getEnvironments(skip = 0): Promise { if (skip) { this.qs.skip = skip; - log.debug(`Fetching environments with skip: ${skip}`, this.exportConfig.context); + log.debug(`Fetching environments with skip value: ${skip}`, this.exportConfig.context); + log.debug(`Fetching environments with skip value: ${skip}`, this.exportConfig.context); } else { - log.debug('Fetching environments with initial query', this.exportConfig.context); + log.debug('Fetching environments with initial query...', this.exportConfig.context); + log.debug('Fetching environments with initial query...', this.exportConfig.context); } log.debug(`Query parameters: ${JSON.stringify(this.qs)}`, this.exportConfig.context); @@ -96,28 +98,34 @@ export default class ExportEnvironments extends BaseClass { log.debug(`Fetched ${items?.length || 0} environments out of total ${count}`, this.exportConfig.context); if (items?.length) { - log.debug(`Processing ${items.length} environments`, this.exportConfig.context); + log.debug(`Processing ${items.length} environments.`, this.exportConfig.context); + log.debug(`Processing ${items.length} environments.`, this.exportConfig.context); this.sanitizeAttribs(items); skip += this.environmentConfig.limit || 100; if (skip >= count) { - log.debug('Completed fetching all environments', this.exportConfig.context); + log.debug('Completed fetching all environments.', this.exportConfig.context); + log.debug('Completed fetching all environments.', this.exportConfig.context); return; } - log.debug(`Continuing to fetch environments with skip: ${skip}`, this.exportConfig.context); + log.debug(`Continuing environment fetch with skip value: ${skip}`, this.exportConfig.context); + log.debug(`Continuing environment fetch with skip value: ${skip}`, this.exportConfig.context); return await this.getEnvironments(skip); } else { - log.debug('No environments found to process', this.exportConfig.context); + log.debug('No environments found to process.', this.exportConfig.context); + log.debug('No environments found to process.', this.exportConfig.context); } }) .catch((error: any) => { - log.debug('Error occurred while fetching environments', this.exportConfig.context); + log.debug('An error occurred while fetching environments.', this.exportConfig.context); + log.debug('An error occurred while fetching environments.', this.exportConfig.context); handleAndLogError(error, { ...this.exportConfig.context }); }); } sanitizeAttribs(environments: Record[]) { - log.debug(`Sanitizing ${environments.length} environments`, this.exportConfig.context); - + log.debug(`Sanitizing ${environments.length} environments...`, this.exportConfig.context); + log.debug(`Sanitizing ${environments.length} environments...`, this.exportConfig.context); + for (let index = 0; index < environments?.length; index++) { const envUID = environments[index].uid; const envName = environments[index]?.name; diff --git a/packages/contentstack-export/src/export/modules/extensions.ts b/packages/contentstack-export/src/export/modules/extensions.ts index 7665aa30c9..0d2d6d11a5 100644 --- a/packages/contentstack-export/src/export/modules/extensions.ts +++ b/packages/contentstack-export/src/export/modules/extensions.ts @@ -38,7 +38,7 @@ export default class ExportExtensions extends BaseClass { this.extensionConfig.dirName, ); await fsUtil.makeDirectory(this.extensionsFolderPath); - log.debug(`Extensions folder path: ${this.extensionsFolderPath}`, this.exportConfig.context); + log.debug(`Extensions folder path is: ${this.extensionsFolderPath}`, this.exportConfig.context); // Get count for progress tracking const countResponse = await this.stack @@ -64,7 +64,7 @@ export default class ExportExtensions extends BaseClass { log.info(messageHandler.parse('EXTENSION_NOT_FOUND'), this.exportConfig.context); } else { const extensionsFilePath = pResolve(this.extensionsFolderPath, this.extensionConfig.fileName); - log.debug(`Writing extensions to: ${extensionsFilePath}`, this.exportConfig.context); + log.debug(`Writing extensions to: ${extensionsFilePath}.`, this.exportConfig.context); fsUtil.writeFile(extensionsFilePath, this.extensions); log.success( messageHandler.parse('EXTENSION_EXPORT_COMPLETE', Object.keys(this.extensions || {}).length), @@ -81,9 +81,11 @@ export default class ExportExtensions extends BaseClass { async getExtensions(skip = 0): Promise { if (skip) { this.qs.skip = skip; - log.debug(`Fetching extensions with skip: ${skip}`, this.exportConfig.context); + log.debug(`Fetching extensions with skip value: ${skip}`, this.exportConfig.context); + log.debug(`Fetching extensions with skip value: ${skip}`, this.exportConfig.context); } else { - log.debug('Fetching extensions with initial query', this.exportConfig.context); + log.debug('Fetching extensions with initial query...', this.exportConfig.context); + log.debug('Fetching extensions with initial query...', this.exportConfig.context); } log.debug(`Query parameters: ${JSON.stringify(this.qs)}`, this.exportConfig.context); @@ -97,21 +99,26 @@ export default class ExportExtensions extends BaseClass { log.debug(`Fetched ${items?.length || 0} extensions out of total ${count}`, this.exportConfig.context); if (items?.length) { - log.debug(`Processing ${items.length} extensions`, this.exportConfig.context); + log.debug(`Processing ${items.length} extensions...`, this.exportConfig.context); + log.debug(`Processing ${items.length} extensions...`, this.exportConfig.context); this.sanitizeAttribs(items); skip += this.extensionConfig.limit || 100; if (skip >= count) { - log.debug('Completed fetching all extensions', this.exportConfig.context); + log.debug('Completed fetching all extensions.', this.exportConfig.context); + log.debug('Completed fetching all extensions.', this.exportConfig.context); return; } - log.debug(`Continuing to fetch extensions with skip: ${skip}`, this.exportConfig.context); + log.debug(`Continuing to fetch extensions with skip: ${skip}.`, this.exportConfig.context); + log.debug(`Continuing to fetch extensions with skip: ${skip}.`, this.exportConfig.context); return await this.getExtensions(skip); } else { - log.debug('No extensions found to process', this.exportConfig.context); + log.debug('No extensions found to process.', this.exportConfig.context); + log.debug('No extensions found to process.', this.exportConfig.context); } }) .catch((error: any) => { - log.debug('Error occurred while fetching extensions', this.exportConfig.context); + log.debug('An error occurred while fetching extensions.', this.exportConfig.context); + log.debug('An error occurred while fetching extensions.', this.exportConfig.context); handleAndLogError(error, { ...this.exportConfig.context }); }); } @@ -131,7 +138,7 @@ export default class ExportExtensions extends BaseClass { } log.debug( - `Sanitization complete. Total extensions processed: ${Object.keys(this.extensions || {}).length}`, + `Sanitization complete. Total extensions processed: ${Object.keys(this.extensions || {}).length}.`, this.exportConfig.context, ); } diff --git a/packages/contentstack-export/src/export/modules/global-fields.ts b/packages/contentstack-export/src/export/modules/global-fields.ts index 0d74f8da3b..03311db300 100644 --- a/packages/contentstack-export/src/export/modules/global-fields.ts +++ b/packages/contentstack-export/src/export/modules/global-fields.ts @@ -74,7 +74,8 @@ export default class GlobalFieldsExport extends BaseClass { await this.getGlobalFields(); const globalFieldsFilePath = path.join(this.globalFieldsDirPath, this.globalFieldsConfig.fileName); - log.debug(`Writing global fields to: ${globalFieldsFilePath}`, this.exportConfig.context); + log.debug(`Writing global fields to: '${globalFieldsFilePath}'`, this.exportConfig.context); + log.debug(`Writing global fields to: '${globalFieldsFilePath}'`, this.exportConfig.context); fsUtil.writeFile(globalFieldsFilePath, this.globalFields); log.success( @@ -84,7 +85,8 @@ export default class GlobalFieldsExport extends BaseClass { this.completeProgress(true); } catch (error) { - log.debug('Error occurred during global fields export', this.exportConfig.context); + log.debug('An error occurred during global fields export.', this.exportConfig.context); + log.debug('An error occurred during global fields export.', this.exportConfig.context); handleAndLogError(error, { ...this.exportConfig.context }); this.completeProgress(false, error?.message || 'Global fields export failed'); } @@ -93,7 +95,8 @@ export default class GlobalFieldsExport extends BaseClass { async getGlobalFields(skip: number = 0): Promise { if (skip) { this.qs.skip = skip; - log.debug(`Fetching global fields with skip: ${skip}`, this.exportConfig.context); + log.debug(`Fetching global fields with skip: ${skip}.`, this.exportConfig.context); + log.debug(`Fetching global fields with skip: ${skip}.`, this.exportConfig.context); } log.debug(`Query parameters: ${JSON.stringify(this.qs)}`, this.exportConfig.context); @@ -107,17 +110,21 @@ export default class GlobalFieldsExport extends BaseClass { ); if (Array.isArray(globalFieldsFetchResponse.items) && globalFieldsFetchResponse.items.length > 0) { - log.debug(`Processing ${globalFieldsFetchResponse.items.length} global fields`, this.exportConfig.context); + log.debug(`Processing ${globalFieldsFetchResponse.items.length} global fields...`, this.exportConfig.context); + log.debug(`Processing ${globalFieldsFetchResponse.items.length} global fields...`, this.exportConfig.context); this.sanitizeAttribs(globalFieldsFetchResponse.items); skip += this.globalFieldsConfig.limit || 100; if (skip >= globalFieldsFetchResponse.count) { - log.debug('Completed fetching all global fields', this.exportConfig.context); + log.debug('Completed fetching all global fields.', this.exportConfig.context); + log.debug('Completed fetching all global fields.', this.exportConfig.context); return; } - log.debug(`Continuing to fetch global fields with skip: ${skip}`, this.exportConfig.context); + log.debug(`Continuing to fetch global fields with skip: ${skip}.`, this.exportConfig.context); + log.debug(`Continuing to fetch global fields with skip: ${skip}.`, this.exportConfig.context); return await this.getGlobalFields(skip); } else { - log.debug('No global fields found to process', this.exportConfig.context); + log.debug('No global fields found to process.', this.exportConfig.context); + log.debug('No global fields found to process.', this.exportConfig.context); } } diff --git a/packages/contentstack-export/src/export/modules/labels.ts b/packages/contentstack-export/src/export/modules/labels.ts index aa9edab2bf..8f5081933e 100644 --- a/packages/contentstack-export/src/export/modules/labels.ts +++ b/packages/contentstack-export/src/export/modules/labels.ts @@ -82,9 +82,11 @@ export default class ExportLabels extends BaseClass { async getLabels(skip = 0): Promise { if (skip) { this.qs.skip = skip; - log.debug(`Fetching labels with skip: ${skip}`, this.exportConfig.context); + log.debug(`Fetching labels with skip: ${skip}.`, this.exportConfig.context); + log.debug(`Fetching labels with skip: ${skip}.`, this.exportConfig.context); } else { - log.debug('Fetching labels with initial query', this.exportConfig.context); + log.debug('Fetching labels with initial query...', this.exportConfig.context); + log.debug('Fetching labels with initial query...', this.exportConfig.context); } log.debug(`Query parameters: ${JSON.stringify(this.qs)}`, this.exportConfig.context); @@ -98,17 +100,21 @@ export default class ExportLabels extends BaseClass { log.debug(`Fetched ${items?.length || 0} labels out of total ${count}`, this.exportConfig.context); if (items?.length) { - log.debug(`Processing ${items.length} labels`, this.exportConfig.context); + log.debug(`Processing ${items.length} labels...`, this.exportConfig.context); + log.debug(`Processing ${items.length} labels...`, this.exportConfig.context); this.sanitizeAttribs(items); skip += this.labelConfig.limit || 100; if (skip >= count) { - log.debug('Completed fetching all labels', this.exportConfig.context); + log.debug('Completed fetching all labels.', this.exportConfig.context); + log.debug('Completed fetching all labels.', this.exportConfig.context); return; } - log.debug(`Continuing to fetch labels with skip: ${skip}`, this.exportConfig.context); + log.debug(`Continuing to fetch labels with skip: ${skip}.`, this.exportConfig.context); + log.debug(`Continuing to fetch labels with skip: ${skip}.`, this.exportConfig.context); return await this.getLabels(skip); } else { - log.debug('No labels found to process', this.exportConfig.context); + log.debug('No labels found to process.', this.exportConfig.context); + log.debug('No labels found to process.', this.exportConfig.context); } }) .catch((error: any) => { diff --git a/packages/contentstack-export/src/export/modules/locales.ts b/packages/contentstack-export/src/export/modules/locales.ts index 58cc3960ee..e73973d0ad 100644 --- a/packages/contentstack-export/src/export/modules/locales.ts +++ b/packages/contentstack-export/src/export/modules/locales.ts @@ -105,7 +105,8 @@ export default class LocaleExport extends BaseClass { async getLocales(skip: number = 0): Promise { if (skip) { this.qs.skip = skip; - log.debug(`Fetching locales with skip: ${skip}`, this.exportConfig.context); + log.debug(`Fetching locales with skip: ${skip}.`, this.exportConfig.context); + log.debug(`Fetching locales with skip: ${skip}.`, this.exportConfig.context); } log.debug(`Query parameters: ${JSON.stringify(this.qs)}`, this.exportConfig.context); @@ -117,18 +118,22 @@ export default class LocaleExport extends BaseClass { ); if (Array.isArray(localesFetchResponse.items) && localesFetchResponse.items.length > 0) { - log.debug(`Processing ${localesFetchResponse.items.length} locales`, this.exportConfig.context); + log.debug(`Processing ${localesFetchResponse.items.length} locales...`, this.exportConfig.context); + log.debug(`Processing ${localesFetchResponse.items.length} locales...`, this.exportConfig.context); this.sanitizeAttribs(localesFetchResponse.items); skip += this.localeConfig.limit || 100; if (skip > localesFetchResponse.count) { - log.debug('Completed fetching all locales', this.exportConfig.context); + log.debug('Completed fetching all locales.', this.exportConfig.context); + log.debug('Completed fetching all locales.', this.exportConfig.context); return; } - log.debug(`Continuing to fetch locales with skip: ${skip}`, this.exportConfig.context); + log.debug(`Continuing to fetch locales with skip: ${skip}.`, this.exportConfig.context); + log.debug(`Continuing to fetch locales with skip: ${skip}.`, this.exportConfig.context); return await this.getLocales(skip); } else { - log.debug('No locales found to process', this.exportConfig.context); + log.debug('No locales found to process.', this.exportConfig.context); + log.debug('No locales found to process.', this.exportConfig.context); } } diff --git a/packages/contentstack-export/src/export/modules/marketplace-apps.ts b/packages/contentstack-export/src/export/modules/marketplace-apps.ts index 9aa5854ef4..4581cb5667 100644 --- a/packages/contentstack-export/src/export/modules/marketplace-apps.ts +++ b/packages/contentstack-export/src/export/modules/marketplace-apps.ts @@ -135,11 +135,13 @@ export default class ExportMarketplaceApps extends BaseClass { this.exportConfig.org_uid = await getOrgUid(this.exportConfig); this.query = { target_uids: this.exportConfig.source_stack }; - log.debug(`Organization UID: ${this.exportConfig.org_uid}`, this.exportConfig.context); + log.debug(`Organization UID: '${this.exportConfig.org_uid}'.`, this.exportConfig.context); + log.debug(`Organization UID: '${this.exportConfig.org_uid}'.`, this.exportConfig.context); // NOTE init marketplace app sdk const host = this.developerHubBaseUrl.split('://').pop(); - log.debug(`Initializing marketplace SDK with host: ${host}`, this.exportConfig.context); + log.debug(`Initializing Marketplace SDK with host: '${host}'...`, this.exportConfig.context); + log.debug(`Initializing Marketplace SDK with host: '${host}'...`, this.exportConfig.context); this.appSdk = await marketplaceSDKClient({ host }); } @@ -182,13 +184,15 @@ export default class ExportMarketplaceApps extends BaseClass { log.debug(`Retrieved ${this.installedApps.length} stack-specific apps`, this.exportConfig.context); if (!this.nodeCrypto && find(this.installedApps, (app) => !isEmpty(app.configuration))) { - log.debug('Initializing NodeCrypto for app configuration encryption', this.exportConfig.context); + log.debug('Initializing NodeCrypto for app configuration encryption...', this.exportConfig.context); + log.debug('Initializing NodeCrypto for app configuration encryption...', this.exportConfig.context); this.nodeCrypto = await createNodeCryptoInstance(this.exportConfig); } this.installedApps = map(this.installedApps, (app) => { if (has(app, 'configuration')) { - log.debug(`Encrypting configuration for app: ${app.manifest?.name || app.uid}`, this.exportConfig.context); + log.debug(`Encrypting configuration for app: '${app.manifest?.name || app.uid}'...`, this.exportConfig.context); + log.debug(`Encrypting configuration for app: '${app.manifest?.name || app.uid}'...`, this.exportConfig.context); app['configuration'] = this.nodeCrypto.encrypt(app.configuration); } return app; @@ -209,13 +213,15 @@ export default class ExportMarketplaceApps extends BaseClass { for (const [index, app] of entries(this.installedApps)) { if (app.manifest.visibility === 'private') { - log.debug(`Processing private app manifest: ${app.manifest.name}`, this.exportConfig.context); + log.debug(`Processing private app manifest: '${app.manifest.name}'...`, this.exportConfig.context); + log.debug(`Processing private app manifest: '${app.manifest.name}'...`, this.exportConfig.context); await this.getPrivateAppsManifest(+index, app); } } for (const [index, app] of entries(this.installedApps)) { - log.debug(`Processing app configurations: ${app.manifest?.name || app.uid}`, this.exportConfig.context); + log.debug(`Processing app configurations for: '${app.manifest?.name || app.uid}'...`, this.exportConfig.context); + log.debug(`Processing app configurations for: '${app.manifest?.name || app.uid}'...`, this.exportConfig.context); await this.getAppConfigurations(+index, app); // Track progress for each app processed @@ -228,7 +234,8 @@ export default class ExportMarketplaceApps extends BaseClass { } const marketplaceAppsFilePath = pResolve(this.marketplaceAppPath, this.marketplaceAppConfig.fileName); - log.debug(`Writing marketplace apps to: ${marketplaceAppsFilePath}`, this.exportConfig.context); + log.debug(`Writing Marketplace Apps to: '${marketplaceAppsFilePath}'`, this.exportConfig.context); + log.debug(`Writing Marketplace Apps to: '${marketplaceAppsFilePath}'`, this.exportConfig.context); fsUtil.writeFile(marketplaceAppsFilePath, this.installedApps); log.success( @@ -294,7 +301,8 @@ export default class ExportMarketplaceApps extends BaseClass { const appName = appInstallation?.manifest?.name; const appUid = appInstallation?.manifest?.uid; const app = appName || appUid; - log.debug(`Fetching app configuration for: ${app}`, this.exportConfig.context); + log.debug(`Fetching app configuration for: '${app}'...`, this.exportConfig.context); + log.debug(`Fetching app configuration for: '${app}'...`, this.exportConfig.context); log.info(messageHandler.parse('MARKETPLACE_APP_CONFIG_EXPORT', app), this.exportConfig.context); await this.appSdk @@ -317,19 +325,22 @@ export default class ExportMarketplaceApps extends BaseClass { } if (!isEmpty(data?.configuration)) { - log.debug(`Encrypting configuration for app: ${app}`, this.exportConfig.context); + log.debug(`Encrypting configuration for app: '${app}'...`, this.exportConfig.context); + log.debug(`Encrypting configuration for app: '${app}'...`, this.exportConfig.context); this.installedApps[index]['configuration'] = this.nodeCrypto.encrypt(data.configuration); } if (!isEmpty(data?.server_configuration)) { - log.debug(`Encrypting server configuration for app: ${app}`, this.exportConfig.context); + log.debug(`Encrypting server configuration for app: '${app}'...`, this.exportConfig.context); + log.debug(`Encrypting server configuration for app: '${app}'...`, this.exportConfig.context); this.installedApps[index]['server_configuration'] = this.nodeCrypto.encrypt(data.server_configuration); log.success(messageHandler.parse('MARKETPLACE_APP_CONFIG_SUCCESS', app), this.exportConfig.context); } else { log.success(messageHandler.parse('MARKETPLACE_APP_EXPORT_SUCCESS', app), this.exportConfig.context); } } else if (error) { - log.debug(`Error in app configuration data for: ${app}`, this.exportConfig.context); + log.debug(`Error in app configuration data for: '${app}'.`, this.exportConfig.context); + log.debug(`Error in app configuration data for: '${app}'.`, this.exportConfig.context); handleAndLogError( error, { @@ -340,7 +351,8 @@ export default class ExportMarketplaceApps extends BaseClass { } }) .catch((error: any) => { - log.debug(`Failed to fetch app configuration for: ${app}`, this.exportConfig.context); + log.debug(`Failed to fetch app configuration for: '${app}'.`, this.exportConfig.context); + log.debug(`Failed to fetch app configuration for: '${app}'.`, this.exportConfig.context); handleAndLogError( error, { @@ -365,7 +377,8 @@ export default class ExportMarketplaceApps extends BaseClass { .installation() .fetchAll({ ...this.query, skip }) .catch((error) => { - log.debug('Error occurred while fetching stack-specific apps', this.exportConfig.context); + log.debug('An error occurred while fetching stack-specific apps.', this.exportConfig.context); + log.debug('An error occurred while fetching stack-specific apps.', this.exportConfig.context); handleAndLogError(error, { ...this.exportConfig.context, }); @@ -393,10 +406,12 @@ export default class ExportMarketplaceApps extends BaseClass { this.installedApps = this.installedApps.concat(installation); if (count - (skip + 50) > 0) { - log.debug(`Continuing to fetch apps with skip: ${skip + 50}`, this.exportConfig.context); + log.debug(`Continuing to fetch apps with skip: ${skip + 50}.`, this.exportConfig.context); + log.debug(`Continuing to fetch apps with skip: ${skip + 50}.`, this.exportConfig.context); await this.getStackSpecificApps(skip + 50); } else { - log.debug('Completed fetching all stack-specific apps', this.exportConfig.context); + log.debug('Completed fetching all stack-specific apps.', this.exportConfig.context); + log.debug('Completed fetching all stack-specific apps.', this.exportConfig.context); } } } diff --git a/packages/contentstack-export/src/export/modules/stack.ts b/packages/contentstack-export/src/export/modules/stack.ts index d2bb149b06..50b885ce1a 100644 --- a/packages/contentstack-export/src/export/modules/stack.ts +++ b/packages/contentstack-export/src/export/modules/stack.ts @@ -50,7 +50,8 @@ export default class ExportStack extends BaseClass { let processCount = 0; if (stackData?.org_uid) { - log.debug(`Found organization UID: ${stackData.org_uid}`, this.exportConfig.context); + log.debug(`Found organization UID: '${stackData.org_uid}'.`, this.exportConfig.context); + log.debug(`Found organization UID: '${stackData.org_uid}'.`, this.exportConfig.context); this.exportConfig.org_uid = stackData.org_uid; this.exportConfig.sourceStackName = stackData.name; log.debug(`Set source stack name: ${stackData.name}`, this.exportConfig.context); @@ -130,20 +131,24 @@ export default class ExportStack extends BaseClass { } async getStack(): Promise { - log.debug(`Fetching stack data for stack: ${this.exportConfig.source_stack}`, this.exportConfig.context); + log.debug(`Fetching stack data for: '${this.exportConfig.source_stack}'...`, this.exportConfig.context); + log.debug(`Fetching stack data for: '${this.exportConfig.source_stack}'...`, this.exportConfig.context); const tempAPIClient = await managementSDKClient({ host: this.exportConfig.host }); - log.debug(`Created management SDK client with host: ${this.exportConfig.host}`, this.exportConfig.context); + log.debug(`Created Management SDK client with host: '${this.exportConfig.host}'.`, this.exportConfig.context); + log.debug(`Created Management SDK client with host: '${this.exportConfig.host}'.`, this.exportConfig.context); return await tempAPIClient .stack({ api_key: this.exportConfig.source_stack }) .fetch() .then((data: any) => { - log.debug(`Successfully fetched stack data for: ${this.exportConfig.source_stack}`, this.exportConfig.context); + log.debug(`Successfully fetched stack data for: '${this.exportConfig.source_stack}'.`, this.exportConfig.context); + log.debug(`Successfully fetched stack data for: '${this.exportConfig.source_stack}'.`, this.exportConfig.context); return data; }) .catch((error: any) => { - log.debug(`Failed to fetch stack data for: ${this.exportConfig.source_stack}`, this.exportConfig.context); + log.debug(`Failed to fetch stack data for: '${this.exportConfig.source_stack}'.`, this.exportConfig.context); + log.debug(`Failed to fetch stack data for: '${this.exportConfig.source_stack}'.`, this.exportConfig.context); return {}; }); } @@ -151,12 +156,15 @@ export default class ExportStack extends BaseClass { async getLocales(skip: number = 0) { if (skip) { this.qs.skip = skip; - log.debug(`Fetching locales with skip: ${skip}`, this.exportConfig.context); + log.debug(`Fetching locales with skip: ${skip}.`, this.exportConfig.context); + log.debug(`Fetching locales with skip: ${skip}.`, this.exportConfig.context); } else { - log.debug('Fetching locales with initial query', this.exportConfig.context); + log.debug('Fetching locales with initial query...', this.exportConfig.context); + log.debug('Fetching locales with initial query...', this.exportConfig.context); } - log.debug(`Query parameters: ${JSON.stringify(this.qs)}`, this.exportConfig.context); + log.debug(`Query parameters: ${JSON.stringify(this.qs)}.`, this.exportConfig.context); + log.debug(`Query parameters: ${JSON.stringify(this.qs)}.`, this.exportConfig.context); return await this.stack .locale() @@ -164,7 +172,8 @@ export default class ExportStack extends BaseClass { .find() .then(async (data: any) => { const { items, count } = data; - log.debug(`Fetched ${items?.length || 0} locales out of total ${count}`, this.exportConfig.context); + log.debug(`Fetched ${items?.length || 0} locales out of ${count}.`, this.exportConfig.context); + log.debug(`Fetched ${items?.length || 0} locales out of ${count}.`, this.exportConfig.context); if (items?.length) { log.debug(`Processing ${items.length} locales to find master locale`, this.exportConfig.context); @@ -175,19 +184,22 @@ export default class ExportStack extends BaseClass { skip += this.stackConfig.limit || 100; const masterLocalObj = find(items, (locale: any) => { if (locale.fallback_locale === null) { - log.debug(`Found master locale: ${locale.name} (${locale.code})`, this.exportConfig.context); + log.debug(`Found master locale: '${locale.name}' (code: ${locale.code}).`, this.exportConfig.context); + log.debug(`Found master locale: '${locale.name}' (code: ${locale.code}).`, this.exportConfig.context); return locale; } }); if (masterLocalObj) { - log.debug(`Returning master locale: ${masterLocalObj.name}`, this.exportConfig.context); + log.debug(`Returning master locale: '${masterLocalObj.name}'.`, this.exportConfig.context); + log.debug(`Returning master locale: '${masterLocalObj.name}'.`, this.exportConfig.context); return masterLocalObj; } else if (skip >= count) { log.error( `Locale locale not found in the stack ${this.exportConfig.source_stack}. Please ensure that the stack has a master locale.`, this.exportConfig.context, ); - log.debug('Completed searching all locales without finding master locale', this.exportConfig.context); + log.debug('Completed search. Master locale not found.', this.exportConfig.context); + log.debug('Completed search. Master locale not found.', this.exportConfig.context); return; } else { log.debug( @@ -197,7 +209,8 @@ export default class ExportStack extends BaseClass { return await this.getLocales(skip); } } else { - log.debug('No locales found to process', this.exportConfig.context); + log.debug('No locales found to process.', this.exportConfig.context); + log.debug('No locales found to process.', this.exportConfig.context); } }) .catch((error: any) => { @@ -221,16 +234,19 @@ export default class ExportStack extends BaseClass { } async exportStack(): Promise { - log.debug(`Starting stack export for: ${this.exportConfig.source_stack}`, this.exportConfig.context); + log.debug(`Starting stack export for: '${this.exportConfig.source_stack}'...`, this.exportConfig.context); + log.debug(`Starting stack export for: '${this.exportConfig.source_stack}'...`, this.exportConfig.context); await fsUtil.makeDirectory(this.stackFolderPath); - log.debug(`Created stack directory at: ${this.stackFolderPath}`, this.exportConfig.context); + log.debug(`Created stack directory at: '${this.stackFolderPath}'`, this.exportConfig.context); + log.debug(`Created stack directory at: '${this.stackFolderPath}'`, this.exportConfig.context); return this.stack .fetch() .then((resp: any) => { const stackFilePath = pResolve(this.stackFolderPath, this.stackConfig.fileName); - log.debug(`Writing stack data to: ${stackFilePath}`, this.exportConfig.context); + log.debug(`Writing stack data to: '${stackFilePath}'`, this.exportConfig.context); + log.debug(`Writing stack data to: '${stackFilePath}'`, this.exportConfig.context); fsUtil.writeFile(stackFilePath, resp); // Track progress for stack export completion @@ -245,7 +261,8 @@ export default class ExportStack extends BaseClass { `Stack details exported successfully for stack ${this.exportConfig.source_stack}`, this.exportConfig.context, ); - log.debug('Stack export completed successfully', this.exportConfig.context); + log.debug('Stack export completed successfully.', this.exportConfig.context); + log.debug('Stack export completed successfully.', this.exportConfig.context); return resp; }) .catch((error: any) => { @@ -261,7 +278,8 @@ export default class ExportStack extends BaseClass { } async exportStackSettings(): Promise { - log.info('Exporting stack settings', this.exportConfig.context); + log.info('Exporting stack settings...', this.exportConfig.context); + log.info('Exporting stack settings...', this.exportConfig.context); await fsUtil.makeDirectory(this.stackFolderPath); return this.stack .settings() diff --git a/packages/contentstack-export/src/export/modules/taxonomies.ts b/packages/contentstack-export/src/export/modules/taxonomies.ts index 6f04e1500e..136abb311f 100644 --- a/packages/contentstack-export/src/export/modules/taxonomies.ts +++ b/packages/contentstack-export/src/export/modules/taxonomies.ts @@ -50,18 +50,18 @@ export default class ExportTaxonomies extends BaseClass { } async start(): Promise { - log.debug('Starting taxonomies export process...', this.exportConfig.context); - + log.debug('Starting export process for taxonomies...', this.exportConfig.context); + //create taxonomies folder this.taxonomiesFolderPath = pResolve( this.exportConfig.data, this.exportConfig.branchName || '', this.taxonomiesConfig.dirName, ); - log.debug(`Taxonomies folder path: ${this.taxonomiesFolderPath}`, this.exportConfig.context); - + log.debug(`Taxonomies folder path: '${this.taxonomiesFolderPath}'`, this.exportConfig.context); + await fsUtil.makeDirectory(this.taxonomiesFolderPath); - log.debug('Created taxonomies directory', this.exportConfig.context); + log.debug('Created taxonomies directory.', this.exportConfig.context); const localesToExport = this.getLocalesToExport(); log.debug( @@ -79,7 +79,11 @@ export default class ExportTaxonomies extends BaseClass { await this.fetchTaxonomies(masterLocale, true); if (!this.isLocaleBasedExportSupported) { - log.debug('Localization disabled, falling back to legacy export method', this.exportConfig.context); + this.taxonomies = {}; + this.taxonomiesByLocale = {}; + + // Fetch taxonomies without locale parameter + await this.fetchTaxonomies(); await this.exportTaxonomies(); await this.writeTaxonomiesMetadata(); } else { @@ -180,15 +184,26 @@ export default class ExportTaxonomies extends BaseClass { log.debug(`Completed fetching all taxonomies ${localeInfo}`, this.exportConfig.context); break; } - } catch (error) { + } catch (error: any) { log.debug(`Error fetching taxonomies ${localeInfo}`, this.exportConfig.context); - handleAndLogError(error, { - ...this.exportConfig.context, - ...(localeCode && { locale: localeCode }), - }); - if (checkLocaleSupport) { + + if (checkLocaleSupport && this.isLocalePlanLimitationError(error)) { + log.debug( + 'Taxonomy localization is not included in your plan. Falling back to non-localized export.', + this.exportConfig.context, + ); + this.isLocaleBasedExportSupported = false; + } else if (checkLocaleSupport) { + log.debug('Locale-based taxonomy export not supported, will use legacy method', this.exportConfig.context); this.isLocaleBasedExportSupported = false; + } else { + // Log actual errors during normal fetch (not locale check) + handleAndLogError(error, { + ...this.exportConfig.context, + ...(localeCode && { locale: localeCode }), + }); } + // Break to avoid infinite retry loop on errors break; } @@ -318,4 +333,15 @@ export default class ExportTaxonomies extends BaseClass { return localesToExport; } + + private isLocalePlanLimitationError(error: any): boolean { + return ( + error?.status === 403 && + error?.errors?.taxonomies?.some( + (msg: string) => + msg.toLowerCase().includes('taxonomy localization') && + msg.toLowerCase().includes('not included in your plan'), + ) + ); + } } diff --git a/packages/contentstack-export/src/types/default-config.ts b/packages/contentstack-export/src/types/default-config.ts index 4345185b8c..b082399275 100644 --- a/packages/contentstack-export/src/types/default-config.ts +++ b/packages/contentstack-export/src/types/default-config.ts @@ -162,6 +162,12 @@ export default interface DefaultConfig { fileName: string; dependencies?: Modules[]; }; + 'composable-studio': { + dirName: string; + fileName: string; + apiBaseUrl: string; + apiVersion: string; + }; masterLocale: { dirName: string; fileName: string; diff --git a/packages/contentstack-export/src/types/index.ts b/packages/contentstack-export/src/types/index.ts index b1b23dddb6..cb85b167aa 100644 --- a/packages/contentstack-export/src/types/index.ts +++ b/packages/contentstack-export/src/types/index.ts @@ -49,7 +49,8 @@ export type Modules = | 'labels' | 'marketplace-apps' | 'taxonomies' - | 'personalize'; + | 'personalize' + | 'composable-studio'; export type ModuleClassParams = { stackAPIClient: ReturnType; @@ -129,6 +130,34 @@ export interface StackConfig { dependencies?: Modules[]; limit?: number; } + +export interface ComposableStudioConfig { + dirName: string; + fileName: string; + apiBaseUrl: string; + apiVersion: string; +} + +export interface ComposableStudioProject { + name: string; + description: string; + canvasUrl: string; + connectedStackApiKey: string; + contentTypeUid: string; + organizationUid: string; + settings: { + configuration: { + environment: string; + locale: string; + }; + }; + createdBy: string; + updatedBy: string; + deletedAt: boolean; + createdAt: string; + updatedAt: string; + uid: string; +} export interface Context { command: string; module: string; diff --git a/packages/contentstack-export/src/utils/basic-login.ts b/packages/contentstack-export/src/utils/basic-login.ts index 60c730f445..650c12d40c 100644 --- a/packages/contentstack-export/src/utils/basic-login.ts +++ b/packages/contentstack-export/src/utils/basic-login.ts @@ -25,7 +25,7 @@ const login = async (config: ExternalConfig): Promise => { log.success(`Contentstack account authenticated successfully!`, config.context); return config; } else { - log.error(`Failed to login, Invalid credentials`, config.context); + log.error(`Failed to log in!`, config.context); process.exit(1); } } else if (!config.email && !config.password && config.source_stack && config.access_token) { diff --git a/packages/contentstack-export/src/utils/export-config-handler.ts b/packages/contentstack-export/src/utils/export-config-handler.ts index 80250fb54d..51afbeab6d 100644 --- a/packages/contentstack-export/src/utils/export-config-handler.ts +++ b/packages/contentstack-export/src/utils/export-config-handler.ts @@ -18,7 +18,7 @@ const setupConfig = async (exportCmdFlags: any): Promise => { // setup the config if (exportCmdFlags['config']) { - log.debug('Loading external configuration file', { configFile: exportCmdFlags['config'] }); + log.debug('Loading external configuration file...', { configFile: exportCmdFlags['config'] }); const externalConfig = await readFile(exportCmdFlags['config']); config = merge.recursive(config, externalConfig); } @@ -28,7 +28,7 @@ const setupConfig = async (exportCmdFlags: any): Promise => { const pattern = /[*$%#<>{}!&?]/g; if (pattern.test(config.exportDir)) { - cliux.print(`\nPlease add a directory path without any of the special characters: (*,&,{,},[,],$,%,<,>,?,!)`, { + cliux.print(`\nPlease enter a directory path without any special characters: (*,&,{,},[,],$,%,<,>,?,!)`, { color: 'yellow', }); config.exportDir = sanitizePath(await askExportDir()); @@ -48,7 +48,7 @@ const setupConfig = async (exportCmdFlags: any): Promise => { config.apiKey = apiKey; authenticationMethod = 'Management Token'; if (!config.management_token) { - log.debug('Management token not found for alias', { alias: managementTokenAlias }); + log.debug('Management token not found for alias!', { alias: managementTokenAlias }); throw new Error(`No management token found on given alias ${managementTokenAlias}`); } @@ -82,7 +82,7 @@ const setupConfig = async (exportCmdFlags: any): Promise => { config.apiKey = exportCmdFlags['stack-uid'] || exportCmdFlags['stack-api-key'] || config.source_stack || (await askAPIKey()); if (typeof config.apiKey !== 'string') { - log.debug('Invalid API key received', { apiKey: config.apiKey }); + log.debug('Invalid API key received!', { apiKey: config.apiKey }); throw new Error('Invalid API key received'); } } @@ -136,7 +136,7 @@ const setupConfig = async (exportCmdFlags: any): Promise => { configHandler.set('log.progressSupportedModule', 'export'); // Add authentication details to config for context tracking config.authenticationMethod = authenticationMethod; - log.debug('Export configuration setup completed', { ...config }); + log.debug('Export configuration setup completed.', { ...config }); return config; }; diff --git a/packages/contentstack-export/src/utils/file-helper.ts b/packages/contentstack-export/src/utils/file-helper.ts index 5afc464418..b96ee98c1e 100644 --- a/packages/contentstack-export/src/utils/file-helper.ts +++ b/packages/contentstack-export/src/utils/file-helper.ts @@ -47,7 +47,7 @@ export const readLargeFile = function (filePath: string, options: { type?: strin resolve(data); }); parseStream.on('error', (error: Error) => { - console.log('error', error); + console.log('Error', error); reject(error); }); readStream.pipe(parseStream); diff --git a/packages/contentstack-export/test/unit/export/modules/entries.test.ts b/packages/contentstack-export/test/unit/export/modules/entries.test.ts new file mode 100644 index 0000000000..32093ea77f --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/entries.test.ts @@ -0,0 +1,1147 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as path from 'path'; +import { FsUtility, handleAndLogError, messageHandler } from '@contentstack/cli-utilities'; +import * as utilities from '@contentstack/cli-utilities'; +import EntriesExport from '../../../../src/export/modules/entries'; +import ExportConfig from '../../../../src/types/export-config'; +import * as variants from '@contentstack/cli-variants'; +import * as fsUtilModule from '../../../../src/utils/file-helper'; + +describe('EntriesExport', () => { + let entriesExport: any; + let mockStackAPIClient: any; + let mockExportConfig: ExportConfig; + let mockFsUtil: any; + let mockExportProjects: any; + let mockVariantEntries: any; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // Mock stack API client + mockStackAPIClient = { + contentType: sandbox.stub() + }; + // Set default return value + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns({ + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [], + count: 0 + }) + }), + fetch: sandbox.stub().resolves({}) + }) + }); + + // Mock ExportConfig + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + context: { + command: 'cm:stacks:export', + module: 'entries', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: {}, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: { + types: ['entries'], + entries: { + dirName: 'entries', + fileName: 'entries.json', + invalidKeys: ['ACL', '_version'], + limit: 100, + chunkFileSize: 1000, + batchLimit: 5, + exportVersions: false + }, + locales: { + dirName: 'locales', + fileName: 'locales.json' + }, + content_types: { + dirName: 'content_types', + fileName: 'schema.json' + }, + personalize: { + baseURL: { + 'us': 'https://personalize-api.contentstack.com', + 'AWS-NA': 'https://personalize-api.contentstack.com', + 'AWS-EU': 'https://eu-personalize-api.contentstack.com' + }, + dirName: 'personalize', + exportOrder: [] + } + }, + org_uid: 'test-org-uid', + query: {} + } as any; + + // Mock fsUtil + mockFsUtil = { + readFile: sandbox.stub(), + makeDirectory: sandbox.stub().resolves(), + writeFile: sandbox.stub() + }; + sandbox.stub(fsUtilModule, 'fsUtil').value(mockFsUtil); + + // Mock ExportProjects + mockExportProjects = { + projects: sandbox.stub().resolves([]) + }; + sandbox.stub(variants, 'ExportProjects').callsFake(() => mockExportProjects as any); + + // Mock VariantEntries + mockVariantEntries = { + exportVariantEntry: sandbox.stub().resolves() + }; + sandbox.stub(variants.Export, 'VariantEntries').callsFake(() => mockVariantEntries as any); + + // Mock handleAndLogError - will be replaced in individual tests if needed + + // Mock FsUtility - stub methods to avoid directory creation + sandbox.stub(FsUtility.prototype, 'writeIntoFile'); + sandbox.stub(FsUtility.prototype, 'completeFile').resolves(); + // Stub the createFolderIfNotExist method that FsUtility calls in constructor + // This method is called synchronously, so we need to stub it + const createFolderStub = sandbox.stub(FsUtility.prototype, 'createFolderIfNotExist' as any); + createFolderStub.callsFake(() => { + // Do nothing - prevent actual directory creation + }); + + entriesExport = new EntriesExport({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackAPIClient, + moduleName: 'entries' + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('Constructor', () => { + it('should initialize with correct paths and configuration', () => { + expect(entriesExport).to.be.instanceOf(EntriesExport); + expect(entriesExport.exportConfig).to.equal(mockExportConfig); + expect(entriesExport.stackAPIClient).to.equal(mockStackAPIClient); + expect(entriesExport.exportConfig.context.module).to.equal('entries'); + expect(entriesExport.exportVariantEntry).to.be.false; + }); + + it('should set up correct directory paths based on exportConfig', () => { + const expectedEntriesPath = path.resolve( + mockExportConfig.data, + mockExportConfig.branchName || '', + mockExportConfig.modules.entries.dirName + ); + const expectedLocalesPath = path.resolve( + mockExportConfig.data, + mockExportConfig.branchName || '', + mockExportConfig.modules.locales.dirName, + mockExportConfig.modules.locales.fileName + ); + const expectedSchemaPath = path.resolve( + mockExportConfig.data, + mockExportConfig.branchName || '', + mockExportConfig.modules.content_types.dirName, + 'schema.json' + ); + + expect(entriesExport.entriesDirPath).to.equal(expectedEntriesPath); + expect(entriesExport.localesFilePath).to.equal(expectedLocalesPath); + expect(entriesExport.schemaFilePath).to.equal(expectedSchemaPath); + }); + + it('should initialize ExportProjects instance', () => { + // Verify projectInstance exists + expect(entriesExport.projectInstance).to.exist; + // The stub intercepts the constructor call, so projectInstance should be the mock + // However, if the actual constructor runs, it will be an ExportProjects instance + // So we just verify it exists and has the expected structure + expect(entriesExport.projectInstance).to.have.property('projects'); + }); + }); + + describe('start() method - Early Returns', () => { + it('should return early when no content types are found', async () => { + mockFsUtil.readFile + .onFirstCall() + .returns([{ code: 'en-us' }]) // locales + .onSecondCall() + .returns([]); // content types + + await entriesExport.start(); + + // Should not attempt to fetch entries + expect(mockStackAPIClient.contentType.called).to.be.false; + // Should read both locales and content types files + expect(mockFsUtil.readFile.calledTwice).to.be.true; + }); + + it('should handle empty locales array gracefully', async () => { + const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }]; + mockFsUtil.readFile + .onFirstCall() + .returns([]) // empty locales + .onSecondCall() + .returns(contentTypes); + + await entriesExport.start(); + + // Should still process entries with master locale + expect(mockStackAPIClient.contentType.called).to.be.true; + }); + + it('should handle non-array locales gracefully', async () => { + const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }]; + // Use empty array instead of null to avoid Object.keys error + // The code checks !Array.isArray first, so empty array will work + mockFsUtil.readFile + .onFirstCall() + .returns([]) // empty locales array + .onSecondCall() + .returns(contentTypes); + + // Mock entry query for when entries are processed + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [], + count: 0 + }) + }) + }; + const contentTypeStub = sandbox.stub().returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + // Update both the mock and entriesExport to use the new stub + mockStackAPIClient.contentType = contentTypeStub; + entriesExport.stackAPIClient = mockStackAPIClient; + + await entriesExport.start(); + + // Should still process entries with master locale (createRequestObjects uses master locale when locales is empty) + expect(contentTypeStub.called).to.be.true; + }); + }); + + describe('start() method - Personalization and Variant Entries', () => { + it('should enable variant entry export when personalization is enabled and project is found', async () => { + mockExportConfig.personalizationEnabled = true; + entriesExport.exportConfig.personalizationEnabled = true; + const project = [{ uid: 'project-123' }]; + // Ensure projectInstance is the mock so projects() returns the expected value + entriesExport.projectInstance = mockExportProjects; + mockExportProjects.projects.resolves(project); + + const locales = [{ code: 'en-us' }]; + const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }]; + mockFsUtil.readFile + .onFirstCall() + .returns(locales) + .onSecondCall() + .returns(contentTypes); + + // Mock successful entry fetch - use callsFake to preserve call tracking + const contentTypeStub = sandbox.stub().returns({ + entry: sandbox.stub().returns({ + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [], + count: 0 + }) + }) + }) + }); + mockStackAPIClient.contentType = contentTypeStub; + // Update entriesExport to use the new mock + entriesExport.stackAPIClient = mockStackAPIClient; + + await entriesExport.start(); + + // Should check for projects + // Note: projectInstance is created in constructor, so we need to check if it was called + // The actual call happens in start() method, so we verify the behavior instead + // If exportVariantEntry is true, it means projects() was called and returned a project + // Should enable variant entry export + expect(entriesExport.exportVariantEntry).to.be.true; + // Should initialize VariantEntries with project_id + const variantEntriesStub = variants.Export.VariantEntries as unknown as sinon.SinonStub; + expect(variantEntriesStub.called).to.be.true; + expect(variantEntriesStub.firstCall.args[0]).to.include({ + project_id: 'project-123' + }); + // Verify the flow completed successfully + // The key behavior is that exportVariantEntry is enabled when project is found + expect(entriesExport.exportVariantEntry).to.be.true; + // Verify that start() completed without throwing errors + // This confirms that the entire flow executed, including processing entries + }); + + it('should not enable variant entry export when personalization is enabled but no project is found', async () => { + mockExportConfig.personalizationEnabled = true; + entriesExport.exportConfig.personalizationEnabled = true; + mockExportProjects.projects.resolves([]); + + const locales = [{ code: 'en-us' }]; + const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }]; + mockFsUtil.readFile + .onFirstCall() + .returns(locales) + .onSecondCall() + .returns(contentTypes); + + const contentTypeStub = sandbox.stub().returns({ + entry: sandbox.stub().returns({ + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [], + count: 0 + }) + }) + }) + }); + mockStackAPIClient.contentType = contentTypeStub; + // Update entriesExport to use the new mock + entriesExport.stackAPIClient = mockStackAPIClient; + + await entriesExport.start(); + + // Should not enable variant entry export + // If exportVariantEntry is false, it means either projects() wasn't called, + // or it returned an empty array, or no project was found + expect(entriesExport.exportVariantEntry).to.be.false; + // Verify the flow completed successfully + // The key behavior is that exportVariantEntry is NOT enabled when no project is found + expect(entriesExport.exportVariantEntry).to.be.false; + // Verify that start() completed without throwing errors + // This confirms that the entire flow executed, including processing entries + }); + + it('should handle errors when fetching projects gracefully', async () => { + mockExportConfig.personalizationEnabled = true; + entriesExport.exportConfig.personalizationEnabled = true; + const projectError = new Error('Project fetch failed'); + mockExportProjects.projects.rejects(projectError); + const handleAndLogErrorSpy = sandbox.spy(); + try { + sandbox.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + } catch (e) { + // Already replaced, restore first + sandbox.restore(); + sandbox.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + } + + const locales = [{ code: 'en-us' }]; + const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }]; + mockFsUtil.readFile + .onFirstCall() + .returns(locales) + .onSecondCall() + .returns(contentTypes); + + const contentTypeStub = sandbox.stub().returns({ + entry: sandbox.stub().returns({ + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [], + count: 0 + }) + }) + }) + }); + mockStackAPIClient.contentType = contentTypeStub; + // Update entriesExport to use the new mock + entriesExport.stackAPIClient = mockStackAPIClient; + + await entriesExport.start(); + + // Should not enable variant entry export (error occurred, so no project was set) + expect(entriesExport.exportVariantEntry).to.be.false; + // Should handle error - verify error was logged + // Note: handleAndLogError might be called, but we verify the behavior (exportVariantEntry is false) + // which confirms the error was handled and processing continued + // Verify the flow completed successfully despite the error + // The key behavior is that exportVariantEntry is NOT enabled when project fetch fails + expect(entriesExport.exportVariantEntry).to.be.false; + // Verify that start() completed without throwing errors (error was handled) + // This confirms that the entire flow executed, including processing entries + }); + }); + + describe('createRequestObjects() method', () => { + it('should create request objects for each content type and locale combination', () => { + const locales = [ + { code: 'en-us' }, + { code: 'fr-fr' } + ]; + const contentTypes = [ + { uid: 'ct-1', title: 'Content Type 1' }, + { uid: 'ct-2', title: 'Content Type 2' } + ]; + + const requestObjects = entriesExport.createRequestObjects(locales, contentTypes); + + // Should create: (2 locales + 1 master) * 2 content types = 6 request objects + // But actually: 2 content types * (2 locales + 1 master) = 6 + expect(requestObjects).to.have.length(6); + expect(requestObjects).to.deep.include({ + contentType: 'ct-1', + locale: 'en-us' + }); + expect(requestObjects).to.deep.include({ + contentType: 'ct-1', + locale: 'fr-fr' + }); + expect(requestObjects).to.deep.include({ + contentType: 'ct-1', + locale: mockExportConfig.master_locale.code + }); + expect(requestObjects).to.deep.include({ + contentType: 'ct-2', + locale: 'en-us' + }); + }); + + it('should return empty array when no content types are provided', () => { + const locales = [{ code: 'en-us' }]; + const contentTypes: any[] = []; + + const requestObjects = entriesExport.createRequestObjects(locales, contentTypes); + + expect(requestObjects).to.be.an('array').that.is.empty; + }); + + it('should use master locale only when locales array is empty', () => { + const locales: any[] = []; + const contentTypes = [ + { uid: 'ct-1', title: 'Content Type 1' } + ]; + + const requestObjects = entriesExport.createRequestObjects(locales, contentTypes); + + // Should create 1 request object with master locale only + expect(requestObjects).to.have.length(1); + expect(requestObjects[0]).to.deep.equal({ + contentType: 'ct-1', + locale: mockExportConfig.master_locale.code + }); + }); + + it('should use master locale only when locales is not an array', () => { + const locales = {} as any; + const contentTypes = [ + { uid: 'ct-1', title: 'Content Type 1' } + ]; + + const requestObjects = entriesExport.createRequestObjects(locales, contentTypes); + + // Should create 1 request object with master locale only + expect(requestObjects).to.have.length(1); + expect(requestObjects[0].locale).to.equal(mockExportConfig.master_locale.code); + }); + + it('should always include master locale for each content type', () => { + const locales = [{ code: 'de-de' }]; + const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }]; + + const requestObjects = entriesExport.createRequestObjects(locales, contentTypes); + + // Should have 2 objects: one for de-de and one for master locale + expect(requestObjects).to.have.length(2); + const masterLocaleObjects = requestObjects.filter( + (obj: any) => obj.locale === mockExportConfig.master_locale.code + ); + expect(masterLocaleObjects).to.have.length(1); + }); + }); + + describe('getEntries() method - Basic Functionality', () => { + it('should fetch entries and create directory structure on first call', async () => { + const options = { + contentType: 'ct-1', + locale: 'en-us', + skip: 0 + }; + + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [ + { uid: 'entry-1', title: 'Entry 1' }, + { uid: 'entry-2', title: 'Entry 2' } + ], + count: 2 + }) + }) + }; + + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + + await entriesExport.getEntries(options); + + // Should create directory + const expectedPath = path.join( + entriesExport.entriesDirPath, + 'ct-1', + 'en-us' + ); + expect(mockFsUtil.makeDirectory.called).to.be.true; + expect(mockFsUtil.makeDirectory.calledWith(expectedPath)).to.be.true; + // Should initialize FsUtility + expect(entriesExport.entriesFileHelper).to.be.instanceOf(FsUtility); + // Should write entries to file + expect((FsUtility.prototype.writeIntoFile as sinon.SinonStub).called).to.be.true; + expect((FsUtility.prototype.writeIntoFile as sinon.SinonStub).calledWith( + sinon.match.array, + { mapKeyVal: true } + )).to.be.true; + // Should query with correct parameters + expect(mockEntryQuery.query.called).to.be.true; + }); + + it('should not create directory on subsequent pagination calls', async () => { + const options = { + contentType: 'ct-1', + locale: 'en-us', + skip: 0 + }; + + // Initialize FsUtility on first call + entriesExport.entriesFileHelper = new FsUtility({ + moduleName: 'entries', + indexFileName: 'index.json', + basePath: '/test/path', + chunkFileSize: 1000, + keepMetadata: false, + omitKeys: [] + }); + + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [{ uid: 'entry-1' }], + count: 150 // More than limit, will paginate + }) + }) + }; + + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + + // First call + await entriesExport.getEntries({ ...options, skip: 0 }); + const firstCallMakeDirCount = mockFsUtil.makeDirectory.callCount; + + // Second call (pagination) + await entriesExport.getEntries({ ...options, skip: 100 }); + const secondCallMakeDirCount = mockFsUtil.makeDirectory.callCount; + + // Should not create directory again on pagination + expect(secondCallMakeDirCount).to.equal(firstCallMakeDirCount); + }); + + it('should handle pagination correctly when entries exceed limit', async () => { + const options = { + contentType: 'ct-1', + locale: 'en-us', + skip: 0 + }; + + let callCount = 0; + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: Array(100).fill(null).map((_, i) => ({ uid: `entry-${i}` })), + count: 250 // Total entries + }); + } else if (callCount === 2) { + return Promise.resolve({ + items: Array(100).fill(null).map((_, i) => ({ uid: `entry-${100 + i}` })), + count: 250 + }); + } else { + return Promise.resolve({ + items: Array(50).fill(null).map((_, i) => ({ uid: `entry-${200 + i}` })), + count: 250 + }); + } + }) + }) + }; + + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + + await entriesExport.getEntries(options); + + // Should make 3 calls for pagination (100 + 100 + 50 = 250 entries) + expect(mockEntryQuery.query.calledThrice).to.be.true; + // Should write entries 3 times + expect((FsUtility.prototype.writeIntoFile as sinon.SinonStub).calledThrice).to.be.true; + }); + + it('should return early when no entries are found', async () => { + const options = { + contentType: 'ct-1', + locale: 'en-us', + skip: 0 + }; + + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [], + count: 0 + }) + }) + }; + + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + + await entriesExport.getEntries(options); + + // Should not create directory or initialize FsUtility + expect(mockFsUtil.makeDirectory.called).to.be.false; + expect(entriesExport.entriesFileHelper).to.be.undefined; + // Should not write to file + expect((FsUtility.prototype.writeIntoFile as sinon.SinonStub).called).to.be.false; + }); + + it('should handle API errors and propagate them', async () => { + const options = { + contentType: 'ct-1', + locale: 'en-us', + skip: 0 + }; + + const apiError = new Error('API Error'); + const handleAndLogErrorSpy = sandbox.spy(); + try { + sandbox.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + } catch (e) { + // Already replaced, restore first + sandbox.restore(); + sandbox.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + } + + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().rejects(apiError) + }) + }; + + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + + try { + await entriesExport.getEntries(options); + expect.fail('Should have thrown error'); + } catch (error) { + expect(error).to.equal(apiError); + // Should handle and log error with context + expect(handleAndLogErrorSpy.called).to.be.true; + expect(handleAndLogErrorSpy.calledWith( + apiError, + sinon.match.has('contentType', 'ct-1') + )).to.be.true; + expect(handleAndLogErrorSpy.getCall(0).args[1]).to.include({ + locale: 'en-us', + contentType: 'ct-1' + }); + } + }); + }); + + describe('getEntries() method - Version Export', () => { + beforeEach(() => { + mockExportConfig.modules.entries.exportVersions = true; + entriesExport = new EntriesExport({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackAPIClient, + moduleName: 'entries' + }); + }); + + it('should export versions when exportVersions is enabled', async () => { + const options = { + contentType: 'ct-1', + locale: 'en-us', + skip: 0 + }; + + const entries = [ + { uid: 'entry-1', _version: 3 }, + { uid: 'entry-2', _version: 2 } + ]; + + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: entries, + count: 2 + }) + }) + }; + + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + + // Stub fetchEntriesVersions + sandbox.stub(entriesExport, 'fetchEntriesVersions').resolves(); + + await entriesExport.getEntries(options); + + // Should call fetchEntriesVersions with entries + expect((entriesExport.fetchEntriesVersions as sinon.SinonStub).called).to.be.true; + expect((entriesExport.fetchEntriesVersions as sinon.SinonStub).calledWith( + entries, + sinon.match({ + locale: 'en-us', + contentType: 'ct-1', + versionedEntryPath: sinon.match.string + }) + )).to.be.true; + // Should create versions directory + expect(mockFsUtil.makeDirectory.called).to.be.true; + const makeDirCalls = mockFsUtil.makeDirectory.getCalls(); + const versionsCall = makeDirCalls.find((call: any) => call.args[0].includes('versions')); + expect(versionsCall).to.exist; + }); + + it('should not export versions when exportVersions is disabled', async () => { + mockExportConfig.modules.entries.exportVersions = false; + entriesExport = new EntriesExport({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackAPIClient, + moduleName: 'entries' + }); + + const options = { + contentType: 'ct-1', + locale: 'en-us', + skip: 0 + }; + + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [{ uid: 'entry-1' }], + count: 1 + }) + }) + }; + + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + + sandbox.stub(entriesExport, 'fetchEntriesVersions').resolves(); + + await entriesExport.getEntries(options); + + // Should not call fetchEntriesVersions + expect((entriesExport.fetchEntriesVersions as sinon.SinonStub).called).to.be.false; + }); + }); + + describe('getEntries() method - Variant Entry Export', () => { + it('should export variant entries when exportVariantEntry is enabled', async () => { + entriesExport.exportVariantEntry = true; + entriesExport.variantEntries = mockVariantEntries; + + const options = { + contentType: 'ct-1', + locale: 'en-us', + skip: 0 + }; + + const entries = [ + { uid: 'entry-1', title: 'Entry 1' }, + { uid: 'entry-2', title: 'Entry 2' } + ]; + + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: entries, + count: 2 + }) + }) + }; + + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + + await entriesExport.getEntries(options); + + // Should call exportVariantEntry with correct parameters + expect(mockVariantEntries.exportVariantEntry.called).to.be.true; + expect(mockVariantEntries.exportVariantEntry.calledWith({ + locale: 'en-us', + contentTypeUid: 'ct-1', + entries: entries + })).to.be.true; + }); + + it('should not export variant entries when exportVariantEntry is disabled', async () => { + entriesExport.exportVariantEntry = false; + + const options = { + contentType: 'ct-1', + locale: 'en-us', + skip: 0 + }; + + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [{ uid: 'entry-1' }], + count: 1 + }) + }) + }; + + mockStackAPIClient.contentType.returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + + await entriesExport.getEntries(options); + + // Should not call exportVariantEntry + if (entriesExport.variantEntries) { + expect(mockVariantEntries.exportVariantEntry.called).to.be.false; + } + }); + }); + + describe('fetchEntriesVersions() method', () => { + it('should process entries through makeConcurrentCall with correct configuration', async () => { + const entries = [ + { uid: 'entry-1', _version: 2 }, + { uid: 'entry-2', _version: 1 } + ]; + const options = { + locale: 'en-us', + contentType: 'ct-1', + versionedEntryPath: '/test/versions' + }; + + // Stub makeConcurrentCall + const makeConcurrentCallStub = sandbox.stub(entriesExport, 'makeConcurrentCall').resolves(); + + await entriesExport.fetchEntriesVersions(entries, options); + + // Should call makeConcurrentCall with correct configuration + expect(makeConcurrentCallStub.calledOnce).to.be.true; + const callArgs = makeConcurrentCallStub.getCall(0).args[0]; + expect(callArgs.module).to.equal('versioned-entries'); + expect(callArgs.apiBatches).to.deep.equal([entries]); + expect(callArgs.totalCount).to.equal(entries.length); + expect(callArgs.concurrencyLimit).to.equal(mockExportConfig.modules.entries.batchLimit); + expect(callArgs.apiParams.module).to.equal('versioned-entries'); + expect(callArgs.apiParams.queryParam).to.deep.equal(options); + expect(callArgs.apiParams.resolve).to.be.a('function'); + expect(callArgs.apiParams.reject).to.be.a('function'); + // Should pass entryVersionHandler as the handler + expect(makeConcurrentCallStub.getCall(0).args[1]).to.be.a('function'); + }); + }); + + describe('entryVersionHandler() method', () => { + it('should successfully fetch and resolve entry versions', async () => { + const entry = { uid: 'entry-1', _version: 2 }; + const apiParams = { + module: 'versioned-entries', + queryParam: { + locale: 'en-us', + contentType: 'ct-1' + }, + resolve: sandbox.spy(), + reject: sandbox.spy() + }; + + const versions = [{ uid: 'entry-1', _version: 1 }, { uid: 'entry-1', _version: 2 }]; + sandbox.stub(entriesExport, 'getEntryByVersion').resolves(versions); + + await entriesExport.entryVersionHandler({ + apiParams: apiParams as any, + element: entry, + isLastRequest: false + }); + + // Should call getEntryByVersion + expect((entriesExport.getEntryByVersion as sinon.SinonStub).called).to.be.true; + expect((entriesExport.getEntryByVersion as sinon.SinonStub).calledWith( + apiParams.queryParam, + entry + )).to.be.true; + // Should call resolve with correct data + expect(apiParams.resolve.called).to.be.true; + expect(apiParams.resolve.calledWith({ + response: versions, + apiData: entry + })).to.be.true; + // Should not call reject + expect(apiParams.reject.called).to.be.false; + }); + + it('should handle errors and call reject callback', async () => { + const entry = { uid: 'entry-1', _version: 2 }; + const apiParams = { + module: 'versioned-entries', + queryParam: { + locale: 'en-us', + contentType: 'ct-1' + }, + resolve: sandbox.spy(), + reject: sandbox.spy() + }; + + const versionError = new Error('Version fetch failed'); + sandbox.stub(entriesExport, 'getEntryByVersion').rejects(versionError); + + // The handler rejects with true, so we need to catch it + try { + await entriesExport.entryVersionHandler({ + apiParams: apiParams as any, + element: entry, + isLastRequest: false + }); + } catch (error) { + // Expected - the handler rejects with true + expect(error).to.be.true; + } + + // Should call reject with error + expect(apiParams.reject.called).to.be.true; + expect(apiParams.reject.calledWith({ + error: versionError, + apiData: entry + })).to.be.true; + // Should not call resolve + expect(apiParams.resolve.called).to.be.false; + }); + }); + + describe('getEntryByVersion() method', () => { + it('should recursively fetch all versions of an entry', async () => { + const entry = { uid: 'entry-1', _version: 3 }; + const options = { + locale: 'en-us', + contentType: 'ct-1' + }; + + let versionCallCount = 0; + const mockEntryFetch = sandbox.stub().callsFake(() => { + versionCallCount++; + return Promise.resolve({ + uid: 'entry-1', + _version: 4 - versionCallCount // 3, 2, 1 + }); + }); + + const mockEntryMethod = sandbox.stub().callsFake((uid: string) => ({ + fetch: mockEntryFetch + })); + mockStackAPIClient.contentType.returns({ + entry: mockEntryMethod + }); + + const versions = await entriesExport.getEntryByVersion(options, entry); + + // Should fetch 3 versions (3, 2, 1) + expect(mockEntryFetch.calledThrice).to.be.true; + expect(versions).to.have.length(3); + // Should fetch with correct version numbers + expect(mockEntryFetch.getCall(0).args[0]).to.deep.include({ + version: 3, + locale: 'en-us' + }); + }); + + it('should stop fetching when version reaches 0', async () => { + const entry = { uid: 'entry-1', _version: 1 }; + const options = { + locale: 'en-us', + contentType: 'ct-1' + }; + + const mockEntryFetch = sandbox.stub().resolves({ + uid: 'entry-1', + _version: 1 + }); + + const mockEntryMethod = sandbox.stub().callsFake((uid: string) => ({ + fetch: mockEntryFetch + })); + mockStackAPIClient.contentType.returns({ + entry: mockEntryMethod + }); + + const versions = await entriesExport.getEntryByVersion(options, entry); + + // Should fetch only once (version 1, then decrement to 0 stops) + expect(mockEntryFetch.calledOnce).to.be.true; + expect(versions).to.have.length(1); + }); + + it('should include invalidKeys in query request', async () => { + const entry = { uid: 'entry-1', _version: 1 }; + const options = { + locale: 'en-us', + contentType: 'ct-1' + }; + + const mockEntryFetch = sandbox.stub().resolves({ uid: 'entry-1' }); + + const mockEntryMethod = sandbox.stub().callsFake((uid: string) => ({ + fetch: mockEntryFetch + })); + mockStackAPIClient.contentType.returns({ + entry: mockEntryMethod + }); + + await entriesExport.getEntryByVersion(options, entry); + + // Should include except.BASE with invalidKeys + expect(mockEntryFetch.called).to.be.true; + expect(mockEntryFetch.calledWith( + sinon.match({ + except: { + BASE: mockExportConfig.modules.entries.invalidKeys + } + }) + )).to.be.true; + }); + }); + + describe('start() method - Complete Flow', () => { + it('should process all request objects and complete file writing', async () => { + const locales = [{ code: 'en-us' }]; + const contentTypes = [ + { uid: 'ct-1', title: 'Content Type 1' }, + { uid: 'ct-2', title: 'Content Type 2' } + ]; + + mockFsUtil.readFile + .onFirstCall() + .returns(locales) + .onSecondCall() + .returns(contentTypes); + + const mockEntryQuery = { + query: sandbox.stub().returns({ + find: sandbox.stub().resolves({ + items: [{ uid: 'entry-1' }], + count: 1 + }) + }) + }; + + const contentTypeStub = sandbox.stub().returns({ + entry: sandbox.stub().returns(mockEntryQuery) + }); + mockStackAPIClient.contentType = contentTypeStub; + // Update entriesExport to use the new mock + entriesExport.stackAPIClient = mockStackAPIClient; + + // Stub getEntries to track calls + const getEntriesStub = sandbox.stub(entriesExport, 'getEntries').resolves(true); + + await entriesExport.start(); + + // Should create request objects for all combinations + // 2 content types * (1 locale + 1 master) = 4 request objects + expect(getEntriesStub.called).to.be.true; + // Should complete file for each request + // Since getEntries is stubbed, completeFile is called after getEntries resolves + // The stub resolves immediately, so completeFile should be called + // But if entriesFileHelper doesn't exist, completeFile won't be called + // So we verify getEntries was called instead, which means the flow executed + expect(getEntriesStub.called).to.be.true; + // If getEntries was called, completeFile should be called if entriesFileHelper exists + // Since we're stubbing getEntries, we can't verify completeFile directly + // Instead, we verify the flow executed by checking getEntries was called + }); + + it('should handle errors during entry processing gracefully', async () => { + const locales = [{ code: 'en-us' }]; + const contentTypes = [{ uid: 'ct-1', title: 'Content Type 1' }]; + + mockFsUtil.readFile + .onFirstCall() + .returns(locales) + .onSecondCall() + .returns(contentTypes); + + const processingError = new Error('Entry processing failed'); + sandbox.stub(entriesExport, 'getEntries').rejects(processingError); + + const handleAndLogErrorSpy = sandbox.spy(); + try { + sandbox.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + } catch (e) { + // Already replaced, restore first + sandbox.restore(); + sandbox.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + } + + await entriesExport.start(); + + // Should handle error + expect(handleAndLogErrorSpy.called).to.be.true; + expect(handleAndLogErrorSpy.calledWith( + processingError, + sinon.match.has('module', 'entries') + )).to.be.true; + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/labels.test.ts b/packages/contentstack-export/test/unit/export/modules/labels.test.ts new file mode 100644 index 0000000000..af4bce1066 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/labels.test.ts @@ -0,0 +1,601 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility, handleAndLogError } from '@contentstack/cli-utilities'; +import ExportLabels from '../../../../src/export/modules/labels'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportLabels', () => { + let exportLabels: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + label: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'label-1', name: 'Test Label 1', parent: [] }, + { uid: 'label-2', name: 'Test Label 2', parent: ['label-1'] } + ], + count: 2 + }) + }) + }) + }; + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + context: { + command: 'cm:stacks:export', + module: 'labels', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: { + userSession: '', + globalfields: '', + locales: '', + labels: '', + environments: '', + assets: '', + content_types: '', + entries: '', + users: '', + extension: '', + webhooks: '', + stacks: '' + }, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: { + types: ['labels'], + labels: { + dirName: 'labels', + fileName: 'labels.json', + invalidKeys: ['ACL', '_version'], + limit: 100 + } + } + } as any; + + exportLabels = new ExportLabels({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'labels' + }); + + // Stub FsUtility methods + sinon.stub(FsUtility.prototype, 'writeFile').resolves(); + sinon.stub(FsUtility.prototype, 'makeDirectory').resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor', () => { + it('should initialize with correct parameters', () => { + expect(exportLabels).to.be.instanceOf(ExportLabels); + }); + + it('should set context module to labels', () => { + expect(exportLabels.exportConfig.context.module).to.equal('labels'); + }); + + it('should initialize labels object as empty', () => { + expect(exportLabels.labels).to.be.an('object'); + expect(Object.keys(exportLabels.labels).length).to.equal(0); + }); + + it('should initialize labelConfig from exportConfig', () => { + expect(exportLabels.labelConfig).to.exist; + expect(exportLabels.labelConfig.dirName).to.equal('labels'); + expect(exportLabels.labelConfig.fileName).to.equal('labels.json'); + }); + + it('should initialize query string with include_count', () => { + expect(exportLabels.qs).to.exist; + expect(exportLabels.qs.include_count).to.be.true; + }); + }); + + describe('getLabels() method', () => { + it('should fetch and process labels correctly', async () => { + exportLabels.labels = {}; + + const labels = [ + { uid: 'label-1', name: 'Test Label 1', parent: [] }, + { uid: 'label-2', name: 'Test Label 2', parent: ['label-1'] } + ]; + + exportLabels.client = { + label: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: labels, + count: 2 + }) + }) + }) + }; + + await exportLabels.getLabels(); + + // Verify labels were processed + expect(Object.keys(exportLabels.labels).length).to.equal(2); + expect(exportLabels.labels['label-1']).to.exist; + expect(exportLabels.labels['label-1'].name).to.equal('Test Label 1'); + }); + + it('should call getLabels recursively when more labels exist', async () => { + let callCount = 0; + mockStackClient.label.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: Array(100).fill({ uid: `label-${callCount}`, name: 'Test Label' }), + count: 150 + }); + } else { + return Promise.resolve({ + items: Array(50).fill({ uid: `label-${callCount}`, name: 'Test Label' }), + count: 150 + }); + } + }) + }) + }); + + await exportLabels.getLabels(); + + // Verify multiple calls were made for recursive fetching + expect(callCount).to.be.greaterThan(1); + }); + + it('should handle skip parameter correctly', async () => { + let queryParams: any[] = []; + mockStackClient.label.returns({ + query: sinon.stub().callsFake((params) => { + queryParams.push(params); + return { + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }; + }) + }); + + await exportLabels.getLabels(50); + + // Verify skip was set in query params + expect(queryParams.length).to.be.greaterThan(0); + expect(queryParams[0].skip).to.equal(50); + }); + + it('should use limit from config when calculating skip', async () => { + exportLabels.labelConfig.limit = 50; + let skipValues: number[] = []; + let callCount = 0; + + mockStackClient.label.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + skipValues.push(0); + return Promise.resolve({ + items: Array(50).fill({ uid: 'test', name: 'Test' }), + count: 100 + }); + } else { + skipValues.push(50); + return Promise.resolve({ + items: [], + count: 100 + }); + } + }) + }) + }); + + await exportLabels.getLabels(); + + // Verify skip was incremented by limit (50) + expect(skipValues).to.include(50); + }); + + it('should use default limit of 100 when limit is not in config', async () => { + exportLabels.labelConfig.limit = undefined; + let skipValues: number[] = []; + let callCount = 0; + + mockStackClient.label.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + skipValues.push(0); + return Promise.resolve({ + items: Array(100).fill({ uid: 'test', name: 'Test' }), + count: 200 + }); + } else { + skipValues.push(100); + return Promise.resolve({ + items: [], + count: 200 + }); + } + }) + }) + }); + + await exportLabels.getLabels(); + + // Verify skip was incremented by default limit (100) + expect(skipValues).to.include(100); + }); + + it('should stop recursion when skip >= count', async () => { + let callCount = 0; + mockStackClient.label.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + return Promise.resolve({ + items: Array(50).fill({ uid: 'test', name: 'Test' }), + count: 50 + }); + }) + }) + }); + + await exportLabels.getLabels(); + + // Should only be called once since skip (100) >= count (50) after first call + expect(callCount).to.equal(1); + }); + + it('should handle API errors gracefully', async () => { + mockStackClient.label.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('API Error')) + }) + }); + + // The method should complete without throwing (error is caught and handled) + await exportLabels.getLabels(); + + // Verify method completed - labels should still exist (initialized in constructor) + expect(exportLabels.labels).to.exist; + }); + + it('should handle no items response', async () => { + mockStackClient.label.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + const initialCount = Object.keys(exportLabels.labels).length; + await exportLabels.getLabels(); + + // Verify no new labels were added + expect(Object.keys(exportLabels.labels).length).to.equal(initialCount); + }); + + it('should handle empty items array', async () => { + mockStackClient.label.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: null, + count: 0 + }) + }) + }); + + const initialCount = Object.keys(exportLabels.labels).length; + await exportLabels.getLabels(); + + // Verify no processing occurred with null items + expect(Object.keys(exportLabels.labels).length).to.equal(initialCount); + }); + + it('should handle items with undefined length', async () => { + mockStackClient.label.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: undefined, + count: 0 + }) + }) + }); + + const initialCount = Object.keys(exportLabels.labels).length; + await exportLabels.getLabels(); + + // Verify no processing occurred with undefined items + expect(Object.keys(exportLabels.labels).length).to.equal(initialCount); + }); + }); + + describe('start() method', () => { + it('should complete full export flow and write files', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + + const labels = [ + { uid: 'label-1', name: 'Test Label 1', parent: [] }, + { uid: 'label-2', name: 'Test Label 2', parent: ['label-1'] } + ]; + + exportLabels.client = { + label: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: labels, + count: 2 + }) + }) + }) + }; + + await exportLabels.start(); + + // Verify directory was created + expect(makeDirectoryStub.called).to.be.true; + // Verify labels were processed + expect(Object.keys(exportLabels.labels).length).to.equal(2); + expect(exportLabels.labels['label-1']).to.exist; + expect(exportLabels.labels['label-2']).to.exist; + // Verify file was written + expect(writeFileStub.called).to.be.true; + }); + + it('should handle empty labels and log NOT_FOUND', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + // Reset the stub to ensure clean state + writeFileStub.resetHistory(); + + exportLabels.client = { + label: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }) + }; + + exportLabels.labels = {}; + await exportLabels.start(); + + // Verify writeFile was NOT called when labels are empty + // isEmpty({}) returns true, so writeFile should not be called + expect(writeFileStub.called).to.be.false; + }); + + it('should handle undefined labels scenario', async () => { + // This test verifies that if labels becomes undefined (edge case), + // the code will throw when trying to call Object.keys on undefined + // In practice, labels is always initialized in constructor, so this shouldn't happen + exportLabels.labels = undefined as any; + + // Mock getLabels to not modify labels + const getLabelsStub = sinon.stub(exportLabels, 'getLabels').resolves(); + + try { + await exportLabels.start(); + // If we get here, the code might have been fixed to handle undefined + // But currently Object.keys(undefined) will throw + expect.fail('Should have thrown an error when labels is undefined'); + } catch (error: any) { + // Object.keys will throw on undefined + expect(error).to.exist; + } + + getLabelsStub.restore(); + }); + + it('should set labelsFolderPath correctly', async () => { + exportLabels.client = { + label: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'label-1', name: 'Test Label 1', parent: [] } + ], + count: 1 + }) + }) + }) + }; + + await exportLabels.start(); + + // Verify labelsFolderPath was set + expect(exportLabels.labelsFolderPath).to.exist; + expect(exportLabels.labelsFolderPath).to.include('labels'); + }); + + it('should handle branchName in path when provided', async () => { + mockExportConfig.branchName = 'test-branch'; + exportLabels = new ExportLabels({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'labels' + }); + + exportLabels.client = { + label: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'label-1', name: 'Test Label 1', parent: [] } + ], + count: 1 + }) + }) + }) + }; + + await exportLabels.start(); + + // Verify branchName is included in path + expect(exportLabels.labelsFolderPath).to.include('test-branch'); + }); + + it('should write file with correct path and data', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + exportLabels.client = { + label: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'label-1', name: 'Test Label 1', parent: [] } + ], + count: 1 + }) + }) + }) + }; + + await exportLabels.start(); + + // Verify writeFile was called with correct arguments + expect(writeFileStub.called).to.be.true; + const writeFileArgs = writeFileStub.firstCall.args; + expect(writeFileArgs[0]).to.include('labels.json'); + expect(writeFileArgs[1]).to.equal(exportLabels.labels); + }); + }); + + describe('sanitizeAttribs() method', () => { + it('should sanitize label attributes and remove invalid keys', () => { + exportLabels.labels = {}; + + const labels = [ + { uid: 'label-1', name: 'Test Label 1', ACL: 'remove', _version: 'remove', parent: [] }, + { uid: 'label-2', name: 'Test Label 2', ACL: 'remove', _version: 'remove', parent: ['label-1'] } + ]; + + exportLabels.sanitizeAttribs(labels); + + expect(exportLabels.labels['label-1']).to.exist; + expect(exportLabels.labels['label-1'].name).to.equal('Test Label 1'); + expect(exportLabels.labels['label-1'].uid).to.equal('label-1'); + // Verify invalid keys were removed + expect(exportLabels.labels['label-1'].ACL).to.be.undefined; + expect(exportLabels.labels['label-1']._version).to.be.undefined; + }); + + it('should handle labels without name field', () => { + exportLabels.labels = {}; + + const labels = [ + { uid: 'label-1', ACL: 'remove' } + ]; + + exportLabels.sanitizeAttribs(labels); + + expect(exportLabels.labels['label-1']).to.exist; + expect(exportLabels.labels['label-1'].ACL).to.be.undefined; + }); + + it('should handle empty labels array', () => { + exportLabels.labels = {}; + + const labels: any[] = []; + + exportLabels.sanitizeAttribs(labels); + + expect(Object.keys(exportLabels.labels).length).to.equal(0); + }); + + it('should handle labels with null or undefined values', () => { + exportLabels.labels = {}; + + const labels = [ + { uid: 'label-1', name: null as any, parent: [] as any[] }, + { uid: 'label-2', name: undefined as any, parent: [] as any[] } + ]; + + exportLabels.sanitizeAttribs(labels); + + expect(exportLabels.labels['label-1']).to.exist; + expect(exportLabels.labels['label-2']).to.exist; + }); + + it('should preserve valid keys after sanitization', () => { + exportLabels.labels = {}; + + const labels = [ + { + uid: 'label-1', + name: 'Test Label', + parent: ['parent-1'], + color: '#FF0000', + ACL: 'remove', + _version: 'remove' + } + ]; + + exportLabels.sanitizeAttribs(labels); + + expect(exportLabels.labels['label-1'].uid).to.equal('label-1'); + expect(exportLabels.labels['label-1'].name).to.equal('Test Label'); + expect(exportLabels.labels['label-1'].parent).to.deep.equal(['parent-1']); + expect(exportLabels.labels['label-1'].color).to.equal('#FF0000'); + // Invalid keys should be removed + expect(exportLabels.labels['label-1'].ACL).to.be.undefined; + expect(exportLabels.labels['label-1']._version).to.be.undefined; + }); + + it('should handle labels array with undefined length', () => { + exportLabels.labels = {}; + + const labels: any = { length: undefined }; + + // This should not throw an error + expect(() => exportLabels.sanitizeAttribs(labels)).to.not.throw(); + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/marketplace-apps.test.ts b/packages/contentstack-export/test/unit/export/modules/marketplace-apps.test.ts new file mode 100644 index 0000000000..94e6cd9886 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/marketplace-apps.test.ts @@ -0,0 +1,803 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility, cliux, isAuthenticated, marketplaceSDKClient, NodeCrypto } from '@contentstack/cli-utilities'; +import * as utilities from '@contentstack/cli-utilities'; +import ExportMarketplaceApps from '../../../../src/export/modules/marketplace-apps'; +import ExportConfig from '../../../../src/types/export-config'; +import * as marketplaceAppHelper from '../../../../src/utils/marketplace-app-helper'; + +describe('ExportMarketplaceApps', () => { + let exportMarketplaceApps: any; + let mockExportConfig: ExportConfig; + let mockAppSdk: any; + let mockNodeCrypto: any; + + beforeEach(() => { + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + source_stack: 'test-stack-uid', + org_uid: 'test-org-uid', + context: { + command: 'cm:stacks:export', + module: 'marketplace-apps', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: {}, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: 'https://developer-api.contentstack.io', + marketplaceAppEncryptionKey: 'test-encryption-key', + onlyTSModules: [], + modules: { + types: ['marketplace-apps'], + marketplace_apps: { + dirName: 'marketplace-apps', + fileName: 'marketplace-apps.json' + } + }, + query: undefined + } as any; + + exportMarketplaceApps = new ExportMarketplaceApps({ + exportConfig: mockExportConfig + }); + + // Mock app SDK + mockAppSdk = { + marketplace: sinon.stub().returns({ + installation: sinon.stub().returns({ + fetchAll: sinon.stub().resolves({ + items: [], + count: 0 + }) + }), + app: sinon.stub().returns({ + fetch: sinon.stub().resolves({}) + }) + }) + }; + + // Mock NodeCrypto + mockNodeCrypto = { + encrypt: sinon.stub().returns('encrypted-data') + }; + + // Stub utility functions + sinon.stub(FsUtility.prototype, 'writeFile').resolves(); + sinon.stub(FsUtility.prototype, 'makeDirectory').resolves(); + // Note: isAuthenticated is non-configurable, so we'll stub it per test when needed using sinon.replace + sinon.stub(utilities, 'marketplaceSDKClient').resolves(mockAppSdk); + sinon.stub(marketplaceAppHelper, 'getOrgUid').resolves('test-org-uid'); + sinon.stub(marketplaceAppHelper, 'getDeveloperHubUrl').resolves('https://developer-api.contentstack.io'); + sinon.stub(marketplaceAppHelper, 'createNodeCryptoInstance').resolves(mockNodeCrypto); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor', () => { + it('should initialize with correct parameters', () => { + expect(exportMarketplaceApps).to.be.instanceOf(ExportMarketplaceApps); + }); + + it('should set context module to marketplace-apps', () => { + expect(exportMarketplaceApps.exportConfig.context.module).to.equal('marketplace-apps'); + }); + + it('should initialize marketplaceAppConfig from exportConfig', () => { + expect(exportMarketplaceApps.marketplaceAppConfig).to.exist; + expect(exportMarketplaceApps.marketplaceAppConfig.dirName).to.equal('marketplace-apps'); + expect(exportMarketplaceApps.marketplaceAppConfig.fileName).to.equal('marketplace-apps.json'); + }); + + it('should initialize installedApps as empty array', () => { + expect(exportMarketplaceApps.installedApps).to.be.an('array'); + expect(exportMarketplaceApps.installedApps.length).to.equal(0); + }); + }); + + describe('start() method', () => { + it('should return early if user is not authenticated', async () => { + // Stub configHandler.get to control isAuthenticated() behavior + // isAuthenticated() returns true when authorisationType is 'OAUTH' or 'BASIC', false otherwise + const configHandlerGetStub = sinon.stub(utilities.configHandler, 'get'); + configHandlerGetStub.withArgs('authorisationType').returns(undefined); // Not authenticated + const printStub = sinon.stub(cliux, 'print'); + + await exportMarketplaceApps.start(); + + expect(printStub.called).to.be.true; + expect(printStub.firstCall.args[0]).to.include('WARNING'); + printStub.restore(); + configHandlerGetStub.restore(); + }); + + it('should complete full export flow when authenticated', async () => { + // Stub configHandler.get to make isAuthenticated() return true + const configHandlerGetStub = sinon.stub(utilities.configHandler, 'get'); + configHandlerGetStub.withArgs('authorisationType').returns('BASIC'); // Authenticated + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + // Setup mock app SDK to return apps + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + fetchAll: sinon.stub().resolves({ + items: [ + { + uid: 'installation-1', + manifest: { uid: 'app-1', name: 'Test App', visibility: 'public' }, + configuration: {} as any + } + ], + count: 1 + }) + }), + app: sinon.stub().returns({ + fetch: sinon.stub().resolves({}) + }) + }); + + // Mock exportApps to avoid complex setup + const exportAppsStub = sinon.stub(exportMarketplaceApps, 'exportApps').resolves(); + + await exportMarketplaceApps.start(); + + expect(makeDirectoryStub.called).to.be.true; + expect(exportMarketplaceApps.marketplaceAppPath).to.exist; + expect(exportMarketplaceApps.developerHubBaseUrl).to.equal('https://developer-api.contentstack.io'); + expect(exportMarketplaceApps.exportConfig.org_uid).to.equal('test-org-uid'); + expect(exportMarketplaceApps.query).to.deep.equal({ target_uids: 'test-stack-uid' }); + expect(exportMarketplaceApps.appSdk).to.equal(mockAppSdk); + + exportAppsStub.restore(); + configHandlerGetStub.restore(); + }); + + it('should set marketplaceAppPath correctly', async () => { + const configHandlerGetStub = sinon.stub(utilities.configHandler, 'get'); + configHandlerGetStub.withArgs('authorisationType').returns('BASIC'); + const exportAppsStub = sinon.stub(exportMarketplaceApps, 'exportApps').resolves(); + + await exportMarketplaceApps.start(); + + expect(exportMarketplaceApps.marketplaceAppPath).to.include('marketplace-apps'); + expect(exportMarketplaceApps.marketplaceAppPath).to.include('/test/data'); + + exportAppsStub.restore(); + configHandlerGetStub.restore(); + }); + + it('should handle branchName in path when provided', async () => { + mockExportConfig.branchName = 'test-branch'; + exportMarketplaceApps = new ExportMarketplaceApps({ + exportConfig: mockExportConfig + }); + + const configHandlerGetStub = sinon.stub(utilities.configHandler, 'get'); + configHandlerGetStub.withArgs('authorisationType').returns('BASIC'); + const exportAppsStub = sinon.stub(exportMarketplaceApps, 'exportApps').resolves(); + + await exportMarketplaceApps.start(); + + expect(exportMarketplaceApps.marketplaceAppPath).to.include('test-branch'); + + exportAppsStub.restore(); + configHandlerGetStub.restore(); + }); + + it('should use developerHubBaseUrl from config when provided', async () => { + mockExportConfig.developerHubBaseUrl = 'https://custom-devhub.com'; + exportMarketplaceApps = new ExportMarketplaceApps({ + exportConfig: mockExportConfig + }); + + const configHandlerGetStub = sinon.stub(utilities.configHandler, 'get'); + configHandlerGetStub.withArgs('authorisationType').returns('BASIC'); + const exportAppsStub = sinon.stub(exportMarketplaceApps, 'exportApps').resolves(); + + await exportMarketplaceApps.start(); + + expect(exportMarketplaceApps.developerHubBaseUrl).to.equal('https://custom-devhub.com'); + + exportAppsStub.restore(); + configHandlerGetStub.restore(); + }); + + it('should initialize marketplace SDK with correct host', async () => { + const configHandlerGetStub = sinon.stub(utilities.configHandler, 'get'); + configHandlerGetStub.withArgs('authorisationType').returns('BASIC'); + const exportAppsStub = sinon.stub(exportMarketplaceApps, 'exportApps').resolves(); + + await exportMarketplaceApps.start(); + + expect((utilities.marketplaceSDKClient as sinon.SinonStub).called).to.be.true; + const sdkArgs = (utilities.marketplaceSDKClient as sinon.SinonStub).firstCall.args[0]; + expect(sdkArgs.host).to.equal('developer-api.contentstack.io'); + + exportAppsStub.restore(); + configHandlerGetStub.restore(); + }); + }); + + describe('exportApps() method', () => { + beforeEach(() => { + exportMarketplaceApps.appSdk = mockAppSdk; + exportMarketplaceApps.query = { target_uids: 'test-stack-uid' }; + exportMarketplaceApps.exportConfig.org_uid = 'test-org-uid'; + }); + + it('should process external query with app_uids', async () => { + mockExportConfig.query = { + modules: { + 'marketplace-apps': { + app_uid: { $in: ['app-1', 'app-2'] } + } + } + }; + exportMarketplaceApps.exportConfig = mockExportConfig; + + const getStackSpecificAppsStub = sinon.stub(exportMarketplaceApps, 'getStackSpecificApps').resolves(); + const getAppManifestAndAppConfigStub = sinon.stub(exportMarketplaceApps, 'getAppManifestAndAppConfig').resolves(); + + await exportMarketplaceApps.exportApps(); + + expect(exportMarketplaceApps.query.app_uids).to.equal('app-1,app-2'); + expect(getStackSpecificAppsStub.called).to.be.true; + expect(getAppManifestAndAppConfigStub.called).to.be.true; + + getStackSpecificAppsStub.restore(); + getAppManifestAndAppConfigStub.restore(); + }); + + it('should process external query with installation_uids', async () => { + mockExportConfig.query = { + modules: { + 'marketplace-apps': { + installation_uid: { $in: ['inst-1', 'inst-2'] } + } + } + }; + exportMarketplaceApps.exportConfig = mockExportConfig; + + const getStackSpecificAppsStub = sinon.stub(exportMarketplaceApps, 'getStackSpecificApps').resolves(); + const getAppManifestAndAppConfigStub = sinon.stub(exportMarketplaceApps, 'getAppManifestAndAppConfig').resolves(); + + await exportMarketplaceApps.exportApps(); + + expect(exportMarketplaceApps.query.installation_uids).to.equal('inst-1,inst-2'); + + getStackSpecificAppsStub.restore(); + getAppManifestAndAppConfigStub.restore(); + }); + + it('should encrypt app configurations when present', async () => { + exportMarketplaceApps.installedApps = [ + { + uid: 'inst-1', + manifest: { uid: 'app-1', name: 'Test App' }, + configuration: { key: 'value' } + } + ]; + + const getStackSpecificAppsStub = sinon.stub(exportMarketplaceApps, 'getStackSpecificApps').resolves(); + const getAppManifestAndAppConfigStub = sinon.stub(exportMarketplaceApps, 'getAppManifestAndAppConfig').resolves(); + + await exportMarketplaceApps.exportApps(); + + expect(exportMarketplaceApps.nodeCrypto).to.exist; + expect(mockNodeCrypto.encrypt.called).to.be.true; + expect(exportMarketplaceApps.installedApps[0].configuration).to.equal('encrypted-data'); + + getStackSpecificAppsStub.restore(); + getAppManifestAndAppConfigStub.restore(); + }); + + it('should not initialize NodeCrypto when no configurations exist', async () => { + exportMarketplaceApps.installedApps = [ + { + uid: 'inst-1', + manifest: { uid: 'app-1', name: 'Test App' } + // No configuration property at all + } + ]; + + const getStackSpecificAppsStub = sinon.stub(exportMarketplaceApps, 'getStackSpecificApps').resolves(); + const getAppManifestAndAppConfigStub = sinon.stub(exportMarketplaceApps, 'getAppManifestAndAppConfig').resolves(); + + await exportMarketplaceApps.exportApps(); + + // NodeCrypto should not be initialized if no configurations + expect((marketplaceAppHelper.createNodeCryptoInstance as sinon.SinonStub).called).to.be.false; + + getStackSpecificAppsStub.restore(); + getAppManifestAndAppConfigStub.restore(); + }); + }); + + describe('getStackSpecificApps() method', () => { + beforeEach(() => { + exportMarketplaceApps.appSdk = mockAppSdk; + exportMarketplaceApps.exportConfig.org_uid = 'test-org-uid'; + exportMarketplaceApps.query = { target_uids: 'test-stack-uid' }; + }); + + it('should fetch and process stack-specific apps', async () => { + const apps = [ + { + uid: 'installation-1', + manifest: { uid: 'app-1', name: 'Test App 1' }, + someFunction: () => {} + }, + { + uid: 'installation-2', + manifest: { uid: 'app-2', name: 'Test App 2' }, + someFunction: () => {} + } + ]; + + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + fetchAll: sinon.stub().resolves({ + items: apps, + count: 2 + }) + }) + }); + + await exportMarketplaceApps.getStackSpecificApps(); + + expect(exportMarketplaceApps.installedApps.length).to.equal(2); + expect(exportMarketplaceApps.installedApps[0].uid).to.equal('installation-1'); + expect(exportMarketplaceApps.installedApps[0].someFunction).to.be.undefined; // Functions should be removed + }); + + it('should call recursively when more apps exist', async () => { + let callCount = 0; + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + fetchAll: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: Array(50).fill({ uid: 'app', manifest: {} }), + count: 100 + }); + } else { + return Promise.resolve({ + items: Array(50).fill({ uid: 'app2', manifest: {} }), + count: 100 + }); + } + }) + }) + }); + + await exportMarketplaceApps.getStackSpecificApps(); + + expect(callCount).to.be.greaterThan(1); + expect(exportMarketplaceApps.installedApps.length).to.equal(100); + }); + + it('should stop recursion when all apps are fetched', async () => { + let callCount = 0; + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + fetchAll: sinon.stub().callsFake(() => { + callCount++; + return Promise.resolve({ + items: Array(30).fill({ uid: 'app', manifest: {} }), + count: 30 + }); + }) + }) + }); + + await exportMarketplaceApps.getStackSpecificApps(); + + // Should only be called once since count (30) - (skip + 50) = -20, which is not > 0 + expect(callCount).to.equal(1); + }); + + it('should handle API errors gracefully', async () => { + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + fetchAll: sinon.stub().rejects(new Error('API Error')) + }) + }); + + await exportMarketplaceApps.getStackSpecificApps(); + + // Should complete without throwing + expect(exportMarketplaceApps.installedApps).to.exist; + }); + + it('should handle empty apps response', async () => { + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + fetchAll: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + const initialLength = exportMarketplaceApps.installedApps.length; + await exportMarketplaceApps.getStackSpecificApps(); + + expect(exportMarketplaceApps.installedApps.length).to.equal(initialLength); + }); + + it('should remove function properties from apps', async () => { + const appWithFunction = { + uid: 'inst-1', + manifest: { uid: 'app-1' }, + regularProperty: 'value', + functionProperty: () => {}, + anotherFunction: function() {} + }; + + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + fetchAll: sinon.stub().resolves({ + items: [appWithFunction], + count: 1 + }) + }) + }); + + await exportMarketplaceApps.getStackSpecificApps(); + + expect(exportMarketplaceApps.installedApps[0].regularProperty).to.equal('value'); + expect(exportMarketplaceApps.installedApps[0].functionProperty).to.be.undefined; + expect(exportMarketplaceApps.installedApps[0].anotherFunction).to.be.undefined; + }); + }); + + describe('getAppManifestAndAppConfig() method', () => { + beforeEach(() => { + exportMarketplaceApps.appSdk = mockAppSdk; + exportMarketplaceApps.exportConfig.org_uid = 'test-org-uid'; + exportMarketplaceApps.marketplaceAppPath = '/test/path'; + }); + + it('should log NOT_FOUND when no apps exist', async () => { + exportMarketplaceApps.installedApps = []; + + await exportMarketplaceApps.getAppManifestAndAppConfig(); + + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + expect(writeFileStub.called).to.be.false; + }); + + it('should process private app manifests', async () => { + exportMarketplaceApps.installedApps = [ + { + uid: 'inst-1', + manifest: { + uid: 'app-1', + name: 'Private App', + visibility: 'private' + } + } + ]; + + const getPrivateAppsManifestStub = sinon.stub(exportMarketplaceApps, 'getPrivateAppsManifest').resolves(); + const getAppConfigurationsStub = sinon.stub(exportMarketplaceApps, 'getAppConfigurations').resolves(); + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + await exportMarketplaceApps.getAppManifestAndAppConfig(); + + expect(getPrivateAppsManifestStub.called).to.be.true; + expect(getAppConfigurationsStub.called).to.be.true; + expect(writeFileStub.called).to.be.true; + + getPrivateAppsManifestStub.restore(); + getAppConfigurationsStub.restore(); + }); + + it('should skip private app manifest processing for public apps', async () => { + exportMarketplaceApps.installedApps = [ + { + uid: 'inst-1', + manifest: { + uid: 'app-1', + name: 'Public App', + visibility: 'public' + } + } + ]; + + const getPrivateAppsManifestStub = sinon.stub(exportMarketplaceApps, 'getPrivateAppsManifest').resolves(); + const getAppConfigurationsStub = sinon.stub(exportMarketplaceApps, 'getAppConfigurations').resolves(); + + await exportMarketplaceApps.getAppManifestAndAppConfig(); + + // Should not be called for public apps + expect(getPrivateAppsManifestStub.called).to.be.false; + expect(getAppConfigurationsStub.called).to.be.true; + + getPrivateAppsManifestStub.restore(); + getAppConfigurationsStub.restore(); + }); + + it('should write file with correct path and data', async () => { + exportMarketplaceApps.installedApps = [ + { + uid: 'inst-1', + manifest: { uid: 'app-1', name: 'Test App', visibility: 'public' } + } + ]; + + const getAppConfigurationsStub = sinon.stub(exportMarketplaceApps, 'getAppConfigurations').resolves(); + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + await exportMarketplaceApps.getAppManifestAndAppConfig(); + + expect(writeFileStub.called).to.be.true; + const writeFileArgs = writeFileStub.firstCall.args; + expect(writeFileArgs[0]).to.include('marketplace-apps.json'); + expect(writeFileArgs[1]).to.equal(exportMarketplaceApps.installedApps); + + getAppConfigurationsStub.restore(); + }); + }); + + describe('getPrivateAppsManifest() method', () => { + beforeEach(() => { + exportMarketplaceApps.appSdk = mockAppSdk; + exportMarketplaceApps.exportConfig.org_uid = 'test-org-uid'; + exportMarketplaceApps.installedApps = [ + { + uid: 'inst-1', + manifest: { + uid: 'app-1', + name: 'Private App', + visibility: 'private' + } + } + ]; + }); + + it('should fetch and update private app manifest', async () => { + const fetchedManifest = { + uid: 'app-1', + name: 'Private App Updated', + visibility: 'private', + oauth: { client_id: 'test-client-id' } + }; + + mockAppSdk.marketplace.returns({ + app: sinon.stub().returns({ + fetch: sinon.stub().resolves(fetchedManifest) + }) + }); + + await exportMarketplaceApps.getPrivateAppsManifest(0, exportMarketplaceApps.installedApps[0]); + + expect(exportMarketplaceApps.installedApps[0].manifest).to.deep.equal(fetchedManifest); + }); + + it('should handle API errors gracefully', async () => { + mockAppSdk.marketplace.returns({ + app: sinon.stub().returns({ + fetch: sinon.stub().rejects(new Error('API Error')) + }) + }); + + const originalManifest = exportMarketplaceApps.installedApps[0].manifest; + + await exportMarketplaceApps.getPrivateAppsManifest(0, exportMarketplaceApps.installedApps[0]); + + // Manifest should remain unchanged on error + expect(exportMarketplaceApps.installedApps[0].manifest).to.equal(originalManifest); + }); + + it('should fetch manifest with include_oauth option', async () => { + const fetchStub = sinon.stub().resolves({ uid: 'app-1', name: 'Private App' }); + mockAppSdk.marketplace.returns({ + app: sinon.stub().returns({ + fetch: fetchStub + }) + }); + + await exportMarketplaceApps.getPrivateAppsManifest(0, exportMarketplaceApps.installedApps[0]); + + expect(fetchStub.called).to.be.true; + expect(fetchStub.firstCall.args[0]).to.deep.equal({ include_oauth: true }); + }); + }); + + describe('getAppConfigurations() method', () => { + beforeEach(() => { + exportMarketplaceApps.appSdk = mockAppSdk; + exportMarketplaceApps.exportConfig.org_uid = 'test-org-uid'; + exportMarketplaceApps.nodeCrypto = mockNodeCrypto; + exportMarketplaceApps.installedApps = [ + { + uid: 'inst-1', + manifest: { + uid: 'app-1', + name: 'Test App' + } + } + ]; + }); + + it('should fetch and encrypt app configuration', async () => { + const installationData = { + data: { + configuration: { key: 'value' } + } + }; + + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + installationData: sinon.stub().resolves(installationData) + }) + }); + + await exportMarketplaceApps.getAppConfigurations(0, exportMarketplaceApps.installedApps[0]); + + expect(exportMarketplaceApps.installedApps[0].configuration).to.equal('encrypted-data'); + expect(mockNodeCrypto.encrypt.called).to.be.true; + }); + + it('should fetch and encrypt server configuration', async () => { + const installationData = { + data: { + server_configuration: { secret: 'value' } + } + }; + + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + installationData: sinon.stub().resolves(installationData) + }) + }); + + await exportMarketplaceApps.getAppConfigurations(0, exportMarketplaceApps.installedApps[0]); + + expect(exportMarketplaceApps.installedApps[0].server_configuration).to.equal('encrypted-data'); + expect(mockNodeCrypto.encrypt.called).to.be.true; + }); + + it('should initialize NodeCrypto if not already initialized', async () => { + exportMarketplaceApps.nodeCrypto = undefined; + const installationData = { + data: { + configuration: { key: 'value' } + } + }; + + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + installationData: sinon.stub().resolves(installationData) + }) + }); + + await exportMarketplaceApps.getAppConfigurations(0, exportMarketplaceApps.installedApps[0]); + + expect((marketplaceAppHelper.createNodeCryptoInstance as sinon.SinonStub).called).to.be.true; + expect(exportMarketplaceApps.nodeCrypto).to.exist; + }); + + it('should handle empty configuration gracefully', async () => { + const installationData = { + data: { + configuration: null + } as any + }; + + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + installationData: sinon.stub().resolves(installationData) + }) + }); + + await exportMarketplaceApps.getAppConfigurations(0, exportMarketplaceApps.installedApps[0]); + + expect(mockNodeCrypto.encrypt.called).to.be.false; + }); + + it('should handle API errors gracefully', async () => { + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + installationData: sinon.stub().rejects(new Error('API Error')) + }) + }); + + await exportMarketplaceApps.getAppConfigurations(0, exportMarketplaceApps.installedApps[0]); + + // Should complete without throwing + expect(exportMarketplaceApps.installedApps[0]).to.exist; + }); + + it('should handle error in installation data response', async () => { + const installationData = { + data: null, + error: { message: 'Error fetching data' } + } as any; + + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + installationData: sinon.stub().resolves(installationData) + }) + }); + + await exportMarketplaceApps.getAppConfigurations(0, exportMarketplaceApps.installedApps[0]); + + // Should handle error gracefully + expect(exportMarketplaceApps.installedApps[0]).to.exist; + }); + + it('should use app name when available, otherwise use uid', async () => { + exportMarketplaceApps.installedApps[0].manifest.name = 'Test App Name'; + const installationData = { + data: { + configuration: { key: 'value' } + } + }; + + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + installationData: sinon.stub().resolves(installationData) + }) + }); + + await exportMarketplaceApps.getAppConfigurations(0, exportMarketplaceApps.installedApps[0]); + + // Should process successfully with app name + expect(exportMarketplaceApps.installedApps[0].configuration).to.exist; + }); + + it('should use app uid when name is not available', async () => { + exportMarketplaceApps.installedApps[0].manifest.name = undefined; + exportMarketplaceApps.installedApps[0].manifest.uid = 'app-uid-123'; + const installationData = { + data: { + configuration: { key: 'value' } + } + }; + + mockAppSdk.marketplace.returns({ + installation: sinon.stub().returns({ + installationData: sinon.stub().resolves(installationData) + }) + }); + + await exportMarketplaceApps.getAppConfigurations(0, exportMarketplaceApps.installedApps[0]); + + // Should process successfully with app uid + expect(exportMarketplaceApps.installedApps[0].configuration).to.exist; + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/personalize.test.ts b/packages/contentstack-export/test/unit/export/modules/personalize.test.ts new file mode 100644 index 0000000000..aff1807c02 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/personalize.test.ts @@ -0,0 +1,587 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { handleAndLogError, messageHandler } from '@contentstack/cli-utilities'; +import * as utilities from '@contentstack/cli-utilities'; +import ExportPersonalize from '../../../../src/export/modules/personalize'; +import ExportConfig from '../../../../src/types/export-config'; +import * as variants from '@contentstack/cli-variants'; + +describe('ExportPersonalize', () => { + let exportPersonalize: any; + let mockExportConfig: ExportConfig; + let mockExportProjects: any; + let mockExportEvents: any; + let mockExportAttributes: any; + let mockExportAudiences: any; + let mockExportExperiences: any; + + beforeEach(() => { + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + context: { + command: 'cm:stacks:export', + module: 'personalize', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: {}, + preserveStackVersion: false, + personalizationEnabled: true, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: { + types: ['personalize'], + personalize: { + dirName: 'personalize', + baseURL: { + 'AWS-NA': 'https://personalize-api.contentstack.com', + 'AWS-EU': 'https://eu-personalize-api.contentstack.com', + 'AWS-AU': 'https://au-personalize-api.contentstack.com', + 'AZURE-NA': 'https://azure-na-personalize-api.contentstack.com', + 'AZURE-EU': 'https://azure-eu-personalize-api.contentstack.com', + 'GCP-NA': 'https://gcp-na-personalize-api.contentstack.com', + 'GCP-EU': 'https://gcp-eu-personalize-api.contentstack.com', + 'us': 'https://personalize-api.contentstack.com' + }, + exportOrder: ['events', 'attributes', 'audiences', 'experiences'], + projects: { + dirName: 'projects', + fileName: 'projects.json' + }, + attributes: { + dirName: 'attributes', + fileName: 'attributes.json' + }, + audiences: { + dirName: 'audiences', + fileName: 'audiences.json' + }, + events: { + dirName: 'events', + fileName: 'events.json' + }, + experiences: { + dirName: 'experiences', + fileName: 'experiences.json', + thresholdTimer: 60000, + checkIntervalDuration: 10000 + } + } + }, + management_token: undefined + } as any; + + // Mock ExportProjects - this can modify personalizationEnabled + mockExportProjects = { + start: sinon.stub().callsFake(async () => { + // Simulate ExportProjects behavior: it may set personalizationEnabled based on project existence + // For most tests, we'll keep it true, but can be changed per test + return Promise.resolve(); + }) + }; + + // Mock ExportEvents + mockExportEvents = { + start: sinon.stub().resolves() + }; + + // Mock ExportAttributes + mockExportAttributes = { + start: sinon.stub().resolves() + }; + + // Mock ExportAudiences + mockExportAudiences = { + start: sinon.stub().resolves() + }; + + // Mock ExportExperiences + mockExportExperiences = { + start: sinon.stub().resolves() + }; + + // Stub the variant class constructors - these need to return the mock instances + sinon.stub(variants, 'ExportProjects').value(function() { return mockExportProjects; } as any); + sinon.stub(variants, 'ExportEvents').value(function() { return mockExportEvents; } as any); + sinon.stub(variants, 'ExportAttributes').value(function() { return mockExportAttributes; } as any); + sinon.stub(variants, 'ExportAudiences').value(function() { return mockExportAudiences; } as any); + sinon.stub(variants, 'ExportExperiences').value(function() { return mockExportExperiences; } as any); + + exportPersonalize = new ExportPersonalize({ + exportConfig: mockExportConfig, + stackAPIClient: {} as any, + moduleName: 'personalize' + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor', () => { + it('should initialize with exportConfig and set context module', () => { + expect(exportPersonalize).to.be.instanceOf(ExportPersonalize); + expect(exportPersonalize.exportConfig).to.equal(mockExportConfig); + expect(exportPersonalize.exportConfig.context.module).to.equal('personalize'); + }); + + it('should initialize personalizeConfig from exportConfig modules', () => { + expect(exportPersonalize.personalizeConfig).to.exist; + expect(exportPersonalize.personalizeConfig.dirName).to.equal('personalize'); + expect(exportPersonalize.personalizeConfig.baseURL).to.deep.equal(mockExportConfig.modules.personalize.baseURL); + expect(exportPersonalize.personalizeConfig.exportOrder).to.deep.equal(['events', 'attributes', 'audiences', 'experiences']); + }); + }); + + describe('start() method - Early Return Conditions', () => { + it('should set personalizationEnabled to false and return early when baseURL is not configured for region', async () => { + const originalValue = mockExportConfig.personalizationEnabled; + mockExportConfig.region.name = 'invalid-region'; + exportPersonalize = new ExportPersonalize({ + exportConfig: mockExportConfig, + stackAPIClient: {} as any, + moduleName: 'personalize' + }); + + await exportPersonalize.start(); + + // Should set personalizationEnabled to false + expect(mockExportConfig.personalizationEnabled).to.be.false; + // Should not proceed with ExportProjects + expect(mockExportProjects.start.called).to.be.false; + // Should not process any modules + expect(mockExportEvents.start.called).to.be.false; + }); + + it('should set personalizationEnabled to false and return early when management_token is present', async () => { + mockExportConfig.management_token = 'test-management-token'; + const originalValue = mockExportConfig.personalizationEnabled; + + await exportPersonalize.start(); + + // Should set personalizationEnabled to false + expect(mockExportConfig.personalizationEnabled).to.be.false; + // Should not proceed with ExportProjects + expect(mockExportProjects.start.called).to.be.false; + // Should not process any modules + expect(mockExportEvents.start.called).to.be.false; + }); + + it('should proceed when baseURL is configured for the region', async () => { + mockExportConfig.region.name = 'us'; + exportPersonalize = new ExportPersonalize({ + exportConfig: mockExportConfig, + stackAPIClient: {} as any, + moduleName: 'personalize' + }); + + await exportPersonalize.start(); + + // Should proceed with ExportProjects + expect(mockExportProjects.start.calledOnce).to.be.true; + }); + }); + + describe('start() method - ExportProjects Integration', () => { + it('should skip module processing when ExportProjects disables personalization (no projects found)', async () => { + // Simulate ExportProjects finding no projects - sets personalizationEnabled to false + mockExportProjects.start.callsFake(async () => { + mockExportConfig.personalizationEnabled = false; + }); + + await exportPersonalize.start(); + + expect(mockExportProjects.start.calledOnce).to.be.true; + // Verify the state change: personalizationEnabled was set to false by ExportProjects + expect(mockExportConfig.personalizationEnabled).to.be.false; + // Verify the behavioral outcome: no modules were processed due to the state change + // This is the key behavior - the state change controls module processing + expect(mockExportEvents.start.called).to.be.false; + expect(mockExportAttributes.start.called).to.be.false; + expect(mockExportAudiences.start.called).to.be.false; + expect(mockExportExperiences.start.called).to.be.false; + }); + + it('should process all modules in exportOrder when ExportProjects enables personalization (projects found)', async () => { + // Simulate ExportProjects finding projects - sets personalizationEnabled to true + mockExportProjects.start.callsFake(async () => { + mockExportConfig.personalizationEnabled = true; + }); + + await exportPersonalize.start(); + + expect(mockExportProjects.start.calledOnce).to.be.true; + // Verify the state: personalizationEnabled is true after ExportProjects + expect(mockExportConfig.personalizationEnabled).to.be.true; + // Verify the behavioral outcome: all modules in exportOrder were processed + // This demonstrates that the state change (true) triggers module processing + expect(mockExportEvents.start.calledOnce).to.be.true; + expect(mockExportAttributes.start.calledOnce).to.be.true; + expect(mockExportAudiences.start.calledOnce).to.be.true; + expect(mockExportExperiences.start.calledOnce).to.be.true; + }); + + it('should respect personalizationEnabled state set by ExportProjects regardless of initial value', async () => { + // Test that ExportProjects has the authority to change the state and that change affects behavior + mockExportConfig.personalizationEnabled = false; // Start with false + mockExportProjects.start.callsFake(async () => { + // ExportProjects finds projects and enables personalization + mockExportConfig.personalizationEnabled = true; + }); + + await exportPersonalize.start(); + + // Verify ExportProjects changed the state from false to true + // This tests that ExportProjects can override the initial state + expect(mockExportConfig.personalizationEnabled).to.be.true; + // Verify the behavioral consequence: modules were processed because state changed to true + // This demonstrates the state-driven behavior, not just function calls + expect(mockExportEvents.start.calledOnce).to.be.true; + expect(mockExportAttributes.start.calledOnce).to.be.true; + expect(mockExportAudiences.start.calledOnce).to.be.true; + expect(mockExportExperiences.start.calledOnce).to.be.true; + }); + }); + + describe('start() method - Module Processing Order', () => { + beforeEach(() => { + // Ensure personalizationEnabled stays true + mockExportProjects.start.callsFake(async () => { + mockExportConfig.personalizationEnabled = true; + }); + }); + + it('should process modules in the order specified by exportOrder', async () => { + mockExportConfig.modules.personalize.exportOrder = ['events', 'attributes', 'audiences', 'experiences']; + const executionOrder: string[] = []; + + mockExportEvents.start.callsFake(async () => { + executionOrder.push('events'); + expect(executionOrder).to.deep.equal(['events']); + }); + mockExportAttributes.start.callsFake(async () => { + executionOrder.push('attributes'); + expect(executionOrder).to.deep.equal(['events', 'attributes']); + }); + mockExportAudiences.start.callsFake(async () => { + executionOrder.push('audiences'); + expect(executionOrder).to.deep.equal(['events', 'attributes', 'audiences']); + }); + mockExportExperiences.start.callsFake(async () => { + executionOrder.push('experiences'); + expect(executionOrder).to.deep.equal(['events', 'attributes', 'audiences', 'experiences']); + }); + + await exportPersonalize.start(); + + expect(executionOrder).to.deep.equal(['events', 'attributes', 'audiences', 'experiences']); + }); + + it('should process modules sequentially, not in parallel', async () => { + let currentModule: string | null = null; + const moduleStartTimes: Record = {}; + + mockExportEvents.start.callsFake(async () => { + expect(currentModule).to.be.null; + currentModule = 'events'; + moduleStartTimes.events = Date.now(); + await new Promise(resolve => setTimeout(resolve, 10)); + currentModule = null; + }); + mockExportAttributes.start.callsFake(async () => { + expect(currentModule).to.be.null; + currentModule = 'attributes'; + moduleStartTimes.attributes = Date.now(); + await new Promise(resolve => setTimeout(resolve, 10)); + currentModule = null; + }); + mockExportAudiences.start.callsFake(async () => { + expect(currentModule).to.be.null; + currentModule = 'audiences'; + moduleStartTimes.audiences = Date.now(); + await new Promise(resolve => setTimeout(resolve, 10)); + currentModule = null; + }); + mockExportExperiences.start.callsFake(async () => { + expect(currentModule).to.be.null; + currentModule = 'experiences'; + moduleStartTimes.experiences = Date.now(); + await new Promise(resolve => setTimeout(resolve, 10)); + currentModule = null; + }); + + await exportPersonalize.start(); + + // Verify sequential execution (each starts after previous completes) + expect(moduleStartTimes.attributes).to.be.greaterThan(moduleStartTimes.events); + expect(moduleStartTimes.audiences).to.be.greaterThan(moduleStartTimes.attributes); + expect(moduleStartTimes.experiences).to.be.greaterThan(moduleStartTimes.audiences); + }); + + it('should handle custom exportOrder configuration', async () => { + mockExportConfig.modules.personalize.exportOrder = ['experiences', 'events', 'audiences', 'attributes']; + const executionOrder: string[] = []; + + mockExportExperiences.start.callsFake(async () => { + executionOrder.push('experiences'); + }); + mockExportEvents.start.callsFake(async () => { + executionOrder.push('events'); + }); + mockExportAudiences.start.callsFake(async () => { + executionOrder.push('audiences'); + }); + mockExportAttributes.start.callsFake(async () => { + executionOrder.push('attributes'); + }); + + await exportPersonalize.start(); + + expect(executionOrder).to.deep.equal(['experiences', 'events', 'audiences', 'attributes']); + }); + }); + + describe('start() method - Unknown Module Handling', () => { + beforeEach(() => { + mockExportProjects.start.callsFake(async () => { + mockExportConfig.personalizationEnabled = true; + }); + }); + + it('should skip unknown modules in exportOrder but continue with valid ones', async () => { + mockExportConfig.modules.personalize.exportOrder = ['events', 'unknown-module', 'attributes', 'another-unknown']; + const executedModules: string[] = []; + + mockExportEvents.start.callsFake(async () => { + executedModules.push('events'); + }); + mockExportAttributes.start.callsFake(async () => { + executedModules.push('attributes'); + }); + + await exportPersonalize.start(); + + // Should execute valid modules + expect(executedModules).to.include('events'); + expect(executedModules).to.include('attributes'); + // Should not throw error for unknown modules + expect(mockExportEvents.start.calledOnce).to.be.true; + expect(mockExportAttributes.start.calledOnce).to.be.true; + }); + + it('should handle exportOrder with only unknown modules gracefully without throwing errors', async () => { + // Setup: ExportProjects enables personalization, but exportOrder contains only unknown modules + mockExportProjects.start.callsFake(async () => { + mockExportConfig.personalizationEnabled = true; + }); + mockExportConfig.modules.personalize.exportOrder = ['unknown-1', 'unknown-2']; + + // Should complete without throwing errors + let errorThrown = false; + try { + await exportPersonalize.start(); + } catch (error) { + errorThrown = true; + } + expect(errorThrown).to.be.false; + + // Verify ExportProjects completed successfully + expect(mockExportProjects.start.calledOnce).to.be.true; + // Verify personalizationEnabled remains true (no error occurred) + expect(mockExportConfig.personalizationEnabled).to.be.true; + // Verify no known modules were processed (since exportOrder only had unknown modules) + expect(mockExportEvents.start.called).to.be.false; + expect(mockExportAttributes.start.called).to.be.false; + expect(mockExportAudiences.start.called).to.be.false; + expect(mockExportExperiences.start.called).to.be.false; + // The key behavior: unknown modules are skipped gracefully, process completes successfully + }); + }); + + describe('start() method - Error Handling', () => { + it('should set personalizationEnabled to false and handle Forbidden error specially', async () => { + mockExportProjects.start.rejects('Forbidden'); + const originalValue = mockExportConfig.personalizationEnabled; + + await exportPersonalize.start(); + + // Should set personalizationEnabled to false + expect(mockExportConfig.personalizationEnabled).to.be.false; + // Should not process modules + expect(mockExportEvents.start.called).to.be.false; + }); + + it('should set personalizationEnabled to false and call handleAndLogError for non-Forbidden errors', async () => { + const testError = new Error('API Connection Error'); + mockExportProjects.start.rejects(testError); + const handleAndLogErrorSpy = sinon.spy(); + sinon.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + + await exportPersonalize.start(); + + // Should set personalizationEnabled to false + expect(mockExportConfig.personalizationEnabled).to.be.false; + // Should call handleAndLogError with the error and context + expect(handleAndLogErrorSpy.calledOnce).to.be.true; + expect(handleAndLogErrorSpy.getCall(0).args[0]).to.equal(testError); + expect(handleAndLogErrorSpy.getCall(0).args[1]).to.deep.include(mockExportConfig.context); + }); + + it('should set personalizationEnabled to false when module processing fails', async () => { + mockExportProjects.start.callsFake(async () => { + mockExportConfig.personalizationEnabled = true; + }); + const moduleError = new Error('Events export failed'); + mockExportEvents.start.rejects(moduleError); + const handleAndLogErrorSpy = sinon.spy(); + sinon.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + + try { + await exportPersonalize.start(); + } catch (error) { + // Error may propagate + } + + // Should set personalizationEnabled to false on error + expect(mockExportConfig.personalizationEnabled).to.be.false; + }); + + it('should handle errors in ExportProjects and prevent module processing', async () => { + const projectsError = new Error('Projects export failed'); + mockExportProjects.start.rejects(projectsError); + const handleAndLogErrorSpy = sinon.spy(); + sinon.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + + await exportPersonalize.start(); + + // Should not process modules after error + expect(mockExportEvents.start.called).to.be.false; + expect(mockExportAttributes.start.called).to.be.false; + expect(mockExportConfig.personalizationEnabled).to.be.false; + }); + }); + + describe('start() method - Region Configuration', () => { + it('should work with all supported region names', async () => { + const supportedRegions = ['AWS-NA', 'AWS-EU', 'AWS-AU', 'AZURE-NA', 'AZURE-EU', 'GCP-NA', 'GCP-EU', 'us']; + + for (const regionName of supportedRegions) { + mockExportConfig.region.name = regionName; + exportPersonalize = new ExportPersonalize({ + exportConfig: mockExportConfig, + stackAPIClient: {} as any, + moduleName: 'personalize' + }); + + mockExportProjects.start.resetHistory(); + + await exportPersonalize.start(); + + // Should proceed with ExportProjects for all supported regions + expect(mockExportProjects.start.calledOnce, `Should work for region: ${regionName}`).to.be.true; + } + }); + }); + + describe('start() method - Complete Flow', () => { + it('should complete full export flow successfully when all conditions are met', async () => { + mockExportProjects.start.callsFake(async () => { + mockExportConfig.personalizationEnabled = true; + }); + + // Track execution order to verify sequential processing + const executionOrder: string[] = []; + mockExportEvents.start.callsFake(async () => { + executionOrder.push('events'); + return Promise.resolve(); + }); + mockExportAttributes.start.callsFake(async () => { + executionOrder.push('attributes'); + return Promise.resolve(); + }); + mockExportAudiences.start.callsFake(async () => { + executionOrder.push('audiences'); + return Promise.resolve(); + }); + mockExportExperiences.start.callsFake(async () => { + executionOrder.push('experiences'); + return Promise.resolve(); + }); + + // Execute the full flow + await exportPersonalize.start(); + + expect(mockExportProjects.start.calledOnce).to.be.true; + // Verify all modules were processed in the correct order + expect(executionOrder).to.deep.equal(['events', 'attributes', 'audiences', 'experiences']); + expect(mockExportEvents.start.calledOnce).to.be.true; + expect(mockExportAttributes.start.calledOnce).to.be.true; + expect(mockExportAudiences.start.calledOnce).to.be.true; + expect(mockExportExperiences.start.calledOnce).to.be.true; + expect(mockExportConfig.personalizationEnabled).to.be.true; + }); + + it('should handle partial module failures: stop processing, log error, and disable personalization', async () => { + // Setup: ExportProjects enables personalization, first module succeeds, second fails + mockExportProjects.start.callsFake(async () => { + mockExportConfig.personalizationEnabled = true; + }); + + const attributesError = new Error('Attributes export failed'); + mockExportEvents.start.resolves(); + mockExportAttributes.start.rejects(attributesError); + + const handleAndLogErrorSpy = sinon.spy(); + sinon.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + + try { + await exportPersonalize.start(); + } catch (error) { + // Error may propagate, but should be handled in catch block + } + + // Verify ExportProjects completed + expect(mockExportProjects.start.calledOnce).to.be.true; + // Verify first module (events) was processed successfully + expect(mockExportEvents.start.calledOnce).to.be.true; + // Should have attempted to process attributes (second module, which fails) + expect(mockExportAttributes.start.calledOnce).to.be.true; + // Verify error handling: handleAndLogError was called with correct error and context + expect(handleAndLogErrorSpy.calledOnce).to.be.true; + expect(handleAndLogErrorSpy.getCall(0).args[0]).to.equal(attributesError); + expect(handleAndLogErrorSpy.getCall(0).args[1]).to.deep.include(mockExportConfig.context); + // Verify state change: personalizationEnabled set to false due to error + expect(mockExportConfig.personalizationEnabled).to.be.false; + // Verify subsequent modules were NOT processed after the error + // This is the key behavior - error stops the processing chain + expect(mockExportAudiences.start.called).to.be.false; + expect(mockExportExperiences.start.called).to.be.false; + }); + }); +}); diff --git a/packages/contentstack-export/test/unit/export/modules/stack.test.ts b/packages/contentstack-export/test/unit/export/modules/stack.test.ts index 828fdd85f0..8fa749c724 100644 --- a/packages/contentstack-export/test/unit/export/modules/stack.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/stack.test.ts @@ -1,6 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { FsUtility } from '@contentstack/cli-utilities'; +import { FsUtility, isAuthenticated, managementSDKClient, handleAndLogError } from '@contentstack/cli-utilities'; +import * as utilities from '@contentstack/cli-utilities'; import ExportStack from '../../../../src/export/modules/stack'; import ExportConfig from '../../../../src/types/export-config'; @@ -265,11 +266,6 @@ describe('ExportStack', () => { }); }); - describe('getStack() method', () => { - - - }); - describe('getLocales() method', () => { it('should fetch and return master locale', async () => { const locale = await exportStack.getLocales(); @@ -342,6 +338,78 @@ describe('ExportStack', () => { expect(locale).to.be.undefined; }); + it('should handle master locale not found after searching all pages', async () => { + let callCount = 0; + const limit = (exportStack as any).stackConfig.limit || 100; + const localeStub = { + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + // Return batches without master locale until all pages are exhausted + // First call: 100 items, count 100, skip will be 100, which equals count, so it stops + return Promise.resolve({ + items: Array(limit).fill({ uid: `locale-${callCount}`, code: 'en', fallback_locale: 'en-us' }), + count: limit // Only limit items, so skip will equal count and stop + }); + }) + }) + }; + + mockStackClient.locale.returns(localeStub); + const locale = await exportStack.getLocales(); + + // Should return undefined when master locale not found after all pages + expect(locale).to.be.undefined; + // Should have searched through available pages + expect(callCount).to.be.greaterThan(0); + }); + + it('should handle getLocales with skip parameter', async () => { + const localeStub = { + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [{ uid: 'locale-master', code: 'en-us', fallback_locale: null, name: 'English' }], + count: 1 + }) + }) + }; + + mockStackClient.locale.returns(localeStub); + const locale = await exportStack.getLocales(100); + + // Should find master locale even when starting with skip + expect(locale).to.exist; + expect(locale.code).to.equal('en-us'); + // Verify skip was set in query + expect((exportStack as any).qs.skip).to.equal(100); + }); + + it('should handle error and propagate it when fetching locales fails', async () => { + const localeError = new Error('Locale fetch failed'); + const localeStub = { + query: sinon.stub().returns({ + find: sinon.stub().rejects(localeError) + }) + }; + + mockStackClient.locale.returns(localeStub); + const handleAndLogErrorSpy = sinon.spy(); + sinon.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + + try { + await exportStack.getLocales(); + expect.fail('Should have thrown error'); + } catch (error) { + expect(error).to.equal(localeError); + // Should handle and log error + expect(handleAndLogErrorSpy.called).to.be.true; + expect(handleAndLogErrorSpy.calledWith( + localeError, + sinon.match.has('module', 'stack') + )).to.be.true; + } + }); + it('should find master locale in first batch when present', async () => { const localeStub = { query: sinon.stub().returns({ @@ -366,19 +434,52 @@ describe('ExportStack', () => { it('should export stack successfully and write to file', async () => { const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + const stackData = { name: 'Test Stack', uid: 'stack-uid', org_uid: 'org-123' }; + mockStackClient.fetch = sinon.stub().resolves(stackData); - await exportStack.exportStack(); + const result = await exportStack.exportStack(); expect(writeFileStub.called).to.be.true; expect(makeDirectoryStub.called).to.be.true; + // Should return the stack data + expect(result).to.deep.equal(stackData); + // Verify file was written with correct path + const writeCall = writeFileStub.getCall(0); + expect(writeCall.args[0]).to.include('stack.json'); + expect(writeCall.args[1]).to.deep.equal(stackData); }); it('should handle errors when exporting stack without throwing', async () => { - mockStackClient.fetch = sinon.stub().rejects(new Error('Stack fetch failed')); + const stackError = new Error('Stack fetch failed'); + mockStackClient.fetch = sinon.stub().rejects(stackError); + const handleAndLogErrorSpy = sinon.spy(); + sinon.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); // Should complete without throwing despite error - // The assertion is that await doesn't throw + const result = await exportStack.exportStack(); + + // Should return undefined on error + expect(result).to.be.undefined; + // Should handle and log error + expect(handleAndLogErrorSpy.called).to.be.true; + expect(handleAndLogErrorSpy.calledWith( + stackError, + sinon.match.has('module', 'stack') + )).to.be.true; + }); + + it('should create directory before writing stack file', async () => { + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + mockStackClient.fetch = sinon.stub().resolves({ name: 'Test Stack' }); + await exportStack.exportStack(); + + // Directory should be created before file write + expect(makeDirectoryStub.called).to.be.true; + expect(writeFileStub.called).to.be.true; + // Verify directory creation happens before file write + expect(makeDirectoryStub.calledBefore(writeFileStub)).to.be.true; }); }); @@ -386,19 +487,56 @@ describe('ExportStack', () => { it('should export stack settings successfully and write to file', async () => { const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + const settingsData = { + name: 'Stack Settings', + description: 'Settings description', + settings: { global: { example: 'value' } } + }; + mockStackClient.settings = sinon.stub().resolves(settingsData); - await exportStack.exportStackSettings(); + const result = await exportStack.exportStackSettings(); expect(writeFileStub.called).to.be.true; expect(makeDirectoryStub.called).to.be.true; + // Should return the settings data + expect(result).to.deep.equal(settingsData); + // Verify file was written with correct path + const writeCall = writeFileStub.getCall(0); + expect(writeCall.args[0]).to.include('settings.json'); + expect(writeCall.args[1]).to.deep.equal(settingsData); }); it('should handle errors when exporting settings without throwing', async () => { - mockStackClient.settings = sinon.stub().rejects(new Error('Settings fetch failed')); + const settingsError = new Error('Settings fetch failed'); + mockStackClient.settings = sinon.stub().rejects(settingsError); + const handleAndLogErrorSpy = sinon.spy(); + sinon.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); // Should complete without throwing despite error - // The assertion is that await doesn't throw + const result = await exportStack.exportStackSettings(); + + // Should return undefined on error + expect(result).to.be.undefined; + // Should handle and log error + expect(handleAndLogErrorSpy.called).to.be.true; + expect(handleAndLogErrorSpy.calledWith( + settingsError, + sinon.match.has('module', 'stack') + )).to.be.true; + }); + + it('should create directory before writing settings file', async () => { + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + mockStackClient.settings = sinon.stub().resolves({ name: 'Settings' }); + await exportStack.exportStackSettings(); + + // Directory should be created before file write + expect(makeDirectoryStub.called).to.be.true; + expect(writeFileStub.called).to.be.true; + // Verify directory creation happens before file write + expect(makeDirectoryStub.calledBefore(writeFileStub)).to.be.true; }); }); diff --git a/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts b/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts index 91eddcf39c..06b86229ad 100644 --- a/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts @@ -311,6 +311,376 @@ describe('ExportTaxonomies', () => { expect(Object.keys(exportTaxonomies.taxonomies).length).to.equal(0); }); + + // const taxonomies = [ + // { uid: 'taxonomy-1', name: 'Category' }, + // { uid: 'taxonomy-2', name: 'Tag' } + // ]; + + // exportTaxonomies.sanitizeTaxonomiesAttribs(taxonomies, 'en-us'); + + // expect(exportTaxonomies.taxonomies['taxonomy-1']).to.exist; + // expect(exportTaxonomies.taxonomies['taxonomy-2']).to.exist; + // // Verify taxonomies are tracked by locale + // expect(exportTaxonomies.taxonomiesByLocale['en-us']).to.exist; + // expect(exportTaxonomies.taxonomiesByLocale['en-us'].has('taxonomy-1')).to.be.true; + // expect(exportTaxonomies.taxonomiesByLocale['en-us'].has('taxonomy-2')).to.be.true; + // }); + + it('should not duplicate taxonomy metadata when processing same taxonomy multiple times', () => { + const taxonomies1 = [{ uid: 'taxonomy-1', name: 'Category', field1: 'value1' }]; + const taxonomies2 = [{ uid: 'taxonomy-1', name: 'Category', field2: 'value2' }]; + + exportTaxonomies.sanitizeTaxonomiesAttribs(taxonomies1); + exportTaxonomies.sanitizeTaxonomiesAttribs(taxonomies2); + + // Should only have one entry for taxonomy-1 + expect(Object.keys(exportTaxonomies.taxonomies).length).to.equal(1); + // Should have the first processed version (field1, not field2) + expect(exportTaxonomies.taxonomies['taxonomy-1'].field1).to.equal('value1'); + expect(exportTaxonomies.taxonomies['taxonomy-1'].field2).to.be.undefined; + }); + }); + + describe('getLocalesToExport() method', () => { + it('should return master locale when no locales file exists', () => { + const readFileStub = FsUtility.prototype.readFile as sinon.SinonStub; + readFileStub.throws(new Error('File not found')); + + const locales = exportTaxonomies.getLocalesToExport(); + + expect(locales).to.be.an('array'); + expect(locales.length).to.equal(1); + expect(locales[0]).to.equal('en-us'); // master locale + }); + + // const localesData = { + // 'locale-1': { code: 'en-us', name: 'English' }, + // 'locale-2': { code: 'es-es', name: 'Spanish' }, + // 'locale-3': { code: 'fr-fr', name: 'French' } + // }; + // const readFileStub = FsUtility.prototype.readFile as sinon.SinonStub; + // readFileStub.returns(localesData); + + // const locales = exportTaxonomies.getLocalesToExport(); + + // expect(locales.length).to.equal(4); // 3 from file + 1 master locale + // expect(locales).to.include('en-us'); + // expect(locales).to.include('es-es'); + // expect(locales).to.include('fr-fr'); + // }); + + it('should handle locales file with missing code field', () => { + const localesData = { + 'locale-1': { name: 'English' }, // missing code + 'locale-2': { code: 'es-es', name: 'Spanish' } + }; + const readFileStub = FsUtility.prototype.readFile as sinon.SinonStub; + readFileStub.returns(localesData); + + const locales = exportTaxonomies.getLocalesToExport(); + + // Should only include locales with code field + expect(locales.length).to.equal(2); // 1 from file + 1 master locale + expect(locales).to.include('en-us'); + expect(locales).to.include('es-es'); + }); + + it('should deduplicate locales with same code', () => { + const localesData = { + 'locale-1': { code: 'en-us', name: 'English US' }, + 'locale-2': { code: 'en-us', name: 'English UK' }, // duplicate code + 'locale-3': { code: 'es-es', name: 'Spanish' } + }; + const readFileStub = FsUtility.prototype.readFile as sinon.SinonStub; + readFileStub.returns(localesData); + + const locales = exportTaxonomies.getLocalesToExport(); + + // Should deduplicate en-us + expect(locales.length).to.equal(2); // 1 unique from file + 1 master locale (but master is also en-us, so total 2) + expect(locales).to.include('en-us'); + expect(locales).to.include('es-es'); + }); + + it('should handle empty locales file', () => { + const readFileStub = FsUtility.prototype.readFile as sinon.SinonStub; + readFileStub.returns({}); + + const locales = exportTaxonomies.getLocalesToExport(); + + expect(locales.length).to.equal(1); // Only master locale + expect(locales[0]).to.equal('en-us'); + }); + }); + + describe('processLocaleExport() method', () => { + it('should export taxonomies for locale when taxonomies exist', async () => { + const exportTaxonomiesStub = sinon.stub(exportTaxonomies, 'exportTaxonomies').resolves(); + exportTaxonomies.taxonomiesByLocale['en-us'] = new Set(['taxonomy-1', 'taxonomy-2']); + + await exportTaxonomies.processLocaleExport('en-us'); + + expect(exportTaxonomiesStub.called).to.be.true; + expect(exportTaxonomiesStub.calledWith('en-us')).to.be.true; + + exportTaxonomiesStub.restore(); + }); + + it('should skip export when no taxonomies exist for locale', async () => { + const exportTaxonomiesStub = sinon.stub(exportTaxonomies, 'exportTaxonomies').resolves(); + exportTaxonomies.taxonomiesByLocale['en-us'] = new Set(); + + await exportTaxonomies.processLocaleExport('en-us'); + + expect(exportTaxonomiesStub.called).to.be.false; + + exportTaxonomiesStub.restore(); + }); + + it('should handle locale with undefined taxonomies set', async () => { + const exportTaxonomiesStub = sinon.stub(exportTaxonomies, 'exportTaxonomies').resolves(); + exportTaxonomies.taxonomiesByLocale['en-us'] = undefined as any; + + await exportTaxonomies.processLocaleExport('en-us'); + + expect(exportTaxonomiesStub.called).to.be.false; + + exportTaxonomiesStub.restore(); + }); + }); + + describe('writeTaxonomiesMetadata() method', () => { + + it('should skip writing when taxonomies object is empty', () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + exportTaxonomies.taxonomies = {}; + + exportTaxonomies.writeTaxonomiesMetadata(); + + expect(writeFileStub.called).to.be.false; + }); + + it('should skip writing when taxonomies is null or undefined', () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + exportTaxonomies.taxonomies = null as any; + + exportTaxonomies.writeTaxonomiesMetadata(); + + expect(writeFileStub.called).to.be.false; + }); + }); + + describe('fetchTaxonomies() method - locale-based export', () => { + it('should fetch taxonomies with locale code', async () => { + const taxonomies = [ + { uid: 'taxonomy-1', name: 'Category', locale: 'en-us' }, + { uid: 'taxonomy-2', name: 'Tag', locale: 'en-us' } + ]; + + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: taxonomies, + count: 2 + }) + }) + }); + + await exportTaxonomies.fetchTaxonomies('en-us'); + + expect(Object.keys(exportTaxonomies.taxonomies).length).to.equal(2); + expect(exportTaxonomies.taxonomiesByLocale['en-us']).to.exist; + expect(exportTaxonomies.taxonomiesByLocale['en-us'].has('taxonomy-1')).to.be.true; + }); + + it('should detect locale-based export support when items have locale field', async () => { + const taxonomies = [ + { uid: 'taxonomy-1', name: 'Category', locale: 'en-us' } + ]; + + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: taxonomies, + count: 1 + }) + }) + }); + + await exportTaxonomies.fetchTaxonomies('en-us', true); + + // Should support locale-based export when items have locale field + expect(exportTaxonomies.isLocaleBasedExportSupported).to.be.true; + }); + + it('should disable locale-based export when items lack locale field', async () => { + const taxonomies = [ + { uid: 'taxonomy-1', name: 'Category' } // no locale field + ]; + + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: taxonomies, + count: 1 + }) + }) + }); + + await exportTaxonomies.fetchTaxonomies('en-us', true); + + // Should disable locale-based export when items lack locale field + expect(exportTaxonomies.isLocaleBasedExportSupported).to.be.false; + }); + + it('should disable locale-based export on API error when checkLocaleSupport is true', async () => { + // Create a structured API error (not a plan limitation error) + const apiError: any = new Error('API Error'); + apiError.status = 500; + apiError.errors = { general: ['Internal server error'] }; + + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(apiError) + }) + }); + + await exportTaxonomies.fetchTaxonomies('en-us', true); + + // Should disable locale-based export on error + expect(exportTaxonomies.isLocaleBasedExportSupported).to.be.false; + }); + + it('should handle taxonomy localization plan limitation error gracefully', async () => { + // Create the exact 403 error from the plan limitation + const planLimitationError: any = new Error('Forbidden'); + planLimitationError.status = 403; + planLimitationError.statusText = 'Forbidden'; + planLimitationError.errors = { + taxonomies: ['Taxonomy localization is not included in your plan. Please contact the support@contentstack.com team for assistance.'] + }; + + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(planLimitationError) + }) + }); + + await exportTaxonomies.fetchTaxonomies('en-us', true); + + // Should disable locale-based export and not throw error + expect(exportTaxonomies.isLocaleBasedExportSupported).to.be.false; + }); + }); + + describe('exportTaxonomies() method - locale-based export', () => { + + it('should skip export when no taxonomies for locale', async () => { + const mockMakeAPICall = sinon.stub(exportTaxonomies, 'makeAPICall').resolves(); + exportTaxonomies.taxonomiesByLocale['en-us'] = new Set(); + + await exportTaxonomies.exportTaxonomies('en-us'); + + expect(mockMakeAPICall.called).to.be.false; + + mockMakeAPICall.restore(); + }); + }); + + describe('start() method - locale-based export scenarios', () => { + it('should use legacy export when locale-based export is not supported', async () => { + const mockFetchTaxonomies = sinon.stub(exportTaxonomies, 'fetchTaxonomies').callsFake(async (locale, checkSupport) => { + if (checkSupport) { + exportTaxonomies.isLocaleBasedExportSupported = false; + } + }); + const mockExportTaxonomies = sinon.stub(exportTaxonomies, 'exportTaxonomies').resolves(); + const mockWriteMetadata = sinon.stub(exportTaxonomies, 'writeTaxonomiesMetadata').resolves(); + const mockGetLocales = sinon.stub(exportTaxonomies, 'getLocalesToExport').returns(['en-us']); + + await exportTaxonomies.start(); + + // Should use legacy export (no locale parameter) + expect(mockExportTaxonomies.called).to.be.true; + expect(mockExportTaxonomies.calledWith()).to.be.true; // Called without locale + expect(mockWriteMetadata.called).to.be.true; + + mockFetchTaxonomies.restore(); + mockExportTaxonomies.restore(); + mockWriteMetadata.restore(); + mockGetLocales.restore(); + }); + + it('should clear taxonomies and re-fetch when falling back to legacy export', async () => { + let fetchCallCount = 0; + const mockFetchTaxonomies = sinon.stub(exportTaxonomies, 'fetchTaxonomies').callsFake(async (locale, checkSupport) => { + fetchCallCount++; + if (checkSupport) { + // First call fails locale check + exportTaxonomies.isLocaleBasedExportSupported = false; + exportTaxonomies.taxonomies = { 'partial-data': { uid: 'partial-data' } }; // Simulate partial data + } else { + // Second call should have cleared data + expect(exportTaxonomies.taxonomies).to.deep.equal({}); + } + }); + const mockExportTaxonomies = sinon.stub(exportTaxonomies, 'exportTaxonomies').resolves(); + const mockWriteMetadata = sinon.stub(exportTaxonomies, 'writeTaxonomiesMetadata').resolves(); + const mockGetLocales = sinon.stub(exportTaxonomies, 'getLocalesToExport').returns(['en-us']); + + await exportTaxonomies.start(); + + // Should call fetchTaxonomies twice: once for check, once for legacy + expect(fetchCallCount).to.equal(2); + // First call with locale, second without + expect(mockFetchTaxonomies.firstCall.args).to.deep.equal(['en-us', true]); + expect(mockFetchTaxonomies.secondCall.args).to.deep.equal([]); + + mockFetchTaxonomies.restore(); + mockExportTaxonomies.restore(); + mockWriteMetadata.restore(); + mockGetLocales.restore(); + }); + + it('should use locale-based export when supported', async () => { + const mockFetchTaxonomies = sinon.stub(exportTaxonomies, 'fetchTaxonomies').callsFake(async (locale, checkSupport) => { + if (checkSupport) { + exportTaxonomies.isLocaleBasedExportSupported = true; + } + if (locale && typeof locale === 'string' && !exportTaxonomies.taxonomiesByLocale[locale]) { + exportTaxonomies.taxonomiesByLocale[locale] = new Set(['taxonomy-1']); + } + }); + const mockProcessLocale = sinon.stub(exportTaxonomies, 'processLocaleExport').resolves(); + const mockWriteMetadata = sinon.stub(exportTaxonomies, 'writeTaxonomiesMetadata').resolves(); + const mockGetLocales = sinon.stub(exportTaxonomies, 'getLocalesToExport').returns(['en-us', 'es-es']); + + await exportTaxonomies.start(); + + // Should process each locale + expect(mockProcessLocale.called).to.be.true; + expect(mockProcessLocale.callCount).to.equal(2); // Two locales + expect(mockWriteMetadata.called).to.be.true; + + mockFetchTaxonomies.restore(); + mockProcessLocale.restore(); + mockWriteMetadata.restore(); + mockGetLocales.restore(); + }); + + it('should return early when no locales to export', async () => { + const mockGetLocales = sinon.stub(exportTaxonomies, 'getLocalesToExport').returns([]); + const mockFetchTaxonomies = sinon.stub(exportTaxonomies, 'fetchTaxonomies').resolves(); + + await exportTaxonomies.start(); + + // Should not fetch taxonomies when no locales + expect(mockFetchTaxonomies.called).to.be.false; + + mockGetLocales.restore(); + mockFetchTaxonomies.restore(); + }); }); }); diff --git a/packages/contentstack-export/test/unit/utils/logger.test.ts b/packages/contentstack-export/test/unit/utils/logger.test.ts new file mode 100644 index 0000000000..9e973037f5 --- /dev/null +++ b/packages/contentstack-export/test/unit/utils/logger.test.ts @@ -0,0 +1,205 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as os from 'os'; +import * as path from 'path'; +import * as loggerModule from '../../../src/utils/logger'; +import { ExportConfig } from '../../../src/types'; + +describe('Logger', () => { + let mockExportConfig: ExportConfig; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: path.join(os.tmpdir(), 'test-export'), + data: path.join(os.tmpdir(), 'test-data'), + cliLogsPath: path.join(os.tmpdir(), 'test-logs') as string, + branchName: '', + context: { + command: 'cm:stacks:export', + module: 'test', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: {}, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: {} + } as any; + }); + + afterEach(() => { + sandbox.restore(); + // Clean up loggers after each test + loggerModule.unlinkFileLogger(); + }); + + describe('log() function', () => { + it('should log message when type is not error', async () => { + // Should complete without throwing + await loggerModule.log(mockExportConfig, 'Test message', 'info'); + + // Verify function completed successfully + expect(true).to.be.true; // Basic assertion that function executed + }); + + it('should log error message when type is error', async () => { + // Should complete without throwing + await loggerModule.log(mockExportConfig, 'Error message', 'error'); + + // Verify function completed successfully + expect(true).to.be.true; // Basic assertion that function executed + }); + + it('should use cliLogsPath when available', async () => { + // Should complete without throwing + await loggerModule.log(mockExportConfig, 'Test', 'info'); + + // Verify function completed successfully + expect(true).to.be.true; + }); + + it('should fallback to data path when cliLogsPath is not available', async () => { + const configWithoutLogsPath = { ...mockExportConfig, cliLogsPath: undefined as any }; + + // Should complete without throwing + await loggerModule.log(configWithoutLogsPath, 'Test', 'info'); + + // Verify function completed successfully + expect(true).to.be.true; + }); + + it('should handle object arguments in log message', async () => { + const testObject = { key: 'value', message: 'test' }; + + // Should complete without throwing + await loggerModule.log(mockExportConfig, testObject, 'info'); + + // Verify function completed successfully + expect(true).to.be.true; + }); + + it('should remove ANSI escape codes from log messages', async () => { + const ansiMessage = '\u001B[31mRed text\u001B[0m'; + + // Should complete without throwing + await loggerModule.log(mockExportConfig, ansiMessage, 'info'); + + // Verify function completed successfully + expect(true).to.be.true; + }); + + it('should handle null message arguments', async () => { + // Should complete without throwing + await loggerModule.log(mockExportConfig, null as any, 'info'); + + // Verify function completed successfully + expect(true).to.be.true; + }); + + it('should handle undefined message arguments', async () => { + // Should complete without throwing + await loggerModule.log(mockExportConfig, undefined as any, 'info'); + + // Verify function completed successfully + expect(true).to.be.true; + }); + }); + + describe('unlinkFileLogger() function', () => { + it('should handle undefined logger gracefully', () => { + // Should not throw when logger is not initialized + expect(() => loggerModule.unlinkFileLogger()).to.not.throw(); + }); + + it('should remove file transports after logger is initialized', async () => { + // Initialize logger by calling log + await loggerModule.log(mockExportConfig, 'init', 'info'); + + // Should not throw when removing file transports + expect(() => loggerModule.unlinkFileLogger()).to.not.throw(); + }); + + it('should handle multiple calls gracefully', async () => { + // Initialize logger + await loggerModule.log(mockExportConfig, 'init', 'info'); + + // Should handle multiple calls + loggerModule.unlinkFileLogger(); + expect(() => loggerModule.unlinkFileLogger()).to.not.throw(); + }); + }); + + describe('Logger behavior - integration', () => { + it('should handle different log types correctly', async () => { + // Test all log types + await loggerModule.log(mockExportConfig, 'Info message', 'info'); + await loggerModule.log(mockExportConfig, 'Error message', 'error'); + + // Verify all completed successfully + expect(true).to.be.true; + }); + + it('should handle complex object logging', async () => { + const complexObject = { + nested: { + data: 'value', + array: [1, 2, 3], + nullValue: null as any, + undefinedValue: undefined as any + } + }; + + // Should complete without throwing + await loggerModule.log(mockExportConfig, complexObject, 'info'); + + // Verify function completed successfully + expect(true).to.be.true; + }); + + it('should handle empty string messages', async () => { + // Should complete without throwing + await loggerModule.log(mockExportConfig, '', 'info'); + + // Verify function completed successfully + expect(true).to.be.true; + }); + + it('should handle very long messages', async () => { + const longMessage = 'A'.repeat(10); + + // Should complete without throwing + await loggerModule.log(mockExportConfig, longMessage, 'info'); + + // Verify function completed successfully + expect(true).to.be.true; + }); + }); +}); diff --git a/packages/contentstack-export/test/unit/utils/marketplace-app-helper.test.ts b/packages/contentstack-export/test/unit/utils/marketplace-app-helper.test.ts new file mode 100644 index 0000000000..561b2673fa --- /dev/null +++ b/packages/contentstack-export/test/unit/utils/marketplace-app-helper.test.ts @@ -0,0 +1,376 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as utilities from '@contentstack/cli-utilities'; +import { getDeveloperHubUrl, getOrgUid, createNodeCryptoInstance } from '../../../src/utils/marketplace-app-helper'; +import { ExportConfig } from '../../../src/types'; + +describe('Marketplace App Helper Utils', () => { + let sandbox: sinon.SinonSandbox; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + source_stack: 'test-stack-uid', + context: { + command: 'cm:stacks:export', + module: 'marketplace-apps', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: {}, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: 'test-encryption-key', + onlyTSModules: [], + modules: { + types: ['marketplace-apps'], + marketplace_apps: { + dirName: 'marketplace-apps', + fileName: 'marketplace-apps.json' + } + } + } as any; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('getDeveloperHubUrl', () => { + it('should return developer hub URL by calling createDeveloperHubUrl', async () => { + // Since createDeveloperHubUrl is non-configurable, we test the actual behavior + // The function is a simple wrapper, so we verify it returns a value + const result = await getDeveloperHubUrl(mockExportConfig); + + // Should return a URL (actual implementation behavior) + expect(result).to.be.a('string'); + expect(result).to.include('developer'); + }); + + it('should handle different host URLs', async () => { + mockExportConfig.host = 'https://eu-api.contentstack.com'; + + const result = await getDeveloperHubUrl(mockExportConfig); + + // Should return a URL based on the host + expect(result).to.be.a('string'); + expect(result.length).to.be.greaterThan(0); + }); + + it('should return a valid URL string', async () => { + const result = await getDeveloperHubUrl(mockExportConfig); + + expect(result).to.be.a('string'); + expect(result.length).to.be.greaterThan(0); + }); + }); + + describe('getOrgUid', () => { + it('should fetch and return org_uid from stack data', async () => { + const mockStackData = { + org_uid: 'test-org-uid-123', + name: 'Test Stack', + uid: 'stack-uid' + }; + + const mockFetch = sandbox.stub().resolves(mockStackData); + const mockStack = sandbox.stub().returns({ fetch: mockFetch }); + const mockAPIClient = { + stack: mockStack + }; + + // Use replaceGetter since managementSDKClient is a getter + const managementSDKClientSpy = sandbox.spy(async (config: any) => { + expect(config).to.deep.equal({ host: 'https://api.contentstack.io' }); + return mockAPIClient; + }); + sandbox.replaceGetter(utilities, 'managementSDKClient', () => managementSDKClientSpy); + + const result = await getOrgUid(mockExportConfig); + + expect(managementSDKClientSpy.calledOnce).to.be.true; + expect(mockStack.calledOnce).to.be.true; + expect(mockStack.firstCall.args[0]).to.deep.equal({ api_key: 'test-stack-uid' }); + expect(mockFetch.calledOnce).to.be.true; + expect(result).to.equal('test-org-uid-123'); + }); + + it('should use source_stack from config as api_key', async () => { + mockExportConfig.source_stack = 'custom-stack-key'; + const mockStackData = { org_uid: 'org-123' }; + + const mockFetch = sandbox.stub().resolves(mockStackData); + const mockStack = sandbox.stub().returns({ fetch: mockFetch }); + const mockAPIClient = { stack: mockStack }; + + sandbox.replaceGetter(utilities, 'managementSDKClient', () => async () => mockAPIClient); + + await getOrgUid(mockExportConfig); + + expect(mockStack.firstCall.args[0]).to.deep.equal({ api_key: 'custom-stack-key' }); + }); + + it('should handle API errors gracefully', async () => { + const mockError = new Error('API Error'); + const handleAndLogErrorSpy = sandbox.spy(); + sandbox.replaceGetter(utilities, 'handleAndLogError', () => handleAndLogErrorSpy); + + const mockFetch = sandbox.stub().rejects(mockError); + const mockStack = sandbox.stub().returns({ fetch: mockFetch }); + const mockAPIClient = { stack: mockStack }; + + const managementSDKClientSpy = sandbox.spy(async () => mockAPIClient); + sandbox.replaceGetter(utilities, 'managementSDKClient', () => managementSDKClientSpy); + + const result = await getOrgUid(mockExportConfig); + + expect(handleAndLogErrorSpy.calledOnce).to.be.true; + expect(handleAndLogErrorSpy.getCall(0).args[0]).to.equal(mockError); + expect(handleAndLogErrorSpy.getCall(0).args[1]).to.deep.equal(mockExportConfig.context); + expect(result).to.be.undefined; + }); + + it('should return undefined when stack data is null', async () => { + const mockFetch = sandbox.stub().resolves(null); + const mockStack = sandbox.stub().returns({ fetch: mockFetch }); + const mockAPIClient = { stack: mockStack }; + + sandbox.replaceGetter(utilities, 'managementSDKClient', () => async () => mockAPIClient); + + const result = await getOrgUid(mockExportConfig); + + expect(result).to.be.undefined; + }); + + it('should return undefined when stack data has no org_uid', async () => { + const mockStackData = { + name: 'Test Stack', + uid: 'stack-uid' + // No org_uid property + }; + + const mockFetch = sandbox.stub().resolves(mockStackData); + const mockStack = sandbox.stub().returns({ fetch: mockFetch }); + const mockAPIClient = { stack: mockStack }; + + sandbox.replaceGetter(utilities, 'managementSDKClient', () => async () => mockAPIClient); + + const result = await getOrgUid(mockExportConfig); + + expect(result).to.be.undefined; + }); + + it('should use the correct host from config', async () => { + mockExportConfig.host = 'https://eu-api.contentstack.com'; + const mockStackData = { org_uid: 'org-123' }; + + const mockFetch = sandbox.stub().resolves(mockStackData); + const mockStack = sandbox.stub().returns({ fetch: mockFetch }); + const mockAPIClient = { stack: mockStack }; + + const managementSDKClientSpy = sandbox.spy(async (config: any) => { + expect(config).to.deep.equal({ host: 'https://eu-api.contentstack.com' }); + return mockAPIClient; + }); + sandbox.replaceGetter(utilities, 'managementSDKClient', () => managementSDKClientSpy); + + await getOrgUid(mockExportConfig); + + expect(managementSDKClientSpy.calledOnce).to.be.true; + }); + }); + + describe('createNodeCryptoInstance', () => { + it('should use marketplaceAppEncryptionKey when forceStopMarketplaceAppsPrompt is true', async () => { + mockExportConfig.forceStopMarketplaceAppsPrompt = true; + mockExportConfig.marketplaceAppEncryptionKey = 'test-key-123'; + + const mockNodeCrypto = { encrypt: sandbox.stub() }; + const nodeCryptoConstructorSpy = sandbox.spy((args: any) => { + expect(args.encryptionKey).to.equal('test-key-123'); + return mockNodeCrypto; + }); + sandbox.replaceGetter(utilities, 'NodeCrypto', () => nodeCryptoConstructorSpy as any); + + const result = await createNodeCryptoInstance(mockExportConfig); + + expect(nodeCryptoConstructorSpy.calledOnce).to.be.true; + expect(nodeCryptoConstructorSpy.getCall(0).args[0].encryptionKey).to.equal('test-key-123'); + expect(result).to.exist; + }); + + it('should prompt user for encryption key when forceStopMarketplaceAppsPrompt is false', async () => { + mockExportConfig.forceStopMarketplaceAppsPrompt = false; + mockExportConfig.marketplaceAppEncryptionKey = 'default-key'; + + const mockInquireResponse = 'user-entered-key'; + const inquireStub = sandbox.stub(utilities.cliux, 'inquire').resolves(mockInquireResponse); + + const mockNodeCrypto = { encrypt: sandbox.stub() }; + const nodeCryptoConstructorSpy = sandbox.spy((args: any) => { + expect(args.encryptionKey).to.equal(mockInquireResponse); + return mockNodeCrypto; + }); + sandbox.replaceGetter(utilities, 'NodeCrypto', () => nodeCryptoConstructorSpy as any); + + const result = await createNodeCryptoInstance(mockExportConfig); + + expect(inquireStub.calledOnce).to.be.true; + const inquireArgs = inquireStub.getCall(0).args[0] as any; + expect(inquireArgs.type).to.equal('input'); + expect(inquireArgs.name).to.equal('name'); + expect(inquireArgs.default).to.equal('default-key'); + expect(inquireArgs.message).to.equal('Enter Marketplace app configurations encryption key'); + expect(inquireArgs.validate).to.be.a('function'); + + // Test validation function + expect(inquireArgs.validate('')).to.equal("Encryption key can't be empty."); + expect(inquireArgs.validate('valid-key')).to.equal(true); + + expect(nodeCryptoConstructorSpy.calledOnce).to.be.true; + expect(nodeCryptoConstructorSpy.getCall(0).args[0].encryptionKey).to.equal(mockInquireResponse); + expect(result).to.exist; + }); + + it('should use default encryption key from config when prompting', async () => { + mockExportConfig.forceStopMarketplaceAppsPrompt = false; + mockExportConfig.marketplaceAppEncryptionKey = 'my-default-key'; + + const inquireStub = sandbox.stub(utilities.cliux, 'inquire').resolves('user-key'); + + sandbox.replaceGetter(utilities, 'NodeCrypto', () => sandbox.fake.returns({ encrypt: sandbox.stub() } as any) as any); + + await createNodeCryptoInstance(mockExportConfig); + + const inquireArgs = inquireStub.firstCall.args[0] as any; + expect(inquireArgs.default).to.equal('my-default-key'); + }); + + it('should validate that encryption key is not empty', async () => { + mockExportConfig.forceStopMarketplaceAppsPrompt = false; + + const inquireStub = sandbox.stub(utilities.cliux, 'inquire').callsFake(async (options: any) => { + // Test validation + const opts = Array.isArray(options) ? options[0] : options; + // Empty string should return error message + expect(opts.validate('')).to.equal("Encryption key can't be empty."); + // Non-empty strings should return true (validation doesn't trim) + expect(opts.validate('valid-key')).to.equal(true); + expect(opts.validate('another-valid-key-123')).to.equal(true); + + return 'valid-key'; + }); + + sandbox.replaceGetter(utilities, 'NodeCrypto', () => sandbox.fake.returns({ encrypt: sandbox.stub() } as any) as any); + + await createNodeCryptoInstance(mockExportConfig); + + expect(inquireStub.calledOnce).to.be.true; + }); + + it('should create NodeCrypto instance with correct arguments', async () => { + mockExportConfig.forceStopMarketplaceAppsPrompt = true; + mockExportConfig.marketplaceAppEncryptionKey = 'test-encryption-key'; + + let capturedArgs: any; + const nodeCryptoConstructorSpy = sandbox.spy((args: any) => { + capturedArgs = args; + return { encrypt: sandbox.stub() } as any; + }); + sandbox.replaceGetter(utilities, 'NodeCrypto', () => nodeCryptoConstructorSpy as any); + + await createNodeCryptoInstance(mockExportConfig); + + expect(nodeCryptoConstructorSpy.calledOnce).to.be.true; + expect(capturedArgs).to.deep.equal({ encryptionKey: 'test-encryption-key' }); + }); + + it('should handle empty marketplaceAppEncryptionKey in config', async () => { + mockExportConfig.forceStopMarketplaceAppsPrompt = true; + mockExportConfig.marketplaceAppEncryptionKey = ''; + + const nodeCryptoConstructorSpy = sandbox.spy((args: any) => { + expect(args.encryptionKey).to.equal(''); + return { encrypt: sandbox.stub() } as any; + }); + sandbox.replaceGetter(utilities, 'NodeCrypto', () => nodeCryptoConstructorSpy as any); + + await createNodeCryptoInstance(mockExportConfig); + + expect(nodeCryptoConstructorSpy.getCall(0).args[0].encryptionKey).to.equal(''); + }); + + it('should handle undefined marketplaceAppEncryptionKey in config when prompting', async () => { + mockExportConfig.forceStopMarketplaceAppsPrompt = false; + mockExportConfig.marketplaceAppEncryptionKey = undefined as any; + + const inquireStub = sandbox.stub(utilities.cliux, 'inquire').resolves('prompted-key'); + + sandbox.replaceGetter(utilities, 'NodeCrypto', () => sandbox.fake.returns({ encrypt: sandbox.stub() } as any) as any); + + await createNodeCryptoInstance(mockExportConfig); + + const inquireArgs = inquireStub.firstCall.args[0] as any; + expect(inquireArgs.default).to.be.undefined; + }); + + it('should return NodeCrypto instance', async () => { + mockExportConfig.forceStopMarketplaceAppsPrompt = true; + mockExportConfig.marketplaceAppEncryptionKey = 'test-key'; + + const mockNodeCrypto = { + encrypt: sandbox.stub().returns('encrypted-data') + }; + + sandbox.replaceGetter(utilities, 'NodeCrypto', () => sandbox.fake.returns(mockNodeCrypto) as any); + + const result = await createNodeCryptoInstance(mockExportConfig); + + expect(result).to.equal(mockNodeCrypto); + expect(result.encrypt).to.be.a('function'); + }); + + it('should not prompt when forceStopMarketplaceAppsPrompt is true even if key is empty', async () => { + mockExportConfig.forceStopMarketplaceAppsPrompt = true; + mockExportConfig.marketplaceAppEncryptionKey = ''; + + const inquireStub = sandbox.stub(utilities.cliux, 'inquire'); + + sandbox.replaceGetter(utilities, 'NodeCrypto', () => sandbox.fake.returns({ encrypt: sandbox.stub() } as any) as any); + + await createNodeCryptoInstance(mockExportConfig); + + expect(inquireStub.called).to.be.false; + }); + }); +}); + diff --git a/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts b/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts index af1e0dd937..4fa2afddbf 100644 --- a/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts +++ b/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts @@ -13,8 +13,8 @@ import { configHandler, } from '@contentstack/cli-utilities'; -import { ImportConfig } from '../../../types'; -import { setupImportConfig, log } from '../../../utils'; +import { ImportConfig, Context } from '../../../types'; +import { setupImportConfig } from '../../../utils'; import { ImportSetup } from '../../../import'; export default class ImportSetupCommand extends Command { @@ -69,6 +69,10 @@ export default class ImportSetupCommand extends Command { try { const { flags } = await this.parse(ImportSetupCommand); let importSetupConfig = await setupImportConfig(flags); + // Prepare the context object + const context = this.createImportSetupContext(importSetupConfig.apiKey, (importSetupConfig as any).authenticationMethod); + importSetupConfig.context = { ...context }; + // Note setting host to create cma client importSetupConfig.host = this.cmaHost; importSetupConfig.region = this.region; @@ -97,12 +101,11 @@ export default class ImportSetupCommand extends Command { log( importSetupConfig, `Backup folder and mapper files have been successfully created for the stack using the API key ${importSetupConfig.apiKey}.`, - 'success', + importSetupConfig.context, ); - log( - importSetupConfig, + log.success( `The backup folder has been created at '${pathValidator(path.join(importSetupConfig.backupDir))}'.`, - 'success', + importSetupConfig.context, ); } catch (error) { CLIProgressManager.printGlobalSummary(); diff --git a/packages/contentstack-import-setup/src/import/import-setup.ts b/packages/contentstack-import-setup/src/import/import-setup.ts index 76eaaf2de6..716ee60c08 100644 --- a/packages/contentstack-import-setup/src/import/import-setup.ts +++ b/packages/contentstack-import-setup/src/import/import-setup.ts @@ -1,7 +1,6 @@ import { ImportConfig, Modules } from '../types'; -import { backupHandler, log, setupBranchConfig } from '../utils'; -import { ContentstackClient } from '@contentstack/cli-utilities'; -import { validateBranch } from '../utils'; +import { backupHandler, setupBranchConfig, validateBranch } from '../utils'; +import { ContentstackClient, log, handleAndLogError } from '@contentstack/cli-utilities'; export default class ImportSetup { protected config: ImportConfig; @@ -64,8 +63,10 @@ export default class ImportSetup { * @returns {Promise} */ protected async runModuleImports() { + log.debug('Starting module imports', { modules: Object.keys(this.dependencyTree) }); for (const moduleName in this.dependencyTree) { try { + log.debug(`Importing module: ${moduleName}`, { moduleName, dependencies: this.dependencyTree[moduleName] }); const modulePath = `./modules/${moduleName}`; const { default: ModuleClass } = await import(modulePath); @@ -77,11 +78,13 @@ export default class ImportSetup { const moduleInstance = new ModuleClass(modulePayload); await moduleInstance.start(); + log.debug(`Module ${moduleName} imported successfully`); } catch (error) { - log(this.config, `Error occurred while importing '${moduleName}'`, 'error'); + handleAndLogError(error, { ...this.config.context, moduleName }, `Error occurred while importing '${moduleName}'`); throw error; } } + log.debug('All module imports completed'); } /** @@ -98,16 +101,22 @@ export default class ImportSetup { this.config.org_uid = stackDetails.org_uid as string; } + log.debug('Creating backup directory'); const backupDir = await backupHandler(this.config); if (backupDir) { this.config.backupDir = backupDir; + log.debug('Backup directory created', { backupDir }); } + + log.debug('Setting up branch configuration'); await setupBranchConfig(this.config, this.stackAPIClient); + log.debug('Branch configuration completed', { branchName: this.config.branchName }); await this.generateDependencyTree(); await this.runModuleImports(); + log.debug('Import setup process completed successfully'); } catch (error) { - console.log(error); + handleAndLogError(error, { ...this.config.context }, 'Import setup failed'); throw error; } } diff --git a/packages/contentstack-import-setup/src/import/modules/assets.ts b/packages/contentstack-import-setup/src/import/modules/assets.ts index 70e3220304..ef19f832e5 100644 --- a/packages/contentstack-import-setup/src/import/modules/assets.ts +++ b/packages/contentstack-import-setup/src/import/modules/assets.ts @@ -1,9 +1,9 @@ import * as chalk from 'chalk'; -import { log, fsUtil } from '../../utils'; +import { fsUtil } from '../../utils'; import { join } from 'path'; import { AssetRecord, ImportConfig, ModuleClassParams } from '../../types'; import { isEmpty, orderBy, values } from 'lodash'; -import { formatError, FsUtility, sanitizePath } from '@contentstack/cli-utilities'; +import { FsUtility, sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilities'; import BaseImportSetup from './base-setup'; import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; @@ -96,6 +96,7 @@ export default class AssetImportSetup extends BaseImportSetup { * @returns {Promise} Promise */ async fetchAndMapAssets(): Promise { + log.debug('Starting asset fetch and mapping', { assetsFolderPath: this.assetsFolderPath }); const processName = 'mapping assets'; const indexFileName = 'assets.json'; const basePath = this.assetsFolderPath; @@ -132,7 +133,7 @@ export default class AssetImportSetup extends BaseImportSetup { /* eslint-disable @typescript-eslint/no-unused-vars, guard-for-in */ for (const index in indexer) { const chunk = await fs.readChunkFiles.next().catch((error) => { - log(this.config, error, 'error'); + log.error(String(error), { error }); }); if (chunk) { @@ -163,7 +164,7 @@ export default class AssetImportSetup extends BaseImportSetup { } if (!isEmpty(this.duplicateAssets)) { fsUtil.writeFile(this.duplicateAssetPath, this.duplicateAssets); - log(this.config, `Duplicate asset files are stored at: ${this.duplicateAssetPath}.`, 'info'); + log.info(`Duplicate asset files are stored at: ${this.duplicateAssetPath}.`); } } } diff --git a/packages/contentstack-import-setup/src/import/modules/base-setup.ts b/packages/contentstack-import-setup/src/import/modules/base-setup.ts index 69bea1cde1..799d49324e 100644 --- a/packages/contentstack-import-setup/src/import/modules/base-setup.ts +++ b/packages/contentstack-import-setup/src/import/modules/base-setup.ts @@ -1,5 +1,5 @@ -import { log, fsUtil } from '../../utils'; -import { ApiOptions, CustomPromiseHandler, EnvType, ImportConfig, ModuleClassParams } from '../../types'; +import { fsUtil } from '../../utils'; +import { ApiOptions, CustomPromiseHandler, EnvType, ImportConfig, ModuleClassParams, Modules } from '../../types'; import { chunk, entries, isEmpty, isEqual, last } from 'lodash'; import { CLIProgressManager, configHandler } from '@contentstack/cli-utilities'; @@ -16,9 +16,22 @@ export default class BaseImportSetup { this.dependencies = dependencies; } + /** + * Set the module name in context directly + * @param module - Module name to set + * @returns {void} + */ + protected initializeContext(module?: Modules): void { + if (this.config.context && module) { + this.config.context.module = module; + } + } + async setupDependencies() { + log.debug('Setting up dependencies', { dependencies: this.dependencies }); for (const moduleName of this.dependencies) { try { + log.debug(`Importing dependency module: ${moduleName}`); const modulePath = `./${moduleName}`; const { default: ModuleClass } = await import(modulePath); @@ -29,8 +42,9 @@ export default class BaseImportSetup { const moduleInstance = new ModuleClass(modulePayload); await moduleInstance.start(); + log.debug(`Dependency module ${moduleName} imported successfully`); } catch (error) { - log(this.config, `Error importing '${moduleName}': ${error.message}`, 'error'); + handleAndLogError(error, { ...this.config.context }, `Error importing '${moduleName}'`); } } } @@ -143,7 +157,7 @@ export default class BaseImportSetup { // info: Batch No. 20 of import assets is complete if (currentIndexer) batchMsg += `Current chunk processing is (${currentIndexer}/${indexerCount})`; - log(this.config, `Batch No. (${batchNo}/${totelBatches}) of ${processName} is complete`, 'success'); + log.success(`Batch No. (${batchNo}/${totelBatches}) of ${processName} is complete`); } // if (this.config.modules.assets.displayExecutionTime) { diff --git a/packages/contentstack-import-setup/src/import/modules/entries.ts b/packages/contentstack-import-setup/src/import/modules/entries.ts index 412a5e6a76..24ce30f29b 100644 --- a/packages/contentstack-import-setup/src/import/modules/entries.ts +++ b/packages/contentstack-import-setup/src/import/modules/entries.ts @@ -1,4 +1,3 @@ -import { log } from '../../utils'; import { ModuleClassParams } from '../../types'; import BaseImportSetup from './base-setup'; import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; diff --git a/packages/contentstack-import-setup/src/import/modules/extensions.ts b/packages/contentstack-import-setup/src/import/modules/extensions.ts index 31e108d7f4..2bb7666315 100644 --- a/packages/contentstack-import-setup/src/import/modules/extensions.ts +++ b/packages/contentstack-import-setup/src/import/modules/extensions.ts @@ -1,5 +1,5 @@ import * as chalk from 'chalk'; -import { log, fsUtil } from '../../utils'; +import { fsUtil, fileHelper } from '../../utils'; import { join } from 'path'; import { ImportConfig, ModuleClassParams } from '../../types'; import { isEmpty } from 'lodash'; @@ -71,9 +71,9 @@ export default class ExtensionImportSetup extends BaseImportSetup { progress.completeProcess(PROCESS_NAMES.EXTENSIONS_MAPPER_GENERATION, true); this.completeProgress(true); - log(this.config, `The required setup files for extensions have been generated successfully.`, 'success'); + log.success(`The required setup files for extensions have been generated successfully.`); } else { - log(this.config, 'No extensions found in the content folder.', 'info'); + log.info('No extensions found in the content folder.'); } } catch (error) { this.completeProgress(false, error?.message || 'Extensions mapper generation failed'); diff --git a/packages/contentstack-import-setup/src/import/modules/global-fields.ts b/packages/contentstack-import-setup/src/import/modules/global-fields.ts index 1ade770000..1b2b1e1a40 100644 --- a/packages/contentstack-import-setup/src/import/modules/global-fields.ts +++ b/packages/contentstack-import-setup/src/import/modules/global-fields.ts @@ -1,5 +1,5 @@ import * as chalk from 'chalk'; -import { log, fsUtil } from '../../utils'; +import { fsUtil } from '../../utils'; import { join } from 'path'; import { ImportConfig, ModuleClassParams } from '../../types'; import BaseImportSetup from './base-setup'; diff --git a/packages/contentstack-import-setup/src/import/modules/marketplace-apps.ts b/packages/contentstack-import-setup/src/import/modules/marketplace-apps.ts index 7757a12acd..b59c44b4bc 100644 --- a/packages/contentstack-import-setup/src/import/modules/marketplace-apps.ts +++ b/packages/contentstack-import-setup/src/import/modules/marketplace-apps.ts @@ -1,4 +1,4 @@ -import { log, fsUtil } from '../../utils'; +import { fsUtil, fileHelper } from '../../utils'; import { join } from 'path'; import { ImportConfig, ModuleClassParams } from '../../types'; import { get, isEmpty } from 'lodash'; @@ -10,6 +10,8 @@ import { NodeCrypto, createDeveloperHubUrl, sanitizePath, + log, + handleAndLogError, } from '@contentstack/cli-utilities'; import BaseImportSetup from './base-setup'; import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; @@ -93,7 +95,7 @@ export default class marketplaceAppImportSetup extends BaseImportSetup { log(this.config, `The required setup files for Marketplace apps have been generated successfully.`, 'success'); } else { - log(this.config, 'No Marketplace apps found in the content folder.', 'info'); + log.info('No Marketplace apps found in the content folder.'); } } catch (error) { this.completeProgress(false, error?.message || 'Marketplace apps mapper generation failed'); diff --git a/packages/contentstack-import-setup/src/import/modules/taxonomies.ts b/packages/contentstack-import-setup/src/import/modules/taxonomies.ts index 10e1f8cf56..98e8f6a15c 100644 --- a/packages/contentstack-import-setup/src/import/modules/taxonomies.ts +++ b/packages/contentstack-import-setup/src/import/modules/taxonomies.ts @@ -2,7 +2,7 @@ import { join } from 'path'; import omit from 'lodash/omit'; import isEmpty from 'lodash/isEmpty'; -import { log, fsUtil, fileHelper } from '../../utils'; +import { fsUtil, fileHelper } from '../../utils'; import { ImportConfig, ModuleClassParams, TaxonomyQueryParams } from '../../types'; import { sanitizePath } from '@contentstack/cli-utilities'; import BaseImportSetup from './base-setup'; @@ -111,7 +111,7 @@ export default class TaxonomiesImportSetup extends BaseImportSetup { this.completeProgress(true); log(this.config, `The required setup files for taxonomies have been generated successfully.`, 'success'); } else { - log(this.config, 'No taxonomies found in the content folder.', 'info'); + log.info('No taxonomies found in the content folder.'); } } catch (error) { this.completeProgress(false, error?.message || 'Taxonomies mapper generation failed'); @@ -159,7 +159,7 @@ export default class TaxonomiesImportSetup extends BaseImportSetup { const locales = this.loadAvailableLocales(); for (const localeCode of Object.keys(locales)) { - log(this.config, `Processing taxonomies for locale: ${localeCode}`, 'info'); + log.info(`Processing taxonomies for locale: ${localeCode}`); for (const taxonomy of Object.values(taxonomies) as any) { try { @@ -213,11 +213,11 @@ export default class TaxonomiesImportSetup extends BaseImportSetup { // Check if master locale folder exists (indicates new locale-based structure) if (!fileHelper.fileExistsSync(masterLocaleFolder)) { - log(this.config, 'No locale-based folder structure detected', 'info'); + log.info('No locale-based folder structure detected'); return false; } - log(this.config, 'Locale-based folder structure detected', 'info'); + log.info('Locale-based folder structure detected'); return true; } @@ -237,17 +237,17 @@ export default class TaxonomiesImportSetup extends BaseImportSetup { // The file contains an object with UID as key, extract the code const firstLocale = Object.values(masterLocaleData)[0]; if (firstLocale?.code) { - log(this.config, `Master locale loaded from file: ${firstLocale.code}`, 'info'); + log.info(`Master locale loaded from file: ${firstLocale.code}`); return firstLocale.code; } } catch (error) { - log(this.config, 'Error reading master-locale.json, using fallback', 'warn'); + log.warn('Error reading master-locale.json, using fallback', { error }); } } // Fallback to config or default const fallbackCode = this.config.master_locale?.code || 'en-us'; - log(this.config, `Using fallback master locale: ${fallbackCode}`, 'info'); + log.info(`Using fallback master locale: ${fallbackCode}`); return fallbackCode; } @@ -264,7 +264,7 @@ export default class TaxonomiesImportSetup extends BaseImportSetup { // Then load additional locales from locales.json if it exists if (!fileHelper.fileExistsSync(this.localesFilePath)) { - log(this.config, 'No locales file found, using only master locale', 'info'); + log.info('No locales file found, using only master locale'); return locales; } @@ -277,14 +277,10 @@ export default class TaxonomiesImportSetup extends BaseImportSetup { } } - log( - this.config, - `Loaded ${Object.keys(locales).length} locales (1 master + ${Object.keys(locales).length - 1} additional)`, - 'info', - ); + log.info(`Loaded ${Object.keys(locales).length} locales (1 master + ${Object.keys(locales).length - 1} additional)`); return locales; } catch (error) { - log(this.config, 'Error loading locales file, using only master locale', 'error'); + log.error('Error loading locales file, using only master locale', { error }); return locales; } } @@ -374,10 +370,9 @@ export default class TaxonomiesImportSetup extends BaseImportSetup { if (err?.errorMessage || err?.message) { const errorMsg = err?.errorMessage || err?.errors?.taxonomy || err?.errors?.term || err?.message; - log(this.config, `${errorMsg}${taxInfo}`, 'error'); + log.error(`${errorMsg}${taxInfo}`, { error: err, taxonomyUid, locale }); } else { - log(this.config, `Error fetching taxonomy data${taxInfo}!`, 'error'); - log(this.config, err, 'error'); + log.error(`Error fetching taxonomy data${taxInfo}!`, { error: err, taxonomyUid, locale }); } } } diff --git a/packages/contentstack-import-setup/src/types/import-config.ts b/packages/contentstack-import-setup/src/types/import-config.ts index 5c68eb773c..eed1ca79ca 100644 --- a/packages/contentstack-import-setup/src/types/import-config.ts +++ b/packages/contentstack-import-setup/src/types/import-config.ts @@ -1,4 +1,4 @@ -import { Modules } from '.'; +import { Modules, Context } from '.'; import DefaultConfig from './default-config'; export interface ExternalConfig { @@ -23,6 +23,7 @@ export interface Context { } export default interface ImportConfig extends DefaultConfig, ExternalConfig { + context?: Context; cliLogsPath?: string; contentDir: string; data: string; diff --git a/packages/contentstack-import-setup/src/types/index.ts b/packages/contentstack-import-setup/src/types/index.ts index 844e765f5a..df9c0b0bda 100644 --- a/packages/contentstack-import-setup/src/types/index.ts +++ b/packages/contentstack-import-setup/src/types/index.ts @@ -152,3 +152,14 @@ export type TaxonomyQueryParams = { depth?: number; locale?: string; }; + +export interface Context { + command: string; + module: string; + userId: string | undefined; + email: string | undefined; + sessionId: string | undefined; + apiKey: string; + orgId: string; + authenticationMethod?: string; +} diff --git a/packages/contentstack-import-setup/src/utils/common-helper.ts b/packages/contentstack-import-setup/src/utils/common-helper.ts index 9ab103e1d0..40aad278a0 100644 --- a/packages/contentstack-import-setup/src/utils/common-helper.ts +++ b/packages/contentstack-import-setup/src/utils/common-helper.ts @@ -1,5 +1,5 @@ import chalk from 'chalk'; -import { log } from '../utils'; +import { log } from '@contentstack/cli-utilities'; import { ImportConfig } from 'src/types'; export const validateBranch = async (stackAPIClient: any, config: ImportConfig, branch: any) => { @@ -8,8 +8,8 @@ export const validateBranch = async (stackAPIClient: any, config: ImportConfig, const data = await stackAPIClient.branch(branch).fetch(); if (data && typeof data === 'object') { if (data.error_message) { - log(config, chalk.red(data.error_message), 'error'); - log(config, chalk.red('No branch found with the name ' + branch), 'error'); + log.error(chalk.red(data.error_message), { error: data.error_message }); + log.error(chalk.red('No branch found with the name ' + branch), { branch }); reject({ message: 'No branch found with the name ' + branch, error: data.error_message }); } else { resolve(data); @@ -18,7 +18,7 @@ export const validateBranch = async (stackAPIClient: any, config: ImportConfig, reject({ message: 'No branch found with the name ' + branch, error: {} }); } } catch (error) { - log(config, chalk.red('No branch found with the name ' + branch), 'error'); + log.error(chalk.red('No branch found with the name ' + branch), { error, branch }); reject({ message: 'No branch found with the name ' + branch, error }); } }); diff --git a/packages/contentstack-import-setup/src/utils/file-helper.ts b/packages/contentstack-import-setup/src/utils/file-helper.ts index e5c613b90d..3c972dd436 100644 --- a/packages/contentstack-import-setup/src/utils/file-helper.ts +++ b/packages/contentstack-import-setup/src/utils/file-helper.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import mkdirp from 'mkdirp'; import * as bigJSON from 'big-json'; -import { FsUtility, sanitizePath } from '@contentstack/cli-utilities'; +import { FsUtility, sanitizePath, log } from '@contentstack/cli-utilities'; export const readFileSync = function (filePath: string, parse: boolean = true): any { let data; @@ -53,7 +53,7 @@ export const readLargeFile = function (filePath: string, opts?: any): Promise => { config.contentDir = sanitizePath(importCmdFlags['data'] || importCmdFlags['data-dir'] || config.data || (await askContentDir())); const pattern = /[*$%#<>{}!&?]/g; if (pattern.test(config.contentDir)) { - cliux.print(`\nPlease add a directory path without any of the special characters: (*,&,{,},[,],$,%,<,>,?,!)`, { + cliux.print(`\nPlease enter a directory path without special characters: (*,&,{,},[,],$,%,<,>,?,!)`, { color: 'yellow', }); config.contentDir = sanitizePath(await askContentDir()); diff --git a/packages/contentstack-import-setup/src/utils/login-handler.ts b/packages/contentstack-import-setup/src/utils/login-handler.ts index 996e7797a8..62692807a1 100644 --- a/packages/contentstack-import-setup/src/utils/login-handler.ts +++ b/packages/contentstack-import-setup/src/utils/login-handler.ts @@ -7,8 +7,7 @@ * MIT Licensed */ -import { log } from './logger'; -import { managementSDKClient, isAuthenticated } from '@contentstack/cli-utilities'; +import { managementSDKClient, isAuthenticated, log } from '@contentstack/cli-utilities'; import { ImportConfig } from '../types'; const login = async (config: ImportConfig): Promise => { @@ -22,7 +21,7 @@ const login = async (config: ImportConfig): Promise => { authtoken: config.authtoken, 'X-User-Agent': 'contentstack-export/v', }; - log(config, 'Contentstack account authenticated successfully!', 'success'); + log.success('Contentstack account authenticated successfully!'); return config; } else { throw new Error('Invalid auth token received after login'); @@ -37,10 +36,10 @@ const login = async (config: ImportConfig): Promise => { const stack = await stackAPIClient.fetch().catch((error: any) => { let errorstack_key = error?.errors?.api_key; if (errorstack_key) { - log(config, 'Stack Api key ' + errorstack_key[0] + 'Please enter valid Key', 'error'); + log.error('Stack Api key ' + errorstack_key[0] + 'Please enter valid Key', { error }); throw error; } - log(config, error?.errorMessage, 'error'); + log.error(error?.errorMessage || 'Unknown error', { error }); throw error; }); config.destinationStackName = stack.name; diff --git a/packages/contentstack-import-setup/src/utils/setup-branch.ts b/packages/contentstack-import-setup/src/utils/setup-branch.ts index 8bcdd81238..2d7ac98151 100644 --- a/packages/contentstack-import-setup/src/utils/setup-branch.ts +++ b/packages/contentstack-import-setup/src/utils/setup-branch.ts @@ -30,6 +30,6 @@ export const setupBranchConfig = async ( log.debug(`Setting default target branch to 'main'`); } } catch (error) { - log.debug('Failed to fetch branches', { error }); + log.error('Failed to fetch branches', { error }); } }; diff --git a/packages/contentstack-import/messages/index.json b/packages/contentstack-import/messages/index.json index 9e26dfeeb6..32280640fb 100644 --- a/packages/contentstack-import/messages/index.json +++ b/packages/contentstack-import/messages/index.json @@ -1 +1,10 @@ -{} \ No newline at end of file +{ + "COMPOSABLE_STUDIO_IMPORT_START": "Starting Studio project import...", + "COMPOSABLE_STUDIO_NOT_FOUND": "No Studio project found in exported data", + "COMPOSABLE_STUDIO_SKIP_EXISTING": "Skipping Studio import - target stack already has a connected project", + "COMPOSABLE_STUDIO_IMPORT_COMPLETE": "Successfully imported Studio project '%s'", + "COMPOSABLE_STUDIO_IMPORT_FAILED": "Failed to import Studio project: %s", + "COMPOSABLE_STUDIO_NAME_CONFLICT": "Project name '%s' already exists. Please provide a new name:", + "COMPOSABLE_STUDIO_SUGGEST_NAME": "Suggested name: %s", + "COMPOSABLE_STUDIO_ENV_MAPPING_FAILED": "Warning: Could not map environment '%s', using empty environment" +} diff --git a/packages/contentstack-import/src/commands/cm/stacks/import.ts b/packages/contentstack-import/src/commands/cm/stacks/import.ts index 968c348cc3..19dd8e1968 100644 --- a/packages/contentstack-import/src/commands/cm/stacks/import.ts +++ b/packages/contentstack-import/src/commands/cm/stacks/import.ts @@ -76,7 +76,8 @@ export default class ImportCommand extends Command { required: false, char: 'm', description: - '[optional] Specify the module to import into the target stack. If not specified, the import command will import all the modules into the stack. The available modules are assets, content-types, entries, environments, extensions, marketplace-apps, global-fields, labels, locales, webhooks, workflows, custom-roles, personalize projects, and taxonomies.', + '[optional] Specify the module to import into the target stack. If not specified, the import command will import all the modules into the stack. The available modules are assets, content-types, entries, environments, extensions, marketplace-apps, global-fields, labels, locales, webhooks, workflows, custom-roles, personalize projects, taxonomies, and composable-studio.', + '[optional] Specify the module to import into the target stack. If not specified, the import command will import all the modules into the stack. The available modules are assets, content-types, entries, environments, extensions, marketplace-apps, global-fields, labels, locales, webhooks, workflows, custom-roles, personalize projects, taxonomies, and composable-studio.', parse: printFlagDeprecation(['-m'], ['--module']), }), 'backup-dir': flags.string({ @@ -93,7 +94,8 @@ export default class ImportCommand extends Command { }), 'branch-alias': flags.string({ description: - "Specify the branch alias where you want to import your content. If not specified, the content is imported into the main branch by default.", + 'Specify the branch alias where you want to import your content. If not specified, the content is imported into the main branch by default.', + 'Specify the branch alias where you want to import your content. If not specified, the content is imported into the main branch by default.', exclusive: ['branch'], }), 'import-webhook-status': flags.string({ @@ -159,13 +161,16 @@ export default class ImportCommand extends Command { // Prepare the context object const context = this.createImportContext(importConfig.apiKey, importConfig.authenticationMethod); importConfig.context = { ...context }; - //log.info(`Using Cli Version: ${this.context?.cliVersion}`, importConfig.context); + // log.info(`Using CLI version: ${this.context?.cliVersion}`, importConfig.context); + // log.info(`Using CLI version: ${this.context?.cliVersion}`, importConfig.context); // Note setting host to create cma client importConfig.host = this.cmaHost; importConfig.region = this.region; if (this.developerHubUrl) importConfig.developerHubBaseUrl = this.developerHubUrl; if (this.personalizeUrl) importConfig.modules.personalize.baseURL[importConfig.region.name] = this.personalizeUrl; + if (this.composableStudioUrl) importConfig.modules['composable-studio'].apiBaseUrl = this.composableStudioUrl; + if (this.composableStudioUrl) importConfig.modules['composable-studio'].apiBaseUrl = this.composableStudioUrl; const managementAPIClient: ContentstackClient = await managementSDKClient(importConfig); diff --git a/packages/contentstack-import/src/config/index.ts b/packages/contentstack-import/src/config/index.ts index 8afa6d5115..9dcbba22d9 100644 --- a/packages/contentstack-import/src/config/index.ts +++ b/packages/contentstack-import/src/config/index.ts @@ -44,6 +44,7 @@ const config: DefaultConfig = { 'variant-entries', 'labels', 'webhooks', + 'composable-studio', ], locales: { dirName: 'locales', @@ -199,6 +200,12 @@ const config: DefaultConfig = { locale: 'en-us', }, }, + 'composable-studio': { + dirName: 'composable_studio', + fileName: 'composable_studio.json', + apiBaseUrl: 'https://composable-studio-api.contentstack.com', + apiVersion: 'v1', + }, }, languagesCode: [ 'af-za', diff --git a/packages/contentstack-import/src/import/modules/assets.ts b/packages/contentstack-import/src/import/modules/assets.ts index e5625e1a3c..e4c67a58b2 100644 --- a/packages/contentstack-import/src/import/modules/assets.ts +++ b/packages/contentstack-import/src/import/modules/assets.ts @@ -127,13 +127,16 @@ export default class ImportAssets extends BaseClass { const folders = this.fs.readFile(foldersPath); if (isEmpty(folders)) { - log.info('No folders found to import', this.importConfig.context); + log.info('No folders found to import.', this.importConfig.context); + log.info('No folders found to import.', this.importConfig.context); return; } - log.debug(`Found ${folders.length} folders to import`, this.importConfig.context); + log.debug(`Found ${folders.length} folders to import.`, this.importConfig.context); + log.debug(`Found ${folders.length} folders to import.`, this.importConfig.context); const batches = this.constructFolderImportOrder(folders); - log.debug(`Organized folders into ${batches.length} batches for import`, this.importConfig.context); + log.debug(`Organized folders into ${batches.length} batches for import.`, this.importConfig.context); + log.debug(`Organized folders into ${batches.length} batches for import.`, this.importConfig.context); const onSuccess = ({ response, apiData: { uid, name } = { uid: null, name: '' } }: any) => { this.assetsFolderMap[uid] = response.uid; @@ -166,7 +169,8 @@ export default class ImportAssets extends BaseClass { }; const batch = map(unionBy(batches, 'parent_uid'), 'parent_uid'); - log.debug(`Processing ${batch.length} folder batches`, this.importConfig.context); + log.debug(`Processing ${batch.length} folder batches.`, this.importConfig.context); + log.debug(`Processing ${batch.length} folder batches.`, this.importConfig.context); for (const parent_uid of batch) { const currentBatch = filter(batches, { parent_uid }); @@ -252,7 +256,8 @@ export default class ImportAssets extends BaseClass { log.debug(`Processing ${apiContent.length} assets in chunk`, this.importConfig.context); if (isVersion && this.assetConfig.importSameStructure) { - log.debug('Processing version 1 assets first', this.importConfig.context); + log.debug('Processing version 1 assets first...', this.importConfig.context); + log.debug('Processing version 1 assets first...', this.importConfig.context); const versionOneAssets = filter(apiContent, ({ _version }) => _version === 1); await this.makeConcurrentCall({ @@ -357,7 +362,8 @@ export default class ImportAssets extends BaseClass { async publish() { const fs = new FsUtility({ basePath: this.assetsPath, indexFileName: 'assets.json' }); if (isEmpty(this.assetsUidMap)) { - log.debug('Loading asset UID mappings from file', this.importConfig.context); + log.debug('Loading asset UID mappings from file...', this.importConfig.context); + log.debug('Loading asset UID mappings from file...', this.importConfig.context); this.assetsUidMap = fs.readFile(this.assetUidMapperPath, true) as any; } @@ -393,7 +399,8 @@ export default class ImportAssets extends BaseClass { if (environments.length === 0 || locales.length === 0) { log.debug( - `Skipping publish for asset ${asset.uid} - no valid environments/locales`, + `Skipping publish for asset ${asset.uid}: no valid environments/locales`, + `Skipping publish for asset ${asset.uid}: no valid environments/locales`, this.importConfig.context, ); apiOptions.entity = undefined; @@ -409,7 +416,8 @@ export default class ImportAssets extends BaseClass { apiOptions.uid = this.assetsUidMap[asset.uid] as string; if (!apiOptions.uid) { - log.debug(`Skipping publish for asset ${asset.uid} - no UID mapping found`, this.importConfig.context); + log.debug(`Skipping publish for asset ${asset.uid}: no UID mapping found.`, this.importConfig.context); + log.debug(`Skipping publish for asset ${asset.uid}: no UID mapping found.`, this.importConfig.context); apiOptions.entity = undefined; } @@ -468,7 +476,8 @@ export default class ImportAssets extends BaseClass { } if (this.importConfig.replaceExisting) { - log.debug('Setting up root folder for import', this.importConfig.context); + log.debug('Setting up root folder for import...', this.importConfig.context); + log.debug('Setting up root folder for import...', this.importConfig.context); // Note: adds a root folder to distinguish latest asset uploads // Todo: This temporary approach should be updated with asset and folder overwrite strategy, which follows // folder overwrite @@ -492,7 +501,8 @@ export default class ImportAssets extends BaseClass { }); importOrder.unshift(this.rootFolder); - log.debug('Added root folder to import order', this.importConfig.context); + log.debug('Added root folder to import order.', this.importConfig.context); + log.debug('Added root folder to import order.', this.importConfig.context); } return importOrder; } diff --git a/packages/contentstack-import/src/import/modules/composable-studio.ts b/packages/contentstack-import/src/import/modules/composable-studio.ts new file mode 100644 index 0000000000..6925aa9c42 --- /dev/null +++ b/packages/contentstack-import/src/import/modules/composable-studio.ts @@ -0,0 +1,301 @@ +import { join } from 'node:path'; +import { + cliux, + log, + messageHandler, + handleAndLogError, + HttpClient, + authenticationHandler, +} from '@contentstack/cli-utilities'; +import isEmpty from 'lodash/isEmpty'; + +import { fsUtil, fileHelper } from '../../utils'; +import { ImportConfig, ModuleClassParams, ComposableStudioConfig, ComposableStudioProject } from '../../types'; + +export default class ImportComposableStudio { + private importConfig: ImportConfig; + private composableStudioConfig: ComposableStudioConfig; + private composableStudioPath: string; + private composableStudioFilePath: string; + private apiClient: HttpClient; + private envUidMapperPath: string; + private envUidMapper: Record; + + constructor({ importConfig }: ModuleClassParams) { + this.importConfig = importConfig; + this.importConfig.context.module = 'composable-studio'; + this.composableStudioConfig = importConfig.modules['composable-studio']; + + // Setup paths + this.composableStudioPath = join(this.importConfig.backupDir, this.composableStudioConfig.dirName); + this.composableStudioFilePath = join(this.composableStudioPath, this.composableStudioConfig.fileName); + this.envUidMapperPath = join(this.importConfig.backupDir, 'mapper', 'environments', 'uid-mapping.json'); + this.envUidMapper = {}; + + // Initialize HttpClient with Studio API base URL + this.apiClient = new HttpClient(); + this.apiClient.baseUrl(this.composableStudioConfig.apiBaseUrl + this.composableStudioConfig.apiVersion); + } + + /** + * Entry point for Studio import + */ + async start(): Promise { + if (this.importConfig.management_token) { + log.warn('Skipping Studio project import when using management token', this.importConfig.context); + return; + } + + log.debug('Starting Studio project import process...', this.importConfig.context); + + try { + // Initialize authentication + const authInitialized = await this.addAuthHeaders(); + if (!authInitialized) { + log.warn('Skipping Studio project import when using OAuth authentication', this.importConfig.context); + return; + } + + // Load environment UID mapper + await this.loadEnvironmentMapper(); + + // Read exported project data + const exportedProject = await this.readExportedProject(); + if (!exportedProject) { + log.warn(messageHandler.parse('COMPOSABLE_STUDIO_NOT_FOUND'), this.importConfig.context); + return; + } + + log.debug(`Exported project found: ${exportedProject.name}`, this.importConfig.context); + + // Check if target stack already has a connected project + const existingProject = await this.getExistingProject(); + if (existingProject) { + log.warn(messageHandler.parse('COMPOSABLE_STUDIO_SKIP_EXISTING'), this.importConfig.context); + return; + } + + // Import the project with name conflict handling + await this.importProject(exportedProject); + + log.success( + messageHandler.parse('COMPOSABLE_STUDIO_IMPORT_COMPLETE', exportedProject.name), + this.importConfig.context, + ); + } catch (error) { + handleAndLogError(error, { ...this.importConfig.context }); + } + } + + /** + * Initialize authentication headers for API calls + */ + async addAuthHeaders(): Promise { + log.debug('Initializing Studio API authentication...', this.importConfig.context); + + // Get authentication details - following personalization-api-adapter pattern + await authenticationHandler.getAuthDetails(); + const token = authenticationHandler.accessToken; + log.debug( + `Authentication type: ${authenticationHandler.isOauthEnabled ? 'OAuth' : 'Token'}`, + this.importConfig.context, + ); + + // Set authentication headers based on auth type + if (authenticationHandler.isOauthEnabled) { + log.debug( + 'Skipping setting OAuth authorization header when using OAuth authentication', + this.importConfig.context, + ); + return false; + } else { + // TODO: Currenlty assuming if auth type is not OAuth, it is Basic Auth and we are setting authtoken header + log.debug('Setting authtoken header', this.importConfig.context); + this.apiClient.headers({ authtoken: token }); + } + + // Set organization_uid header + this.apiClient.headers({ + organization_uid: this.importConfig.org_uid, + 'Content-Type': 'application/json', + Accept: 'application/json', + }); + + log.debug('Studio API authentication initialized', this.importConfig.context); + return true; + } + + /** + * Load environment UID mapper from backup directory + */ + async loadEnvironmentMapper(): Promise { + log.debug('Loading environment UID mapper...', this.importConfig.context); + + if (fileHelper.fileExistsSync(this.envUidMapperPath)) { + this.envUidMapper = fileHelper.readFileSync(this.envUidMapperPath) as Record; + log.debug( + `Environment mapper loaded with ${Object.keys(this.envUidMapper).length} mappings`, + this.importConfig.context, + ); + } else { + log.debug('No environment UID mapper found', this.importConfig.context); + } + } + + /** + * Read exported project from file system + */ + async readExportedProject(): Promise { + log.debug(`Reading exported project from: ${this.composableStudioFilePath}`, this.importConfig.context); + + if (!fileHelper.fileExistsSync(this.composableStudioFilePath)) { + log.debug('Studio project file does not exist', this.importConfig.context); + return null; + } + + const projectData = fileHelper.readFileSync(this.composableStudioFilePath) as ComposableStudioProject; + + if (!projectData || isEmpty(projectData)) { + log.debug('Studio project file is empty', this.importConfig.context); + return null; + } + + return projectData; + } + + /** + * Check if target stack already has a connected project + */ + async getExistingProject(): Promise { + log.debug('Checking if target stack already has a connected project...', this.importConfig.context); + + try { + const apiUrl = '/projects'; + log.debug( + `Fetching projects from: ${this.composableStudioConfig.apiBaseUrl}${apiUrl}`, + this.importConfig.context, + ); + + const response = await this.apiClient.get(apiUrl); + + if (response.status < 200 || response.status >= 300) { + throw new Error(`API call failed with status ${response.status}: ${JSON.stringify(response.data)}`); + } + + const projects = response.data?.projects || []; + log.debug(`Found ${projects.length} projects in organization`, this.importConfig.context); + + // Filter projects by connected stack API key + const connectedProject = projects.find( + (project: ComposableStudioProject) => project.connectedStackApiKey === this.importConfig.apiKey, + ); + + if (connectedProject) { + log.debug(`Target stack already has connected project: ${connectedProject.name}`, this.importConfig.context); + return connectedProject; + } + + log.debug('Target stack does not have a connected project', this.importConfig.context); + return null; + } catch (error) { + log.debug(`Error checking for existing project: ${error.message}`, this.importConfig.context); + throw error; + } + } + + /** + * Import project with name conflict handling + */ + async importProject(exportedProject: ComposableStudioProject): Promise { + log.debug('Starting project import...', this.importConfig.context); + + // Map environment UID + const mappedEnvironmentUid = this.mapEnvironmentUid(exportedProject.settings.configuration.environment); + + // Prepare project data for import + const projectData = { + name: exportedProject.name, + connectedStackApiKey: this.importConfig.apiKey, + contentTypeUid: exportedProject.contentTypeUid, + description: exportedProject.description || '', + canvasUrl: exportedProject.canvasUrl || '/', + settings: { + configuration: { + environment: mappedEnvironmentUid, + locale: exportedProject?.settings?.configuration?.locale || '', + }, + }, + }; + + log.debug(`Project data prepared: ${JSON.stringify(projectData, null, 2)}`, this.importConfig.context); + + // Try to create project with name conflict retry loop + let projectCreated = false; + let currentName = projectData.name; + let attemptCount = 0; + + while (!projectCreated) { + attemptCount++; + log.debug(`Attempt ${attemptCount} to create project with name: ${currentName}`, this.importConfig.context); + + projectData.name = currentName; + const response = await this.apiClient.post('/projects', projectData); + + if (response.status >= 200 && response.status < 300) { + projectCreated = true; + log.debug(`Project created successfully with UID: ${response.data?.uid}`, this.importConfig.context); + } else { + throw new Error(`API call failed with status ${response.status}: ${JSON.stringify(response.data)}`); + } + } + } + + /** + * Map environment UID from source to target + */ + mapEnvironmentUid(sourceEnvUid: string): string { + if (!sourceEnvUid) { + log.debug('Source environment UID is empty', this.importConfig.context); + return ''; + } + + log.debug(`Mapping source environment UID: ${sourceEnvUid}`, this.importConfig.context); + + if (isEmpty(this.envUidMapper)) { + log.warn(messageHandler.parse('COMPOSABLE_STUDIO_ENV_MAPPING_FAILED', sourceEnvUid), this.importConfig.context); + return ''; + } + + const mappedUid = this.envUidMapper[sourceEnvUid]; + + if (!mappedUid) { + log.warn(messageHandler.parse('COMPOSABLE_STUDIO_ENV_MAPPING_FAILED', sourceEnvUid), this.importConfig.context); + return ''; + } + + log.debug(`Mapped environment UID: ${sourceEnvUid} → ${mappedUid}`, this.importConfig.context); + return mappedUid; + } + + /** + * Prompt user for a new project name when conflict occurs + */ + async promptForNewProjectName(currentName: string): Promise { + const suggestedName = `Copy of ${currentName}`; + + log.warn(messageHandler.parse('COMPOSABLE_STUDIO_NAME_CONFLICT', currentName), this.importConfig.context); + log.info(messageHandler.parse('COMPOSABLE_STUDIO_SUGGEST_NAME', suggestedName), this.importConfig.context); + + const response: any = await cliux.inquire({ + type: 'input', + name: 'projectName', + message: 'Enter new project name:', + default: suggestedName, + }); + + const newName = response.projectName || suggestedName; + log.debug(`User provided new project name: ${newName}`, this.importConfig.context); + + return newName; + } +} diff --git a/packages/contentstack-import/src/import/modules/content-types.ts b/packages/contentstack-import/src/import/modules/content-types.ts index bb82baaf02..b5d28065c2 100644 --- a/packages/contentstack-import/src/import/modules/content-types.ts +++ b/packages/contentstack-import/src/import/modules/content-types.ts @@ -151,7 +151,8 @@ export default class ContentTypesImport extends BaseClass { PROCESS_NAMES.CONTENT_TYPES_CREATE, ); if (error.errorCode === 115 && (error.errors.uid || error.errors.title)) { - log.info(`${uid} content type already exist`, this.importConfig.context); + log.info(`${uid} content type already exists.`, this.importConfig.context); + log.info(`${uid} content type already exists.`, this.importConfig.context); log.debug(`Skipping existing content type: ${uid}`, this.importConfig.context); } else { handleAndLogError(error, { ...this.importConfig.context, uid }, `Failed to seed content type ${uid}`); @@ -351,7 +352,8 @@ export default class ContentTypesImport extends BaseClass { if (!apiContent || apiContent?.length === 0) { log.info(`No extensions found to be updated.`, this.importConfig.context); - log.debug('Skipping extensions update - no pending extensions', this.importConfig.context); + log.debug('Skipping extensions update – no pending extensions.', this.importConfig.context); + log.debug('Skipping extensions update – no pending extensions.', this.importConfig.context); return; } @@ -387,7 +389,8 @@ export default class ContentTypesImport extends BaseClass { } }; - log.debug('Starting extensions update process', this.importConfig.context); + log.debug('Starting extensions update process...', this.importConfig.context); + log.debug('Starting extensions update process...', this.importConfig.context); return await this.makeConcurrentCall( { apiContent, diff --git a/packages/contentstack-import/src/import/modules/custom-roles.ts b/packages/contentstack-import/src/import/modules/custom-roles.ts index cb8973ffaf..72b79eeedf 100644 --- a/packages/contentstack-import/src/import/modules/custom-roles.ts +++ b/packages/contentstack-import/src/import/modules/custom-roles.ts @@ -83,7 +83,8 @@ export default class ImportCustomRoles extends BaseClass { } async getLocalesUidMap(): Promise { - log.debug('Fetching target stack locales', this.importConfig.context); + log.debug('Fetching target stack locales...', this.importConfig.context); + log.debug('Fetching target stack locales...', this.importConfig.context); const { items } = await this.stack .locale() .query() @@ -100,18 +101,21 @@ export default class ImportCustomRoles extends BaseClass { this.targetLocalesMap = {}; this.sourceLocalesMap = {}; - log.debug('Building target locales mapping', this.importConfig.context); + log.debug('Building target locales mapping...', this.importConfig.context); + log.debug('Building target locales mapping...', this.importConfig.context); forEach(items, (locale: any) => { this.targetLocalesMap[locale.code] = locale.uid; }); - log.debug('Building source locales mapping', this.importConfig.context); + log.debug('Building source locales mapping...', this.importConfig.context); + log.debug('Building source locales mapping...', this.importConfig.context); for (const key in this.customRolesLocales) { const sourceLocales = this.customRolesLocales[key] as Record; this.sourceLocalesMap[sourceLocales.code] = key; } - log.debug('Creating locale UID mapping', this.importConfig.context); + log.debug('Creating locale UID mapping...', this.importConfig.context); + log.debug('Creating locale UID mapping...', this.importConfig.context); for (const key in this.sourceLocalesMap) { const sourceLocaleKey = this.sourceLocalesMap[key] as string; this.localesUidMap[sourceLocaleKey] = this.targetLocalesMap[key]; @@ -122,9 +126,11 @@ export default class ImportCustomRoles extends BaseClass { } async importCustomRoles() { - log.debug('Starting custom roles import process', this.importConfig.context); + log.debug('Starting custom roles import process...', this.importConfig.context); + log.debug('Starting custom roles import process...', this.importConfig.context); if (this.customRoles === undefined || isEmpty(this.customRoles)) { - log.info('No custom-roles found', this.importConfig.context); + log.info('No custom roles found', this.importConfig.context); + log.info('No custom roles found', this.importConfig.context); return; } @@ -255,7 +261,8 @@ export default class ImportCustomRoles extends BaseClass { rule.environments = map(rule.environments, (env: any) => this.environmentsUidMap[env]); log.debug(`Transformed ${originalEnvs} environment UIDs for rule`, this.importConfig.context); } else { - log.debug('No environment UID mappings available for transformation', this.importConfig.context); + log.debug('No environment UID mappings available for transformation.', this.importConfig.context); + log.debug('No environment UID mappings available for transformation.', this.importConfig.context); } } else if (rule.module === 'locale') { if (!isEmpty(this.localesUidMap)) { @@ -263,7 +270,8 @@ export default class ImportCustomRoles extends BaseClass { rule.locales = map(rule.locales, (locale: any) => this.localesUidMap[locale]); log.debug(`Transformed ${originalLocales} locale UIDs for rule`, this.importConfig.context); } else { - log.debug('No locale UID mappings available for transformation', this.importConfig.context); + log.debug('No locale UID mappings available for transformation.', this.importConfig.context); + log.debug('No locale UID mappings available for transformation.', this.importConfig.context); } } else if (rule.module === 'entry') { if (!isEmpty(this.entriesUidMap)) { @@ -271,7 +279,8 @@ export default class ImportCustomRoles extends BaseClass { rule.entries = map(rule.entries, (entry: any) => this.entriesUidMap[entry]); log.debug(`Transformed ${originalEntries} entry UIDs for rule`, this.importConfig.context); } else { - log.debug('No entry UID mappings available for transformation', this.importConfig.context); + log.debug('No entry UID mappings available for transformation.', this.importConfig.context); + log.debug('No entry UID mappings available for transformation.', this.importConfig.context); } } return rule; diff --git a/packages/contentstack-import/src/import/modules/entries.ts b/packages/contentstack-import/src/import/modules/entries.ts index beeee5a90b..1e82538f78 100644 --- a/packages/contentstack-import/src/import/modules/entries.ts +++ b/packages/contentstack-import/src/import/modules/entries.ts @@ -1170,7 +1170,8 @@ export default class EntriesImport extends BaseClass { any >[]; if (!cTsWithFieldRules || cTsWithFieldRules?.length === 0) { - log.debug('No content types with field rules found to update', this.importConfig.context); + log.debug('No content types with field rules found to update.', this.importConfig.context); + log.debug('No content types with field rules found to update.', this.importConfig.context); return; } diff --git a/packages/contentstack-import/src/import/modules/environments.ts b/packages/contentstack-import/src/import/modules/environments.ts index db9ba6ddb2..9ade6551b1 100644 --- a/packages/contentstack-import/src/import/modules/environments.ts +++ b/packages/contentstack-import/src/import/modules/environments.ts @@ -64,9 +64,11 @@ export default class ImportEnvironments extends BaseClass { } async importEnvironments() { - log.debug('Validating environments data', this.importConfig.context); + log.debug('Validating environment data...', this.importConfig.context); + log.debug('Validating environment data...', this.importConfig.context); if (this.environments === undefined || isEmpty(this.environments)) { - log.info('No Environment Found', this.importConfig.context); + log.info('No environment found.', this.importConfig.context); + log.info('No environment found.', this.importConfig.context); return; } @@ -132,7 +134,8 @@ export default class ImportEnvironments extends BaseClass { false, ); - log.debug('Environments import process completed', this.importConfig.context); + log.debug('Environment import process completed.', this.importConfig.context); + log.debug('Environment import process completed.', this.importConfig.context); } /** diff --git a/packages/contentstack-import/src/import/modules/extensions.ts b/packages/contentstack-import/src/import/modules/extensions.ts index eb205d792d..b41b07a258 100644 --- a/packages/contentstack-import/src/import/modules/extensions.ts +++ b/packages/contentstack-import/src/import/modules/extensions.ts @@ -97,7 +97,8 @@ export default class ImportExtensions extends BaseClass { async importExtensions(): Promise { log.debug('Starting Create process', this.importConfig.context); if (this.extensions === undefined || isEmpty(this.extensions)) { - log.info('No Extensions Found', this.importConfig.context); + log.info('No extensions found.', this.importConfig.context); + log.info('No extensions found.', this.importConfig.context); return; } diff --git a/packages/contentstack-import/src/import/modules/global-fields.ts b/packages/contentstack-import/src/import/modules/global-fields.ts index bc13201eaa..1ddf717650 100644 --- a/packages/contentstack-import/src/import/modules/global-fields.ts +++ b/packages/contentstack-import/src/import/modules/global-fields.ts @@ -178,7 +178,8 @@ export default class ImportGlobalFields extends BaseClass { ); } if (!this.importConfig.skipExisting) { - log.info(`Global fields '${uid}' already exist`, this.importConfig.context); + log.info(`Global field '${uid}' already exists.`, this.importConfig.context); + log.info(`Global field '${uid}' already exists.`, this.importConfig.context); } } else { this.progressManager?.tick( diff --git a/packages/contentstack-import/src/import/modules/labels.ts b/packages/contentstack-import/src/import/modules/labels.ts index 397a860987..117dc1b53c 100644 --- a/packages/contentstack-import/src/import/modules/labels.ts +++ b/packages/contentstack-import/src/import/modules/labels.ts @@ -84,7 +84,8 @@ export default class ImportLabels extends BaseClass { async importLabels() { log.debug('Validating labels data', this.importConfig.context); if (this.labels === undefined || isEmpty(this.labels)) { - log.info('No Labels Found', this.importConfig.context); + log.info('No labels found.', this.importConfig.context); + log.info('No labels found.', this.importConfig.context); return; } @@ -150,7 +151,8 @@ export default class ImportLabels extends BaseClass { log.debug(`Serializing label: ${label.name} (${label.uid})`, this.importConfig.context); if (this.labelUidMapper.hasOwnProperty(label.uid)) { - log.info(`Label '${label.name}' already exists. Skipping it to avoid duplicates!`, this.importConfig.context); + log.info(`Label '${label.name}' already exists. Skipping to avoid duplicates.`, this.importConfig.context); + log.info(`Label '${label.name}' already exists. Skipping to avoid duplicates.`, this.importConfig.context); log.debug(`Skipping label serialization for: ${label.uid}`, this.importConfig.context); this.progressManager?.tick( true, @@ -174,7 +176,8 @@ export default class ImportLabels extends BaseClass { } async updateLabels() { - log.debug('Starting labels update process', this.importConfig.context); + log.debug('Starting labels update process...', this.importConfig.context); + log.debug('Starting labels update process...', this.importConfig.context); if (!isEmpty(this.labels)) { const apiContent = values(this.labels); log.debug(`Updating ${apiContent.length} labels`, this.importConfig.context); @@ -219,10 +222,12 @@ export default class ImportLabels extends BaseClass { false, ); } else { - log.debug('No labels to update', this.importConfig.context); + log.debug('No labels to update.', this.importConfig.context); + log.debug('No labels to update.', this.importConfig.context); } - log.debug('Labels update process completed', this.importConfig.context); + log.debug('Labels update process completed.', this.importConfig.context); + log.debug('Labels update process completed.', this.importConfig.context); } /** diff --git a/packages/contentstack-import/src/import/modules/marketplace-apps.ts b/packages/contentstack-import/src/import/modules/marketplace-apps.ts index 46b8daf3c3..d5fc67a015 100644 --- a/packages/contentstack-import/src/import/modules/marketplace-apps.ts +++ b/packages/contentstack-import/src/import/modules/marketplace-apps.ts @@ -153,30 +153,36 @@ export default class ImportMarketplaceApps extends BaseClass { * validates app installation, and generates a UID mapper. */ async importMarketplaceApps(): Promise { - log.debug('Setting up security configuration for marketplace apps', this.importConfig.context); + log.debug('Setting up security configuration for Marketplace Apps...', this.importConfig.context); + log.debug('Setting up security configuration for Marketplace Apps...', this.importConfig.context); // NOTE set default encryptionKey const cryptoArgs = { encryptionKey: this.importConfig.marketplaceAppEncryptionKey }; if (this.importConfig.forceStopMarketplaceAppsPrompt) { - log.debug('Using forced security configuration without validation', this.importConfig.context); + log.debug('Using forced security configuration without validation.', this.importConfig.context); + log.debug('Using forced security configuration without validation.', this.importConfig.context); this.nodeCrypto = new NodeCrypto(cryptoArgs); } // NOTE getting all apps to validate if it's already installed in the stack to manage conflict - log.debug('Getting all stack-specific apps for validation', this.importConfig.context); + log.debug('Getting all stack-specific apps for validation...', this.importConfig.context); + log.debug('Getting all stack-specific apps for validation...', this.importConfig.context); this.installedApps = await getAllStackSpecificApps(this.importConfig); log.debug(`Found ${this.installedApps?.length || 0} already installed apps`, this.importConfig.context); - log.info('Starting marketplace app installation', this.importConfig.context); + log.info('Starting Marketplace App installation...', this.importConfig.context); + log.info('Starting Marketplace App installation...', this.importConfig.context); for (let app of this.marketplaceApps) { log.debug(`Processing app: ${app.manifest?.name || app.manifest?.uid}`, this.importConfig.context); await this.installApps(app); } - log.debug('Generating UID mapper', this.importConfig.context); + log.debug('Generating UID mapper...', this.importConfig.context); + log.debug('Generating UID mapper...', this.importConfig.context); const uidMapper = await this.generateUidMapper(); - log.debug('Writing UID mappings to file', this.importConfig.context); + log.debug('Writing UID mappings to file...', this.importConfig.context); + log.debug('Writing UID mappings to file...', this.importConfig.context); fsUtil.writeFile(this.marketPlaceUidMapperPath, { app_uid: this.appUidMapping, extension_uid: uidMapper || {}, @@ -197,24 +203,28 @@ export default class ImportMarketplaceApps extends BaseClass { * unknown>`. */ async generateUidMapper(): Promise> { - log.debug('Generating UID mapper for extensions', this.importConfig.context); + log.debug('Generating UID mapper for extensions...', this.importConfig.context); + log.debug('Generating UID mapper for extensions...', this.importConfig.context); const listOfNewMeta = []; const listOfOldMeta = []; const extensionUidMap: Record = {}; // NOTE After installation getting all apps to create mapper. - log.debug('Fetching updated list of installed apps', this.importConfig.context); + log.debug('Fetching updated list of installed apps...', this.importConfig.context); + log.debug('Fetching updated list of installed apps...', this.importConfig.context); this.installedApps = (await getAllStackSpecificApps(this.importConfig)) || []; log.debug(`Found ${this.installedApps?.length || 0} installed apps after installation`, this.importConfig.context); - log.debug('Processing old metadata from marketplace apps', this.importConfig.context); + log.debug('Processing old metadata from Marketplace Apps...', this.importConfig.context); + log.debug('Processing old metadata from Marketplace Apps...', this.importConfig.context); for (const app of this.marketplaceApps) { const appMeta = map(app?.ui_location?.locations, 'meta').flat(); listOfOldMeta.push(...appMeta); log.debug(`Added ${appMeta.length} meta entries from app: ${app.manifest?.name}`, this.importConfig.context); } - log.debug('Processing new metadata from installed apps', this.importConfig.context); + log.debug('Processing new metadata from installed apps...', this.importConfig.context); + log.debug('Processing new metadata from installed apps...', this.importConfig.context); for (const app of this.installedApps) { const appMeta = map(app?.ui_location?.locations, 'meta').flat(); listOfNewMeta.push(...appMeta); @@ -263,7 +273,8 @@ export default class ImportMarketplaceApps extends BaseClass { ); if (!appConfig) { - log.debug('No app configuration found requiring encryption', this.importConfig.context); + log.debug('No app configuration found requiring encryption.', this.importConfig.context); + log.debug('No app configuration found requiring encryption.', this.importConfig.context); return defaultValue; } @@ -273,11 +284,14 @@ export default class ImportMarketplaceApps extends BaseClass { try { appConfig = !isEmpty(appConfig.configuration) ? appConfig.configuration : appConfig.server_configuration; - log.debug('Creating NodeCrypto instance with security configuration', this.importConfig.context); + log.debug('Creating NodeCrypto instance with security configuration...', this.importConfig.context); + log.debug('Creating NodeCrypto instance with security configuration...', this.importConfig.context); this.nodeCrypto = new NodeCrypto({ encryptionKey }); - log.debug('Testing security configuration with app data', this.importConfig.context); + log.debug('Testing security configuration with app data...', this.importConfig.context); + log.debug('Testing security configuration with app data...', this.importConfig.context); this.nodeCrypto.decrypt(appConfig); - log.debug('Security configuration validation successful', this.importConfig.context); + log.debug('Security configuration validation successful.', this.importConfig.context); + log.debug('Security configuration validation successful.', this.importConfig.context); } catch (error) { log.debug(`Security configuration validation failed: ${error.message}`, this.importConfig.context); if (retry < this.importConfig.getEncryptionKeyMaxRetry && error.code === 'ERR_OSSL_EVP_BAD_DECRYPT') { @@ -296,7 +310,8 @@ export default class ImportMarketplaceApps extends BaseClass { `Maximum retry limit exceeded. Closing the process, please try again.! attempt(${retry}/${this.importConfig.getEncryptionKeyMaxRetry})`, { color: 'red' }, ); - log.debug('Maximum retry limit exceeded for encryption validation', this.importConfig.context); + log.debug('Maximum retry limit exceeded for encryption validation.', this.importConfig.context); + log.debug('Maximum retry limit exceeded for encryption validation.', this.importConfig.context); process.exit(1); } } @@ -311,7 +326,8 @@ export default class ImportMarketplaceApps extends BaseClass { * @returns a Promise that resolves to void. */ async handleAllPrivateAppsCreationProcess(): Promise { - log.debug('Filtering private apps from marketplace apps', this.importConfig.context); + log.debug('Filtering private apps from Marketplace Apps...', this.importConfig.context); + log.debug('Filtering private apps from Marketplace Apps...', this.importConfig.context); const privateApps = filter(this.marketplaceApps, { manifest: { visibility: 'private' } }); log.debug(`Found ${privateApps.length} private apps to process`, this.importConfig.context); @@ -326,7 +342,8 @@ export default class ImportMarketplaceApps extends BaseClass { this.importConfig.canCreatePrivateApp = canCreatePrivateApp; if (canCreatePrivateApp) { - log.info('Starting developer hub private apps re-creation', this.importConfig.context); + log.info('Starting Developer Hub private apps re-creation...', this.importConfig.context); + log.info('Starting Developer Hub private apps re-creation...', this.importConfig.context); log.debug(`Processing ${privateApps.length} private apps for creation`, this.importConfig.context); for (let app of privateApps) { @@ -391,12 +408,14 @@ export default class ImportMarketplaceApps extends BaseClass { .installation(app.uid) .fetch() .catch((): void => { - log.debug(`App ${app.manifest?.name} not found in developer hub`, this.importConfig.context); + log.debug(`App ${app.manifest?.name} not found in Developer Hub.`, this.importConfig.context); + log.debug(`App ${app.manifest?.name} not found in Developer Hub.`, this.importConfig.context); return undefined; }); // NOTE Keeping this to avoid Unhandled exception const exists = !isEmpty(installation); - log.debug(`Private app ${app.manifest?.name} exists in developer hub: ${exists}`, this.importConfig.context); + log.debug(`Private app ${app.manifest?.name} exists in Developer Hub: ${exists}`, this.importConfig.context); + log.debug(`Private app ${app.manifest?.name} exists in Developer Hub: ${exists}`, this.importConfig.context); return exists; } @@ -419,7 +438,8 @@ export default class ImportMarketplaceApps extends BaseClass { ); if (updateUiLocation && !isEmpty(app?.ui_location?.locations)) { - log.debug(`Updating UI locations for app: ${app.name}`, this.importConfig.context); + log.debug(`Updating UI locations for app: ${app.name}...`, this.importConfig.context); + log.debug(`Updating UI locations for app: ${app.name}...`, this.importConfig.context); app.ui_location.locations = this.updateManifestUILocations(app?.ui_location?.locations, appSuffix); } @@ -526,7 +546,8 @@ export default class ImportMarketplaceApps extends BaseClass { */ async appCreationCallback(app: any, response: any, appSuffix: number): Promise { const { statusText, message } = response || {}; - log.debug(`Processing app creation callback for: ${app.name} (suffix: ${appSuffix})`, this.importConfig.context); + log.debug(`Processing app creation callback for: ${app.name} (suffix: ${appSuffix})...`, this.importConfig.context); + log.debug(`Processing app creation callback for: ${app.name} (suffix: ${appSuffix})...`, this.importConfig.context); if (message) { log.debug(`App creation response has message: ${message}`, this.importConfig.context); @@ -551,10 +572,12 @@ export default class ImportMarketplaceApps extends BaseClass { ), ) ) { - log.debug('User chose to proceed despite error', this.importConfig.context); + log.debug('User chose to proceed despite error.', this.importConfig.context); + log.debug('User chose to proceed despite error.', this.importConfig.context); Promise.resolve(); } else { - log.debug('User chose to exit due to error', this.importConfig.context); + log.debug('User chose to exit due to error.', this.importConfig.context); + log.debug('User chose to exit due to error.', this.importConfig.context); process.exit(); } } @@ -688,10 +711,12 @@ export default class ImportMarketplaceApps extends BaseClass { async updateAppsConfig(app: Installation): Promise { const { installation_uid, configuration, server_configuration } = app; const appName = app.manifest.name || app.manifest.uid; - log.debug(`Updating app configuration for: ${appName} (${installation_uid})`, this.importConfig.context); + log.debug(`Updating configuration for: ${appName} (${installation_uid})`, this.importConfig.context); + log.debug(`Updating configuration for: ${appName} (${installation_uid})`, this.importConfig.context); if (!isEmpty(configuration)) { - log.debug(`Updating app configuration for: ${appName}`, this.importConfig.context); + log.debug(`Updating configuration for: ${appName}`, this.importConfig.context); + log.debug(`Updating configuration for: ${appName}`, this.importConfig.context); await this.appSdk .marketplace(this.importConfig.org_uid) .installation(installation_uid) @@ -702,18 +727,21 @@ export default class ImportMarketplaceApps extends BaseClass { log.info(formatError(data.message), this.importConfig.context); } else { log.success(`${appName} app config updated successfully.!`, this.importConfig.context); - log.debug(`Configuration update successful for: ${appName}`, this.importConfig.context); + log.debug(`Configuration update was successful for: ${appName}`, this.importConfig.context); + log.debug(`Configuration update was successful for: ${appName}`, this.importConfig.context); } }) .catch((error: any) => { log.debug(error, this.importConfig.context); log.error(formatError(error), this.importConfig.context); - log.debug(`Configuration update failed for: ${appName}`, this.importConfig.context); + log.debug(`Configuration update failed for: ${appName}.`, this.importConfig.context); + log.debug(`Configuration update failed for: ${appName}.`, this.importConfig.context); }); } if (!isEmpty(server_configuration)) { - log.debug(`Updating server configuration for: ${appName}`, this.importConfig.context); + log.debug(`Updating server configuration for: ${appName}...`, this.importConfig.context); + log.debug(`Updating server configuration for: ${appName}...`, this.importConfig.context); await this.appSdk .marketplace(this.importConfig.org_uid) .installation(installation_uid) @@ -724,13 +752,15 @@ export default class ImportMarketplaceApps extends BaseClass { log.error(formatError(data.message), this.importConfig.context); } else { log.success(`${appName} app server config updated successfully.!`, this.importConfig.context); - log.debug(`Server configuration update successful for: ${appName}`, this.importConfig.context); + log.debug(`Server configuration update was successful for: ${appName}.`, this.importConfig.context); + log.debug(`Server configuration update was successful for: ${appName}.`, this.importConfig.context); } }) .catch((error: any) => { log.debug(error, this.importConfig.context); log.error(formatError(error), this.importConfig.context); - log.debug(`Server configuration update failed for: ${appName}`, this.importConfig.context); + log.debug(`Server configuration update failed for: ${appName}.`, this.importConfig.context); + log.debug(`Server configuration update failed for: ${appName}.`, this.importConfig.context); }); } } diff --git a/packages/contentstack-import/src/import/modules/taxonomies.ts b/packages/contentstack-import/src/import/modules/taxonomies.ts index fc25786d81..ba94b0e0d4 100644 --- a/packages/contentstack-import/src/import/modules/taxonomies.ts +++ b/packages/contentstack-import/src/import/modules/taxonomies.ts @@ -191,7 +191,8 @@ export default class ImportTaxonomies extends BaseClass { * @method createSuccessAndFailedFile */ createSuccessAndFailedFile() { - log.debug('Creating success and failed files for taxonomies and terms', this.importConfig.context); + log.debug('Creating success and failed files for taxonomies and terms...', this.importConfig.context); + log.debug('Creating success and failed files for taxonomies and terms...', this.importConfig.context); const createdTaxCount = Object.keys(this.createdTaxonomies || {})?.length; const failedTaxCount = Object.keys(this.failedTaxonomies || {})?.length; diff --git a/packages/contentstack-import/src/types/default-config.ts b/packages/contentstack-import/src/types/default-config.ts index 05d98eb991..2b7c3bd95a 100644 --- a/packages/contentstack-import/src/types/default-config.ts +++ b/packages/contentstack-import/src/types/default-config.ts @@ -158,6 +158,12 @@ export default interface DefaultConfig { locale: string; } & AnyProperty; } & AnyProperty; + 'composable-studio': { + dirName: string; + fileName: string; + apiBaseUrl: string; + apiVersion: string; + }; }; languagesCode: string[]; apis: { @@ -198,5 +204,5 @@ export default interface DefaultConfig { globalModules: string[]; skipAssetsPublish?: boolean; skipEntriesPublish?: boolean; - entriesPublish: boolean, + entriesPublish: boolean; } diff --git a/packages/contentstack-import/src/types/index.ts b/packages/contentstack-import/src/types/index.ts index c9fe5634d6..64b63a7cc4 100644 --- a/packages/contentstack-import/src/types/index.ts +++ b/packages/contentstack-import/src/types/index.ts @@ -50,7 +50,8 @@ export type Modules = | 'marketplace-apps' | 'taxonomies' | 'personalize' - | 'variant-entries'; + | 'variant-entries' + | 'composable-studio'; export type ModuleClassParams = { stackAPIClient: ReturnType; @@ -105,6 +106,47 @@ export interface TaxonomiesConfig { dependencies?: Modules[]; } +export interface ComposableStudioConfig { + dirName: string; + fileName: string; + apiBaseUrl: string; + apiVersion: string; +} + +export interface ComposableStudioProject { + name: string; + description: string; + canvasUrl: string; + connectedStackApiKey: string; + contentTypeUid: string; + organizationUid: string; + settings: { + configuration: { + environment: string; + locale: string; + }; + }; + // Response fields (read-only) + uid?: string; + createdBy?: string; + updatedBy?: string; + deletedAt?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Context { + command: string; + module: string; + userId: string | undefined; + email?: string | undefined; + sessionId: string | undefined; + clientId?: string | undefined; + apiKey: string; + orgId: string; + authenticationMethod?: string; +} + export { default as DefaultConfig } from './default-config'; export { default as ImportConfig } from './import-config'; @@ -127,4 +169,4 @@ export interface Context { apiKey: string; orgId: string; authenticationMethod?: string; -} \ No newline at end of file +} diff --git a/packages/contentstack-import/src/utils/content-type-helper.ts b/packages/contentstack-import/src/utils/content-type-helper.ts index b1fa5fd194..942ddc8c59 100644 --- a/packages/contentstack-import/src/utils/content-type-helper.ts +++ b/packages/contentstack-import/src/utils/content-type-helper.ts @@ -132,7 +132,7 @@ export const removeReferenceFields = async function ( } catch (error) { // Else warn and modify the schema object. isContentTypeError = true; - log.warn(`Content-type ${schema[i].reference_to[j]} does not exist. Removing the field from schema`); + log.warn(`Content type ${schema[i].reference_to[j]} does not exist. Removing the field from schema...`); } } @@ -209,7 +209,7 @@ export const updateFieldRules = function (contentType: any) { fieldDataTypeMap[field.uid] = field.data_type; } - log.debug(`Created field data type mapping for ${Object.keys(fieldDataTypeMap).length} fields`); + log.debug(`Created field data type mapping for ${Object.keys(fieldDataTypeMap).length} fields.`); const fieldRules = [...contentType.field_rules]; let len = fieldRules.length; diff --git a/packages/contentstack-import/src/utils/file-helper.ts b/packages/contentstack-import/src/utils/file-helper.ts index e5c613b90d..225f5c49b5 100644 --- a/packages/contentstack-import/src/utils/file-helper.ts +++ b/packages/contentstack-import/src/utils/file-helper.ts @@ -53,7 +53,7 @@ export const readLargeFile = function (filePath: string, opts?: any): Promise => { ); const pattern = /[*$%#<>{}!&?]/g; if (pattern.test(config.contentDir)) { - cliux.print(`\nPlease add a directory path without any of the special characters: (*,&,{,},[,],$,%,<,>,?,!)`, { + cliux.print(`\nPlease enter a directory path without any special characters: (*,&,{,},[,],$,%,<,>,?,!)`, { color: 'yellow', }); config.contentDir = sanitizePath(await askContentDir()); @@ -140,7 +140,7 @@ const setupConfig = async (importCmdFlags: any): Promise => { // Add authentication details to config for context tracking config.authenticationMethod = authenticationMethod; - log.debug('Import configuration setup completed', { ...config }); + log.debug('Import configuration setup completed.', { ...config }); return config; }; diff --git a/packages/contentstack-import/src/utils/login-handler.ts b/packages/contentstack-import/src/utils/login-handler.ts index c8cfa499c6..859a4bf594 100644 --- a/packages/contentstack-import/src/utils/login-handler.ts +++ b/packages/contentstack-import/src/utils/login-handler.ts @@ -49,7 +49,7 @@ const login = async (config: ImportConfig): Promise => { if (errorstack_key) { const keyError = errorstack_key[0]; - log.error(`Invalid stack API token: ${keyError} Please enter valid stack API token.`); + log.error(`Invalid stack API token: ${keyError}. Please enter a valid stack API token.`); throw error; } diff --git a/packages/contentstack-import/src/utils/taxonomies-helper.ts b/packages/contentstack-import/src/utils/taxonomies-helper.ts index f102cecf5d..e94aaf7154 100644 --- a/packages/contentstack-import/src/utils/taxonomies-helper.ts +++ b/packages/contentstack-import/src/utils/taxonomies-helper.ts @@ -12,7 +12,7 @@ import { ImportConfig } from '../types'; * @param {ImportConfig} importConfig */ export const lookUpTaxonomy = function (importConfig: ImportConfig, schema: any, taxonomies: Record) { - log.debug(`Starting taxonomy lookup for schema with ${Object.keys(schema).length} fields`); + log.debug(`Starting taxonomy lookup for schema with ${Object.keys(schema).length} fields.`); for (let i in schema) { if (schema[i].data_type === 'taxonomy') { diff --git a/packages/contentstack-import/test/unit/import/modules/locales.test.ts b/packages/contentstack-import/test/unit/import/modules/locales.test.ts index 143dbe297e..fc3631e810 100644 --- a/packages/contentstack-import/test/unit/import/modules/locales.test.ts +++ b/packages/contentstack-import/test/unit/import/modules/locales.test.ts @@ -16,10 +16,10 @@ describe('ImportLocales', () => { beforeEach(() => { sandbox = sinon.createSandbox(); tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locales-test-')); - + // Create necessary directories fs.mkdirSync(path.join(tempDir, 'mapper', 'languages'), { recursive: true }); - + // Create mock config mockConfig = { data: tempDir, @@ -33,51 +33,87 @@ describe('ImportLocales', () => { locales: { dirName: 'locales', fileName: 'locales.json', - requiredKeys: ['uid', 'code', 'name'] + requiredKeys: ['uid', 'code', 'name'], }, masterLocale: { dirName: 'locales', fileName: 'master_locale.json', - requiredKeys: ['uid', 'code', 'name'] + requiredKeys: ['uid', 'code', 'name'], + }, + customRoles: { + dirName: 'custom_roles', + fileName: 'custom_roles.json', + customRolesLocalesFileName: 'custom_roles_locales.json', }, - customRoles: { dirName: 'custom_roles', fileName: 'custom_roles.json', customRolesLocalesFileName: 'custom_roles_locales.json' }, environments: { dirName: 'environments', fileName: 'environments.json' }, labels: { dirName: 'labels', fileName: 'labels.json' }, extensions: { dirName: 'extensions', fileName: 'extensions.json', validKeys: ['uid', 'title'] }, webhooks: { dirName: 'webhooks', fileName: 'webhooks.json' }, releases: { dirName: 'releases', fileName: 'releases.json', invalidKeys: ['uid'] }, workflows: { dirName: 'workflows', fileName: 'workflows.json', invalidKeys: ['uid'] }, - assets: { - dirName: 'assets', - assetBatchLimit: 10, - fileName: 'assets.json', - importSameStructure: false, - uploadAssetsConcurrency: 1, - displayExecutionTime: false, - importFoldersConcurrency: 1, - includeVersionedAssets: false, - host: 'https://api.contentstack.io', - folderValidKeys: ['uid', 'name'], - validKeys: ['uid', 'title'] + assets: { + dirName: 'assets', + assetBatchLimit: 10, + fileName: 'assets.json', + importSameStructure: false, + uploadAssetsConcurrency: 1, + displayExecutionTime: false, + importFoldersConcurrency: 1, + includeVersionedAssets: false, + host: 'https://api.contentstack.io', + folderValidKeys: ['uid', 'name'], + validKeys: ['uid', 'title'], + }, + 'assets-old': { + dirName: 'assets', + fileName: 'assets.json', + limit: 100, + host: 'https://api.contentstack.io', + validKeys: ['uid', 'title'], + assetBatchLimit: 10, + uploadAssetsConcurrency: 1, + importFoldersConcurrency: 1, + }, + content_types: { + dirName: 'content_types', + fileName: 'content_types.json', + validKeys: ['uid', 'title'], + limit: 100, + }, + 'content-types': { + dirName: 'content_types', + fileName: 'content_types.json', + validKeys: ['uid', 'title'], + limit: 100, + }, + entries: { + dirName: 'entries', + fileName: 'entries.json', + invalidKeys: ['uid'], + limit: 100, + assetBatchLimit: 10, }, - 'assets-old': { - dirName: 'assets', - fileName: 'assets.json', - limit: 100, - host: 'https://api.contentstack.io', - validKeys: ['uid', 'title'], - assetBatchLimit: 10, - uploadAssetsConcurrency: 1, - importFoldersConcurrency: 1 + globalfields: { + dirName: 'globalfields', + fileName: 'globalfields.json', + validKeys: ['uid', 'title'], + limit: 100, + }, + 'global-fields': { + dirName: 'globalfields', + fileName: 'globalfields.json', + validKeys: ['uid', 'title'], + limit: 100, }, - content_types: { dirName: 'content_types', fileName: 'content_types.json', validKeys: ['uid', 'title'], limit: 100 }, - 'content-types': { dirName: 'content_types', fileName: 'content_types.json', validKeys: ['uid', 'title'], limit: 100 }, - entries: { dirName: 'entries', fileName: 'entries.json', invalidKeys: ['uid'], limit: 100, assetBatchLimit: 10 }, - globalfields: { dirName: 'globalfields', fileName: 'globalfields.json', validKeys: ['uid', 'title'], limit: 100 }, - 'global-fields': { dirName: 'globalfields', fileName: 'globalfields.json', validKeys: ['uid', 'title'], limit: 100 }, stack: { dirName: 'stack', fileName: 'stack.json' }, marketplace_apps: { dirName: 'marketplace_apps', fileName: 'marketplace_apps.json' }, taxonomies: { dirName: 'taxonomies', fileName: 'taxonomies.json' }, + 'composable-studio': { + dirName: 'composable-studio', + fileName: 'composable-studio.json', + apiBaseUrl: 'https://composable-studio-api.contentstack.com/v1', + apiVersion: 'v1', + }, personalize: { baseURL: {}, dirName: 'personalize', @@ -87,9 +123,19 @@ describe('ImportLocales', () => { attributes: { dirName: 'attributes', fileName: 'attributes.json' }, audiences: { dirName: 'audiences', fileName: 'audiences.json' }, events: { dirName: 'events', fileName: 'events.json' }, - experiences: { dirName: 'experiences', fileName: 'experiences.json', thresholdTimer: 1000, checkIntervalDuration: 100 } + experiences: { + dirName: 'experiences', + fileName: 'experiences.json', + thresholdTimer: 1000, + checkIntervalDuration: 100, + }, + }, + variantEntry: { + dirName: 'variant_entries', + fileName: 'variant_entries.json', + apiConcurrency: 1, + query: { locale: 'en-us' }, }, - variantEntry: { dirName: 'variant_entries', fileName: 'variant_entries.json', apiConcurrency: 1, query: { locale: 'en-us' } } }, branches: [{ uid: 'main', source: 'main' }], isAuthenticated: true, @@ -111,7 +157,7 @@ describe('ImportLocales', () => { globalfields: '/v3/globalfields', folders: '/v3/folders', stacks: '/v3/stacks', - labels: '/v3/labels' + labels: '/v3/labels', }, rateLimit: 5, preserveStackVersion: false, @@ -135,29 +181,29 @@ describe('ImportLocales', () => { contentVersion: 1, region: 'us' as any, 'exclude-global-modules': false, - context: { + context: { module: 'locales', command: 'import', userId: 'test-user', email: 'test@example.com', sessionId: 'test-session', - stack: 'test-stack' - } as any + stack: 'test-stack', + } as any, }; // Create mock stack API client mockStackAPIClient = { locale: sandbox.stub().returns({ fetch: sandbox.stub(), - update: sandbox.stub() - }) + update: sandbox.stub(), + }), }; // Create module class params const moduleParams: ModuleClassParams = { importConfig: mockConfig, stackAPIClient: mockStackAPIClient, - moduleName: 'locales' as any + moduleName: 'locales' as any, }; // Create instance @@ -234,14 +280,17 @@ describe('ImportLocales', () => { it('should process languages successfully', async () => { const mockLanguages = [ { uid: 'lang1', code: 'en-us', name: 'English' }, - { uid: 'lang2', code: 'es-es', name: 'Spanish' } + { uid: 'lang2', code: 'es-es', name: 'Spanish' }, ]; const mockMasterLanguage = { uid: 'master', code: 'en-us', name: 'English' }; fsUtilStub - .onFirstCall().returns(mockLanguages) - .onSecondCall().returns(mockMasterLanguage) - .onThirdCall().returns({}); + .onFirstCall() + .returns(mockLanguages) + .onSecondCall() + .returns(mockMasterLanguage) + .onThirdCall() + .returns({}); fileHelperStub.resolves(); makeConcurrentCallStub.resolves(); @@ -253,14 +302,10 @@ describe('ImportLocales', () => { }); it('should handle case when UID mapper file does not exist', async () => { - const mockLanguages = [ - { uid: 'lang1', code: 'en-us', name: 'English' } - ]; + const mockLanguages = [{ uid: 'lang1', code: 'en-us', name: 'English' }]; const mockMasterLanguage = { uid: 'master', code: 'en-us', name: 'English' }; - fsUtilStub - .onFirstCall().returns(mockLanguages) - .onSecondCall().returns(mockMasterLanguage); + fsUtilStub.onFirstCall().returns(mockLanguages).onSecondCall().returns(mockMasterLanguage); fileHelperStub.resolves(); makeConcurrentCallStub.resolves(); @@ -277,15 +322,16 @@ describe('ImportLocales', () => { }); it('should handle case when UID mapper file exists but returns null', async () => { - const mockLanguages = [ - { uid: 'lang1', code: 'en-us', name: 'English' } - ]; + const mockLanguages = [{ uid: 'lang1', code: 'en-us', name: 'English' }]; const mockMasterLanguage = { uid: 'master', code: 'en-us', name: 'English' }; fsUtilStub - .onFirstCall().returns(mockLanguages) - .onSecondCall().returns(mockMasterLanguage) - .onThirdCall().returns(null); // UID mapper file returns null + .onFirstCall() + .returns(mockLanguages) + .onSecondCall() + .returns(mockMasterLanguage) + .onThirdCall() + .returns(null); // UID mapper file returns null fileHelperStub.resolves(); makeConcurrentCallStub.resolves(); @@ -303,15 +349,14 @@ describe('ImportLocales', () => { it('should handle errors in checkAndUpdateMasterLocale', async () => { const mockLanguages = [{ uid: 'lang1', code: 'en-us', name: 'English' }]; - fsUtilStub - .onFirstCall().returns(mockLanguages) - .onSecondCall().returns({}) - .onThirdCall().returns({}); + fsUtilStub.onFirstCall().returns(mockLanguages).onSecondCall().returns({}).onThirdCall().returns({}); fileHelperStub.resolves(); makeConcurrentCallStub.resolves(); // Mock checkAndUpdateMasterLocale to throw error - const checkAndUpdateMasterLocaleStub = sandbox.stub(localesInstance, 'checkAndUpdateMasterLocale').rejects(new Error('Test error')); + const checkAndUpdateMasterLocaleStub = sandbox + .stub(localesInstance, 'checkAndUpdateMasterLocale') + .rejects(new Error('Test error')); await localesInstance.start(); @@ -321,10 +366,7 @@ describe('ImportLocales', () => { it('should handle errors in createLocales', async () => { const mockLanguages = [{ uid: 'lang1', code: 'en-us', name: 'English' }]; - fsUtilStub - .onFirstCall().returns(mockLanguages) - .onSecondCall().returns({}) - .onThirdCall().returns({}); + fsUtilStub.onFirstCall().returns(mockLanguages).onSecondCall().returns({}).onThirdCall().returns({}); fileHelperStub.resolves(); makeConcurrentCallStub.rejects(new Error('Create locales error')); @@ -335,14 +377,9 @@ describe('ImportLocales', () => { it('should handle errors in updateLocales', async () => { const mockLanguages = [{ uid: 'lang1', code: 'en-us', name: 'English' }]; - fsUtilStub - .onFirstCall().returns(mockLanguages) - .onSecondCall().returns({}) - .onThirdCall().returns({}); + fsUtilStub.onFirstCall().returns(mockLanguages).onSecondCall().returns({}).onThirdCall().returns({}); fileHelperStub.resolves(); - makeConcurrentCallStub - .onFirstCall().resolves() - .onSecondCall().rejects(new Error('Update locales error')); + makeConcurrentCallStub.onFirstCall().resolves().onSecondCall().rejects(new Error('Update locales error')); await localesInstance.start(); @@ -379,7 +416,7 @@ describe('ImportLocales', () => { it('should handle master language code mismatch', async () => { localesInstance['sourceMasterLanguage'] = { - 'lang1': { uid: 'lang1', code: 'es-es', name: 'Spanish' } + lang1: { uid: 'lang1', code: 'es-es', name: 'Spanish' }, }; localesInstance['masterLanguage'] = { code: 'en-us' }; @@ -391,13 +428,13 @@ describe('ImportLocales', () => { it('should handle master language code match with same names', async () => { const mockMasterLang = { uid: 'master', code: 'en-us', name: 'English' }; localesInstance['sourceMasterLanguage'] = { - 'master': mockMasterLang + master: mockMasterLang, }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().resolves({ name: 'English' }), - update: sandbox.stub().resolves() + update: sandbox.stub().resolves(), }; mockStackAPIClient.locale.returns(mockLocaleClient); @@ -411,18 +448,20 @@ describe('ImportLocales', () => { it('should handle master language code match with different names - user confirms update', async () => { const mockMasterLang = { uid: 'master', code: 'en-us', name: 'English Updated' }; localesInstance['sourceMasterLanguage'] = { - 'master': mockMasterLang + master: mockMasterLang, }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().resolves({ name: 'English' }), - update: sandbox.stub().resolves() + update: sandbox.stub().resolves(), }; mockStackAPIClient.locale.returns(mockLocaleClient); // Mock cliux.inquire to return true (user confirms) - const inquireStub = sandbox.stub(require('@contentstack/cli-utilities').cliux, 'inquire').resolves({ confirmation: true }); + const inquireStub = sandbox + .stub(require('@contentstack/cli-utilities').cliux, 'inquire') + .resolves({ confirmation: true }); await localesInstance.checkAndUpdateMasterLocale(); @@ -436,18 +475,20 @@ describe('ImportLocales', () => { it('should handle master language code match with different names - user declines update', async () => { const mockMasterLang = { uid: 'master', code: 'en-us', name: 'English Updated' }; localesInstance['sourceMasterLanguage'] = { - 'master': mockMasterLang + master: mockMasterLang, }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().resolves({ name: 'English' }), - update: sandbox.stub().resolves() + update: sandbox.stub().resolves(), }; mockStackAPIClient.locale.returns(mockLocaleClient); // Mock cliux.inquire to return false (user declines) - const inquireStub = sandbox.stub(require('@contentstack/cli-utilities').cliux, 'inquire').resolves({ confirmation: false }); + const inquireStub = sandbox + .stub(require('@contentstack/cli-utilities').cliux, 'inquire') + .resolves({ confirmation: false }); await localesInstance.checkAndUpdateMasterLocale(); @@ -462,18 +503,20 @@ describe('ImportLocales', () => { it('should handle master language code match with different names - user declines update (proper flow)', async () => { const mockMasterLang = { uid: 'master', code: 'en-us', name: 'English Updated' }; localesInstance['sourceMasterLanguage'] = { - 'master': mockMasterLang + master: mockMasterLang, }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().resolves({ name: 'English' }), - update: sandbox.stub().resolves() + update: sandbox.stub().resolves(), }; mockStackAPIClient.locale.returns(mockLocaleClient); // Mock cliux.inquire to return false (user declines) - const inquireStub = sandbox.stub(require('@contentstack/cli-utilities').cliux, 'inquire').resolves({ confirmation: false }); + const inquireStub = sandbox + .stub(require('@contentstack/cli-utilities').cliux, 'inquire') + .resolves({ confirmation: false }); await localesInstance.checkAndUpdateMasterLocale(); @@ -484,22 +527,23 @@ describe('ImportLocales', () => { // Verify line 172 is covered - user declined update }); - it('should handle user declining update with proper error handling', async () => { const mockMasterLang = { uid: 'master', code: 'en-us', name: 'English Updated' }; localesInstance['sourceMasterLanguage'] = { - 'master': mockMasterLang + master: mockMasterLang, }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().resolves({ name: 'English' }), - update: sandbox.stub().resolves() + update: sandbox.stub().resolves(), }; mockStackAPIClient.locale.returns(mockLocaleClient); // Mock cliux.inquire to return false (user declines) - const inquireStub = sandbox.stub(require('@contentstack/cli-utilities').cliux, 'inquire').resolves({ confirmation: false }); + const inquireStub = sandbox + .stub(require('@contentstack/cli-utilities').cliux, 'inquire') + .resolves({ confirmation: false }); // Mock handleAndLogError to prevent any errors const handleAndLogErrorStub = sandbox.stub(require('@contentstack/cli-utilities'), 'handleAndLogError'); @@ -521,18 +565,20 @@ describe('ImportLocales', () => { it('should handle master language not found in source', async () => { const mockMasterLang = { uid: 'master', code: 'en-us', name: 'English Updated' }; localesInstance['sourceMasterLanguage'] = { - 'master': mockMasterLang // Use 'master' key to match the uid + master: mockMasterLang, // Use 'master' key to match the uid }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().resolves({ name: 'English' }), - update: sandbox.stub().resolves() + update: sandbox.stub().resolves(), }; mockStackAPIClient.locale.returns(mockLocaleClient); // Mock cliux.inquire to return true (user confirms) - const inquireStub = sandbox.stub(require('@contentstack/cli-utilities').cliux, 'inquire').resolves({ confirmation: true }); + const inquireStub = sandbox + .stub(require('@contentstack/cli-utilities').cliux, 'inquire') + .resolves({ confirmation: true }); await localesInstance.checkAndUpdateMasterLocale(); @@ -544,23 +590,23 @@ describe('ImportLocales', () => { expect(mockLocaleClient.update.called).to.be.true; }); - - it('should handle master language not found in source with undefined uid', async () => { // Create a scenario where sourceMasterLangDetails[0] exists but has no uid localesInstance['sourceMasterLanguage'] = { - 'some-key': { code: 'en-us', name: 'English Updated' } // No uid property + 'some-key': { code: 'en-us', name: 'English Updated' }, // No uid property }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().resolves({ name: 'English' }), - update: sandbox.stub().resolves() + update: sandbox.stub().resolves(), }; mockStackAPIClient.locale.returns(mockLocaleClient); // Mock cliux.inquire to return true (user confirms) - const inquireStub = sandbox.stub(require('@contentstack/cli-utilities').cliux, 'inquire').resolves({ confirmation: true }); + const inquireStub = sandbox + .stub(require('@contentstack/cli-utilities').cliux, 'inquire') + .resolves({ confirmation: true }); // The code will try to access sourceMasterLanguage.name when sourceMasterLanguage is undefined // So we need to handle this gracefully @@ -579,13 +625,13 @@ describe('ImportLocales', () => { it('should handle fetch error', async () => { const mockMasterLang = { uid: 'master', code: 'en-us', name: 'English Updated' }; localesInstance['sourceMasterLanguage'] = { - 'master': mockMasterLang + master: mockMasterLang, }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().rejects(new Error('Fetch error')), - update: sandbox.stub().resolves() + update: sandbox.stub().resolves(), }; mockStackAPIClient.locale.returns(mockLocaleClient); @@ -606,18 +652,20 @@ describe('ImportLocales', () => { it('should handle update error', async () => { const mockMasterLang = { uid: 'master', code: 'en-us', name: 'English Updated' }; localesInstance['sourceMasterLanguage'] = { - 'master': mockMasterLang + master: mockMasterLang, }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().resolves({ name: 'English' }), - update: sandbox.stub().rejects(new Error('Update error')) + update: sandbox.stub().rejects(new Error('Update error')), }; mockStackAPIClient.locale.returns(mockLocaleClient); // Mock cliux.inquire to return true (user confirms) - const inquireStub = sandbox.stub(require('@contentstack/cli-utilities').cliux, 'inquire').resolves({ confirmation: true }); + const inquireStub = sandbox + .stub(require('@contentstack/cli-utilities').cliux, 'inquire') + .resolves({ confirmation: true }); // The code will try to access this.config.context in the error handler // So we need to handle this gracefully @@ -637,18 +685,20 @@ describe('ImportLocales', () => { it('should handle update error with proper error handling', async () => { const mockMasterLang = { uid: 'master', code: 'en-us', name: 'English Updated' }; localesInstance['sourceMasterLanguage'] = { - 'master': mockMasterLang + master: mockMasterLang, }; localesInstance['masterLanguage'] = { code: 'en-us' }; const mockLocaleClient = { fetch: sandbox.stub().resolves({ name: 'English' }), - update: sandbox.stub().rejects(new Error('Update error')) + update: sandbox.stub().rejects(new Error('Update error')), }; mockStackAPIClient.locale.returns(mockLocaleClient); // Mock cliux.inquire to return true (user confirms) - const inquireStub = sandbox.stub(require('@contentstack/cli-utilities').cliux, 'inquire').resolves({ confirmation: true }); + const inquireStub = sandbox + .stub(require('@contentstack/cli-utilities').cliux, 'inquire') + .resolves({ confirmation: true }); // Mock handleAndLogError to prevent the error from being thrown const handleAndLogErrorStub = sandbox.stub(require('@contentstack/cli-utilities'), 'handleAndLogError'); @@ -668,16 +718,19 @@ describe('ImportLocales', () => { expect(mockLocaleClient.update.called).to.be.true; }); - it('should handle writeConcurrency fallback (line 52)', () => { // Test the branch: this.localeConfig.writeConcurrency || this.config.writeConcurrency const tempConfig = JSON.parse(JSON.stringify(mockConfig)); tempConfig.modules.locales = { ...tempConfig.modules.locales, writeConcurrency: undefined }; tempConfig.writeConcurrency = 5; - - const moduleClassParams = { importConfig: tempConfig, stackAPIClient: mockStackAPIClient, moduleName: 'locales' as any }; + + const moduleClassParams = { + importConfig: tempConfig, + stackAPIClient: mockStackAPIClient, + moduleName: 'locales' as any, + }; const testInstance = new ImportLocales(moduleClassParams); - + expect(testInstance['reqConcurrency']).to.equal(5); }); @@ -685,13 +738,16 @@ describe('ImportLocales', () => { const tempConfig = JSON.parse(JSON.stringify(mockConfig)); tempConfig.modules.locales = { ...tempConfig.modules.locales, writeConcurrency: 10 }; tempConfig.writeConcurrency = 5; - - const moduleClassParams = { importConfig: tempConfig, stackAPIClient: mockStackAPIClient, moduleName: 'locales' as any }; + + const moduleClassParams = { + importConfig: tempConfig, + stackAPIClient: mockStackAPIClient, + moduleName: 'locales' as any, + }; const testInstance = new ImportLocales(moduleClassParams); - + expect(testInstance['reqConcurrency']).to.equal(10); }); - }); describe('createLocales', () => { @@ -707,7 +763,7 @@ describe('ImportLocales', () => { const mockLanguages = [ { uid: 'lang1', code: 'en-us', name: 'English' }, { uid: 'lang2', code: 'es-es', name: 'Spanish' }, - { uid: 'lang3', code: 'fr-fr', name: 'French' } + { uid: 'lang3', code: 'fr-fr', name: 'French' }, ]; localesInstance['languages'] = mockLanguages; localesInstance['masterLanguage'] = { code: 'en-us' }; @@ -738,9 +794,7 @@ describe('ImportLocales', () => { }); it('should handle onSuccess callback', async () => { - const mockLanguages = [ - { uid: 'lang1', code: 'es-es', name: 'Spanish' } - ]; + const mockLanguages = [{ uid: 'lang1', code: 'es-es', name: 'Spanish' }]; localesInstance['languages'] = mockLanguages; localesInstance['masterLanguage'] = { code: 'en-us' }; @@ -759,9 +813,7 @@ describe('ImportLocales', () => { }); it('should handle onReject callback with error code 247', async () => { - const mockLanguages = [ - { uid: 'lang1', code: 'es-es', name: 'Spanish' } - ]; + const mockLanguages = [{ uid: 'lang1', code: 'es-es', name: 'Spanish' }]; localesInstance['languages'] = mockLanguages; localesInstance['masterLanguage'] = { code: 'en-us' }; @@ -779,9 +831,7 @@ describe('ImportLocales', () => { }); it('should handle onReject callback with other error', async () => { - const mockLanguages = [ - { uid: 'lang1', code: 'es-es', name: 'Spanish' } - ]; + const mockLanguages = [{ uid: 'lang1', code: 'es-es', name: 'Spanish' }]; localesInstance['languages'] = mockLanguages; localesInstance['masterLanguage'] = { code: 'en-us' }; @@ -811,7 +861,7 @@ describe('ImportLocales', () => { it('should update all locales', async () => { const mockLanguages = [ { uid: 'lang1', code: 'en-us', name: 'English' }, - { uid: 'lang2', code: 'es-es', name: 'Spanish' } + { uid: 'lang2', code: 'es-es', name: 'Spanish' }, ]; localesInstance['languages'] = mockLanguages; @@ -826,9 +876,7 @@ describe('ImportLocales', () => { }); it('should handle onSuccess callback', async () => { - const mockLanguages = [ - { uid: 'lang1', code: 'en-us', name: 'English' } - ]; + const mockLanguages = [{ uid: 'lang1', code: 'en-us', name: 'English' }]; localesInstance['languages'] = mockLanguages; makeConcurrentCallStub.callsFake(async (args: any) => { @@ -844,9 +892,7 @@ describe('ImportLocales', () => { }); it('should handle onReject callback', async () => { - const mockLanguages = [ - { uid: 'lang1', code: 'en-us', name: 'English' } - ]; + const mockLanguages = [{ uid: 'lang1', code: 'en-us', name: 'English' }]; localesInstance['languages'] = mockLanguages; makeConcurrentCallStub.callsFake(async (args: any) => { @@ -865,9 +911,7 @@ describe('ImportLocales', () => { describe('Edge Cases', () => { it('should handle undefined apiData in callbacks', async () => { const makeConcurrentCallStub = sandbox.stub(localesInstance, 'makeConcurrentCall'); - const mockLanguages = [ - { uid: 'lang1', code: 'es-es', name: 'Spanish' } - ]; + const mockLanguages = [{ uid: 'lang1', code: 'es-es', name: 'Spanish' }]; localesInstance['languages'] = mockLanguages; localesInstance['masterLanguage'] = { code: 'en-us' }; @@ -881,9 +925,7 @@ describe('ImportLocales', () => { it('should handle undefined response in callbacks', async () => { const makeConcurrentCallStub = sandbox.stub(localesInstance, 'makeConcurrentCall'); - const mockLanguages = [ - { uid: 'lang1', code: 'es-es', name: 'Spanish' } - ]; + const mockLanguages = [{ uid: 'lang1', code: 'es-es', name: 'Spanish' }]; localesInstance['languages'] = mockLanguages; localesInstance['masterLanguage'] = { code: 'en-us' }; @@ -894,5 +936,4 @@ describe('ImportLocales', () => { expect(makeConcurrentCallStub.calledOnce).to.be.true; }); }); - }); diff --git a/packages/contentstack-import/test/unit/utils/extension-helper.test.ts b/packages/contentstack-import/test/unit/utils/extension-helper.test.ts index 7871775f3e..c9b5672182 100644 --- a/packages/contentstack-import/test/unit/utils/extension-helper.test.ts +++ b/packages/contentstack-import/test/unit/utils/extension-helper.test.ts @@ -34,44 +34,69 @@ describe('Extension Helper', () => { contentDir: '/test/content', data: '/test/content', modules: { + 'composable-studio': { + dirName: 'composable_studio', + fileName: 'composable_studio.json', + apiBaseUrl: 'https://composable-studio-api.contentstack.com', + apiVersion: 'v1', + }, apiConcurrency: 1, types: [], locales: { dirName: 'locales', fileName: 'locales.json', requiredKeys: ['code', 'name'] }, - customRoles: { dirName: 'custom_roles', fileName: 'custom_roles.json', customRolesLocalesFileName: 'custom_roles_locales.json' }, + customRoles: { + dirName: 'custom_roles', + fileName: 'custom_roles.json', + customRolesLocalesFileName: 'custom_roles_locales.json', + }, environments: { dirName: 'environments', fileName: 'environments.json' }, labels: { dirName: 'labels', fileName: 'labels.json' }, extensions: { dirName: 'extensions', fileName: 'extensions.json', validKeys: ['uid', 'title'] }, webhooks: { dirName: 'webhooks', fileName: 'webhooks.json' }, releases: { dirName: 'releases', fileName: 'releases.json', invalidKeys: ['uid'] }, workflows: { dirName: 'workflows', fileName: 'workflows.json', invalidKeys: ['uid'] }, - assets: { - dirName: 'assets', - assetBatchLimit: 10, - fileName: 'assets.json', - importSameStructure: false, - uploadAssetsConcurrency: 1, - displayExecutionTime: false, - importFoldersConcurrency: 1, - includeVersionedAssets: false, - host: 'https://api.contentstack.io', - folderValidKeys: ['uid', 'name'], - validKeys: ['uid', 'title'] + assets: { + dirName: 'assets', + assetBatchLimit: 10, + fileName: 'assets.json', + importSameStructure: false, + uploadAssetsConcurrency: 1, + displayExecutionTime: false, + importFoldersConcurrency: 1, + includeVersionedAssets: false, + host: 'https://api.contentstack.io', + folderValidKeys: ['uid', 'name'], + validKeys: ['uid', 'title'], + }, + 'assets-old': { + dirName: 'assets', + fileName: 'assets.json', + limit: 100, + host: 'https://api.contentstack.io', + validKeys: ['uid', 'title'], + assetBatchLimit: 10, + uploadAssetsConcurrency: 1, + importFoldersConcurrency: 1, }, - 'assets-old': { - dirName: 'assets', - fileName: 'assets.json', - limit: 100, - host: 'https://api.contentstack.io', - validKeys: ['uid', 'title'], - assetBatchLimit: 10, - uploadAssetsConcurrency: 1, - importFoldersConcurrency: 1 + content_types: { + dirName: 'content_types', + fileName: 'content_types.json', + validKeys: ['uid', 'title'], + limit: 100, + }, + 'content-types': { + dirName: 'content_types', + fileName: 'content_types.json', + validKeys: ['uid', 'title'], + limit: 100, }, - content_types: { dirName: 'content_types', fileName: 'content_types.json', validKeys: ['uid', 'title'], limit: 100 }, - 'content-types': { dirName: 'content_types', fileName: 'content_types.json', validKeys: ['uid', 'title'], limit: 100 }, entries: { dirName: 'entries', fileName: 'entries.json', invalidKeys: ['uid'], limit: 100, assetBatchLimit: 10 }, globalfields: { dirName: 'globalfields', fileName: 'globalfields.json', validKeys: ['uid', 'title'], limit: 100 }, - 'global-fields': { dirName: 'globalfields', fileName: 'globalfields.json', validKeys: ['uid', 'title'], limit: 100 }, + 'global-fields': { + dirName: 'globalfields', + fileName: 'globalfields.json', + validKeys: ['uid', 'title'], + limit: 100, + }, stack: { dirName: 'stack', fileName: 'stack.json' }, marketplace_apps: { dirName: 'marketplace_apps', fileName: 'marketplace_apps.json' }, masterLocale: { dirName: 'master_locale', fileName: 'master_locale.json', requiredKeys: ['code', 'name'] }, @@ -85,9 +110,19 @@ describe('Extension Helper', () => { attributes: { dirName: 'attributes', fileName: 'attributes.json' }, audiences: { dirName: 'audiences', fileName: 'audiences.json' }, events: { dirName: 'events', fileName: 'events.json' }, - experiences: { dirName: 'experiences', fileName: 'experiences.json', thresholdTimer: 1000, checkIntervalDuration: 100 } + experiences: { + dirName: 'experiences', + fileName: 'experiences.json', + thresholdTimer: 1000, + checkIntervalDuration: 100, + }, + }, + variantEntry: { + dirName: 'variant_entries', + fileName: 'variant_entries.json', + apiConcurrency: 1, + query: { locale: 'en-us' }, }, - variantEntry: { dirName: 'variant_entries', fileName: 'variant_entries.json', apiConcurrency: 1, query: { locale: 'en-us' } } }, branches: [{ uid: 'main', source: 'main' }], isAuthenticated: true, @@ -110,7 +145,7 @@ describe('Extension Helper', () => { globalfields: '/v3/globalfields', folders: '/v3/folders', stacks: '/v3/stacks', - labels: '/v3/labels' + labels: '/v3/labels', }, rateLimit: 5, preserveStackVersion: false, @@ -134,7 +169,7 @@ describe('Extension Helper', () => { contentVersion: 1, region: 'us' as any, 'exclude-global-modules': false, - context: {} as any + context: {} as any, }); describe('lookupExtension', () => { @@ -152,10 +187,10 @@ describe('Extension Helper', () => { { uid: 'nested-field', data_type: 'text', - extension_uid: 'ext-123' - } - ] - } + extension_uid: 'ext-123', + }, + ], + }, ]; const preserveStackVersion = false; const installedExtensions = {}; @@ -178,12 +213,12 @@ describe('Extension Helper', () => { schema: [ { uid: 'block-field', - data_type: 'text' - } - ] - } - ] - } + data_type: 'text', + }, + ], + }, + ], + }, ]; const preserveStackVersion = false; const installedExtensions = {}; @@ -207,12 +242,12 @@ describe('Extension Helper', () => { { uid: 'block-field', data_type: 'text', - extension_uid: 'ext-123' - } - ] - } - ] - } + extension_uid: 'ext-123', + }, + ], + }, + ], + }, ]; const preserveStackVersion = false; const installedExtensions = {}; @@ -229,8 +264,8 @@ describe('Extension Helper', () => { uid: 'ref-field', data_type: 'reference', reference_to: 'content-type-1', - field_metadata: {} as any - } + field_metadata: {} as any, + }, ]; const preserveStackVersion = false; const installedExtensions = {}; @@ -248,8 +283,8 @@ describe('Extension Helper', () => { uid: 'ref-field', data_type: 'reference', reference_to: 'content-type-1', - field_metadata: {} as any - } + field_metadata: {} as any, + }, ]; const preserveStackVersion = true; const installedExtensions = {}; @@ -268,13 +303,13 @@ describe('Extension Helper', () => { data_type: 'reference', extension_uid: 'old-ext-123', field_metadata: { - ref_multiple_content_types: true - } - } + ref_multiple_content_types: true, + }, + }, ]; const preserveStackVersion = false; const installedExtensions = { - 'old-ext-123': 'new-ext-456' + 'old-ext-123': 'new-ext-456', }; lookupExtension(config, schema, preserveStackVersion, installedExtensions); @@ -288,12 +323,12 @@ describe('Extension Helper', () => { { uid: 'text-field', data_type: 'text', - extension_uid: 'old-ext-123' - } + extension_uid: 'old-ext-123', + }, ]; const preserveStackVersion = false; const installedExtensions = { - 'old-ext-123': 'new-ext-456' + 'old-ext-123': 'new-ext-456', }; lookupExtension(config, schema, preserveStackVersion, installedExtensions); @@ -307,14 +342,14 @@ describe('Extension Helper', () => { { uid: 'global-field', data_type: 'global_field', - reference_to: 'global-field-123' - } + reference_to: 'global-field-123', + }, ]; const preserveStackVersion = false; const installedExtensions = {}; const globalFieldsMapping = { - 'global-field-123': 'mapped-global-field-456' + 'global-field-123': 'mapped-global-field-456', }; fsUtilityStub.withArgs(path.join(tempDir, 'mapper/globalfields/uid-mapping.json')).returns(globalFieldsMapping); @@ -330,8 +365,8 @@ describe('Extension Helper', () => { { uid: 'global-field', data_type: 'global_field', - reference_to: 'global-field-123' - } + reference_to: 'global-field-123', + }, ]; const preserveStackVersion = false; const installedExtensions = {}; @@ -349,14 +384,14 @@ describe('Extension Helper', () => { { uid: 'ext-field', data_type: 'text', - extension_uid: 'old-ext-123' - } + extension_uid: 'old-ext-123', + }, ]; const preserveStackVersion = false; const installedExtensions = {}; const extensionMapping = { - 'old-ext-123': 'new-ext-456' + 'old-ext-123': 'new-ext-456', }; fsUtilityStub.withArgs(path.join(tempDir, 'mapper/extensions/uid-mapping.json')).returns(extensionMapping); @@ -374,9 +409,9 @@ describe('Extension Helper', () => { data_type: 'text', extension_uid: 'old-ext-123', field_metadata: { - extension: true - } - } + extension: true, + }, + }, ]; const preserveStackVersion = false; const installedExtensions = {}; @@ -396,13 +431,13 @@ describe('Extension Helper', () => { data_type: 'text', extension_uid: 'old-ext-123', field_metadata: { - extension: true - } - } + extension: true, + }, + }, ]; const preserveStackVersion = false; const installedExtensions = { - 'old-ext-123': 'new-ext-456' + 'old-ext-123': 'new-ext-456', }; fsUtilityStub.withArgs(path.join(tempDir, 'mapper/extensions/uid-mapping.json')).returns({}); @@ -418,24 +453,26 @@ describe('Extension Helper', () => { { uid: 'json-field', data_type: 'json', - plugins: ['plugin-1', 'plugin-2'] - } + plugins: ['plugin-1', 'plugin-2'], + }, ]; const preserveStackVersion = false; const installedExtensions = {}; const extensionMapping = { - 'plugin-1': 'mapped-plugin-1' + 'plugin-1': 'mapped-plugin-1', }; const marketplaceMapping = { extension_uid: { - 'plugin-2': 'mapped-plugin-2' - } + 'plugin-2': 'mapped-plugin-2', + }, }; fsUtilityStub.withArgs(path.join(tempDir, 'mapper/extensions/uid-mapping.json')).returns(extensionMapping); - fsUtilityStub.withArgs(path.join(tempDir, 'mapper/marketplace_apps/uid-mapping.json')).returns(marketplaceMapping); + fsUtilityStub + .withArgs(path.join(tempDir, 'mapper/marketplace_apps/uid-mapping.json')) + .returns(marketplaceMapping); lookupExtension(config, schema, preserveStackVersion, installedExtensions); @@ -448,8 +485,8 @@ describe('Extension Helper', () => { { uid: 'json-field', data_type: 'json', - plugins: ['plugin-1', 'plugin-2'] - } + plugins: ['plugin-1', 'plugin-2'], + }, ]; const preserveStackVersion = false; const installedExtensions = {}; @@ -468,8 +505,8 @@ describe('Extension Helper', () => { { uid: 'json-field', data_type: 'json', - plugins: [] as string[] - } + plugins: [] as string[], + }, ]; const preserveStackVersion = false; const installedExtensions = {}; @@ -485,27 +522,27 @@ describe('Extension Helper', () => { { uid: 'text-field', data_type: 'text', - extension_uid: 'ext-1' + extension_uid: 'ext-1', }, { uid: 'ref-field', data_type: 'reference', reference_to: 'content-type-1', - field_metadata: {} as any + field_metadata: {} as any, }, { uid: 'global-field', data_type: 'global_field', - reference_to: 'global-1' - } + reference_to: 'global-1', + }, ]; const preserveStackVersion = false; const installedExtensions = { - 'ext-1': 'new-ext-1' + 'ext-1': 'new-ext-1', }; const globalFieldsMapping = { - 'global-1': 'mapped-global-1' + 'global-1': 'mapped-global-1', }; fsUtilityStub.withArgs(path.join(tempDir, 'mapper/globalfields/uid-mapping.json')).returns(globalFieldsMapping); @@ -523,13 +560,13 @@ describe('Extension Helper', () => { { uid: 'global-field', data_type: 'global_field', - reference_to: 'global-1' + reference_to: 'global-1', }, { uid: 'ext-field', data_type: 'text', - extension_uid: 'ext-1' - } + extension_uid: 'ext-1', + }, ]; const preserveStackVersion = false; const installedExtensions = {}; @@ -561,14 +598,14 @@ describe('Extension Helper', () => { data_type: 'reference', reference_to: 'content-type-1', field_metadata: { - ref_multiple_content_types: true + ref_multiple_content_types: true, }, - extension_uid: 'ext-123' - } + extension_uid: 'ext-123', + }, ]; const preserveStackVersion = false; const installedExtensions = { - 'ext-123': 'new-ext-456' + 'ext-123': 'new-ext-456', }; lookupExtension(config, schema, preserveStackVersion, installedExtensions); @@ -584,24 +621,26 @@ describe('Extension Helper', () => { { uid: 'json-field', data_type: 'json', - plugins: ['marketplace-plugin-1', 'extension-plugin-1'] - } + plugins: ['marketplace-plugin-1', 'extension-plugin-1'], + }, ]; const preserveStackVersion = false; const installedExtensions = {}; const extensionMapping = { - 'extension-plugin-1': 'mapped-extension-plugin-1' + 'extension-plugin-1': 'mapped-extension-plugin-1', }; const marketplaceMapping = { extension_uid: { - 'marketplace-plugin-1': 'mapped-marketplace-plugin-1' - } + 'marketplace-plugin-1': 'mapped-marketplace-plugin-1', + }, }; fsUtilityStub.withArgs(path.join(tempDir, 'mapper/extensions/uid-mapping.json')).returns(extensionMapping); - fsUtilityStub.withArgs(path.join(tempDir, 'mapper/marketplace_apps/uid-mapping.json')).returns(marketplaceMapping); + fsUtilityStub + .withArgs(path.join(tempDir, 'mapper/marketplace_apps/uid-mapping.json')) + .returns(marketplaceMapping); lookupExtension(config, schema, preserveStackVersion, installedExtensions); @@ -614,14 +653,14 @@ describe('Extension Helper', () => { { uid: 'ext-field', data_type: 'text', - extension_uid: 'old-ext-123' - } + extension_uid: 'old-ext-123', + }, ]; const preserveStackVersion = false; const installedExtensions = {}; const extensionMapping = { - 'old-ext-123': 'new-ext-456' + 'old-ext-123': 'new-ext-456', }; fsUtilityStub.withArgs(path.join(tempDir, 'mapper/extensions/uid-mapping.json')).returns(extensionMapping); diff --git a/packages/contentstack-migration/examples/01-transform-contenttype.js b/packages/contentstack-migration/examples/01-transform-contenttype.js index ee95904248..abf6d8e024 100644 --- a/packages/contentstack-migration/examples/01-transform-contenttype.js +++ b/packages/contentstack-migration/examples/01-transform-contenttype.js @@ -109,7 +109,7 @@ module.exports = async ({migration, stackSDKInstance}) => { entries.push(entryObj) } } catch (error) { - console.log(error) + console.log('Error', error) } }, } diff --git a/packages/contentstack-migration/examples/02-publishing-entries.js b/packages/contentstack-migration/examples/02-publishing-entries.js index 2eaf54749f..e21313cff7 100644 --- a/packages/contentstack-migration/examples/02-publishing-entries.js +++ b/packages/contentstack-migration/examples/02-publishing-entries.js @@ -23,7 +23,7 @@ module.exports = async ({migration, stackSDKInstance}) => { entry.publish({publishDetails, locale: 'en-us'}) } } catch (error) { - console.log(error) + console.log('Error', error) } }, } diff --git a/packages/contentstack-migration/examples/06-update-environment.js b/packages/contentstack-migration/examples/06-update-environment.js index 59d364d0fb..a622e6a437 100644 --- a/packages/contentstack-migration/examples/06-update-environment.js +++ b/packages/contentstack-migration/examples/06-update-environment.js @@ -16,7 +16,7 @@ module.exports = async ({ migration, config }) => { fs.accessSync(directory, fs.constants.W_OK); return true; } catch (err) { - console.log(`Permission Denied! You do not have the necessary write access for this directory.`); + console.log(`Permission denied. You do not have the necessary write access for this directory.`); return false; } } @@ -33,7 +33,7 @@ module.exports = async ({ migration, config }) => { if (!source || !destination) { throw new Error(`The Source or Destination Directory Path are not valid`); } else { - console.log(`You have permission to write to directory`); + console.log(`You have permission to write to the directory.`); } } catch (err) { console.log( @@ -63,7 +63,7 @@ module.exports = async ({ migration, config }) => { if (sourceUid && destUid) { envMapper[sourceUid] = destUid; } else { - console.log(`No Mapper Provided for the environment ${sourceName} or ${destName}`); + console.log(`No mapper provided for environment ${sourceName} or ${destName}`); } } } catch (err) { @@ -145,7 +145,7 @@ module.exports = async ({ migration, config }) => { ); }); } else { - console.log(`No Entries Exist for Content-type ${ct} in loclae ${locale}`); + console.log(`No entries exist for content type ${ct} in locale ${locale}`); } } } diff --git a/packages/contentstack-migration/examples/rename-field/01-rename-field.js b/packages/contentstack-migration/examples/rename-field/01-rename-field.js index 57f8fda237..72f461e882 100644 --- a/packages/contentstack-migration/examples/rename-field/01-rename-field.js +++ b/packages/contentstack-migration/examples/rename-field/01-rename-field.js @@ -89,7 +89,7 @@ module.exports = ({ migration, stackSDKInstance }) => { await entry.update() } } catch (error) { - console.log(error) + console.log('Error', error) } }, } diff --git a/packages/contentstack-migration/examples/rename-field/02-publishing-entries.js b/packages/contentstack-migration/examples/rename-field/02-publishing-entries.js index f17c3640a6..cbbe29a11e 100644 --- a/packages/contentstack-migration/examples/rename-field/02-publishing-entries.js +++ b/packages/contentstack-migration/examples/rename-field/02-publishing-entries.js @@ -24,7 +24,7 @@ module.exports = async ({migration, stackSDKInstance}) => { entry.publish({publishDetails, locale: 'en-us'}) } } catch (error) { - console.log(error) + console.log('Error', error) } }, } diff --git a/packages/contentstack-migration/src/modules/parser.js b/packages/contentstack-migration/src/modules/parser.js index 444adeeb22..8fbc763a96 100644 --- a/packages/contentstack-migration/src/modules/parser.js +++ b/packages/contentstack-migration/src/modules/parser.js @@ -72,7 +72,7 @@ class Parser { base.dispatch(callsite, null, { typeErrors }, 'typeError'); } } else { - console.log(error); + console.log('Error', error); // eslint-disable-next-line const [, filename, line] = error.stack.match(/\/([\/\w-_\.]+\.js):(\d*):(\d*)/); const callsite = { diff --git a/packages/contentstack-migration/src/utils/error-helper.js b/packages/contentstack-migration/src/utils/error-helper.js index 4858a1eed5..f1d3e8b9be 100644 --- a/packages/contentstack-migration/src/utils/error-helper.js +++ b/packages/contentstack-migration/src/utils/error-helper.js @@ -95,7 +95,7 @@ module.exports = (errors, filePath) => { } if (isEmpty(messages) && errors !== undefined && isEmpty(errorsByFile)) { logger.log('error', { errors: errors }); - console.log(chalk`{bold.red Migration unsuccessful}`); + console.log(chalk`{bold.red migration unsuccessful}`); } else { logger.log('error', { error: messages.join('\n') }); } diff --git a/packages/contentstack-migration/src/utils/modules.js b/packages/contentstack-migration/src/utils/modules.js index 6af8df0212..758c8313e1 100644 --- a/packages/contentstack-migration/src/utils/modules.js +++ b/packages/contentstack-migration/src/utils/modules.js @@ -12,7 +12,7 @@ function checkWritePermissionToDirectory(directory) { fs.accessSync(directory, fs.constants.W_OK); return true; } catch (err) { - console.log(`Permission Denied! You do not have the necessary write access for this directory.`); + console.log(`Permission denied. You do not have the necessary write access for this directory.`); return false; } } diff --git a/packages/contentstack-seed/src/seed/github/client.ts b/packages/contentstack-seed/src/seed/github/client.ts index 3484c2f4ab..2bd9a70133 100644 --- a/packages/contentstack-seed/src/seed/github/client.ts +++ b/packages/contentstack-seed/src/seed/github/client.ts @@ -111,7 +111,7 @@ export default class GitHubClient { const response: Record = await this.makeHeadApiCall(repo); return response.statusCode === 200; } catch (error) { - console.log(error); + console.log('Error', error); // do nothing } diff --git a/packages/contentstack-utilities/src/auth-handler.ts b/packages/contentstack-utilities/src/auth-handler.ts index a7c2746a2e..c7157fd33e 100644 --- a/packages/contentstack-utilities/src/auth-handler.ts +++ b/packages/contentstack-utilities/src/auth-handler.ts @@ -137,7 +137,7 @@ class AuthHandler { await this.createHTTPServer(); await this.openOAuthURL(); } catch (error) { - this.logger.error('OAuth login failed', error.message); + this.logger.error('OAuth login failed!', error.message); throw error; } } @@ -148,7 +148,7 @@ class AuthHandler { const queryObject = url.parse(req.url, true).query; if (!queryObject.code) { - cliux.error('Error occoured while login with OAuth, please login with command - csdx auth:login --oauth'); + cliux.error('Error occurred while logging in with OAuth!'); return sendErrorResponse(res); } @@ -158,7 +158,7 @@ class AuthHandler { await this.getAccessToken(queryObject.code as string); await this.setOAuthBaseURL(); - cliux.print('Access token fetched using auth code successfully.'); + cliux.print('Access token successfully fetched using auth code.'); cliux.print( `You can review the access permissions on the page - ${this.OAuthBaseURL}/#!/marketplace/authorized-apps`, ); @@ -166,7 +166,7 @@ class AuthHandler { sendSuccessResponse(res); stopServer(); } catch (error) { - cliux.error('Error occoured while login with OAuth, please login with command - csdx auth:login --oauth'); + cliux.error('Error occurred while logging in with OAuth!'); cliux.error(error); sendErrorResponse(res); stopServer(); @@ -340,8 +340,8 @@ class AuthHandler { throw error; } } else { - cliux.error('Invalid/Empty access token.'); - throw new Error('Invalid/Empty access token'); + cliux.error('Invalid or empty access token.'); + throw new Error('Invalid or empty access token.'); } } @@ -394,7 +394,7 @@ class AuthHandler { const oauthValidUpto = new Date(); oauthValidUpto.setTime(oauthDate.getTime() + 59 * 60 * 1000); if (force) { - cliux.print('Force refreshing the token'); + cliux.print('Forcing token refresh...'); return this.refreshToken(); } else { if (oauthValidUpto > now) { @@ -418,7 +418,7 @@ class AuthHandler { } } } else { - cliux.print('No OAuth set'); + cliux.print('No OAuth configuration set.'); this.unsetConfigData(); } } diff --git a/packages/contentstack-utilities/src/config-handler.ts b/packages/contentstack-utilities/src/config-handler.ts index e32c79a355..45f24f49b4 100644 --- a/packages/contentstack-utilities/src/config-handler.ts +++ b/packages/contentstack-utilities/src/config-handler.ts @@ -52,7 +52,7 @@ class Config { this.removeOldConfigStoreFile(); } } catch (error) { - console.log('No data to be imported from Old config file'); + console.log('No data to import from old configuration file.'); } this.set(OLD_CONFIG_BACKUP_FLAG, true); diff --git a/packages/contentstack-utilities/src/flag-deprecation-check.ts b/packages/contentstack-utilities/src/flag-deprecation-check.ts index 4fe3fdf9c4..ce034703af 100644 --- a/packages/contentstack-utilities/src/flag-deprecation-check.ts +++ b/packages/contentstack-utilities/src/flag-deprecation-check.ts @@ -21,19 +21,19 @@ export default function (deprecatedFlags = [], suggestions = [], customMessage?: }); if (isCommandHasDeprecationFlag) { - let depreactionMessage = ''; + let deprecationMessage = ''; if (customMessage) { - depreactionMessage = customMessage; + deprecationMessage = customMessage; } else { - depreactionMessage = `WARNING!!! You're using the old (soon to be deprecated) Contentstack CLI flags (${deprecatedFlags.join( + deprecationMessage = `WARNING!!! You're using the old (soon to be deprecated) Contentstack CLI flags (${deprecatedFlags.join( ', ', )}).`; if (suggestions.length > 0) { - depreactionMessage += ` We recommend you to use the updated flags (${suggestions.join(', ')}).`; + deprecationMessage += ` We recommend you to use the updated flags (${suggestions.join(', ')}).`; } } - cliux.print(depreactionMessage, { color: 'yellow' }); + cliux.print(deprecationMessage, { color: 'yellow' }); } return input; diff --git a/packages/contentstack-utilities/src/logger.ts b/packages/contentstack-utilities/src/logger.ts index a38ca510ac..24d21f52e2 100644 --- a/packages/contentstack-utilities/src/logger.ts +++ b/packages/contentstack-utilities/src/logger.ts @@ -29,7 +29,7 @@ export class LoggerService { try { stringifiedParam = JSON.stringify(info.obj); } catch (error) { - console.log('warning: failed to log the result'); + console.log('Warning: Failed to log the result'); } // parse message info.message = messageHandler.parse(info.message as string); diff --git a/packages/contentstack-utilities/src/logger/log.ts b/packages/contentstack-utilities/src/logger/log.ts index b1ae6861bb..fb40f7f0d1 100644 --- a/packages/contentstack-utilities/src/logger/log.ts +++ b/packages/contentstack-utilities/src/logger/log.ts @@ -5,6 +5,7 @@ import { default as Logger } from './logger'; import { CLIErrorHandler } from './cli-error-handler'; import { ErrorContext } from '../interfaces'; import { configHandler } from '..'; +import { getSessionLogPath } from './session-path'; let loggerInstance: Logger | null = null; @@ -105,4 +106,6 @@ function getLogPath(): string { return path.join(os.homedir(), 'contentstack', 'logs'); } +// Re-export getSessionLogPath for external use +export { getSessionLogPath } from './session-path'; export { v2Logger, cliErrorHandler, handleAndLogError, getLogPath }; diff --git a/packages/contentstack-utilities/src/logger/logger.ts b/packages/contentstack-utilities/src/logger/logger.ts index c3612b7dbd..38f96615e4 100644 --- a/packages/contentstack-utilities/src/logger/logger.ts +++ b/packages/contentstack-utilities/src/logger/logger.ts @@ -4,6 +4,7 @@ import { normalize } from 'path'; import * as winston from 'winston'; import { levelColors, logLevels, PROGRESS_SUPPORTED_MODULES } from '../constants/logging'; import { LoggerConfig, LogLevel, LogType } from '../interfaces/index'; +import { getSessionLogPath } from './session-path'; import { configHandler } from '..'; export default class Logger { @@ -37,7 +38,9 @@ export default class Logger { } getLoggerInstance(level: 'error' | 'info' | 'warn' | 'debug' | 'hidden' = 'info'): winston.Logger { - const filePath = normalize(process.env.CS_CLI_LOG_PATH || this.config.basePath).replace(/^(\.\.(\/|\\|$))+/, ''); + // Use session-based path for date-organized logging + const sessionPath = getSessionLogPath(); + const filePath = normalize(sessionPath).replace(/^(\.\.(\/|\\|$))+/, ''); return this.createLogger(level === 'hidden' ? 'error' : level, filePath); } diff --git a/packages/contentstack-utilities/src/logger/session-path.ts b/packages/contentstack-utilities/src/logger/session-path.ts new file mode 100644 index 0000000000..2d5de07efd --- /dev/null +++ b/packages/contentstack-utilities/src/logger/session-path.ts @@ -0,0 +1,51 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { configHandler, formatDate, formatTime } from '..'; +import { getLogPath } from './log'; + +/** + * Get the session-based log path for date-organized logging + * Structure: {basePath}/{YYYY-MM-DD}/{command}-{YYYYMMDD-HHMMSS}-{sessionId}/ + * + * @returns The session-specific log directory path + */ +export function getSessionLogPath(): string { + // Get base log path + const basePath = getLogPath(); + + // Get current date in YYYY-MM-DD format + const now = new Date(); + const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD + + // Get command ID (fallback to 'unknown' if not set) + let commandId = configHandler.get('currentCommandId') || 'unknown'; + // Sanitize command ID - remove colons and replace with hyphens for folder name + commandId = commandId?.replace(/:/g, '-'); + + // Use helper methods to format date and time + const dateStrFormatted = formatDate(now); // YYYYMMDD + const timeStrFormatted = formatTime(now); // HHMMSS + const timestamp = `${dateStrFormatted}-${timeStrFormatted}`; // YYYYMMDD-HHMMSS + + let sessionId = configHandler.get('sessionId'); + if (!sessionId) { + // Format: first 8 chars of command + timestamp (YYYYMMDDHHMMSS) + const timestampForId = `${dateStrFormatted}${timeStrFormatted}`; // YYYYMMDDHHMMSS + const commandHash = commandId.substring(0, 8).padEnd(8, '0'); // Use first 8 chars of command + sessionId = `${commandHash}-${timestampForId}`; + } + + // Create session folder name: command-YYYYMMDD-HHMMSS-sessionId + const sessionFolderName = `${commandId}-${timestamp}-${sessionId}`; + + // Build full session path + const sessionPath = path.join(basePath, dateStr, sessionFolderName); + + // Ensure directory exists + if (!fs.existsSync(sessionPath)) { + fs.mkdirSync(sessionPath, { recursive: true }); + } + + return sessionPath; +} + diff --git a/packages/contentstack-utilities/src/message-handler.ts b/packages/contentstack-utilities/src/message-handler.ts index 92c37bfe02..bbeb78a096 100644 --- a/packages/contentstack-utilities/src/message-handler.ts +++ b/packages/contentstack-utilities/src/message-handler.ts @@ -23,7 +23,7 @@ class Messages { if (error.code === 'ENOENT') { this.messages = {}; } else { - throw new CLIError(error.message); + throw new CLIError(`Error: ${error.message}`); } } } diff --git a/packages/contentstack-utilities/test/unit/auth-handler.test.ts b/packages/contentstack-utilities/test/unit/auth-handler.test.ts index de7e37279c..295da6f037 100644 --- a/packages/contentstack-utilities/test/unit/auth-handler.test.ts +++ b/packages/contentstack-utilities/test/unit/auth-handler.test.ts @@ -360,7 +360,7 @@ describe('Auth Handler', () => { throw new Error('Expected getUserDetails to throw'); // ensure failure if no error is thrown } catch (error) { expect(error).to.be.instanceOf(Error); - expect(error.message).to.equal('Invalid/Empty access token'); + expect(error.message).to.equal('Invalid or empty access token.'); } }); }); @@ -503,7 +503,7 @@ describe('Auth Handler', () => { await authHandler.compareOAuthExpiry(); } catch (error) { expect(error).to.be.undefined; - expect(cliuxPrintStub.calledOnceWithExactly('Force refreshing the token')).to.be.true; + expect(cliuxPrintStub.calledOnceWithExactly('Forcing token refresh...')).to.be.true; expect(refreshTokenStub.calledOnce).to.be.true; expect(unsetConfigDataStub.called).to.be.false; } diff --git a/packages/contentstack-utilities/test/unit/cliErrorHandler.test.ts b/packages/contentstack-utilities/test/unit/cliErrorHandler.test.ts index 864de0ed0c..baca2e1346 100644 --- a/packages/contentstack-utilities/test/unit/cliErrorHandler.test.ts +++ b/packages/contentstack-utilities/test/unit/cliErrorHandler.test.ts @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { fancy } from 'fancy-test'; import CLIErrorHandler from '../../src/logger/cli-error-handler'; +import CLIErrorHandler from '../../src/logger/cli-error-handler'; import { ERROR_TYPES } from '../../src/constants/errorTypes'; describe('CLIErrorHandler', () => { @@ -36,7 +37,8 @@ describe('CLIErrorHandler', () => { expect(hidden).to.equal(true); }); - fancy.it('should extract debug payload correctly', () => { + fancy.it('should extract error payload correctly', () => { + fancy.it('should extract error payload correctly', () => { const error = new Error('API error'); (error as any).status = 500; (error as any).statusText = 'Internal Server Error'; @@ -52,6 +54,8 @@ describe('CLIErrorHandler', () => { data: { error: 'fail' }, headers: { 'content-type': 'application/json' }, }; + (error as any).status = 500; // Also set status on error directly + (error as any).status = 500; // Also set status on error directly const debugPayload = errorHandler['extractErrorPayload'](error); expect(debugPayload.request.method).to.equal('GET'); diff --git a/packages/contentstack-utilities/test/unit/helper.test.ts b/packages/contentstack-utilities/test/unit/helper.test.ts index c7ee34b3d3..2a7e716e4b 100644 --- a/packages/contentstack-utilities/test/unit/helper.test.ts +++ b/packages/contentstack-utilities/test/unit/helper.test.ts @@ -27,7 +27,7 @@ describe('Testing the Validate function', () => { describe('Testing the getBranchFromAlias function', () => { describe('When branch alias exists and resolves successfully', () => { it('should return the branch UID', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({ uid: 'main-branch' }) }) @@ -60,7 +60,7 @@ describe('Testing the getBranchFromAlias function', () => { }); it('should throw error for null branchAlias', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({ uid: 'main-branch' }) }) @@ -76,7 +76,7 @@ describe('Testing the getBranchFromAlias function', () => { }); it('should throw error for undefined branchAlias', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({ uid: 'main-branch' }) }) @@ -92,14 +92,14 @@ describe('Testing the getBranchFromAlias function', () => { }); it('should throw error for non-string branchAlias', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({ uid: 'main-branch' }) }) }; try { - await getBranchFromAlias(mockStack, 123); + await getBranchFromAlias(mockStack, 123 as any); expect.fail('Expected function to throw an error'); } catch (error) { expect(error).to.be.instanceOf(Error); @@ -108,7 +108,7 @@ describe('Testing the getBranchFromAlias function', () => { }); it('should throw error for empty string branchAlias', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({ uid: 'main-branch' }) }) @@ -126,7 +126,7 @@ describe('Testing the getBranchFromAlias function', () => { describe('When branch alias does not exist', () => { it('should throw an error', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => { throw new Error('Branch alias not found'); @@ -146,7 +146,7 @@ describe('Testing the getBranchFromAlias function', () => { describe('When response is missing UID', () => { it('should throw error for response without uid', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({ name: 'main-branch' }) // missing uid }) @@ -162,7 +162,7 @@ describe('Testing the getBranchFromAlias function', () => { }); it('should throw error for response with null uid', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({ uid: null }) }) @@ -178,7 +178,7 @@ describe('Testing the getBranchFromAlias function', () => { }); it('should throw error for response with undefined uid', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({ uid: undefined }) }) @@ -194,7 +194,7 @@ describe('Testing the getBranchFromAlias function', () => { }); it('should throw error for response with empty string uid', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({ uid: '' }) }) @@ -210,7 +210,7 @@ describe('Testing the getBranchFromAlias function', () => { }); it('should throw error for empty response object', async () => { - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => ({}) }) @@ -229,7 +229,7 @@ describe('Testing the getBranchFromAlias function', () => { describe('When network error occurs', () => { it('should throw the network error', async () => { const networkError = new Error('Network timeout'); - const mockStack = { + const mockStack: any = { branchAlias: (alias: string) => ({ fetch: async () => { throw networkError; diff --git a/packages/contentstack-utilities/test/unit/logger.test.ts b/packages/contentstack-utilities/test/unit/logger.test.ts index d9af947c2c..2c22149885 100644 --- a/packages/contentstack-utilities/test/unit/logger.test.ts +++ b/packages/contentstack-utilities/test/unit/logger.test.ts @@ -1,7 +1,17 @@ import { expect } from 'chai'; import { fancy } from 'fancy-test'; import sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; import Logger from '../../src/logger/logger'; +import { getSessionLogPath } from '../../src/logger/session-path'; +import configHandler from '../../src/config-handler'; +import { getSessionLogPath } from '../../src/logger/session-path'; +import configHandler from '../../src/config-handler'; describe('Logger', () => { let logger: Logger; @@ -32,10 +42,13 @@ describe('Logger', () => { other: 'safe', }; - const redacted = logger['redact'](testMeta); - expect(redacted.password).to.equal('[REDACTED]'); + // Test file mode redaction (consoleMode = false) + const redacted = logger['redact'](testMeta, false); + // In file mode, only token and secret are redacted (not password or email) + // Test file mode redaction (consoleMode = false) + const redacted = logger['redact'](testMeta, false); + // In file mode, only token and secret are redacted (not password or email) expect(redacted.token).to.equal('[REDACTED]'); - expect(redacted.email).to.equal('[REDACTED]'); expect(redacted.other).to.equal('safe'); }); @@ -69,6 +82,8 @@ describe('Logger', () => { fancy.it('should log error messages using error method', () => { const errorLogger = logger['loggers'].error; const spy = sinon.spy(); + const originalError = errorLogger.error.bind(errorLogger); + const originalError = errorLogger.error.bind(errorLogger); errorLogger.error = spy; logger.error('error message', { some: 'meta' }); @@ -87,11 +102,27 @@ describe('Logger', () => { fancy.it('logSuccess should call success info logger', () => { const successLogger = logger['loggers'].success; const spy = sinon.spy(); - successLogger.info = spy; + const originalLog = successLogger.log.bind(successLogger); + successLogger.log = spy; + const originalLog = successLogger.log.bind(successLogger); + successLogger.log = spy; logger.logSuccess({ type: 'test', message: 'Success message' }); expect(spy.calledOnce).to.be.true; - expect(spy.args[0][0].message).to.equal('Success message'); + // logSuccess creates a logPayload object with level, message, timestamp, and meta + const logPayload = spy.args[0][0]; + expect(logPayload.message).to.equal('Success message'); + expect(logPayload.meta.type).to.equal('test'); + + // Restore original + successLogger.log = originalLog; + // logSuccess creates a logPayload object with level, message, timestamp, and meta + const logPayload = spy.args[0][0]; + expect(logPayload.message).to.equal('Success message'); + expect(logPayload.meta.type).to.equal('test'); + + // Restore original + successLogger.log = originalLog; }); fancy.it('shouldLog should handle file target level filtering', () => { @@ -99,13 +130,28 @@ describe('Logger', () => { expect(result).to.equal(false); }); - fancy.it('success logger should include success type in meta', () => { + fancy.it('success logger should call log method', () => { + const successLogger = logger['loggers'].success; + fancy.it('success logger should call log method', () => { + const successLogger = logger['loggers'].success; const spy = sinon.spy(); - logger['loggers'].success.info = spy; + const originalLog = successLogger.log.bind(successLogger); + successLogger.log = spy; + const originalLog = successLogger.log.bind(successLogger); + successLogger.log = spy; logger.success('It worked!', { extra: 'meta' }); expect(spy.calledOnce).to.be.true; - expect(spy.args[0][1].type).to.equal('success'); + // success() calls log('success', message, meta) + expect(spy.calledWith('success', 'It worked!', { extra: 'meta' })).to.be.true; + + // Restore original + successLogger.log = originalLog; + // success() calls log('success', message, meta) + expect(spy.calledWith('success', 'It worked!', { extra: 'meta' })).to.be.true; + + // Restore original + successLogger.log = originalLog; }); fancy.it('logError with hidden true logs to debug logger', () => { @@ -135,9 +181,25 @@ describe('Logger', () => { token: 'abc', [Symbol.for('splat')]: [{ password: '1234' }], }; - const result = logger['redact'](obj); + // Test file mode (consoleMode = false) - token is redacted, password is not + const result = logger['redact'](obj, false); + // Test file mode (consoleMode = false) - token is redacted, password is not + const result = logger['redact'](obj, false); expect(result.token).to.equal('[REDACTED]'); - expect(result[Symbol.for('splat')][0].password).to.equal('[REDACTED]'); + // In file mode, password is not redacted + expect(result[Symbol.for('splat')][0].password).to.equal('1234'); + + // Test console mode (consoleMode = true) - both token and password are redacted + const consoleResult = logger['redact'](obj, true); + expect(consoleResult.token).to.equal('[REDACTED]'); + expect(consoleResult[Symbol.for('splat')][0].password).to.equal('[REDACTED]'); + // In file mode, password is not redacted + expect(result[Symbol.for('splat')][0].password).to.equal('1234'); + + // Test console mode (consoleMode = true) - both token and password are redacted + const consoleResult = logger['redact'](obj, true); + expect(consoleResult.token).to.equal('[REDACTED]'); + expect(consoleResult[Symbol.for('splat')][0].password).to.equal('[REDACTED]'); }); fancy.it('redact should return original if klona fails', () => { diff --git a/packages/contentstack-variants/src/export/audiences.ts b/packages/contentstack-variants/src/export/audiences.ts index 92c1084cc7..339764d650 100644 --- a/packages/contentstack-variants/src/export/audiences.ts +++ b/packages/contentstack-variants/src/export/audiences.ts @@ -52,7 +52,8 @@ export default class ExportAudiences extends PersonalizationAdapter { if (!this.events?.length) { log.debug('No events found, completing export', this.exportConfig.context); - log.info('No Events found with the given project!', this.exportConfig.context); + log.info('No events found for the given project.', this.exportConfig.context); + log.info('No events found for the given project.', this.exportConfig.context); return; } diff --git a/packages/contentstack-variants/src/export/experiences.ts b/packages/contentstack-variants/src/export/experiences.ts index b39d2e1aeb..312439efb3 100644 --- a/packages/contentstack-variants/src/export/experiences.ts +++ b/packages/contentstack-variants/src/export/experiences.ts @@ -61,7 +61,7 @@ export default class ExportExperiences extends PersonalizationAdapter if (!this.projectsData?.length) { log.debug('No projects found, disabling personalization', this.exportConfig.context); - log.info('No Personalize Project connected with the given stack', this.exportConfig.context); + log.info(`No Personalize project connected with the given stack.`, this.exportConfig.context); + log.info(`No Personalize project connected with the given stack.`, this.exportConfig.context); this.exportConfig.personalizationEnabled = false; return; } diff --git a/packages/contentstack-variants/src/import/experiences.ts b/packages/contentstack-variants/src/import/experiences.ts index 4d383cdb12..da8ed1e64e 100644 --- a/packages/contentstack-variants/src/import/experiences.ts +++ b/packages/contentstack-variants/src/import/experiences.ts @@ -310,10 +310,12 @@ export default class Experiences extends PersonalizationAdapter { if (PAUSE) { if (latestVersionUsed) { - log.debug(`Creating new PAUSE version for: ${experience.uid}`, this.config.context); + log.debug(`Creating new PAUSED version for: ${experience.uid}`, this.config.context); + log.debug(`Creating new PAUSED version for: ${experience.uid}`, this.config.context); await this.createExperienceVersion(experience.uid, PAUSE); } else { - log.debug(`Updating experience version to PAUSE for: ${experience.uid}`, this.config.context); + log.debug(`Updating experience version to PAUSED for: ${experience.uid}`, this.config.context); + log.debug(`Updating experience version to PAUSED for: ${experience.uid}`, this.config.context); await this.updateExperienceVersion(experience.uid, experience.latestVersion, PAUSE); } } @@ -343,7 +345,8 @@ export default class Experiences extends PersonalizationAdapter { this.cmsVariantGroups[expUid] = expRes._cms?.variantGroup ?? {}; return expUid; // Return the expUid for filtering later } else { - log.debug(`Variants/variant group not ready for experience: ${expUid}`, this.config.context); + log.debug(`Variants or variant group not ready for experience: ${expUid}`, this.config.context); + log.debug(`Variants or variant group not ready for experience: ${expUid}`, this.config.context); } }); @@ -360,7 +363,7 @@ export default class Experiences extends PersonalizationAdapter { ); return this.validateVariantGroupAndVariantsCreated(retryCount); } else { - log.error('Personalize job failed to create variants and variant groups', this.config.context); + log.error('Personalize job failed to create variants and variant groups.', this.config.context); log.error( `Failed experiences: ${this.pendingVariantAndVariantGrpForExperience.join(', ')}`, this.config.context, @@ -385,7 +388,8 @@ export default class Experiences extends PersonalizationAdapter { // Read the created content types from the file this.createdCTs = fsUtil.readFile(this.cTsSuccessPath, true) as any; if (!this.createdCTs) { - log.debug('No Content types created, skipping following process', this.config.context); + log.warn('No content types created.', this.config.context); + log.warn('No content types created.', this.config.context); return; } diff --git a/packages/contentstack-variants/src/import/variant-entries.ts b/packages/contentstack-variants/src/import/variant-entries.ts index bda2cf61e6..a10c4e5622 100644 --- a/packages/contentstack-variants/src/import/variant-entries.ts +++ b/packages/contentstack-variants/src/import/variant-entries.ts @@ -603,7 +603,8 @@ export default class VariantEntries extends VariantAdapter 0) { log.debug(`Processing ${variantEntry.publish_details.length} publish details`, this.config.context); } else { - log.debug('No publish details found for variant entry', this.config.context); + log.debug('No publish details found for variant entry.', this.config.context); + log.debug('No publish details found for variant entry.', this.config.context); } if (variantEntry.publish_details && variantEntry.publish_details?.length > 0) { diff --git a/packages/contentstack-variants/src/types/export-config.ts b/packages/contentstack-variants/src/types/export-config.ts index 61d3e512e7..8ddc75173e 100644 --- a/packages/contentstack-variants/src/types/export-config.ts +++ b/packages/contentstack-variants/src/types/export-config.ts @@ -20,7 +20,8 @@ export type Modules = | 'labels' | 'marketplace-apps' | 'taxonomies' - | 'personalize'; + | 'personalize' + | 'composable-studio'; export type branch = { uid: string; @@ -182,6 +183,11 @@ export interface DefaultConfig { fileName: string; dependencies?: Modules[]; }; + 'composable-studio': { + dirName: string; + fileName: string; + apiBaseUrl: string; + }; masterLocale: { dirName: string; fileName: string; diff --git a/packages/contentstack-variants/src/utils/personalization-api-adapter.ts b/packages/contentstack-variants/src/utils/personalization-api-adapter.ts index 39e5e2f454..18476602e8 100644 --- a/packages/contentstack-variants/src/utils/personalization-api-adapter.ts +++ b/packages/contentstack-variants/src/utils/personalization-api-adapter.ts @@ -357,7 +357,8 @@ export class PersonalizationAdapter extends AdapterHelper impl } async getAttributes(): Promise { - log.debug('Fetching attributes from personalization API', this.exportConfig?.context ); + log.debug('Fetching attributes from Personalize API...', this.exportConfig?.context ); + log.debug('Fetching attributes from Personalize API...', this.exportConfig?.context ); const data = await this.apiClient.get('/attributes'); const result = (await this.handleVariantAPIRes(data)) as AttributeStruct[]; log.info(`Fetched ${result?.length || 0} attributes`, this.exportConfig?.context ); diff --git a/packages/contentstack-variants/src/utils/variant-api-adapter.ts b/packages/contentstack-variants/src/utils/variant-api-adapter.ts index 5dfcb74037..b8b445d5a1 100644 --- a/packages/contentstack-variants/src/utils/variant-api-adapter.ts +++ b/packages/contentstack-variants/src/utils/variant-api-adapter.ts @@ -53,10 +53,12 @@ export class VariantHttpClient extends AdapterHelper implement this.exportConfig?.context, ); if (authenticationHandler.isOauthEnabled) { - log.debug('Setting OAuth authorization header', this.exportConfig?.context); + log.debug('Setting OAuth authorization header...', this.exportConfig?.context); + log.debug('Setting OAuth authorization header...', this.exportConfig?.context); this.apiClient.headers({ authorization: token }); } else { - log.debug('Setting authtoken header', this.exportConfig?.context); + log.debug('Setting authtoken header...', this.exportConfig?.context); + log.debug('Setting authtoken header...', this.exportConfig?.context); this.apiClient.headers({ authtoken: token }); } } @@ -109,7 +111,8 @@ export class VariantHttpClient extends AdapterHelper implement ); if (variantConfig.serveMockData && callback) { - log.debug('Using mock data for variant entries', this.exportConfig?.context); + log.debug('Using mock data for variant entries...', this.exportConfig?.context); + log.debug('Using mock data for variant entries...', this.exportConfig?.context); let data = [] as Record[]; if (existsSync(variantConfig.mockDataPath)) { @@ -174,10 +177,12 @@ export class VariantHttpClient extends AdapterHelper implement } if (callback) { - log.debug('Executing callback with variant entries', this.exportConfig?.context); + log.debug('Executing callback with variant entries...', this.exportConfig?.context); + log.debug('Executing callback with variant entries...', this.exportConfig?.context); callback(response.entries); } else { - log.debug('Adding variant entries to collection', this.exportConfig?.context); + log.debug('Adding variant entries to collection...', this.exportConfig?.context); + log.debug('Adding variant entries to collection...', this.exportConfig?.context); entries = entries.concat(response.entries); } @@ -200,7 +205,8 @@ export class VariantHttpClient extends AdapterHelper implement } if (returnResult) { - log.debug('Returning variant entries result', this.exportConfig?.context ); + log.debug('Returning variant entries result...', this.exportConfig?.context ); + log.debug('Returning variant entries result...', this.exportConfig?.context ); return { entries }; } } @@ -329,7 +335,8 @@ export class VariantHttpClient extends AdapterHelper implement log.debug(`API response status: ${status}`, this.exportConfig?.context); if (status >= 200 && status < 300) { - log.debug('API request successful', this.exportConfig?.context); + log.debug('API request successful.', this.exportConfig?.context); + log.debug('API request successful.', this.exportConfig?.context); return data; } @@ -406,17 +413,19 @@ export class VariantAdapter { log.debug('Initializing VariantAdapter...', this.exportConfig?.context); if (config.httpClient) { - log.debug('Using HTTP client variant instance', this.exportConfig?.context); + log.debug('Using HTTP client variant instance.', this.exportConfig?.context); + log.debug('Using HTTP client variant instance.', this.exportConfig?.context); const { httpClient, Adapter, ...restConfig } = config; this.variantInstance = new Adapter(restConfig, options); } else { - log.debug('Using SDK variant instance', this.exportConfig?.context); + log.debug('Using SDK variant instance.', this.exportConfig?.context); + log.debug('Using SDK variant instance.', this.exportConfig?.context); const { Adapter, ...restConfig } = config; this.variantInstance = new Adapter(restConfig); } this.messages = messages; - log.debug('VariantAdapter initialized successfully', this.exportConfig?.context); + log.debug('VariantAdapter initialized successfully.', this.exportConfig?.context); } /** diff --git a/packages/contentstack/src/hooks/init/context-init.ts b/packages/contentstack/src/hooks/init/context-init.ts index 28b43008ae..4d87969130 100644 --- a/packages/contentstack/src/hooks/init/context-init.ts +++ b/packages/contentstack/src/hooks/init/context-init.ts @@ -1,8 +1,13 @@ import { CsdxContext } from '../../utils'; +import { configHandler } from '@contentstack/cli-utilities'; /** * Set the cli context */ export default function (opts): void { + // Store command ID for session-based log organization + if (opts.id) { + configHandler.set('currentCommandId', opts.id); + } this.config.context = new CsdxContext(opts, this.config); } diff --git a/packages/contentstack/src/hooks/prerun/auth-guard.ts b/packages/contentstack/src/hooks/prerun/auth-guard.ts index 6eef92a0b6..94898da40f 100644 --- a/packages/contentstack/src/hooks/prerun/auth-guard.ts +++ b/packages/contentstack/src/hooks/prerun/auth-guard.ts @@ -15,27 +15,27 @@ export default async function (opts): Promise { this.exit(); return; } - cliux.print(`\n Currently using ${region.name} region \n`, { color: 'grey' }); + cliux.print(`Currently using region: ${region.name}`, { color: 'grey' }); } // Auth guard if (protectedCommands[opts.Command.id]) { if (!isAuthenticated()) { - newLogger.error('No auth token found for command', opts.Command.id); - cliux.error('Please login to execute the command'); + newLogger.error('No auth token found for command.', opts.Command.id); + cliux.error('Please log in to execute the command'); this.exit(); } const client = await managementSDKClient({host: region.cma}) try { const result = await client.getUser(); if (!result) { - newLogger.error('error in auth validation'); - cliux.error('Please login to execute the command'); + newLogger.error('Error in auth validation'); + cliux.error('Please log in to execute the command'); this.exit(); } - newLogger.debug('logged in user', result.data); + newLogger.debug('Logged-in user', result.data); } catch (error) { - newLogger.error('error in auth validation', error); - cliux.error('Please login to execute the command'); + newLogger.error('Error in auth validation', error); + cliux.error('Please log in to execute the command'); process.exit(); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d12817fdd..daade01d40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3661,6 +3661,7 @@ packages: strip-ansi-cjs: /strip-ansi/6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: /wrap-ansi/7.0.0 + dev: true /@istanbuljs/load-nyc-config/1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} @@ -6657,6 +6658,7 @@ packages: /ansi-regex/6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + dev: true /ansi-styles/2.2.1: resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} @@ -6683,6 +6685,7 @@ packages: /ansi-styles/6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + dev: true /ansicolors/0.3.2: resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==} @@ -7764,6 +7767,7 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + dev: true /crypto-random-string/2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} @@ -8087,6 +8091,7 @@ packages: /eastasianwidth/0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true /ee-first/1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -8122,6 +8127,7 @@ packages: /emoji-regex/9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true /enabled/2.0.0: resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} @@ -10000,6 +10006,7 @@ packages: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 + dev: true /form-data-encoder/2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} @@ -11051,6 +11058,7 @@ packages: /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true /isexe/3.1.1: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} @@ -13112,6 +13120,7 @@ packages: /path-key/3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + dev: true /path-key/4.0.0: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} @@ -13914,6 +13923,7 @@ packages: engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 + dev: true /shebang-regex/1.0.0: resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} @@ -13923,6 +13933,7 @@ packages: /shebang-regex/3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + dev: true /shelljs/0.10.0: resolution: {integrity: sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw==} @@ -14356,6 +14367,7 @@ packages: engines: {node: '>=12'} dependencies: ansi-regex: 6.2.2 + dev: true /strip-bom/3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} @@ -15334,6 +15346,7 @@ packages: hasBin: true dependencies: isexe: 2.0.0 + dev: true /which/4.0.0: resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} @@ -15438,6 +15451,7 @@ packages: ansi-styles: 6.2.3 string-width: 5.1.2 strip-ansi: 7.1.2 + dev: true /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}