diff --git a/package-lock.json b/package-lock.json index 202c94433c..f2ac8d42f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -883,9 +883,9 @@ } }, "node_modules/@contentstack/management": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@contentstack/management/-/management-1.11.0.tgz", - "integrity": "sha512-tv4At2Q5iGgkzL1MFGil/o36URKZfO6DY/KtpNJFYjmTHitZNv7uotH8OXkOPBMxB4xz58SG58lWB6fNTkLrpw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@contentstack/management/-/management-1.12.0.tgz", + "integrity": "sha512-+L+WVhSYEtfdG9v794TjLT8Fd6fCB8meqoho666mg1kNufzXcsqr7hjubX5cSL7GcZFdKntkDpZ2RaOnTHReJg==", "dependencies": { "axios": "^1.5.1", "form-data": "^3.0.1", @@ -2617,9 +2617,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", - "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==" + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==" }, "node_modules/@types/cli-progress": { "version": "3.11.0", @@ -4550,9 +4550,9 @@ } }, "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", + "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -22910,7 +22910,7 @@ "@contentstack/cli-cm-branches": "~1.0.17", "@contentstack/cli-cm-bulk-publish": "~1.3.14", "@contentstack/cli-cm-clone": "~1.7.0", - "@contentstack/cli-cm-export": "~1.9.3", + "@contentstack/cli-cm-export": "~1.10.0", "@contentstack/cli-cm-export-to-csv": "~1.5.0", "@contentstack/cli-cm-import": "~1.11.0", "@contentstack/cli-cm-migrate-rte": "~1.4.14", @@ -23903,7 +23903,7 @@ "license": "MIT", "dependencies": { "@colors/colors": "^1.5.0", - "@contentstack/cli-cm-export": "~1.9.3", + "@contentstack/cli-cm-export": "~1.10.0", "@contentstack/cli-cm-import": "~1.11.0", "@contentstack/cli-command": "~1.2.15", "@contentstack/cli-utilities": "~1.5.5", @@ -24323,7 +24323,7 @@ }, "packages/contentstack-export": { "name": "@contentstack/cli-cm-export", - "version": "1.9.3", + "version": "1.10.0", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.2.15", @@ -24373,7 +24373,7 @@ }, "packages/contentstack-export-to-csv": { "name": "@contentstack/cli-cm-export-to-csv", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.2.15", @@ -24386,12 +24386,15 @@ }, "devDependencies": { "@oclif/test": "^2.2.10", - "chai": "^4.2.0", + "@types/chai": "^4.3.6", + "@types/mocha": "^10.0.1", + "chai": "^4.3.8", "debug": "^4.3.1", + "dotenv": "^16.3.1", "eslint": "^7.32.0", "eslint-config-oclif": "^4.0.0", "globby": "^10.0.2", - "mocha": "^10.0.0", + "mocha": "^10.2.0", "nyc": "^15.1.0", "oclif": "^3.8.1" }, @@ -24442,6 +24445,12 @@ "node": ">=10.10.0" } }, + "packages/contentstack-export-to-csv/node_modules/@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, "packages/contentstack-export-to-csv/node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -24470,6 +24479,32 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "packages/contentstack-export-to-csv/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "packages/contentstack-export-to-csv/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/contentstack-export-to-csv/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "packages/contentstack-export-to-csv/node_modules/eslint": { "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", @@ -24583,6 +24618,26 @@ "node": ">=4" } }, + "packages/contentstack-export-to-csv/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/contentstack-export-to-csv/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -24610,6 +24665,22 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "packages/contentstack-export-to-csv/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/contentstack-export-to-csv/node_modules/mkdirp": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", @@ -24624,6 +24695,94 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/contentstack-export-to-csv/node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "packages/contentstack-export-to-csv/node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "packages/contentstack-export-to-csv/node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "packages/contentstack-export-to-csv/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "packages/contentstack-export-to-csv/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, "packages/contentstack-export/node_modules/@oclif/test": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/@oclif/test/-/test-1.2.9.tgz", @@ -24643,7 +24802,7 @@ "dependencies": { "@contentstack/cli-command": "~1.2.15", "@contentstack/cli-utilities": "~1.5.5", - "@contentstack/management": "~1.11.0", + "@contentstack/management": "~1.12.0", "@oclif/core": "^2.9.3", "axios": "^1.6.0", "big-json": "^3.2.0", @@ -25302,7 +25461,7 @@ }, "packages/contentstack-migration": { "name": "@contentstack/cli-migration", - "version": "1.3.15", + "version": "1.4.0", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.2.15", @@ -25419,7 +25578,7 @@ "version": "1.5.6", "license": "MIT", "dependencies": { - "@contentstack/management": "~1.11.0", + "@contentstack/management": "~1.12.0", "@oclif/core": "^2.9.3", "axios": "^1.6.0", "chalk": "^4.0.0", @@ -26547,6 +26706,37 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/contentstack/node_modules/@contentstack/cli-cm-export-to-csv": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-export-to-csv/-/cli-cm-export-to-csv-1.5.0.tgz", + "integrity": "sha512-16v9WG7CeFpf+4q3OK2ImJZOXlL9pQQLfoSo1Zz5D1NYUSHjWlZNHzgUm8osB0iK776i2tZrZ81pKQb/cRsPkw==", + "dependencies": { + "@contentstack/cli-command": "~1.2.15", + "@contentstack/cli-utilities": "~1.5.5", + "chalk": "^4.1.0", + "fast-csv": "^4.3.6", + "inquirer": "8.2.4", + "inquirer-checkbox-plus-prompt": "1.0.1", + "mkdirp": "^3.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "packages/contentstack/node_modules/@contentstack/cli-cm-export-to-csv/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/contentstack/node_modules/@contentstack/cli-cm-seed/node_modules/@contentstack/cli-cm-import": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-import/-/cli-cm-import-1.10.0.tgz", @@ -26573,6 +26763,53 @@ "engines": { "node": ">=14.0.0" } + }, + "packages/contentstack/node_modules/@contentstack/cli-migration": { + "version": "1.3.15", + "resolved": "https://registry.npmjs.org/@contentstack/cli-migration/-/cli-migration-1.3.15.tgz", + "integrity": "sha512-059UsM55C9aMDwP/PO6JDIKrioVdPWnrqSgAyqR8JEVKEIuUEwzGBH8yTgI+Qbq08T9LpY3bfVorNtnf2uwHgg==", + "dependencies": { + "@contentstack/cli-command": "~1.2.15", + "@contentstack/cli-utilities": "~1.5.5", + "async": "^3.2.4", + "callsites": "^3.1.0", + "cardinal": "^2.1.1", + "chalk": "^4.1.0", + "dot-object": "^2.1.4", + "dotenv": "^16.0.3", + "listr": "^0.14.3", + "winston": "^3.7.2" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "packages/contentstack/node_modules/@contentstack/management": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@contentstack/management/-/management-1.11.0.tgz", + "integrity": "sha512-tv4At2Q5iGgkzL1MFGil/o36URKZfO6DY/KtpNJFYjmTHitZNv7uotH8OXkOPBMxB4xz58SG58lWB6fNTkLrpw==", + "dependencies": { + "axios": "^1.5.1", + "form-data": "^3.0.1", + "lodash": "^4.17.21", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "packages/contentstack/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } } }, "dependencies": { @@ -27153,7 +27390,7 @@ "@contentstack/cli-cm-branches": "~1.0.17", "@contentstack/cli-cm-bulk-publish": "~1.3.14", "@contentstack/cli-cm-clone": "~1.7.0", - "@contentstack/cli-cm-export": "~1.9.3", + "@contentstack/cli-cm-export": "~1.10.0", "@contentstack/cli-cm-export-to-csv": "~1.5.0", "@contentstack/cli-cm-import": "~1.11.0", "@contentstack/cli-cm-migrate-rte": "~1.4.14", @@ -27200,6 +27437,67 @@ "typescript": "^4.9.3", "uuid": "^9.0.0", "winston": "^3.7.2" + }, + "dependencies": { + "@contentstack/cli-cm-export-to-csv": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-export-to-csv/-/cli-cm-export-to-csv-1.5.0.tgz", + "integrity": "sha512-16v9WG7CeFpf+4q3OK2ImJZOXlL9pQQLfoSo1Zz5D1NYUSHjWlZNHzgUm8osB0iK776i2tZrZ81pKQb/cRsPkw==", + "requires": { + "@contentstack/cli-command": "~1.2.15", + "@contentstack/cli-utilities": "~1.5.5", + "chalk": "^4.1.0", + "fast-csv": "^4.3.6", + "inquirer": "8.2.4", + "inquirer-checkbox-plus-prompt": "1.0.1", + "mkdirp": "^3.0.1" + }, + "dependencies": { + "mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" + } + } + }, + "@contentstack/cli-migration": { + "version": "1.3.15", + "resolved": "https://registry.npmjs.org/@contentstack/cli-migration/-/cli-migration-1.3.15.tgz", + "integrity": "sha512-059UsM55C9aMDwP/PO6JDIKrioVdPWnrqSgAyqR8JEVKEIuUEwzGBH8yTgI+Qbq08T9LpY3bfVorNtnf2uwHgg==", + "requires": { + "@contentstack/cli-command": "~1.2.15", + "@contentstack/cli-utilities": "~1.5.5", + "async": "^3.2.4", + "callsites": "^3.1.0", + "cardinal": "^2.1.1", + "chalk": "^4.1.0", + "dot-object": "^2.1.4", + "dotenv": "^16.0.3", + "listr": "^0.14.3", + "winston": "^3.7.2" + } + }, + "@contentstack/management": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@contentstack/management/-/management-1.11.0.tgz", + "integrity": "sha512-tv4At2Q5iGgkzL1MFGil/o36URKZfO6DY/KtpNJFYjmTHitZNv7uotH8OXkOPBMxB4xz58SG58lWB6fNTkLrpw==", + "requires": { + "axios": "^1.5.1", + "form-data": "^3.0.1", + "lodash": "^4.17.21", + "qs": "^6.11.2" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } } }, "@contentstack/cli-audit": { @@ -27907,7 +28205,7 @@ "version": "file:packages/contentstack-clone", "requires": { "@colors/colors": "^1.5.0", - "@contentstack/cli-cm-export": "~1.9.3", + "@contentstack/cli-cm-export": "~1.10.0", "@contentstack/cli-cm-import": "~1.11.0", "@contentstack/cli-command": "~1.2.15", "@contentstack/cli-utilities": "~1.5.5", @@ -28011,9 +28309,12 @@ "@contentstack/cli-command": "~1.2.15", "@contentstack/cli-utilities": "~1.5.5", "@oclif/test": "^2.2.10", - "chai": "^4.2.0", + "@types/chai": "^4.3.6", + "@types/mocha": "^10.0.1", + "chai": "^4.3.8", "chalk": "^4.1.0", "debug": "^4.3.1", + "dotenv": "^16.3.1", "eslint": "^7.32.0", "eslint-config-oclif": "^4.0.0", "fast-csv": "^4.3.6", @@ -28021,7 +28322,7 @@ "inquirer": "8.2.4", "inquirer-checkbox-plus-prompt": "1.0.1", "mkdirp": "^3.0.1", - "mocha": "^10.0.0", + "mocha": "^10.2.0", "nyc": "^15.1.0", "oclif": "^3.8.1" }, @@ -28063,6 +28364,12 @@ "minimatch": "^3.0.4" } }, + "@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -28081,6 +28388,32 @@ "uri-js": "^4.2.2" } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "eslint": { "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", @@ -28171,6 +28504,20 @@ } } }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -28192,10 +28539,90 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, "mkdirp": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" + }, + "mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } } } }, @@ -28204,7 +28631,7 @@ "requires": { "@contentstack/cli-command": "~1.2.15", "@contentstack/cli-utilities": "~1.5.5", - "@contentstack/management": "~1.11.0", + "@contentstack/management": "~1.12.0", "@oclif/core": "^2.9.3", "@oclif/test": "^1.2.6", "@types/big-json": "^3.2.0", @@ -29072,7 +29499,7 @@ "@contentstack/cli-utilities": { "version": "file:packages/contentstack-utilities", "requires": { - "@contentstack/management": "~1.11.0", + "@contentstack/management": "~1.12.0", "@oclif/core": "^2.9.3", "@oclif/test": "^2.2.10", "@types/chai": "^4.2.18", @@ -29929,9 +30356,9 @@ } }, "@contentstack/management": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@contentstack/management/-/management-1.11.0.tgz", - "integrity": "sha512-tv4At2Q5iGgkzL1MFGil/o36URKZfO6DY/KtpNJFYjmTHitZNv7uotH8OXkOPBMxB4xz58SG58lWB6fNTkLrpw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@contentstack/management/-/management-1.12.0.tgz", + "integrity": "sha512-+L+WVhSYEtfdG9v794TjLT8Fd6fCB8meqoho666mg1kNufzXcsqr7hjubX5cSL7GcZFdKntkDpZ2RaOnTHReJg==", "requires": { "axios": "^1.5.1", "form-data": "^3.0.1", @@ -31373,9 +31800,9 @@ } }, "@types/chai": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", - "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==" + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==" }, "@types/cli-progress": { "version": "3.11.0", @@ -32866,9 +33293,9 @@ } }, "chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", + "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", "dev": true, "requires": { "assertion-error": "^1.1.0", diff --git a/packages/contentstack-clone/package.json b/packages/contentstack-clone/package.json index 6f8b22927d..f6f99bba09 100644 --- a/packages/contentstack-clone/package.json +++ b/packages/contentstack-clone/package.json @@ -5,7 +5,7 @@ "author": "Contentstack", "bugs": "https://github.com/rohitmishra209/cli-cm-clone/issues", "dependencies": { - "@contentstack/cli-cm-export": "~1.9.3", + "@contentstack/cli-cm-export": "~1.10.0", "@contentstack/cli-cm-import": "~1.11.0", "@contentstack/cli-command": "~1.2.15", "@contentstack/cli-utilities": "~1.5.5", diff --git a/packages/contentstack-export-to-csv/package.json b/packages/contentstack-export-to-csv/package.json index 8191b662f3..feaadda046 100644 --- a/packages/contentstack-export-to-csv/package.json +++ b/packages/contentstack-export-to-csv/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-export-to-csv", "description": "Export entities to csv", - "version": "1.5.0", + "version": "1.6.0", "author": "Abhinav Gupta @abhinav-from-contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { @@ -15,12 +15,15 @@ }, "devDependencies": { "@oclif/test": "^2.2.10", - "chai": "^4.2.0", + "@types/chai": "^4.3.6", + "@types/mocha": "^10.0.1", + "chai": "^4.3.8", "debug": "^4.3.1", + "dotenv": "^16.3.1", "eslint": "^7.32.0", "eslint-config-oclif": "^4.0.0", "globby": "^10.0.2", - "mocha": "^10.0.0", + "mocha": "^10.2.0", "nyc": "^15.1.0", "oclif": "^3.8.1" }, @@ -44,7 +47,8 @@ "postpack": "rm -f oclif.manifest.json", "prepack": "oclif manifest && oclif readme", "test": "nyc mocha --forbid-only \"test/**/*.test.js\"", - "test:unit": "nyc mocha --timeout 10000 --forbid-only \"test/unit/**/*.test.js\"", + "test:unit": "mocha --timeout 10000 --forbid-only \"test/unit/**/*.test.js\" \"test/util/common-utils.test.js\"", + "test:unit:report": "nyc --extension .js mocha --forbid-only \"test/unit/**/*.test.js\" \"test/util/common-utils.test.js\"", "version": "oclif readme && git add README.md", "clean": "rm -rf ./node_modules tsconfig.build.tsbuildinfo" }, @@ -61,4 +65,4 @@ } }, "repository": "https://github.com/contentstack/cli" -} \ No newline at end of file +} 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 7dfe314abc..692ad66bea 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 @@ -15,8 +15,8 @@ class ExportToCsvCommand extends Command { action: flags.string({ required: false, multiple: false, - options: ['entries', 'users', 'teams'], - description: `Option to export data (entries, users, teams)`, + options: ['entries', 'users', 'teams', 'taxonomies'], + description: `Option to export data (entries, users, teams, taxonomies)`, }), alias: flags.string({ char: 'a', @@ -61,7 +61,14 @@ class ExportToCsvCommand extends Command { }), "team-uid": flags.string({ description: 'Uid of the team whose user data and stack roles are required' - }) + }), + 'taxonomy-uid': flags.string({ + description: 'Provide the taxonomy UID of the related terms you want to export', + }), + delimiter: flags.string({ + description: '[optional] Provide a delimiter to separate individual data fields within the CSV file.', + default: ',', + }), }; async run() { @@ -78,7 +85,9 @@ class ExportToCsvCommand extends Command { 'content-type': contentTypesFlag, alias: managementTokenAlias, branch: branchUid, - "team-uid": teamUid + "team-uid": teamUid, + 'taxonomy-uid': taxonomyUID, + delimiter }, } = await this.parse(ExportToCsvCommand); @@ -106,61 +115,17 @@ class ExportToCsvCommand extends Command { let stackAPIClient; let language; let contentTypes = []; - let stackBranches; - const listOfTokens = configHandler.get('tokens'); - - if (managementTokenAlias && listOfTokens[managementTokenAlias]) { - managementAPIClient = await managementSDKClient({ - host: this.cmaHost, - management_token: listOfTokens[managementTokenAlias].token, - }); - stack = { - name: stackName || managementTokenAlias, - apiKey: listOfTokens[managementTokenAlias].apiKey, - token: listOfTokens[managementTokenAlias].token, - }; - } else if (managementTokenAlias) { - this.error('Provided management token alias not found in your config.!'); + + if (managementTokenAlias) { + const { stackDetails, apiClient } = await this.getAliasDetails(managementTokenAlias, stackName); + managementAPIClient = apiClient; + stack = stackDetails; } else { - let organization; - if (org) { - organization = { uid: org }; - } else { - organization = await util.chooseOrganization(managementAPIClient); // prompt for organization - } - if (!stackAPIKey) { - stack = await util.chooseStack(managementAPIClient, organization.uid); // prompt for stack - } else { - stack = await util.chooseStack(managementAPIClient, organization.uid, stackAPIKey); - } + stack = await this.getStackDetails(managementAPIClient, stackAPIKey, org); } stackAPIClient = this.getStackClient(managementAPIClient, stack); - - if (branchUid) { - try { - const branchExists = await doesBranchExist(stackAPIClient, branchUid); - if (branchExists?.errorCode) { - throw new Error(branchExists.errorMessage); - } - stack.branch_uid = branchUid; - stackAPIClient = this.getStackClient(managementAPIClient, stack); - } catch (error) { - if (error.message || error.errorMessage) { - cliux.error(util.formatError(error)); - this.exit(); - } - } - } else { - stackBranches = await this.getStackBranches(stackAPIClient); - if (stackBranches === undefined) { - stackAPIClient = this.getStackClient(managementAPIClient, stack); - } else { - const { branch } = await util.chooseBranch(stackBranches); - stack.branch_uid = branch; - stackAPIClient = this.getStackClient(managementAPIClient, stack); - } - } + await this.checkAndUpdateBranchDetail(branchUid, stack, stackAPIClient, managementAPIClient); const contentTypeCount = await util.getContentTypeCount(stackAPIClient); @@ -219,7 +184,7 @@ class ExportToCsvCommand extends Command { flatEntries = flatEntries.concat(flatEntriesResult); } let fileName = `${stackName ? stackName : stack.name}_${contentType}_${language.code}_entries_export.csv`; - util.write(this, flatEntries, fileName, 'entries'); // write to file + util.write(this, flatEntries, fileName, 'entries', delimiter); // write to file } } catch (error) { cliux.error(util.formatError(error)); @@ -246,7 +211,7 @@ class ExportToCsvCommand extends Command { (orgName ? orgName : organization.name).replace(config.organizationNameRegex, ''), )}_users_export.csv`; - util.write(this, listOfUsers, fileName, 'organization details'); + util.write(this, listOfUsers, fileName, 'organization details', delimiter); } catch (error) { if (error.message || error.errorMessage) { cliux.error(util.formatError(error)); @@ -264,14 +229,30 @@ class ExportToCsvCommand extends Command { organization = await util.chooseOrganization(managementAPIClient, action); // prompt for organization } - await util.exportTeams(managementAPIClient,organization,teamUid); + await util.exportTeams(managementAPIClient,organization,teamUid, delimiter); } catch (error) { if (error.message || error.errorMessage) { cliux.error(util.formatError(error)); } } + break; + } + case config.exportTaxonomies: + case 'taxonomies': { + let stack; + let stackAPIClient; + if (managementTokenAlias) { + const { stackDetails, apiClient } = await this.getAliasDetails(managementTokenAlias, stackName); + managementAPIClient = apiClient; + stack = stackDetails; + } else { + stack = await this.getStackDetails(managementAPIClient, stackAPIKey, org); + } + + stackAPIClient = this.getStackClient(managementAPIClient, stack); + await this.createTaxonomyAndTermCsvFile(stackAPIClient, stackName, stack, taxonomyUID, delimiter); + break; } - break; } } catch (error) { if (error.message || error.errorMessage) { @@ -287,8 +268,8 @@ class ExportToCsvCommand extends Command { getStackClient(managementAPIClient, stack) { const stackInit = { api_key: stack.apiKey, - branch_uid: stack.branch_uid, }; + if (stack?.branch_uid) stackInit['branch_uid'] = stack.branch_uid; if (stack.token) { return managementAPIClient.stack({ ...stackInit, @@ -306,9 +287,159 @@ class ExportToCsvCommand extends Command { .then(({ items }) => (items !== undefined ? items : [])) .catch((_err) => {}); } + + /** + * check whether branch enabled org or not and update branch details + * @param {string} branchUid + * @param {object} stack + * @param {*} stackAPIClient + * @param {*} managementAPIClient + */ + async checkAndUpdateBranchDetail(branchUid, stack, stackAPIClient, managementAPIClient) { + if (branchUid) { + try { + const branchExists = await doesBranchExist(stackAPIClient, branchUid); + if (branchExists?.errorCode) { + throw new Error(branchExists.errorMessage); + } + stack.branch_uid = branchUid; + stackAPIClient = this.getStackClient(managementAPIClient, stack); + } catch (error) { + if (error?.message || error?.errorMessage) { + cliux.error(util.formatError(error)); + this.exit(); + } + } + } else { + const stackBranches = await this.getStackBranches(stackAPIClient); + if (stackBranches === undefined) { + stackAPIClient = this.getStackClient(managementAPIClient, stack); + } else { + const { branch } = await util.chooseBranch(stackBranches); + stack.branch_uid = branch; + stackAPIClient = this.getStackClient(managementAPIClient, stack); + } + } + } + + /** + * fetch stack details from alias token + * @param {string} managementTokenAlias + * @param {string} stackName + * @returns + */ + async getAliasDetails(managementTokenAlias, stackName) { + let apiClient, stackDetails; + const listOfTokens = configHandler.get('tokens'); + if (managementTokenAlias && listOfTokens[managementTokenAlias]) { + apiClient = await managementSDKClient({ + host: this.cmaHost, + management_token: listOfTokens[managementTokenAlias].token, + }); + stackDetails = { + name: stackName || managementTokenAlias, + apiKey: listOfTokens[managementTokenAlias].apiKey, + token: listOfTokens[managementTokenAlias].token, + }; + } else if (managementTokenAlias) { + this.error('Provided management token alias not found in your config.!'); + } + return { + apiClient, + stackDetails, + }; + } + + /** + * fetch stack details on basis of the selected org and stack + * @param {*} managementAPIClient + * @param {string} stackAPIKey + * @param {string} org + * @returns + */ + async getStackDetails(managementAPIClient, stackAPIKey, org) { + let organization, stackDetails; + + if (!isAuthenticated()) { + this.error(config.CLI_EXPORT_CSV_ENTRIES_ERROR, { + exit: 2, + suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'], + }); + } + + if (org) { + organization = { uid: org }; + } else { + organization = await util.chooseOrganization(managementAPIClient); // prompt for organization + } + if (!stackAPIKey) { + stackDetails = await util.chooseStack(managementAPIClient, organization.uid); // prompt for stack + } else { + stackDetails = await util.chooseStack(managementAPIClient, organization.uid, stackAPIKey); + } + return stackDetails; + } + + /** + * Create a taxonomies csv file for stack and a terms csv file for associated taxonomies + * @param {string} stackName + * @param {object} stack + * @param {string} taxUID + */ + async createTaxonomyAndTermCsvFile(stackAPIClient, stackName, stack, taxUID, delimiter) { + //TODO: Temp variable to export taxonomies in importable format will replaced with flag once decided + const importableCSV = true; + const payload = { + stackAPIClient, + type: '', + limit: config.limit || 100, + }; + //check whether the taxonomy is valid or not + let taxonomies = []; + if (taxUID) { + payload['taxonomyUID'] = taxUID; + const taxonomy = await util.getTaxonomy(payload); + taxonomies.push(taxonomy); + } else { + taxonomies = await util.getAllTaxonomies(payload); + } + + if (!importableCSV) { + const formattedTaxonomiesData = util.formatTaxonomiesData(taxonomies); + if (formattedTaxonomiesData?.length) { + const fileName = `${stackName ? stackName : stack.name}_taxonomies.csv`; + util.write(this, formattedTaxonomiesData, fileName, 'taxonomies', delimiter); + } else { + cliux.print('info: No taxonomies found! Please provide a valid stack.', { color: 'blue' }); + } + + for (let index = 0; index < taxonomies?.length; index++) { + const taxonomy = taxonomies[index]; + const taxonomyUID = taxonomy?.uid; + if (taxonomyUID) { + payload['taxonomyUID'] = taxonomyUID; + const terms = await util.getAllTermsOfTaxonomy(payload); + const formattedTermsData = util.formatTermsOfTaxonomyData(terms, taxonomyUID); + const taxonomyName = taxonomy?.name ?? ''; + const termFileName = `${stackName ?? stack.name}_${taxonomyName}_${taxonomyUID}_terms.csv`; + if (formattedTermsData?.length) { + util.write(this, formattedTermsData, termFileName, 'terms', delimiter); + } else { + cliux.print(`info: No terms found for the taxonomy UID - '${taxonomyUID}'!`, { color: 'blue' }); + } + } + } + } else { + const fileName = `${stackName ?? stack.name}_taxonomies.csv`; + const { taxonomiesData, headers } = await util.createImportableCSV(payload, taxonomies); + if (taxonomiesData?.length) { + util.write(this, taxonomiesData, fileName, 'taxonomies',delimiter, headers); + } + } + } } -ExportToCsvCommand.description = `Export entries or organization users to csv using this command`; +ExportToCsvCommand.description = `Export entries, taxonomies, terms or organization users to csv using this command`; ExportToCsvCommand.examples = [ 'csdx cm:export-to-csv', @@ -339,6 +470,15 @@ ExportToCsvCommand.examples = [ '', 'Exporting Organizations Teams to CSV with org-uid and team uid', 'csdx cm:export-to-csv --action --org --team-uid --org-name ', + '', + 'Exporting taxonomies and related terms to a .CSV file with the provided taxonomy UID', + 'csdx cm:export-to-csv --action --alias --taxonomy-uid ', + '', + 'Exporting taxonomies and respective terms to a .CSV file', + 'csdx cm:export-to-csv --action --alias ', + '', + 'Exporting taxonomies and respective terms to a .CSV file with a delimiter', + 'csdx cm:export-to-csv --action --alias --delimiter ', ]; module.exports = ExportToCsvCommand; diff --git a/packages/contentstack-export-to-csv/src/util/config.js b/packages/contentstack-export-to-csv/src/util/config.js index 92c83aa1d7..7f38828711 100644 --- a/packages/contentstack-export-to-csv/src/util/config.js +++ b/packages/contentstack-export-to-csv/src/util/config.js @@ -1,11 +1,13 @@ module.exports = { + limit:100, cancelString: 'Cancel and Exit', exportEntries: 'Export entries to a .CSV file', exportUsers: "Export organization user's data to a .CSV file", exportTeams: "Export organization team's data to a .csv file", + exportTaxonomies: 'Export taxonomies to a .CSV file', adminError: "Unable to export data. Make sure you're an admin or owner of this organization", organizationNameRegex: /\'/, CLI_EXPORT_CSV_LOGIN_FAILED: "You need to login to execute this command. See: auth:login --help", CLI_EXPORT_CSV_ENTRIES_ERROR: "You need to either login or provide a management token to execute this command", - CLI_EXPORT_CSV_API_FAILED: 'Something went wrong. Please try again!' + CLI_EXPORT_CSV_API_FAILED: 'Something went wrong! Please try again' }; diff --git a/packages/contentstack-export-to-csv/src/util/index.js b/packages/contentstack-export-to-csv/src/util/index.js index 70364f65b7..04fd9b4649 100644 --- a/packages/contentstack-export-to-csv/src/util/index.js +++ b/packages/contentstack-export-to-csv/src/util/index.js @@ -4,12 +4,21 @@ const mkdirp = require('mkdirp'); const find = require('lodash/find'); const cloneDeep = require('lodash/cloneDeep'); const omit = require('lodash/omit'); +const flat = require('lodash/flatten'); const fastcsv = require('fast-csv'); const inquirer = require('inquirer'); const debug = require('debug')('export-to-csv'); const checkboxPlus = require('inquirer-checkbox-plus-prompt'); const config = require('./config.js'); -const { cliux, configHandler, HttpClient } = require('@contentstack/cli-utilities'); +const { + cliux, + configHandler, + HttpClient, + messageHandler, + managementSDKClient, + ContentstackClient, +} = require('@contentstack/cli-utilities'); + const directory = './data'; const delimeter = os.platform() === 'win32' ? '\\' : '/'; @@ -155,9 +164,7 @@ function chooseStack(managementAPIClient, orgUid, stackApiKey) { async function chooseBranch(branchList) { try { - const branches = await branchList; - - const branchesArray = branches.map((branch) => branch.uid); + const branchesArray = branchList.map((branch) => branch.uid); let _chooseBranch = [ { @@ -377,20 +384,20 @@ function exitProgram() { process.exit(); } -function sanitizeEntries(flatEntry) { +function sanitizeData(flatData) { // sanitize against CSV Injections const CSVRegex = /^[\\+\\=@\\-]/; - for (key in flatEntry) { - if (typeof flatEntry[key] === 'string' && flatEntry[key].match(CSVRegex)) { - flatEntry[key] = flatEntry[key].replace(/\"/g, "\"\""); - flatEntry[key] = `"'${flatEntry[key]}"`; - } else if (typeof flatEntry[key] === 'object') { + for (key in flatData) { + if (typeof flatData[key] === 'string' && flatData[key].match(CSVRegex)) { + flatData[key] = flatData[key].replace(/\"/g, "\"\""); + flatData[key] = `"'${flatData[key]}"`; + } else if (typeof flatData[key] === 'object') { // convert any objects or arrays to string // to store this data correctly in csv - flatEntry[key] = JSON.stringify(flatEntry[key]); + flatData[key] = JSON.stringify(flatData[key]); } } - return flatEntry; + return flatData; } function cleanEntries(entries, language, environments, contentTypeUid) { @@ -400,7 +407,7 @@ function cleanEntries(entries, language, environments, contentTypeUid) { return filteredEntries.map((entry) => { let workflow = ''; const envArr = []; - if (entry.publish_details.length) { + if (entry?.publish_details?.length) { entry.publish_details.forEach((env) => { envArr.push(JSON.stringify([environments[env['environment']], env['locale'], env['time']])); }); @@ -415,7 +422,7 @@ function cleanEntries(entries, language, environments, contentTypeUid) { } } entry = flatten(entry); - entry = sanitizeEntries(entry); + entry = sanitizeData(entry); entry['publish_details'] = envArr; entry['_workflow'] = workflow; entry['ACL'] = JSON.stringify({}); // setting ACL to empty obj @@ -443,7 +450,7 @@ function getDateTime() { return dateTime.join('_'); } -function write(command, entries, fileName, message) { +function write(command, entries, fileName, message, delimiter, headers) { // eslint-disable-next-line no-undef if (process.cwd().split(delimeter).pop() !== 'data' && !fs.existsSync(directory)) { mkdirp.sync(directory); @@ -455,7 +462,8 @@ function write(command, entries, fileName, message) { } // eslint-disable-next-line no-undef cliux.print(`Writing ${message} to file: ${process.cwd()}${delimeter}${fileName}`); - fastcsv.writeToPath(fileName, entries, { headers: true }); + if (headers?.length) fastcsv.writeToPath(fileName, entries, { headers, delimiter }); + else fastcsv.writeToPath(fileName, entries, { headers: true, delimiter }); } function startupQuestions() { @@ -465,7 +473,7 @@ function startupQuestions() { type: 'list', name: 'action', message: 'Choose Action', - choices: [config.exportEntries, config.exportUsers, config.exportTeams, 'Exit'], + choices: [config.exportEntries, config.exportUsers, config.exportTeams, config.exportTaxonomies, 'Exit'], }, ]; inquirer @@ -780,7 +788,7 @@ async function cleanTeamsData(data, managementAPIClient, org) { } } -async function exportTeams(managementAPIClient, organization, teamUid) { +async function exportTeams(managementAPIClient, organization, teamUid, delimiter) { cliux.print( `info: Exporting the ${ teamUid && organization?.name @@ -799,13 +807,13 @@ async function exportTeams(managementAPIClient, organization, teamUid) { delete team['stackRoleMapping']; }); const fileName = `${kebabize(organization.name.replace(config.organizationNameRegex, ''))}_teams_export.csv`; - write(this, modifiedTeam, fileName, ' organization Team details'); + write(this, modifiedTeam, fileName, ' organization Team details', delimiter); // exporting teams user data or a single team user data cliux.print( `info: Exporting the teams user data for ${teamUid ? `team ` + teamUid : `organisation ` + organization?.name}`, { color: 'blue' }, ); - await getTeamsDetail(allTeamsData, organization, teamUid); + await getTeamsDetail(allTeamsData, organization, teamUid, delimiter); cliux.print( `info: Exporting the stack role details for ${ teamUid ? `team ` + teamUid : `organisation ` + organization?.name @@ -813,18 +821,18 @@ async function exportTeams(managementAPIClient, organization, teamUid) { { color: 'blue' }, ); // Exporting the stack Role data for all the teams or exporting stack role data for a single team - await exportRoleMappings(managementAPIClient, allTeamsData, teamUid); + await exportRoleMappings(managementAPIClient, allTeamsData, teamUid, delimiter); } } -async function getTeamsDetail(allTeamsData, organization, teamUid) { +async function getTeamsDetail(allTeamsData, organization, teamUid, delimiter) { if (!teamUid) { const userData = await getTeamsUserDetails(allTeamsData); const fileName = `${kebabize( organization.name.replace(config.organizationNameRegex, ''), )}_team_User_Details_export.csv`; - write(this, userData, fileName, 'Team User details'); + write(this, userData, fileName, 'Team User details', delimiter); } else { const team = allTeamsData.filter((team) => team.uid === teamUid)[0]; @@ -839,11 +847,11 @@ async function getTeamsDetail(allTeamsData, organization, teamUid) { organization.name.replace(config.organizationNameRegex, ''), )}_team_${teamUid}_User_Details_export.csv`; - write(this, team.users, fileName, 'Team User details'); + write(this, team.users, fileName, 'Team User details', delimiter); } } -async function exportRoleMappings(managementAPIClient, allTeamsData, teamUid) { +async function exportRoleMappings(managementAPIClient, allTeamsData, teamUid, delimiter) { let stackRoleWithTeamData = []; let flag = false; const stackNotAdmin = []; @@ -900,7 +908,7 @@ async function exportRoleMappings(managementAPIClient, allTeamsData, teamUid) { teamUid ? `_${teamUid}` : '' }.csv`; - write(this, stackRoleWithTeamData, fileName, 'Team Stack Role details'); + write(this, stackRoleWithTeamData, fileName, 'Team Stack Role details', delimiter); } async function mapRoleWithTeams(managementAPIClient, stackRoleMapping, teamName, teamUid) { @@ -949,6 +957,214 @@ async function getTeamsUserDetails(teamsObject) { return allTeamUsers; } +/** + * fetch all taxonomies in the provided stack + * @param {object} payload + * @param {number} skip + * @param {array} taxonomies + * @returns + */ +async function getAllTaxonomies(payload, skip = 0, taxonomies = []) { + payload['type'] = 'taxonomies'; + const { items, count } = await taxonomySDKHandler(payload, skip); + if (items) { + skip += payload.limit; + taxonomies.push(...items); + if (skip >= count) { + return taxonomies; + } else { + return getAllTaxonomies(payload, skip, taxonomies); + } + } + return taxonomies; +} + +/** + * fetch taxonomy related terms + * @param {object} payload + * @param {number} skip + * @param {number} limit + * @param {array} terms + * @returns + */ +async function getAllTermsOfTaxonomy(payload, skip = 0, terms = []) { + payload['type'] = 'terms'; + const { items, count } = await taxonomySDKHandler(payload, skip); + if (items) { + skip += payload.limit; + terms.push(...items); + if (skip >= count) { + return terms; + } else { + return getAllTermsOfTaxonomy(payload, skip, terms); + } + } + return terms; +} + +/** + * Verify the existence of a taxonomy. Obtain its details if it exists and return + * @param {object} payload + * @param {string} taxonomyUID + * @returns + */ +async function getTaxonomy(payload) { + payload['type'] = 'taxonomy'; + const resp = await taxonomySDKHandler(payload); + return resp; +} + +/** + * taxonomy & term sdk handler + * @async + * @method + * @param payload + * @param skip + * @param limit + * @returns {*} Promise + */ +async function taxonomySDKHandler(payload, skip) { + const { stackAPIClient, taxonomyUID, type } = payload; + + const queryParams = { include_count: true, limit: payload.limit }; + if (skip >= 0) queryParams['skip'] = skip || 0; + + switch (type) { + case 'taxonomies': + return await stackAPIClient + .taxonomy() + .query(queryParams) + .find() + .then((data) => data) + .catch((err) => handleErrorMsg(err)); + case 'taxonomy': + return await stackAPIClient + .taxonomy(taxonomyUID) + .fetch() + .then((data) => data) + .catch((err) => handleErrorMsg(err)); + case 'terms': + queryParams['depth'] = 0; + return await stackAPIClient + .taxonomy(taxonomyUID) + .terms() + .query(queryParams) + .find() + .then((data) => data) + .catch((err) => handleErrorMsg(err)); + default: + handleErrorMsg({ errorMessage: 'Invalid module!' }); + } +} + +/** + * Change taxonomies data in required CSV headers format + * @param {array} taxonomies + * @returns + */ +function formatTaxonomiesData(taxonomies) { + if (taxonomies?.length) { + const formattedTaxonomies = taxonomies.map((taxonomy) => { + return sanitizeData({ + 'Taxonomy UID': taxonomy.uid, + Name: taxonomy.name, + Description: taxonomy.description, + }); + }); + return formattedTaxonomies; + } +} + +/** + * Modify the linked taxonomy data's terms in required CSV headers format + * @param {array} terms + * @param {string} taxonomyUID + * @returns + */ +function formatTermsOfTaxonomyData(terms, taxonomyUID) { + if (terms?.length) { + const formattedTerms = terms.map((term) => { + return sanitizeData({ + 'Taxonomy UID': taxonomyUID, + UID: term.uid, + Name: term.name, + 'Parent UID': term.parent_uid, + Depth: term.depth, + }); + }); + return formattedTerms; + } +} + +function handleErrorMsg(err) { + if (err?.errorMessage) { + cliux.print(`Error: ${err.errorMessage}`, { color: 'red' }); + } else if (err?.message) { + const errorMsg = err?.errors?.taxonomy || err?.errors?.term || err?.message; + cliux.print(`Error: ${errorMsg}`, { color: 'red' }); + } else { + console.log(err); + cliux.print(`Error: ${messageHandler.parse('CLI_EXPORT_CSV_API_FAILED')}`, { color: 'red' }); + } + process.exit(1); +} + +/** + * create an importable CSV file, to utilize with the migration script. + * @param {*} payload api request payload + * @param {*} taxonomies taxonomies data + * @returns + */ +async function createImportableCSV(payload, taxonomies) { + let taxonomiesData = []; + let headers = ['Taxonomy Name','Taxonomy UID','Taxonomy Description']; + for (let index = 0; index < taxonomies?.length; index++) { + const taxonomy = taxonomies[index]; + const taxonomyUID = taxonomy?.uid; + if (taxonomyUID) { + const sanitizedTaxonomy = sanitizeData({ + 'Taxonomy Name': taxonomy?.name, + 'Taxonomy UID': taxonomyUID, + 'Taxonomy Description': taxonomy?.description, + }); + taxonomiesData.push(sanitizedTaxonomy); + payload['taxonomyUID'] = taxonomyUID; + const terms = await getAllTermsOfTaxonomy(payload); + //fetch all parent terms + const parentTerms = terms.filter((term) => term?.parent_uid === null); + const termsData = getParentAndChildTerms(parentTerms, terms, headers); + taxonomiesData.push(...termsData) + } + } + + return {taxonomiesData, headers}; +} + +/** + * Get the parent and child terms, then arrange them hierarchically in a CSV file. + * @param {*} parentTerms list of parent terms + * @param {*} terms respective terms of taxonomies + * @param {*} headers list of csv headers include taxonomy and terms column + * @param {*} termsData parent and child terms + */ +function getParentAndChildTerms(parentTerms, terms, headers, termsData=[]) { + for (let i = 0; i < parentTerms?.length; i++) { + const parentTerm = parentTerms[i]; + const levelUID = `Term Level${parentTerm.depth} UID`; + const levelName = `Term Level${parentTerm.depth} Name`; + if (headers.indexOf(levelName) === -1) headers.push(levelName); + if (headers.indexOf(levelUID) === -1) headers.push(levelUID); + const sanitizedTermData = sanitizeData({ [levelName]: parentTerm.name, [levelUID]: parentTerm.uid }); + termsData.push(sanitizedTermData); + //fetch all sibling terms + const newParents = terms.filter((term) => term.parent_uid === parentTerm.uid); + if (newParents?.length) { + getParentAndChildTerms(newParents, terms, headers, termsData); + } + } + return termsData; +} + module.exports = { chooseOrganization: chooseOrganization, chooseStack: chooseStack, @@ -977,4 +1193,11 @@ module.exports = { formatError: formatError, exportOrgTeams: exportOrgTeams, exportTeams: exportTeams, + getAllTaxonomies, + getAllTermsOfTaxonomy, + formatTaxonomiesData, + formatTermsOfTaxonomyData, + getTaxonomy, + getStacks, + createImportableCSV, }; diff --git a/packages/contentstack-export-to-csv/test/mock-data/common.mock.json b/packages/contentstack-export-to-csv/test/mock-data/common.mock.json index 48d9f7630a..24ad077062 100644 --- a/packages/contentstack-export-to-csv/test/mock-data/common.mock.json +++ b/packages/contentstack-export-to-csv/test/mock-data/common.mock.json @@ -108,34 +108,24 @@ "name": "test org 2" }, { - "uid": "uid2", - "email": "test@gmail.abc", - "user_uid": "user2", - "org_uid": "test-uid-2", - "invited_by": "user3", - "invited_at": "2023-08-21T11:08:41.038Z", - "status": "accepted", - "acceptance_token": "thor", - "created_at": "2023-08-21T11:08:41.036Z", - "updated_at": "2023-08-21T11:09:11.342Z", - "urlPath": "/user", - "organizations": [ - { - "uid": "org_uid_1_teams", - "name": "Teams Org", - "org_roles": [ - { - "uid": "role1", - "name": "Admin", - "description": "Admin Role", - "org_uid": "test-uid-1", - "admin": true, - "default": true - } - ] - } + "organizations": [ + { + "uid": "org_uid_1_teams", + "name": "Teams Org", + "org_roles": [ + { + "uid": "role1", + "name": "Admin", + "description": "Admin Role", + "org_uid": "test-uid-1", + "admin": true, + "default": true + } + ] + } ] - } + + } ], "roles": [ { @@ -281,10 +271,7 @@ { "_id": "team_1_uid", "name": "Test_Team_1", - - - - + "organizationUid": "org_uid_1_teams", "users": [], "stackRoleMapping": [ @@ -294,7 +281,7 @@ } ], "organizationRole": "org_role_uid_1", - + "uid": "team_1_uid", "createdByUserName": "team_creator", "updatedByUserName": "team_creator" @@ -303,98 +290,88 @@ } }, "roless": { - "roles": [{ - "name": "Developer", - "description": "Developer can perform all Content Manager's actions, view audit logs, create roles, invite users, manage content types, languages, and environments.", - "uid": "stack_role_uid_3", - "users": [], - "owner": "ownerEmail@domain.com", - "stack": { - - - "uid": "stack_uid", - "name": "CLI Test", - "org_uid": "org_uid_1_teams", - "api_key": "stack_api_key", - "master_locale": "en-us", - - "owner_uid": "team_owner_uid", - "user_uids": ["team_owner_uid"] - }, - "SYS_ACL": {} - }, - { - "name": "Content Manager", - "description": "Content Managers can view all content types, manage entries and assets. They cannot edit content types or access stack settings.", - "uid": "stack_role_uid_1", - - "updated_by": "team_owner_uid", - - - "owner": "ownerEmail@domain.com", - "stack": { - - - "uid": "stack_uid", - "name": "CLI Test", - "org_uid": "org_uid_1_teams", - "api_key": "stack_api_key", - "master_locale": "en-us", - - "owner_uid": "team_owner_uid", - "user_uids": ["team_owner_uid"] + "roles": [ + { + "name": "Developer", + "description": "Developer can perform all Content Manager's actions, view audit logs, create roles, invite users, manage content types, languages, and environments.", + "uid": "stack_role_uid_3", + "users": [], + "owner": "ownerEmail@domain.com", + "stack": { + "uid": "stack_uid", + "name": "CLI Test", + "org_uid": "org_uid_1_teams", + "api_key": "stack_api_key", + "master_locale": "en-us", + + "owner_uid": "team_owner_uid", + "user_uids": ["team_owner_uid"] + }, + "SYS_ACL": {} }, - "SYS_ACL": {} - }, - { - "name": "Admin", - "description": "Admin can perform all actions and manage all settings of the stack, except the ability to delete or transfer ownership of the stack.", - "uid": "role_uid_1", - - - - "updated_at": "2023-10-26T04:44:51.529Z", - "users": [], - "owner": "ownerEmail@domain.com", - "stack": { - - - "uid": "stack_uid", - "name": "CLI Test", - "org_uid": "org_uid_1_teams", - "api_key": "stack_api_key", - "master_locale": "en-us", - - "owner_uid": "team_owner_uid", - "user_uids": ["team_owner_uid"] + { + "name": "Content Manager", + "description": "Content Managers can view all content types, manage entries and assets. They cannot edit content types or access stack settings.", + "uid": "stack_role_uid_1", + + "updated_by": "team_owner_uid", + + "owner": "ownerEmail@domain.com", + "stack": { + "uid": "stack_uid", + "name": "CLI Test", + "org_uid": "org_uid_1_teams", + "api_key": "stack_api_key", + "master_locale": "en-us", + + "owner_uid": "team_owner_uid", + "user_uids": ["team_owner_uid"] + }, + "SYS_ACL": {} }, - "SYS_ACL": {} - }, - { - "name": "Custom_Role_1", - "description": "", - "users": [], - "uid": "stack_role_uid_2", - - "updated_by": "team_owner_uid", - - - "owner": "ownerEmail@domain.com", - "stack": { - - - "uid": "stack_uid", - "name": "CLI Test", - "org_uid": "org_uid_1_teams", - "api_key": "stack_api_key", - "master_locale": "en-us", - - "owner_uid": "team_owner_uid", - "user_uids": ["team_owner_uid"] + { + "name": "Admin", + "description": "Admin can perform all actions and manage all settings of the stack, except the ability to delete or transfer ownership of the stack.", + "uid": "role_uid_1", + + "updated_at": "2023-10-26T04:44:51.529Z", + "users": [], + "owner": "ownerEmail@domain.com", + "stack": { + "uid": "stack_uid", + "name": "CLI Test", + "org_uid": "org_uid_1_teams", + "api_key": "stack_api_key", + "master_locale": "en-us", + + "owner_uid": "team_owner_uid", + "user_uids": ["team_owner_uid"] + }, + "SYS_ACL": {} }, - "SYS_ACL": {} - } -]}, + { + "name": "Custom_Role_1", + "description": "", + "users": [], + "uid": "stack_role_uid_2", + + "updated_by": "team_owner_uid", + + "owner": "ownerEmail@domain.com", + "stack": { + "uid": "stack_uid", + "name": "CLI Test", + "org_uid": "org_uid_1_teams", + "api_key": "stack_api_key", + "master_locale": "en-us", + + "owner_uid": "team_owner_uid", + "user_uids": ["team_owner_uid"] + }, + "SYS_ACL": {} + } + ] + }, "org_roles": { "roles": [ { diff --git a/packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.js b/packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.js index 26f421b4d6..fb5c12e36f 100644 --- a/packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.js +++ b/packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.js @@ -1,212 +1,412 @@ -/* eslint-disable no-undef */ -const { describe, it, beforeEach, afterEach } = require('mocha'); -const ExportToCsvCommand = require('../../../src/commands/cm/export-to-csv'); -const { stub, assert } = require('sinon'); -const { config } = require('dotenv'); -const inquirer = require('inquirer'); -const { cliux } = require('@contentstack/cli-utilities'); -const mockData = require('../../mock-data/common.mock.json'); -const { test, expect } = require('@oclif/test'); const fs = require('fs'); const mkdirp = require('mkdirp'); +const { test, expect } = require('@oclif/test'); +const { join } = require('path'); +const { PassThrough } = require('stream'); +const inquirer = require('inquirer'); +const mockData = require('../../mock-data/common.mock.json'); const { configHandler } = require('@contentstack/cli-utilities'); -const { kebabize } = require('../../../src/util/index'); + const { cma } = configHandler.get('region'); -const { PassThrough } = require('stream'); -describe('Testing the teams support in cli export-to-csv',()=>{ +describe('export-to-csv with action taxonomies', () => { + describe('Create taxonomies & terms csv file with all flags including taxonomy uid', () => { + test + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .nock(cma, (api) => { + api + .get(`/v3/stacks?&query={"org_uid":"${mockData.organizations[0].uid}"}`) + .reply(200, { stacks: mockData.stacks }); + }) + .nock(cma, (api) => { + api + .get(`/v3/taxonomies/${mockData.taxonomiesResp.taxonomies[0].uid}`) + .reply(200, { taxonomy: mockData.taxonomiesResp.taxonomies[0] }); + }) + .nock(cma, (api) => { + api + .get( + `/v3/taxonomies/${mockData.taxonomiesResp.taxonomies[0].uid}/terms?include_count=true&limit=100&skip=0&depth=0`, + ) + .reply(200, mockData.termsResp); + }) + .command([ + 'cm:export-to-csv', + '--action', + 'taxonomies', + '--taxonomy-uid', + mockData.taxonomiesResp.taxonomies[0].uid, + '--stack-api-key', + mockData.stacks[0].api_key, + '--org', + mockData.organizations[0].uid, + ]) + .it('CSV file should be created'); + }); + + describe('Create taxonomies & terms csv file with all flags excluding taxonomy uid', () => { + test + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .nock(cma, (api) => { + api + .get(`/v3/stacks?&query={"org_uid":"${mockData.organizations[0].uid}"}`) + .reply(200, { stacks: mockData.stacks }); + }) + .nock(cma, (api) => { + api.get('/v3/taxonomies?include_count=true&limit=100&skip=0').reply(200, mockData.taxonomiesResp); + }) + .nock(cma, (api) => { + api + .get( + `/v3/taxonomies/${mockData.taxonomiesResp.taxonomies[0].uid}/terms?include_count=true&limit=100&skip=0&depth=0`, + ) + .reply(200, mockData.termsResp); + }) + .nock(cma, (api) => { + api + .get( + `/v3/taxonomies/${mockData.taxonomiesResp.taxonomies[1].uid}/terms?include_count=true&limit=100&skip=0&depth=0`, + ) + .reply(200, { terms: [], count: 0 }); + }) + .command([ + 'cm:export-to-csv', + '--action', + 'taxonomies', + '--stack-api-key', + mockData.stacks[0].api_key, + '--org', + mockData.organizations[0].uid, + ]) + .it('file should be created'); + }); + + describe('Create taxonomies & terms csv file with prompt', () => { + test + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .stub(inquirer, 'registerPrompt', () => {}) + .stub(inquirer, 'prompt', () => { + return Promise.resolve({ + action: 'taxonomies', + chosenOrg: mockData.organizations[0].name, + chosenStack: mockData.stacks[0].name, + }); + }) + .nock(cma, (api) => { + api + .get(`/v3/stacks?&query={"org_uid":"${mockData.organizations[0].uid}"}`) + .reply(200, { stacks: mockData.stacks }); + }) + .nock(cma, (api) => { + api.get(`/v3/organizations?limit=100`).reply(200, { organizations: mockData.organizations }); + }) + .nock(cma, (api) => { + api + .get(`/v3/taxonomies/${mockData.taxonomiesResp.taxonomies[0].uid}`) + .reply(200, { taxonomy: mockData.taxonomiesResp.taxonomies[0] }); + }) + .nock(cma, (api) => { + api + .get( + `/v3/taxonomies/${mockData.taxonomiesResp.taxonomies[0].uid}/terms?include_count=true&limit=100&skip=0&depth=0`, + ) + .reply(200, mockData.termsResp); + }) + .command(['cm:export-to-csv', '--taxonomy-uid', 'taxonomy_uid_1']) + .it('CSV file should be created'); + }); +}); + +describe('export-to-csv with action entries', () => { + describe('Create entries csv file with flags', () => { + test + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .nock(cma, (api) => { + api + .get(`/v3/stacks?&query={"org_uid":"${mockData.organizations[0].uid}"}`) + .reply(200, { stacks: mockData.stacks }); + }) + .nock(cma, (api) => { + api.get('/v3/environments').reply(200, { environments: mockData.environments }); + }) + .nock(cma, (api) => { + api.get('/v3/content_types?count=true').reply(200, { content_types: 2 }); + }) + .nock(cma, (api) => { + api.get('/v3/content_types').reply(200, { content_types: mockData.contentTypes }); + }) + .nock(cma, (api) => { + api + .get( + `/v3/content_types/${mockData.contentTypes[0].uid}/entries?include_publish_details=true&locale=en1&count=true`, + ) + .reply(200, { entries: 2 }); + }) + .nock(cma, (api) => { + api + .get( + `/v3/content_types/${mockData.contentTypes[0].uid}/entries?include_publish_details=true&locale=en1&skip=0&limit=100&include_workflow=true`, + ) + .reply(200, { entries: mockData.entry }); + }) + .command([ + 'cm:export-to-csv', + '--action', + 'entries', + '--stack-api-key', + mockData.stacks[0].api_key, + '--org', + mockData.organizations[0].uid, + '--branch', + mockData.branch.uid, + '--locale', + 'en1', + '--content-type', + mockData.contentTypes[0].uid, + ]) + .it('Entries CSV file should be created'); + }); + + describe('Create entries csv file with prompt', () => { + test + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .stub(inquirer, 'registerPrompt', () => {}) + .stub(inquirer, 'prompt', () => { + return Promise.resolve({ + action: 'entries', + chosenOrg: mockData.organizations[0].name, + chosenLanguage: mockData.locales[0].name, + chosenStack: mockData.stacks[0].name, + chosenContentTypes: [mockData.contentTypes[0].uid], + branch: mockData.branch.uid, + }); + }) + .nock(cma, (api) => { + api.get(`/v3/organizations?limit=100`).reply(200, { organizations: mockData.organizations }); + }) + .nock(cma, (api) => { + api + .get(`/v3/stacks?&query={"org_uid":"${mockData.organizations[0].uid}"}`) + .reply(200, { stacks: mockData.stacks }); + }) + .nock(cma, (api) => { + api.get('/v3/environments').reply(200, { environments: mockData.environments }); + }) + .nock(cma, (api) => { + api.get('/v3/locales').reply(200, { locales: mockData.locales }); + }) + .nock(cma, (api) => { + api.get('/v3/stacks/branches').reply(200, { branches: mockData.branch }); + }) + .nock(cma, (api) => { + api.get('/v3/content_types?count=true').reply(200, { content_types: 2 }); + }) + .nock(cma, (api) => { + api.get('/v3/content_types?skip=0&include_branch=true').reply(200, { content_types: mockData.contentTypes }); + }) + .nock(cma, (api) => { + api + .get( + `/v3/content_types/${mockData.contentTypes[0].uid}/entries?include_publish_details=true&locale=${mockData.locales[0].code}&count=true`, + ) + .reply(200, { entries: 1 }); + }) + .nock(cma, (api) => { + api + .get( + `/v3/content_types/${mockData.contentTypes[0].uid}/entries?include_publish_details=true&locale=${mockData.locales[0].code}&skip=0&limit=100&include_workflow=true`, + ) + .reply(200, { entries: mockData.entry }); + }) + .command(['cm:export-to-csv']) + .it('Entries CSV file should be created with prompt'); + }); +}); + +describe('export-to-csv with action users', () => { + describe('Export users csv file with flags', () => { + test + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .nock(cma, (api) => { + api.get('/v3/user?include_orgs_roles=true').reply(200, { user: mockData.users[0] }).persist(); + }) + .nock(cma, (api) => { + api.get(`/v3/organizations/${mockData.organizations[0].uid}/roles`).reply(200, { roles: mockData.roles }); + }) + .nock(cma, (api) => { + api + .get(`/v3/organizations/${mockData.organizations[0].uid}/share?skip=0&page=1&limit=100`) + .reply(200, { users: mockData.users }); + }) + .command(['cm:export-to-csv', '--action', 'users', '--org', mockData.organizations[0].uid]) + .it('Users csv file should be successfully created'); + }); + + describe('Export users csv file with prompt', () => { + test + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .stub(inquirer, 'registerPrompt', () => {}) + .stub(inquirer, 'prompt', () => { + return Promise.resolve({ action: 'users', chosenOrg: mockData.organizations[0].name }); + }) + .nock(cma, (api) => { + api.get(`/v3/organizations?limit=100`).reply(200, { organizations: mockData.organizations }); + }) + .nock(cma, (api) => { + api.get('/v3/user?include_orgs_roles=true').reply(200, { user: mockData.users[0] }).persist(); + }) + .nock(cma, (api) => { + api.get(`/v3/organizations/${mockData.organizations[0].uid}/roles`).reply(200, { roles: mockData.roles }); + }) + .nock(cma, (api) => { + api + .get(`/v3/organizations/${mockData.organizations[0].uid}/share?skip=0&page=1&limit=100`) + .reply(200, { users: mockData.users }); + }) + .command(['cm:export-to-csv']) + .it('Users csv file should be successfully created'); + }); +}); + +describe('Testing the teams support in cli export-to-csv', () => { describe('Testing Teams Command with using org flag and team flag', () => { - test - .stdout({ print: process.env.PRINT === 'true' || true }) + test + .stdout({ print: process.env.PRINT === 'true' || false }) .stub(fs, 'createWriteStream', () => new PassThrough()) .stub(mkdirp, 'sync', () => {}) .stub(process, 'chdir', () => {}) .nock(cma, (api) => { - api - .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) - .reply(200, mockData.Teams.allTeams); - }) - .nock(cma, (api) => { - api - .get(`/v3/organizations/org_uid_1_teams/roles`) - .reply(200, mockData.org_roles); - }) - .nock(cma, (api) => { - api - .get(`/v3/roles`) - .reply(200, {roles: mockData.roless.roles}); - }) - .command([ - 'cm:export-to-csv', - '--action', - 'teams', - '--org', - 'org_uid_1_teams', - '--team-uid', - 'team_1_uid' - ]) - .it('CSV file should be created'); + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api.get(`/v3/organizations/org_uid_1_teams/roles`).reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api.get(`/v3/roles`).reply(200, { roles: mockData.roless.roles }); + }) + .command(['cm:export-to-csv', '--action', 'teams', '--org', 'org_uid_1_teams', '--team-uid', 'team_1_uid']) + .it('CSV file should be created'); }); + describe('Testing Teams Command with using org flag and team flag and there are no teams', () => { - test - .stdout({ print: process.env.PRINT === 'true' || true }) + test + .stdout({ print: process.env.PRINT === 'true' || false }) .stub(fs, 'createWriteStream', () => new PassThrough()) .stub(mkdirp, 'sync', () => {}) .stub(process, 'chdir', () => {}) .nock(cma, (api) => { - api - .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) - .reply(200, mockData.Teams.allTeams); - }) - .nock(cma, (api) => { - api - .get(`/v3/organizations/org_uid_1_teams/roles`) - .reply(200, mockData.org_roles); - }) - .nock(cma, (api) => { - api - .get(`/v3/roles`) - .reply(200, {roles: mockData.roless.roles}); - }) - .command([ - 'cm:export-to-csv', - '--action', - 'teams', - '--org', - 'org_uid_1_teams', - '--team-uid', - 'team_1_uid' - ]) - .it('CSV file should be created'); + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api.get(`/v3/organizations/org_uid_1_teams/roles`).reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api.get(`/v3/roles`).reply(200, { roles: mockData.roless.roles }); + }) + .command(['cm:export-to-csv', '--action', 'teams', '--org', 'org_uid_1_teams', '--team-uid', 'team_1_uid']) + .it('CSV file should be created'); }); + describe('Testing Teams Command with using org flag', () => { - test - .stdout({ print: process.env.PRINT === 'true' || true }) + test + .stdout({ print: process.env.PRINT === 'true' || false }) .stub(fs, 'createWriteStream', () => new PassThrough()) .stub(mkdirp, 'sync', () => {}) .stub(process, 'chdir', () => {}) .nock(cma, (api) => { - api - .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) - .reply(200, mockData.Teams.allTeams); - }) - .nock(cma, (api) => { - api - .get(`/v3/organizations/org_uid_1_teams/roles`) - .reply(200, mockData.org_roles); - }) - .nock(cma, (api) => { - api - .get(`/v3/roles`) - .reply(200, {roles: mockData.roless.roles}); - }) - .command([ - 'cm:export-to-csv', - '--action', - 'teams', - '--org', - 'org_uid_1_teams' - ]) - .it('CSV file should be created'); + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api.get(`/v3/organizations/org_uid_1_teams/roles`).reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api.get(`/v3/roles`).reply(200, { roles: mockData.roless.roles }); + }) + .command(['cm:export-to-csv', '--action', 'teams', '--org', 'org_uid_1_teams']) + .it('CSV file should be created'); }); + describe('Testing Teams Command with prompt', () => { - test - .stdout({ print: process.env.PRINT === 'true' || true }) + test + .stdout({ print: process.env.PRINT === 'true' || false }) .stub(fs, 'createWriteStream', () => new PassThrough()) .stub(mkdirp, 'sync', () => {}) .stub(process, 'chdir', () => {}) .stub(inquirer, 'registerPrompt', () => {}) .stub(inquirer, 'prompt', () => { - return Promise.resolve({ action: 'teams', chosenOrg: mockData.organizations[2].name }); - }) - .nock(cma, (api) => { - api.get('/v3/user?include_orgs_roles=true').reply(200, { user: mockData.users[2] }); - }) - .nock(cma, (api) => { - api - .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) - .reply(200, mockData.Teams.allTeams); - }) - .nock(cma, (api) => { - api - .get(`/v3/organizations/org_uid_1_teams/roles`) - .reply(200, mockData.org_roles); - }) - .nock(cma, (api) => { - api - .get(`/v3/roles`) - .reply(200, {roles: mockData.roless.roles}); - }) - .command([ - 'cm:export-to-csv' - ]) - .it('CSV file should be created'); + return Promise.resolve({ action: 'teams', chosenOrg: mockData.organizations[2].name }); + }) + .nock(cma, (api) => { + api.get('/v3/user?include_orgs_roles=true').reply(200, { user: mockData.users[2] }); + }) + .nock(cma, (api) => { + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api.get(`/v3/organizations/org_uid_1_teams/roles`).reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api.get(`/v3/roles`).reply(200, { roles: mockData.roless.roles }); + }) + .command(['cm:export-to-csv']) + .it('CSV file should be created'); }); + describe('Testing Teams Command with prompt and no stack role data', () => { test - .stdout({ print: process.env.PRINT === 'true' || true }) - .stub(fs, 'createWriteStream', () => new PassThrough()) - .stub(mkdirp, 'sync', () => {}) - .stub(process, 'chdir', () => {}) - .stub(inquirer, 'registerPrompt', () => {}) - .stub(inquirer, 'prompt', () => { - return Promise.resolve({ action: 'teams', chosenOrg: mockData.organizations[2].name, chooseExport: 'yes'}); - }) + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .stub(inquirer, 'registerPrompt', () => {}) + .stub(inquirer, 'prompt', () => { + return Promise.resolve({ action: 'teams', chosenOrg: mockData.organizations[2].name, chooseExport: 'yes' }); + }) .nock(cma, (api) => { api.get('/v3/user?include_orgs_roles=true').reply(200, { user: mockData.users[2] }); }) - .nock(cma, (api) => { + .nock(cma, (api) => { api .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) .reply(200, mockData.Teams.allTeams); }) .nock(cma, (api) => { - api - .get(`/v3/organizations/org_uid_1_teams/roles`) - .reply(200, mockData.org_roles); + api.get(`/v3/organizations/org_uid_1_teams/roles`).reply(200, mockData.org_roles); }) .nock(cma, (api) => { - api - .get(`/v3/roles`) - .reply(200, {roles: {}}); + api.get(`/v3/roles`).reply(200, { roles: {} }); }) - .command([ - 'cm:export-to-csv' - ]) - .it('CSV file should be created'); -}); -describe('Testing Teams Command with prompt and no stack role data', () => { - test - .stdout({ print: process.env.PRINT === 'true' || true }) - .stub(fs, 'createWriteStream', () => new PassThrough()) - .stub(mkdirp, 'sync', () => {}) - .stub(process, 'chdir', () => {}) - .stub(inquirer, 'registerPrompt', () => {}) - .stub(inquirer, 'prompt', () => { - return Promise.resolve({ action: 'teams', chosenOrg: mockData.organizations[2].name, chooseExport: 'no'}); - }) - .nock(cma, (api) => { - api.get('/v3/user?include_orgs_roles=true').reply(200, { user: mockData.users[2] }); - }) - .nock(cma, (api) => { - api - .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) - .reply(200, mockData.Teams.allTeams); - }) - .nock(cma, (api) => { - api - .get(`/v3/organizations/org_uid_1_teams/roles`) - .reply(200, mockData.org_roles); - }) - .nock(cma, (api) => { - api - .get(`/v3/roles`) - .reply(200, {roles: {}}); - }) - .command([ - 'cm:export-to-csv' - ]) - .it('No CSV file should be created'); -}); + .command(['cm:export-to-csv']) + .it('CSV file should be created'); + }); }); \ No newline at end of file diff --git a/packages/contentstack-export-to-csv/test/util/common-utils.test.js b/packages/contentstack-export-to-csv/test/util/common-utils.test.js new file mode 100644 index 0000000000..042ff4b84b --- /dev/null +++ b/packages/contentstack-export-to-csv/test/util/common-utils.test.js @@ -0,0 +1,52 @@ +const { fancy } = require('fancy-test'); +const { test, expect } = require('@oclif/test'); +const inquirer = require('inquirer'); +const { cliux, configHandler, ContentstackClient, managementSDKClient } = require('@contentstack/cli-utilities'); + +const mockData = require('../mock-data/common.mock.json'); +const { getStacks, chooseBranch } = require('../../src/util/index'); + +const { cma } = configHandler.get('region'); + +describe('common utils', () => { + let managementSdk; + before(async () => { + managementSdk = await managementSDKClient({ + host: cma.replace('https://', ''), + }); + }); + + describe('chooseStack', () => { + describe('choose stack from list of stacks', () => { + fancy + .nock(cma, (api) => + api + .get(`/v3/stacks?&query={"org_uid":"${mockData.organizations[0].uid}"}`) + .reply(200, { stacks: mockData.stacks }), + ) + .stub(inquirer, 'prompt', () => { + return Promise.resolve({ + chosenStack: mockData.stacks[0].name, + }); + }) + .it('Returns list of stacks', async () => { + await getStacks(managementSdk, mockData.organizations[0].uid); + }); + }); + }); + + describe('chooseBranch', () => { + describe('choose branch from list of branch', () => { + fancy + .stub(inquirer, 'prompt', () => { + return Promise.resolve({ + branch: mockData.branch.uid, + }); + }) + .it('Returns list of stacks', async () => { + const { branch } = await chooseBranch([mockData.branch]); + expect(branch).to.equal(mockData.branch.uid); + }); + }); + }); +}); diff --git a/packages/contentstack-export/package.json b/packages/contentstack-export/package.json index 1f5df1360e..40152dce90 100644 --- a/packages/contentstack-export/package.json +++ b/packages/contentstack-export/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-export", "description": "Contentstack CLI plugin to export content from stack", - "version": "1.9.4", + "version": "1.10.0", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { diff --git a/packages/contentstack-export/src/config/index.ts b/packages/contentstack-export/src/config/index.ts index ceb09c1cb0..3f166a79d7 100644 --- a/packages/contentstack-export/src/config/index.ts +++ b/packages/contentstack-export/src/config/index.ts @@ -10,7 +10,6 @@ const config: DefaultConfig = { 'https://eu-api.contentstack.com': 'https://eu-developerhub-api.contentstack.com', 'https://azure-na-api.contentstack.com': 'https://azure-na-developerhub-api.contentstack.com', 'https://azure-eu-api.contentstack.com': 'https://azure-eu-developerhub-api.contentstack.com', - 'https://stag-api.csnonprod.com': 'https://stag-developerhub-api.csnonprod.com', }, // use below hosts for eu region // host:'https://eu-api.contentstack.com/v3', @@ -26,6 +25,7 @@ const config: DefaultConfig = { 'environments', 'extensions', 'webhooks', + 'taxonomies', 'global-fields', 'content-types', 'custom-roles', @@ -159,6 +159,16 @@ const config: DefaultConfig = { dirName: 'marketplace_apps', fileName: 'marketplace_apps.json', }, + taxonomies: { + dirName: 'taxonomies', + fileName: 'taxonomies.json', + invalidKeys: ['updated_at', 'created_by', 'updated_by', 'stackHeaders', 'urlPath'], + }, + terms: { + dirName: 'terms', + fileName: 'terms.json', + invalidKeys: ['updated_at', 'created_by', 'updated_by', 'stackHeaders', 'urlPath'], + }, }, languagesCode: [ 'af-za', @@ -389,6 +399,7 @@ const config: DefaultConfig = { writeConcurrency: 5, developerHubBaseUrl: '', marketplaceAppEncryptionKey: 'nF2ejRQcTv', + onlyTSModules: ['taxonomies'], }; export default config; diff --git a/packages/contentstack-export/src/export/module-exporter.ts b/packages/contentstack-export/src/export/module-exporter.ts index 47d130b743..4daaa797f9 100644 --- a/packages/contentstack-export/src/export/module-exporter.ts +++ b/packages/contentstack-export/src/export/module-exporter.ts @@ -70,11 +70,14 @@ class ModuleExporter { moduleName, }); } else { - exportedModuleResponse = await startJSModuleExport({ - stackAPIClient: this.stackAPIClient, - exportConfig: this.exportConfig, - moduleName, - }); + //NOTE - new modules support only ts + if (this.exportConfig.onlyTSModules.indexOf(moduleName) === -1) { + exportedModuleResponse = await startJSModuleExport({ + stackAPIClient: this.stackAPIClient, + exportConfig: this.exportConfig, + moduleName, + }); + } } // set master locale to config diff --git a/packages/contentstack-export/src/export/modules/taxonomies.ts b/packages/contentstack-export/src/export/modules/taxonomies.ts new file mode 100644 index 0000000000..7fbe221247 --- /dev/null +++ b/packages/contentstack-export/src/export/modules/taxonomies.ts @@ -0,0 +1,182 @@ +import omit from 'lodash/omit'; +import keys from 'lodash/keys'; +import isEmpty from 'lodash/isEmpty'; +import { resolve as pResolve } from 'node:path'; + +import BaseClass from './base-class'; +import { log, fsUtil } from '../../utils'; +import { TaxonomiesConfig, TermsConfig, ModuleClassParams } from '../../types'; + +export default class ExportTaxonomies extends BaseClass { + private taxonomies: Record>; + private terms: Record[]; + private taxonomiesConfig: TaxonomiesConfig; + private termsConfig: TermsConfig; + private qs: { + include_count: boolean; + skip: number; + asc: string; + depth?: number; + }; + public taxonomiesFolderPath: string; + public termsFolderPath: string; + + constructor({ exportConfig, stackAPIClient }: ModuleClassParams) { + super({ exportConfig, stackAPIClient }); + this.taxonomies = {}; + this.terms = []; + this.taxonomiesConfig = exportConfig.modules.taxonomies; + this.termsConfig = exportConfig.modules.terms; + this.qs = { include_count: true, skip: 0, asc: 'created_at' }; + } + + async start(): Promise { + log(this.exportConfig, 'Starting taxonomies export', 'info'); + + //create taxonomies and terms folder in data directory path + this.taxonomiesFolderPath = pResolve( + this.exportConfig.data, + this.exportConfig.branchName || '', + this.taxonomiesConfig.dirName, + ); + await fsUtil.makeDirectory(this.taxonomiesFolderPath); + this.termsFolderPath = pResolve(this.taxonomiesFolderPath, this.termsConfig.dirName); + await fsUtil.makeDirectory(this.termsFolderPath); + + //fetch all taxonomies and write into taxonomies folder + await this.getAllTaxonomies(); + if (this.taxonomies === undefined || isEmpty(this.taxonomies)) { + log(this.exportConfig, 'No taxonomies found!', 'info'); + return; + } else { + fsUtil.writeFile(pResolve(this.taxonomiesFolderPath, this.taxonomiesConfig.fileName), this.taxonomies); + log(this.exportConfig, 'All taxonomies exported successfully!', 'success'); + } + + //fetch all terms of respective and write into taxonomies/terms folder + await this.getAllTerms(); + } + + /** + * fetch all taxonomies in the provided stack + * @param {number} skip + * @returns {Promise} + */ + async getAllTaxonomies(skip = 0): Promise { + if (skip) { + this.qs.skip = skip; + } + await this.stack + .taxonomy() + .query(this.qs) + .find() + .then(async (data: any) => { + const { items, count } = data; + const taxonomiesCount = count !== undefined ? count : items?.length; + + if (items?.length) { + this.sanitizeTaxonomiesAttribs(items); + skip += this.taxonomiesConfig.limit || 100; + if (skip >= taxonomiesCount) { + return; + } + return await this.getAllTaxonomies(skip); + } + }) + .catch((error: any) => { + this.handleErrorMsg(error); + }); + } + + /** + * remove invalid keys and write data into taxonomies + * @function sanitizeTaxonomiesAttribs + * @param taxonomies + */ + sanitizeTaxonomiesAttribs(taxonomies: Record[]) { + for (let index = 0; index < taxonomies?.length; index++) { + const taxonomyUID = taxonomies[index].uid; + this.taxonomies[taxonomyUID] = omit(taxonomies[index], this.taxonomiesConfig.invalidKeys); + log(this.exportConfig, `'${taxonomyUID}' taxonomy exported successfully!`, 'success'); + } + } + + /** + * fetch all terms of respective taxonomy and write it into -terms file + * @returns {Promise} + */ + async getAllTerms() { + const taxonomiesUID = keys(this.taxonomies) || []; + this.qs.depth = 0; + + for (let index = 0; index < taxonomiesUID?.length; index++) { + const taxonomyUID = taxonomiesUID[index]; + this.terms = []; + await this.fetchTermsOfTaxonomy(taxonomyUID); + if (!this.terms?.length) { + log(this.exportConfig, `No terms found for taxonomy - '${taxonomyUID}'!`, 'info'); + } else { + fsUtil.writeFile(pResolve(this.termsFolderPath, `${taxonomyUID}-${this.termsConfig.fileName}`), this.terms); + log(this.exportConfig, `Terms from taxonomy '${taxonomyUID}' exported successfully!`, 'success'); + } + } + log(this.exportConfig, `All the terms exported successfully!`, 'success'); + } + + /** + * fetch all terms of the provided taxonomy uid + * @async + * @param {string} taxonomyUID + * @param {number} skip + * @returns {Promise} + */ + async fetchTermsOfTaxonomy(taxonomyUID: string, skip = 0): Promise { + if (skip) { + this.qs.skip = skip; + } + await this.stack + .taxonomy(taxonomyUID) + .terms() + .query(this.qs) + .find() + .then(async (data: any) => { + const { items, count } = data; + const termsCount = count !== undefined ? count : items?.length; + + if (items?.length) { + this.sanitizeTermsAttribs(items); + skip += this.taxonomiesConfig.limit || 100; + if (skip >= termsCount) { + return; + } + return await this.fetchTermsOfTaxonomy(taxonomyUID, skip); + } + }) + .catch((error: any) => { + this.handleErrorMsg(error); + }); + } + + /** + * remove invalid keys and write data into taxonomies + * @function sanitizeTaxonomiesAttribs + * @param terms + */ + sanitizeTermsAttribs(terms: Record[]) { + for (let index = 0; index < terms?.length; index++) { + const term = omit(terms[index], this.termsConfig.invalidKeys); + this.terms.push(term) + } + } + + handleErrorMsg(err: any) { + if (err?.errorMessage) { + log(this.exportConfig, `Failed to export! ${err.errorMessage}`, 'error'); + } else if (err?.message) { + const errorMsg = err?.errors?.taxonomy || err?.errors?.term || err?.message; + log(this.exportConfig, `Failed to export! ${errorMsg}`, 'error'); + }else{ + log(this.exportConfig, `Failed to export! ${err}`, 'error'); + } + } +} diff --git a/packages/contentstack-export/src/types/default-config.ts b/packages/contentstack-export/src/types/default-config.ts index e3f92cbc63..b63bba06c1 100644 --- a/packages/contentstack-export/src/types/default-config.ts +++ b/packages/contentstack-export/src/types/default-config.ts @@ -144,6 +144,18 @@ export default interface DefaultConfig { fileName: string; requiredKeys: string[]; }; + taxonomies: { + dirName: string; + fileName: string; + invalidKeys: string[]; + dependencies?: Modules[]; + }; + terms: { + dirName: string; + fileName: string; + invalidKeys: string[]; + dependencies?: Modules[]; + }; }; languagesCode: string[]; apis: { @@ -165,4 +177,5 @@ export default interface DefaultConfig { writeConcurrency: number; developerHubBaseUrl: string; marketplaceAppEncryptionKey: string; + onlyTSModules: string[]; } diff --git a/packages/contentstack-export/src/types/index.ts b/packages/contentstack-export/src/types/index.ts index ccf95c0918..ee5121491b 100644 --- a/packages/contentstack-export/src/types/index.ts +++ b/packages/contentstack-export/src/types/index.ts @@ -40,7 +40,8 @@ export type Modules = | 'custom-roles' | 'workflows' | 'labels' - | 'marketplace-apps'; + | 'marketplace-apps' + | 'taxonomies'; export type ModuleClassParams = { stackAPIClient: ReturnType; @@ -121,5 +122,21 @@ export interface StackConfig{ limit?: number; } +export interface TaxonomiesConfig{ + dirName: string; + fileName: string; + invalidKeys: string[]; + dependencies?: Modules[]; + limit?: number; +} + +export interface TermsConfig{ + dirName: string; + fileName: string; + invalidKeys: string[]; + dependencies?: Modules[]; + limit?: number; +} + export { default as DefaultConfig } from './default-config'; export { default as ExportConfig } from './export-config'; diff --git a/packages/contentstack-import/package.json b/packages/contentstack-import/package.json index 756eba878c..bbe42d7137 100644 --- a/packages/contentstack-import/package.json +++ b/packages/contentstack-import/package.json @@ -7,7 +7,7 @@ "dependencies": { "@contentstack/cli-command": "~1.2.15", "@contentstack/cli-utilities": "~1.5.5", - "@contentstack/management": "~1.11.0", + "@contentstack/management": "~1.12.0", "@oclif/core": "^2.9.3", "axios": "^1.6.0", "big-json": "^3.2.0", diff --git a/packages/contentstack-import/src/config/index.ts b/packages/contentstack-import/src/config/index.ts index c9003f3622..ae588e5748 100644 --- a/packages/contentstack-import/src/config/index.ts +++ b/packages/contentstack-import/src/config/index.ts @@ -18,7 +18,6 @@ const config: DefaultConfig = { 'https://eu-api.contentstack.com': 'https://eu-developerhub-api.contentstack.com', 'https://azure-na-api.contentstack.com': 'https://azure-na-developerhub-api.contentstack.com', 'https://azure-eu-api.contentstack.com': 'https://azure-eu-developerhub-api.contentstack.com', - 'https://stag-api.csnonprod.com': 'https://stag-developerhub-api.csnonprod.com', }, modules: { apiConcurrency: 5, @@ -26,6 +25,7 @@ const config: DefaultConfig = { 'locales', 'environments', 'assets', + 'taxonomies', 'extensions', 'marketplace-apps', 'global-fields', @@ -142,6 +142,14 @@ const config: DefaultConfig = { dirName: 'marketplace_apps', fileName: 'marketplace_apps.json', }, + taxonomies: { + dirName: 'taxonomies', + fileName: 'taxonomies.json', + }, + terms: { + dirName: 'terms', + fileName: 'terms.json', + }, }, languagesCode: [ 'af-za', @@ -367,6 +375,7 @@ const config: DefaultConfig = { stacks: '/stacks/', labels: '/labels/', }, + overwriteSupportedModules: ['extensions', 'global-fields', 'content-types'], rateLimit: 5, preserveStackVersion: false, entriesPublish: true, @@ -379,6 +388,7 @@ const config: DefaultConfig = { getEncryptionKeyMaxRetry: 3, // useBackedupDir: '', // backupConcurrency: 10, + onlyTSModules: ['taxonomies'], }; export default config; diff --git a/packages/contentstack-import/src/import/module-importer.ts b/packages/contentstack-import/src/import/module-importer.ts index 293616e483..1f1a09fd41 100755 --- a/packages/contentstack-import/src/import/module-importer.ts +++ b/packages/contentstack-import/src/import/module-importer.ts @@ -82,12 +82,16 @@ class ModuleImporter { importConfig: this.importConfig, moduleName, }); + } else { + //NOTE - new modules support only ts + if (this.importConfig.onlyTSModules.indexOf(moduleName) === -1) { + return startJSModuleImport({ + stackAPIClient: this.stackAPIClient, + importConfig: this.importConfig, + moduleName, + }); + } } - return startJSModuleImport({ - stackAPIClient: this.stackAPIClient, - importConfig: this.importConfig, - moduleName, - }); } async importAllModules(): Promise { diff --git a/packages/contentstack-import/src/import/modules/base-class.ts b/packages/contentstack-import/src/import/modules/base-class.ts index 6f601a1f21..d4a69296c9 100644 --- a/packages/contentstack-import/src/import/modules/base-class.ts +++ b/packages/contentstack-import/src/import/modules/base-class.ts @@ -48,7 +48,9 @@ export type ApiModuleType = | 'create-entries' | 'update-entries' | 'publish-entries' - | 'delete-entries'; + | 'delete-entries' + | 'create-taxonomies' + | 'create-terms'; export type ApiOptions = { uid?: string; @@ -382,6 +384,17 @@ export default abstract class BaseClass { .delete({ locale: additionalInfo.locale }) .then(onSuccess) .catch(onReject); + case 'create-taxonomies': + return this.stack.taxonomy().create({ taxonomy: apiData }).then(onSuccess).catch(onReject); + case 'create-terms': + if (apiData?.taxonomy_uid) { + return this.stack + .taxonomy(apiData.taxonomy_uid) + .terms() + .create({ term: apiData }) + .then(onSuccess) + .catch(onReject); + } default: return Promise.resolve(); } diff --git a/packages/contentstack-import/src/import/modules/content-types.ts b/packages/contentstack-import/src/import/modules/content-types.ts index 86557f6a8b..0ca255da31 100644 --- a/packages/contentstack-import/src/import/modules/content-types.ts +++ b/packages/contentstack-import/src/import/modules/content-types.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import { isEmpty, find, cloneDeep, map } from 'lodash'; -import { fsUtil, log, formatError, schemaTemplate, lookupExtension } from '../../utils'; +import { fsUtil, log, formatError, schemaTemplate, lookupExtension, lookUpTaxonomy } from '../../utils'; import { ImportConfig, ModuleClassParams } from '../../types'; import BaseClass, { ApiOptions } from './base-class'; import { updateFieldRules } from '../../utils/content-type-helper'; @@ -49,6 +49,8 @@ export default class ContentTypesImport extends BaseClass { limit: number; writeConcurrency?: number; }; + private taxonomiesPath: string; + public taxonomies: Record; constructor({ importConfig, stackAPIClient }: ModuleClassParams) { super({ importConfig, stackAPIClient }); @@ -75,6 +77,7 @@ export default class ContentTypesImport extends BaseClass { this.gFs = []; this.createdGFs = []; this.pendingGFs = []; + this.taxonomiesPath = path.join(importConfig.data, 'mapper/taxonomies', 'success.json'); } async start(): Promise { @@ -85,7 +88,6 @@ export default class ContentTypesImport extends BaseClass { * Update pending global fields * write field rules */ - this.cTs = fsUtil.readFile(path.join(this.cTsFolderPath, 'schema.json')) as Record[]; if (!this.cTs || isEmpty(this.cTs)) { log(this.importConfig, 'No content type found to import', 'info'); @@ -95,6 +97,7 @@ export default class ContentTypesImport extends BaseClass { this.installedExtensions = ( ((await fsUtil.readFile(this.marketplaceAppMapperPath)) as any) || { extension_uid: {} } ).extension_uid; + this.taxonomies = fsUtil.readFile(this.taxonomiesPath) as Record; await this.seedCTs(); log(this.importConfig, 'Created content types', 'success'); @@ -152,7 +155,7 @@ export default class ContentTypesImport extends BaseClass { async updateCTs(): Promise { const onSuccess = ({ response: contentType, apiData: { uid } }: any) => { - log(this.importConfig, `${uid} updated with references`, 'success'); + log(this.importConfig, `'${uid}' updated with references`, 'success'); }; const onReject = ({ error, apiData: { uid } }: any) => { log(this.importConfig, formatError(error), 'error'); @@ -186,6 +189,8 @@ export default class ContentTypesImport extends BaseClass { } this.fieldRules.push(contentType.uid); } + //will remove taxonomy if taxonomy doesn't exists in stack + lookUpTaxonomy(this.importConfig, contentType.schema, this.taxonomies); lookupExtension( this.importConfig, contentType.schema, diff --git a/packages/contentstack-import/src/import/modules/entries.ts b/packages/contentstack-import/src/import/modules/entries.ts index d8059dc8c4..6bb3d0a5b7 100644 --- a/packages/contentstack-import/src/import/modules/entries.ts +++ b/packages/contentstack-import/src/import/modules/entries.ts @@ -20,6 +20,7 @@ import { lookupEntries, lookupAssets, fileHelper, + lookUpTerms, } from '../../utils'; import { ModuleClassParams } from '../../types'; import BaseClass, { ApiOptions } from './base-class'; @@ -53,6 +54,8 @@ export default class EntriesImport extends BaseClass { private entriesUidMapper: Record; private envs: Record; private autoCreatedEntries: Record[]; + private taxonomiesPath: string; + public taxonomies: Record; constructor({ importConfig, stackAPIClient }: ModuleClassParams) { super({ importConfig, stackAPIClient }); @@ -64,6 +67,7 @@ export default class EntriesImport extends BaseClass { this.uniqueUidMapperPath = path.join(this.entriesMapperPath, 'unique-mapping.json'); this.modifiedCTsPath = path.join(this.entriesMapperPath, 'modified-schemas.json'); this.marketplaceAppMapperPath = path.join(this.importConfig.data, 'mapper', 'marketplace_apps', 'uid-mapping.json'); + this.taxonomiesPath = path.join(this.importConfig.data, 'mapper', 'taxonomies', 'terms', 'success.json'); this.entriesConfig = importConfig.modules.entries; this.entriesPath = path.resolve(importConfig.data, this.entriesConfig.dirName); this.cTsPath = path.resolve(importConfig.data, importConfig.modules['content-types'].dirName); @@ -96,6 +100,8 @@ export default class EntriesImport extends BaseClass { this.assetUidMapper = (fsUtil.readFile(this.assetUidMapperPath) as Record) || {}; this.assetUrlMapper = (fsUtil.readFile(this.assetUrlMapperPath) as Record) || {}; + + this.taxonomies = (fsUtil.readFile(this.taxonomiesPath) as Record); fsUtil.makeDirectory(this.entriesMapperPath); await this.disableMandatoryCTReferences(); @@ -409,6 +415,8 @@ export default class EntriesImport extends BaseClass { if (this.jsonRteCTsWithRef.indexOf(cTUid) > -1) { entry = removeEntryRefsFromJSONRTE(entry, contentType.schema); } + //will remove term if term doesn't exists in taxonomy + lookUpTerms(contentType?.schema, entry, this.taxonomies, this.importConfig); // will replace all old asset uid/urls with new ones entry = lookupAssets( { diff --git a/packages/contentstack-import/src/import/modules/taxonomies.ts b/packages/contentstack-import/src/import/modules/taxonomies.ts new file mode 100644 index 0000000000..5c1e320d12 --- /dev/null +++ b/packages/contentstack-import/src/import/modules/taxonomies.ts @@ -0,0 +1,258 @@ +import keys from 'lodash/keys'; +import pick from 'lodash/pick'; +import { join } from 'node:path'; +import values from 'lodash/values'; +import isEmpty from 'lodash/isEmpty'; + +import BaseClass, { ApiOptions } from './base-class'; +import { log, formatError, fsUtil, fileHelper } from '../../utils'; +import { ModuleClassParams, TaxonomiesConfig, TermsConfig } from '../../types'; + +export default class ImportTaxonomies extends BaseClass { + private taxonomiesMapperDirPath: string; + private taxonomiesFolderPath: string; + private taxSuccessPath: string; + private taxFailsPath: string; + private taxonomiesConfig: TaxonomiesConfig; + private taxonomies: Record; + private termsFolderPath: string; + private termsMapperDirPath: string; + private termsConfig: TermsConfig; + private termsSuccessPath: string; + private termsFailsPath: string; + public taxonomiesSuccess: Record = {}; + public taxonomiesFailed: Record = {}; + public termsSuccess: Record> = {}; + public termsFailed: Record> = {}; + public terms: Record = []; + public taxonomyUIDs: string[] = []; + + constructor({ importConfig, stackAPIClient }: ModuleClassParams) { + super({ importConfig, stackAPIClient }); + this.taxonomiesConfig = importConfig.modules.taxonomies; + this.termsConfig = importConfig.modules.terms; + this.taxonomiesMapperDirPath = join(importConfig.backupDir, 'mapper', 'taxonomies'); + this.termsMapperDirPath = join(this.taxonomiesMapperDirPath, 'terms'); + this.taxonomiesFolderPath = join(importConfig.backupDir, this.taxonomiesConfig.dirName); + this.termsFolderPath = join(this.taxonomiesFolderPath, this.termsConfig.dirName); + this.taxSuccessPath = join(this.taxonomiesMapperDirPath, 'success.json'); + this.taxFailsPath = join(this.taxonomiesMapperDirPath, 'fails.json'); + this.termsSuccessPath = join(this.termsMapperDirPath, 'success.json'); + this.termsFailsPath = join(this.termsMapperDirPath, 'fails.json'); + } + + /** + * @method start + * @returns {Promise} Promise + */ + async start(): Promise { + log(this.importConfig, 'Migrating taxonomies...', 'info'); + + //Step1 check folder exists or not + if (fileHelper.fileExistsSync(this.taxonomiesFolderPath)) { + this.taxonomies = fsUtil.readFile(join(this.taxonomiesFolderPath, 'taxonomies.json'), true) as Record< + string, + unknown + >; + } else { + log(this.importConfig, `No such file or directory - '${this.taxonomiesFolderPath}'`, 'error'); + return; + } + + //Step 2 create taxonomies & terms mapper directory + await fsUtil.makeDirectory(this.taxonomiesMapperDirPath); + await fsUtil.makeDirectory(this.termsMapperDirPath); + + //Step 3 import taxonomy and create success & failure file + await this.importTaxonomies(); + this.createTaxonomySuccessAndFailedFile(); + + if (!fileHelper.fileExistsSync(this.termsFolderPath)) { + log(this.importConfig, `No such file or directory - '${this.termsFolderPath}'`, 'error'); + return; + } + //Step 4 import terms and create success & failure file + await this.importTerms(); + this.createTermSuccessAndFailedFile(); + + log(this.importConfig, 'Taxonomies imported successfully!', 'success'); + } + + /** + * create taxonomy and enter success & failure related data into taxonomies mapper file + * @method importTaxonomies + * @async + * @returns {Promise} Promise + */ + async importTaxonomies(): Promise { + if (this.taxonomies === undefined || isEmpty(this.taxonomies)) { + log(this.importConfig, 'No Taxonomies Found!', 'info'); + return; + } + + const apiContent = values(this.taxonomies) as Record[]; + this.taxonomyUIDs = keys(this.taxonomies); + + const onSuccess = ({ response }: any) => { + const { uid } = response; + this.taxonomiesSuccess[uid] = pick(response, ['name', 'description']); + log(this.importConfig, `Taxonomy '${uid}' imported successfully!`, 'success'); + }; + + const onReject = ({ error, apiData }: any) => { + const err = error?.message ? JSON.parse(error.message) : error; + const { uid } = apiData; + if (err?.errors?.taxonomy) { + this.taxonomiesFailed[uid] = apiData; + log(this.importConfig, `Taxonomy '${uid}' failed to be import! ${err.errors.taxonomy}`, 'error'); + } else { + this.taxonomiesFailed[apiData.uid] = apiData; + log(this.importConfig, `Taxonomy '${uid}' failed to be import! ${formatError(error)}`, 'error'); + } + }; + + await this.makeConcurrentCall( + { + apiContent, + processName: 'import taxonomies', + apiParams: { + serializeData: this.serializeTaxonomy.bind(this), + reject: onReject, + resolve: onSuccess, + entity: 'create-taxonomies', + includeParamOnCompletion: true, + }, + concurrencyLimit: this.importConfig.concurrency || this.importConfig.fetchConcurrency || 1, + }, + undefined, + false, + ); + } + + /** + * @method serializeTaxonomy + * @param {ApiOptions} apiOptions ApiOptions + * @returns {ApiOptions} ApiOptions + */ + serializeTaxonomy(apiOptions: ApiOptions): ApiOptions { + const { apiData: taxonomy } = apiOptions; + apiOptions.apiData = taxonomy; + return apiOptions; + } + + /** + * create taxonomies success and fail in (mapper/taxonomies) + * @method createTaxonomySuccessAndFailedFile + */ + createTaxonomySuccessAndFailedFile() { + if (this.taxonomiesSuccess !== undefined && !isEmpty(this.taxonomiesSuccess)) { + fsUtil.writeFile(this.taxSuccessPath, this.taxonomiesSuccess); + } + + if (this.taxonomiesFailed !== undefined && !isEmpty(this.taxonomiesFailed)) { + fsUtil.writeFile(this.taxFailsPath, this.taxonomiesFailed); + } + } + + /** + * create terms and enter success & failure related data into terms mapper file + * @method importTerms + * @async + * @returns {Promise} Promise + */ + async importTerms(): Promise { + if (!this.taxonomyUIDs?.length) { + return; + } + + const onSuccess = ({ response, apiData: { taxonomy_uid } = { taxonomy_uid: null } }: any) => { + const { uid } = response; + if (!this.termsSuccess?.[taxonomy_uid]) this.termsSuccess[taxonomy_uid] = {}; + this.termsSuccess[taxonomy_uid][uid] = pick(response, ['name']); + log(this.importConfig, `Term '${uid}' imported successfully!`, 'success'); + }; + + const onReject = ({ error, apiData }: any) => { + const { taxonomy_uid, uid } = apiData; + if (!this.termsFailed?.[taxonomy_uid]) this.termsFailed[taxonomy_uid] = {}; + const err = error?.message ? JSON.parse(error.message) : error; + + if (err?.errors?.term) { + this.termsFailed[taxonomy_uid][uid] = apiData; + log(this.importConfig, `Term '${uid}' failed to be import! ${err.errors.term}`, 'error'); + } else { + this.termsFailed[taxonomy_uid][uid] = apiData; + log(this.importConfig, `Term '${uid}' failed to be import! ${formatError(error)}`, 'error'); + } + }; + + for (const taxUID of this.taxonomyUIDs) { + //read terms from respective taxonomy + this.terms = fsUtil.readFile( + join(this.termsFolderPath, `${taxUID}-${this.termsConfig.fileName}`), + true, + ) as Record; + + if (this.terms?.length) { + const apiContent = this.terms as Record[]; + await this.makeConcurrentCall( + { + apiContent, + processName: 'import terms', + apiParams: { + serializeData: this.serializeTerms.bind(this), + reject: onReject, + resolve: onSuccess, + entity: 'create-terms', + includeParamOnCompletion: true, + }, + concurrencyLimit: this.importConfig.concurrency || this.importConfig.fetchConcurrency || 1, + }, + undefined, + false, + ); + } + } + } + + /** + * @method serializeTerms + * @param {ApiOptions} apiOptions ApiOptions + * @returns {ApiOptions} ApiOptions + */ + serializeTerms(apiOptions: ApiOptions): ApiOptions { + const { apiData: term } = apiOptions; + const {parent_uid, taxonomy_uid} = term; + + //check whether parent term exists or not in taxonomy + if (parent_uid !== null) { + if (!this.termsSuccess?.[taxonomy_uid]?.[parent_uid]) { + log( + this.importConfig, + `Parent term '${term?.parent_uid}' does not exist! Skipping '${term.uid}' creation to avoid further issues.`, + 'info', + ); + apiOptions.apiData = undefined; + } else { + apiOptions.apiData = term; + } + } else { + apiOptions.apiData = term; + } + return apiOptions; + } + + /** + * create terms success and fail in (mapper/taxonomies/terms) + * @method createTermSuccessAndFailedFile + */ + createTermSuccessAndFailedFile() { + if (this.termsSuccess !== undefined && !isEmpty(this.termsSuccess)) { + fsUtil.writeFile(this.termsSuccessPath, this.termsSuccess); + } + + if (this.termsFailed !== undefined && !isEmpty(this.termsFailed)) { + fsUtil.writeFile(this.termsFailsPath, this.termsFailed); + } + } +} diff --git a/packages/contentstack-import/src/types/default-config.ts b/packages/contentstack-import/src/types/default-config.ts index c41b949fdb..9242d5b2bf 100644 --- a/packages/contentstack-import/src/types/default-config.ts +++ b/packages/contentstack-import/src/types/default-config.ts @@ -113,6 +113,16 @@ export default interface DefaultConfig { fileName: string; requiredKeys: string[]; }; + taxonomies: { + dirName: string; + fileName: string; + dependencies?: Modules[]; + }; + terms: { + dirName: string; + fileName: string; + dependencies?: Modules[]; + }; }; languagesCode: string[]; apis: { @@ -140,4 +150,6 @@ export default interface DefaultConfig { marketplaceAppEncryptionKey: string; getEncryptionKeyMaxRetry: number; createBackupDir?: string; + overwriteSupportedModules: string[]; + onlyTSModules: string[]; } diff --git a/packages/contentstack-import/src/types/index.ts b/packages/contentstack-import/src/types/index.ts index 2b5fee4e28..bf612e188b 100644 --- a/packages/contentstack-import/src/types/index.ts +++ b/packages/contentstack-import/src/types/index.ts @@ -40,7 +40,8 @@ export type Modules = | 'custom-roles' | 'workflows' | 'labels' - | 'marketplace-apps'; + | 'marketplace-apps' + | 'taxonomies'; export type ModuleClassParams = { stackAPIClient: ReturnType; @@ -89,5 +90,17 @@ export interface CustomRoleConfig { customRolesLocalesFileName: string; } +export interface TaxonomiesConfig{ + dirName: string; + fileName: string; + dependencies?: Modules[]; +} + +export interface TermsConfig{ + dirName: string; + fileName: string; + dependencies?: Modules[]; +} + export { default as DefaultConfig } from './default-config'; export { default as ImportConfig } from './import-config'; diff --git a/packages/contentstack-import/src/utils/index.ts b/packages/contentstack-import/src/utils/index.ts index f0c9b9a5a1..0e3301abc4 100644 --- a/packages/contentstack-import/src/utils/index.ts +++ b/packages/contentstack-import/src/utils/index.ts @@ -27,4 +27,5 @@ export { restoreJsonRteEntryRefs, } from './entries-helper'; export * from './common-helper'; -export * from './log' \ No newline at end of file +export * from './log'; +export { lookUpTaxonomy, lookUpTerms } from './taxonomies-helper'; diff --git a/packages/contentstack-import/src/utils/logger.ts b/packages/contentstack-import/src/utils/logger.ts index 139e0b4092..207f89766d 100644 --- a/packages/contentstack-import/src/utils/logger.ts +++ b/packages/contentstack-import/src/utils/logger.ts @@ -78,7 +78,12 @@ function init(_logPath: string) { logger = winston.createLogger({ transports: [ new winston.transports.File(successTransport), - new winston.transports.Console({ format: winston.format.simple() }), + new winston.transports.Console({ + format: winston.format.combine( + winston.format.simple(), + winston.format.colorize({ all: true, colors: { warn: 'yellow', info: 'white' } }), + ), + }), ], levels: myCustomLevels.levels, }); @@ -106,7 +111,7 @@ function init(_logPath: string) { logger.log('info', logString); } }, - warn: function () { + warn: function (message: any) { let args = slice.call(arguments); let logString = returnString(args); if (logString) { @@ -135,7 +140,8 @@ export const log = async (config: ImportConfig, message: any, type: string) => { // ignoring the type argument, as we are not using it to create a logfile anymore if (type !== 'error') { // removed type argument from init method - init(config.data).log(message); + if (type === 'warn') init(config.data).warn(message); //logged warning message in log file + else init(config.data).log(message); } else { init(config.data).error(message); } diff --git a/packages/contentstack-import/src/utils/taxonomies-helper.ts b/packages/contentstack-import/src/utils/taxonomies-helper.ts new file mode 100644 index 0000000000..09755fc2e2 --- /dev/null +++ b/packages/contentstack-import/src/utils/taxonomies-helper.ts @@ -0,0 +1,132 @@ +/** + * taxonomy lookup + */ +import { log } from './'; +import { ImportConfig } from '../types'; + +/** + * check and remove if referenced taxonomy doesn't exists in stack + * @param {any} schema content type schema + * @param {Record} taxonomies created taxonomies + * @param {ImportConfig} importConfig + */ +export const lookUpTaxonomy = function (importConfig: ImportConfig, schema: any, taxonomies: Record) { + for (let i in schema) { + if (schema[i].data_type === 'taxonomy') { + const taxonomyFieldData = schema[i].taxonomies as Record[]; + const { updatedTaxonomyData, isTaxonomyFieldRemoved } = verifyAndRemoveTaxonomy( + taxonomyFieldData, + taxonomies, + importConfig, + ); + + //Handle API error -> The 'taxonomies' property must have atleast one taxonomy object. Remove taxonomy field from schema. + if (isTaxonomyFieldRemoved) { + schema.splice(i, 1); + } else { + schema[i].taxonomies = updatedTaxonomyData; + } + } + } +}; + +/** + * verify and remove referenced taxonomy with warning from respective content type + * @param {Record[]} taxonomyFieldData + * @param {Record} taxonomies created taxonomies + * @param {ImportConfig} importConfig + * @returns + */ +const verifyAndRemoveTaxonomy = function ( + taxonomyFieldData: Record[], + taxonomies: Record, + importConfig: ImportConfig, +): { + updatedTaxonomyData: Record[]; + isTaxonomyFieldRemoved: boolean; +} { + let isTaxonomyFieldRemoved: boolean = false; + + for (let index = 0; index < taxonomyFieldData?.length; index++) { + const taxonomyData = taxonomyFieldData[index]; + + if (taxonomies === undefined || !taxonomies.hasOwnProperty(taxonomyData?.taxonomy_uid)) { + // remove taxonomy from taxonomies field data with warning if respective taxonomy doesn't exists + log( + importConfig, + `Taxonomy '${taxonomyData?.taxonomy_uid}' does not exist. Removing the data from the taxonomies field`, + 'warn', + ); + taxonomyFieldData.splice(index, 1); + --index; + } + } + + if (!taxonomyFieldData?.length) { + log( + importConfig, + 'Taxonomy does not exist. Removing the field from content type', + 'warn', + ); + isTaxonomyFieldRemoved = true; + } + + return { + updatedTaxonomyData: taxonomyFieldData, + isTaxonomyFieldRemoved, + }; +}; + +/** + * check and remove if referenced terms doesn't exists in taxonomy + * @param {Record[]} ctSchema content type schema + * @param {any} entry + * @param {Record} taxonomiesAndTermData created taxonomies and terms + * @param {ImportConfig} importConfig + */ +export const lookUpTerms = function ( + ctSchema: Record[], + entry: any, + taxonomiesAndTermData: Record, + importConfig: ImportConfig, +) { + for (let index = 0; index < ctSchema?.length; index++) { + if (ctSchema[index].data_type === 'taxonomy') { + const taxonomyFieldData = entry[ctSchema[index].uid]; + const updatedTaxonomyData = verifyAndRemoveTerms(taxonomyFieldData, taxonomiesAndTermData, importConfig); + entry[ctSchema[index].uid] = updatedTaxonomyData; + } + } +}; + +/** + * verify and remove referenced term with warning from respective entry + * @param {Record[]} taxonomyFieldData entry taxonomies data + * @param {Record} taxonomiesAndTermData created taxonomies and terms + * @param {ImportConfig} importConfig + * @returns { Record[]} + */ +const verifyAndRemoveTerms = function ( + taxonomyFieldData: Record[], + taxonomiesAndTermData: Record, + importConfig: ImportConfig, +): Record[] { + for (let index = 0; index < taxonomyFieldData?.length; index++) { + const taxonomyData = taxonomyFieldData[index]; + const taxUID = taxonomyData?.taxonomy_uid; + const termUID = taxonomyData?.term_uid; + + if ( + taxonomiesAndTermData === undefined || + !taxonomiesAndTermData.hasOwnProperty(taxUID) || + (taxonomiesAndTermData.hasOwnProperty(taxUID) && !taxonomiesAndTermData[taxUID].hasOwnProperty(termUID)) + ) { + // remove term from taxonomies field data with warning if respective term doesn't exists + log(importConfig, `Term '${termUID}' does not exist. Removing it from taxonomy - '${taxUID}'`, 'warn'); + taxonomyFieldData.splice(index, 1); + --index; + } + } + + return taxonomyFieldData; +}; \ No newline at end of file diff --git a/packages/contentstack-launch/src/config/index.ts b/packages/contentstack-launch/src/config/index.ts index e0f758dd7e..7aab1b2fce 100755 --- a/packages/contentstack-launch/src/config/index.ts +++ b/packages/contentstack-launch/src/config/index.ts @@ -26,8 +26,7 @@ const config = { 'https://api.contentstack.io': 'https://launch-api.contentstack.com', 'https://eu-api.contentstack.com': '', 'https://azure-na-api.contentstack.com': '', - 'https://dev11-api.csnonprod.com': 'https://dev-launch-api.csnonprod.com', - 'https://stag-api.csnonprod.com': 'https://stag-launch-api.csnonprod.com', + 'https://azure-eu-api.contentstack.com': '', }, supportedAdapters: ['GitHub'], deploymentStatus: ['LIVE', 'FAILED', 'SKIPPED', 'DEPLOYED'], diff --git a/packages/contentstack-migration/docs/api-reference.md b/packages/contentstack-migration/docs/api-reference.md index 6db907e107..da8e9a473f 100644 --- a/packages/contentstack-migration/docs/api-reference.md +++ b/packages/contentstack-migration/docs/api-reference.md @@ -53,10 +53,12 @@ Creates content type by passing content type name and options **Example** ```js -module.exports = {migrations} => { - const blog = migrations.createContentType('blog', { - title: 'blog' - }) +module.exports = ({migration}) => { + const blog = migration + .createContentType('blog') + .title('blog title') + .description('blog 1') + blog.createField('title').display_name('Title').data_type('text').mandatory(true); } ``` @@ -98,10 +100,8 @@ Edits content type by passing content type name and options **Example** ```js -module.exports = {migrations} => { - const blog = migrations.editContentType('blog', { - title: 'blog' - }); +module.exports = ({migration}) => { + const blog = migration.editContentType('blog'); blog.description('Changed description'); } ``` @@ -177,6 +177,8 @@ Chained function takes boolean value for force while deleting content type * [.unique(value)](#Field+unique) ⇒ [Field](#Field) * [.reference_to(value)](#Field+reference_to) ⇒ [Field](#Field) * [.ref_multiple(value)](#Field+ref_multiple) ⇒ [Field](#Field) + * [.taxonomies(value)](#Field+taxonomies) ⇒ [Field](#Field) + * [.multiple(value)](#Field+multiple) ⇒ [Field](#Field) * [.ref_multipleContentType(value)](#Field+ref_multipleContentType) ⇒ [Field](#Field) * [.getTaskDefinition()](#Field+getTaskDefinition) ⇒ [Task](#Task) @@ -203,11 +205,24 @@ Creates a field with provided uid. module.exports =({ migration })=> { const blog = migration.editContentType('blog'); - blog.createField('author'); + blog.createField('author') .display_name('Author') .data_type('text') .mandatory(false); }; + +Create a taxonomy field + + module.exports =({ migration })=> { + const blog = migration.editContentType('blog'); + + blog.createField('taxonomies') + .display_name('Taxonomy1') + .data_type('taxonomy') + .taxonomies([{ "taxonomy_uid": "test_taxonomy1", "max_terms": 2, "mandatory": false}]) + .multiple(true) + .mandatory(false); +}; ``` @@ -356,6 +371,28 @@ module.exports = ({migration}) => { | --- | --- | --- | | value | string | set true if accepts multiple entries as reference | + + +### field.taxonomies(value) ⇒ [Field](#Field) +The 'taxonomies' property should contain at least one taxonomy object + +**Kind**: instance method of [Field](#Field) +**Returns**: [Field](#Field) - current instance of field object to chain further methods. + +| Param | Type | Description | +| --- | --- | --- | +| value | string \| Array.<string> | list of taxonomies. | + + + +### field.multiple(value) ⇒ [Field](#Field) +**Kind**: instance method of [Field](#Field) +**Returns**: [Field](#Field) - current instance of field object to chain further methods. + +| Param | Type | Description | +| --- | --- | --- | +| value | boolean | set true if field is multiple | + ### field.ref\_multipleContentType(value) ⇒ [Field](#Field) diff --git a/packages/contentstack-migration/examples/taxonomies/import-taxonomies.js b/packages/contentstack-migration/examples/taxonomies/import-taxonomies.js new file mode 100644 index 0000000000..c889e5f4bd --- /dev/null +++ b/packages/contentstack-migration/examples/taxonomies/import-taxonomies.js @@ -0,0 +1,151 @@ +const fs = require('fs'); +const fastcsv = require('fast-csv'); +const find = require('lodash/find'); + +module.exports = ({ migration, stackSDKInstance, managementAPIClient, config }) => { + const dataDir = config['data-dir']; + const delimiter = config['delimiter'] ?? ','; // default comma + //parent and child term pointer + let parentDetails = { taxonomy_uid: '' }; + let stack; + let depth = 1, + parentUID = null; + + const stackClient = () => { + return managementAPIClient.stack({ api_key: stackSDKInstance.api_key }); + }; + + const createTaxonomy = async (apiData) => { + await stack + .taxonomy() + .create({ taxonomy: apiData }) + .catch((err) => handleErrorMsg(err)); + }; + + const createTerm = async (apiData) => { + await stack + .taxonomy(apiData.taxonomy_uid) + .terms() + .create({ term: apiData }) + .catch((err) => handleErrorMsg(err)); + }; + + function handleErrorMsg(err) { + let errMsg = 'Something went wrong! Please try again'; + if (err?.errorMessage) { + errMsg = err.errorMessage; + } else if (err?.message) { + errMsg = err?.errors?.taxonomy || err?.errors?.term || JSON.stringify(err?.errors) || err?.message; + } + throw errMsg; + } + + const readCsv = (path, options) => { + return new Promise((resolve, reject) => { + const taxonomies = []; + fastcsv + .parseFile(path, options) + .on('error', reject) + .on('data', (data) => { + taxonomies.push(data); + }) + .on('end', () => { + resolve(taxonomies); + }); + }); + }; + + const toSnakeCase = (str) => + str + .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) + .map((x) => x.toLowerCase()) + .join('_'); + + const createTaxonomyTask = (params) => { + return { + title: 'Create Taxonomies', + successMessage: 'Taxonomies created successfully!', + failedMessage: 'Failed to create taxonomies!', + task: async (params) => { + try { + stack = stackClient(); + if (!fs.existsSync(dataDir)) { + throw new Error(`No such file or directory - ${dataDir}`); + } + const taxonomies = await readCsv(dataDir, { headers: true, delimiter }); + + if (!taxonomies?.length) { + throw new Error('No Taxonomies found!'); + } + + for (let row = 0; row < taxonomies?.length; row++) { + const taxDetails = taxonomies[row]; + //taxonomy name required + if (taxDetails['Taxonomy Name']?.length) { + const taxonomyName = taxDetails['Taxonomy Name']; + const taxonomyUID = taxDetails['Taxonomy UID'] ?? ''; + const taxonomyDesc = taxDetails['Taxonomy Description'] ?? ''; + const reqTaxonomyObj = { + name: taxonomyName, + uid: taxonomyUID?.length ? taxonomyUID : toSnakeCase(taxonomyName), + description: taxonomyDesc, + }; + parentDetails = {}; + parentDetails['taxonomy_uid'] = reqTaxonomyObj.uid; + await createTaxonomy(reqTaxonomyObj); + } else if (!taxDetails['Taxonomy Name']?.length && parentDetails['taxonomy_uid']) { + const column = find(Object.keys(taxDetails), (col) => { + if (taxDetails[col] !== '') { + return col; + } + }); + const termLevel = column?.split(' ')?.[1] ?? ''; // Output:- Level1/Level2 + const termDepth = +termLevel.replace(/[^0-9]/g, ''); // fetch depth from header + const termName = taxDetails[`Term ${termLevel} Name`] ?? ''; + + //term name required field + if (termName?.length && termDepth) { + const termUID = taxDetails[`Term ${termLevel} UID`]?.length + ? taxDetails[`Term ${termLevel} UID`] + : toSnakeCase(termName); + if (termDepth > depth) { + //child term case + parentUID = parentDetails[termDepth - 1] || null; + depth = termDepth; + parentDetails[depth] = termUID; + } else if (termDepth === 1) { + //parent term case + depth = 1; + parentUID = null; + parentDetails[depth] = termUID; + } else if (termDepth === depth) { + //sibling term case + parentUID = parentDetails[termDepth - 1]; + } else if (termDepth < depth) { + //diff parent term case + parentUID = parentDetails[termDepth - 1] || null; + depth = termDepth; + parentDetails[depth] = termUID; + } + const reqTermObj = { + uid: termUID, + name: termName, + parent_uid: parentUID, + taxonomy_uid: parentDetails['taxonomy_uid'], + }; + await createTerm(reqTermObj); + } + } + } + } catch (error) { + throw error; + } + }, + }; + }; + if (config?.['data-dir']) { + migration.addTask(createTaxonomyTask()); + } else { + console.error('Please provide config using --config data-dir: ""'); + } +}; diff --git a/packages/contentstack-migration/examples/taxonomies/test_taxonomies.csv b/packages/contentstack-migration/examples/taxonomies/test_taxonomies.csv new file mode 100644 index 0000000000..f18b4eb9dc --- /dev/null +++ b/packages/contentstack-migration/examples/taxonomies/test_taxonomies.csv @@ -0,0 +1,22 @@ +Taxonomy Name,Taxonomy UID,Taxonomy Description,Term Level1 Name,Term Level1 UID,Term Level2 Name,Term Level2 UID,Term Level3 Name,Term Level3 UID,Term Level4 Name,Term Level4 UID +name taxonomy,name_taxonomy,,,,,,,,, +,,,term 2,term_2,,,,,, +,,,,,term 8,term_8,,,, +,,,,,,,term 9,term_9,, +,,,,,,,,,term 10,term_10 +,,,term 1,term_1,,,,,, +,,,,,term 4,term_4,,,, +,,,,,,,term 5,term_5,, +,,,,,,,,,term 7,term_7 +,,,,,term 3,term_3,,,, +,,,,,,,term 6,term_6,, +taxonomy1,taxonomy1,,,,,,,,, +,,,May,may,,,,,, +,,,,,Summer 2023,summer_2023,,,, +,,,December,december,,,,,, +,,,,,Winter 2022,winter_2022,,,, +clothes,clothes,clothes taxonomy,,,,,,,, +,,,fdshfgadf,fdshfgadf,,,,,, +,,,jeans,jeans,,,,,, +,,,shirt,shirt,,,,,, +,,,,,dfgdshfga,dfgdshfga,,,, \ No newline at end of file diff --git a/packages/contentstack-migration/package.json b/packages/contentstack-migration/package.json index 0934b26912..0f6dea237b 100644 --- a/packages/contentstack-migration/package.json +++ b/packages/contentstack-migration/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/cli-migration", - "version": "1.3.15", + "version": "1.4.0", "author": "@contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { diff --git a/packages/contentstack-migration/src/config/default-options.js b/packages/contentstack-migration/src/config/default-options.js index 90d34e835f..72dc159fe8 100644 --- a/packages/contentstack-migration/src/config/default-options.js +++ b/packages/contentstack-migration/src/config/default-options.js @@ -2,6 +2,6 @@ 'use strict'; module.exports = { - is_page: true, + is_page: false, singleton: false, }; diff --git a/packages/contentstack-migration/src/modules/content-types.js b/packages/contentstack-migration/src/modules/content-types.js index b47b0598c3..e597ece69a 100644 --- a/packages/contentstack-migration/src/modules/content-types.js +++ b/packages/contentstack-migration/src/modules/content-types.js @@ -38,10 +38,12 @@ class ContentType extends Base { * @param {Object} opts Optional: Content type fields definition * @returns {Field} instance of Field * @example - * module.exports = {migrations} => { - * const blog = migrations.createContentType('blog', { - * title: 'blog' - * }) + * module.exports = ({migration}) => { + * const blog = migration + * .createContentType('blog') + * .title('blog title') + * .description('blog 1') + * blog.createField('title').display_name('Title').data_type('text').mandatory(true); * } */ createContentType(id, opts = {}) { @@ -114,10 +116,8 @@ class ContentType extends Base { * @param {Object} opts Optional: Content type fields definition * @returns {Field} instance of Field * @example - * module.exports = {migrations} => { - * const blog = migrations.editContentType('blog', { - * title: 'blog' - * }); + * module.exports = ({migration}) => { + * const blog = migration.editContentType('blog'); * blog.description('Changed description'); * } */ diff --git a/packages/contentstack-migration/src/modules/fields.js b/packages/contentstack-migration/src/modules/fields.js index 9ace6c74d9..0b6e69f9e6 100644 --- a/packages/contentstack-migration/src/modules/fields.js +++ b/packages/contentstack-migration/src/modules/fields.js @@ -16,6 +16,8 @@ const { field_metadata, reference_to, actions: _actions, + taxonomies, + multiple, } = constants; // Base class @@ -52,11 +54,24 @@ class Field extends Base { * module.exports =({ migration })=> { * const blog = migration.editContentType('blog'); * - * blog.createField('author'); + * blog.createField('author') * .display_name('Author') * .data_type('text') * .mandatory(false); * }; + * + * Create a taxonomy field + * + * module.exports =({ migration })=> { + * const blog = migration.editContentType('blog'); + * + * blog.createField('taxonomies') + * .display_name('Taxonomy1') + * .data_type('taxonomy') + * .taxonomies([{ "taxonomy_uid": "test_taxonomy1", "max_terms": 2, "mandatory": false}]) + * .multiple(true) + * .mandatory(false); + * }; */ createField(field, opts) { this.updateContentTypeSchema(field); @@ -227,6 +242,26 @@ class Field extends Base { return this; } + /** + * The 'taxonomies' property should contain at least one taxonomy object + * @param {string | string[]} value list of taxonomies. + * @returns {Field} current instance of field object to chain further methods. + */ + taxonomies(value) { + this.buildSchema(taxonomies, this.field, value); + return this; + } + + /** + * + * @param {boolean} value set true if field is multiple + * @returns {Field} current instance of field object to chain further methods. + */ + multiple(value) { + this.buildSchema(multiple, this.field, value); + return this; + } + /** * * @param {boolean} value set true if refer to multiple content types diff --git a/packages/contentstack-migration/src/utils/constants.js b/packages/contentstack-migration/src/utils/constants.js index 1ebc59fa80..0705c0117c 100644 --- a/packages/contentstack-migration/src/utils/constants.js +++ b/packages/contentstack-migration/src/utils/constants.js @@ -21,6 +21,8 @@ exports.unique = 'unique'; exports.display_name = 'display_name'; exports.reference_to = 'reference_to'; exports.field_metadata = 'field_metadata'; +exports.taxonomies = 'taxonomies'; +exports.multiple = 'multiple'; exports.actions = { CUSTOM_TASK: 'CUSTOM_TASK', diff --git a/packages/contentstack-utilities/package.json b/packages/contentstack-utilities/package.json index 08b65d90dc..34ff07bfd4 100644 --- a/packages/contentstack-utilities/package.json +++ b/packages/contentstack-utilities/package.json @@ -32,7 +32,7 @@ "author": "contentstack", "license": "MIT", "dependencies": { - "@contentstack/management": "~1.11.0", + "@contentstack/management": "~1.12.0", "@oclif/core": "^2.9.3", "axios": "^1.6.0", "chalk": "^4.0.0", diff --git a/packages/contentstack/README.md b/packages/contentstack/README.md index 6d9a1e0d45..703725adf5 100644 --- a/packages/contentstack/README.md +++ b/packages/contentstack/README.md @@ -2027,28 +2027,31 @@ EXAMPLES ## `csdx cm:export-to-csv` -Export entries or organization users to csv using this command +Export entries, taxonomies, terms or organization users to csv using this command ``` USAGE - $ csdx cm:export-to-csv [--action entries|users|teams] [-a ] [--org ] [-n ] [-k ] - [--org-name ] [--locale ] [--content-type ] [--branch ] [--team-uid ] + $ csdx cm:export-to-csv [--action entries|users|teams|taxonomies] [-a ] [--org ] [-n ] [-k + ] [--org-name ] [--locale ] [--content-type ] [--branch ] [--team-uid ] + [--taxonomy-uid ] [--delimiter ] FLAGS -a, --alias= Alias of the management token -k, --stack-api-key= API key of the source stack -n, --stack-name= Name of the stack that needs to be created as csv filename. - --action=