diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3566f965ec..ef4bb176cc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v3.5.3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/jira.yml b/.github/workflows/jira.yml index 5ddf87a65c..c4f9d9f575 100644 --- a/.github/workflows/jira.yml +++ b/.github/workflows/jira.yml @@ -7,7 +7,7 @@ jobs: if: ${{ github.actor == 'dependabot[bot]' || github.actor == 'snyk-bot' || contains(github.event.pull_request.head.ref, 'snyk-fix-') || contains(github.event.pull_request.head.ref, 'snyk-upgrade-')}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.5.3 - name: Login into JIRA uses: atlassian/gajira-login@master env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c4c326084..1e45cf7549 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,11 +8,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@v3.5.3 + - uses: pnpm/action-setup@v2.2.4 with: version: 7 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3.7.0 with: node-version: '18.x' cache: 'pnpm' @@ -32,7 +32,7 @@ jobs: working-directory: ./packages/contentstack-dev-dependencies run: npm run prepack - name: Publishing dev dependencies - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.dev-dependencies-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -47,7 +47,7 @@ jobs: working-directory: ./packages/contentstack-utilities run: npm run prepack - name: Publishing utilities - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.utilities-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -62,7 +62,7 @@ jobs: working-directory: ./packages/contentstack-command run: npm run prepack - name: Publishing command - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.command-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -77,7 +77,7 @@ jobs: working-directory: ./packages/contentstack-config run: npm run prepack - name: Publishing config - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.config-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -92,7 +92,7 @@ jobs: working-directory: ./packages/contentstack-auth run: npm run prepack - name: Publishing auth - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.auth-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -107,7 +107,7 @@ jobs: working-directory: ./packages/contentstack-export run: npm run prepack - name: Publishing export - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.export-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -122,7 +122,7 @@ jobs: working-directory: ./packages/contentstack-import run: npm run prepack - name: Publishing import - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.import-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -133,7 +133,7 @@ jobs: working-directory: ./packages/contentstack-clone run: npm install - name: Publishing clone - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.clone-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -144,7 +144,7 @@ jobs: working-directory: ./packages/contentstack-export-to-csv run: npm install - name: Publishing export to csv - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.export-to-csv-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -155,7 +155,7 @@ jobs: working-directory: ./packages/contentstack-migrate-rte run: npm install - name: Publishing migrate rte - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.migrate-rte-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -166,7 +166,7 @@ jobs: working-directory: ./packages/contentstack-migration run: npm install - name: Publishing migration - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.migration-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -181,7 +181,7 @@ jobs: working-directory: ./packages/contentstack-seed run: npm run prepack - name: Publishing seed - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.seed-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -196,7 +196,7 @@ jobs: working-directory: ./packages/contentstack-bootstrap run: npm run prepack - name: Publishing bootstrap - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.bootstrap-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -207,7 +207,7 @@ jobs: working-directory: ./packages/contentstack-bulk-publish run: npm install - name: Publishing bulk publish - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.bulk-publish-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -222,7 +222,7 @@ jobs: working-directory: ./packages/contentstack-launch run: npm run prepack - name: Publishing launch - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.launch-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -238,7 +238,7 @@ jobs: working-directory: ./packages/contentstack-branches run: npm run prepack - name: Publishing branches - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.branches-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} @@ -251,12 +251,12 @@ jobs: run: npm install - name: Publishing core id: publish-core - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@v2.2.1 if: ${{ steps.core-installation.conclusion == 'success' }} with: token: ${{ secrets.NPM_TOKEN }} package: ./packages/contentstack/package.json - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.5.3 with: ref: 'prod-qa-pipeline' - run: echo ${{ steps.publish-core.outputs.version }} > version.md @@ -264,12 +264,7 @@ jobs: with: message: 'Released version' - name: Create Release - uses: actions/create-release@v1 id: create_release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: v${{ steps.publish-core.outputs.version }} - release_name: Release ${{ steps.publish-core.outputs.version }} - draft: false # Default value, but nice to set explicitly - prerelease: false # Default value, but nice to set explicitly + run: gh release create v${{ steps.publish-core.outputs.version }} --title "Release ${{ steps.publish-core.outputs.version }}" --generate-notes diff --git a/.github/workflows/sca-scan.yml b/.github/workflows/sca-scan.yml index 1857400ff9..21ac795a5b 100644 --- a/.github/workflows/sca-scan.yml +++ b/.github/workflows/sca-scan.yml @@ -10,8 +10,8 @@ jobs: - uses: pnpm/action-setup@v2 with: version: 7 - - name: Use Node.js 16.x - uses: actions/setup-node@v3 + - name: Use Node.js 18.x + uses: actions/setup-node@v3.7.0 with: node-version: '18.x' cache: 'pnpm' diff --git a/package-lock.json b/package-lock.json index ed4e058c42..4eb63ff6ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18893,9 +18893,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "peer": true, "engines": { @@ -20298,23 +20298,23 @@ }, "packages/contentstack": { "name": "@contentstack/cli", - "version": "1.8.1", + "version": "1.8.2", "license": "MIT", "dependencies": { "@contentstack/cli-auth": "~1.3.12", "@contentstack/cli-cm-bootstrap": "~1.4.14", - "@contentstack/cli-cm-branches": "~1.0.10", + "@contentstack/cli-cm-branches": "~1.0.11", "@contentstack/cli-cm-bulk-publish": "~1.3.10", "@contentstack/cli-cm-clone": "~1.4.15", "@contentstack/cli-cm-export": "~1.8.0", - "@contentstack/cli-cm-export-to-csv": "~1.3.12", - "@contentstack/cli-cm-import": "~1.8.1", + "@contentstack/cli-cm-export-to-csv": "~1.4.0", + "@contentstack/cli-cm-import": "~1.8.2", "@contentstack/cli-cm-migrate-rte": "~1.4.10", "@contentstack/cli-cm-seed": "~1.4.14", "@contentstack/cli-command": "~1.2.11", "@contentstack/cli-config": "~1.4.10", "@contentstack/cli-launch": "~1.0.10", - "@contentstack/cli-migration": "~1.3.10", + "@contentstack/cli-migration": "~1.3.11", "@contentstack/cli-utilities": "~1.5.1", "@contentstack/management": "~1.10.0", "@oclif/core": "^2.9.3", @@ -20485,7 +20485,7 @@ }, "packages/contentstack-branches": { "name": "@contentstack/cli-cm-branches", - "version": "1.0.10", + "version": "1.0.11", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.2.11", @@ -21072,7 +21072,7 @@ }, "packages/contentstack-export-to-csv": { "name": "@contentstack/cli-cm-export-to-csv", - "version": "1.3.12", + "version": "1.4.0", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.2.11", @@ -21323,7 +21323,7 @@ }, "packages/contentstack-import": { "name": "@contentstack/cli-cm-import", - "version": "1.8.1", + "version": "1.8.2", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.2.11", @@ -21985,7 +21985,7 @@ }, "packages/contentstack-migration": { "name": "@contentstack/cli-migration", - "version": "1.3.10", + "version": "1.3.11", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.2.11", @@ -23703,18 +23703,18 @@ "requires": { "@contentstack/cli-auth": "~1.3.12", "@contentstack/cli-cm-bootstrap": "~1.4.14", - "@contentstack/cli-cm-branches": "~1.0.10", + "@contentstack/cli-cm-branches": "~1.0.11", "@contentstack/cli-cm-bulk-publish": "~1.3.10", "@contentstack/cli-cm-clone": "~1.4.15", "@contentstack/cli-cm-export": "~1.8.0", - "@contentstack/cli-cm-export-to-csv": "~1.3.12", - "@contentstack/cli-cm-import": "~1.8.1", + "@contentstack/cli-cm-export-to-csv": "~1.4.0", + "@contentstack/cli-cm-import": "~1.8.2", "@contentstack/cli-cm-migrate-rte": "~1.4.10", "@contentstack/cli-cm-seed": "~1.4.14", "@contentstack/cli-command": "~1.2.11", "@contentstack/cli-config": "~1.4.10", "@contentstack/cli-launch": "~1.0.10", - "@contentstack/cli-migration": "~1.3.10", + "@contentstack/cli-migration": "~1.3.11", "@contentstack/cli-utilities": "~1.5.1", "@contentstack/management": "~1.10.0", "@oclif/core": "^2.9.3", @@ -39923,9 +39923,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "peer": true }, diff --git a/packages/contentstack-branches/README.md b/packages/contentstack-branches/README.md index e961926c4c..d6c9aeb884 100755 --- a/packages/contentstack-branches/README.md +++ b/packages/contentstack-branches/README.md @@ -37,7 +37,7 @@ $ npm install -g @contentstack/cli-cm-branches $ csdx COMMAND running command... $ csdx (--version) -@contentstack/cli-cm-branches/1.0.10 darwin-arm64 node-v20.3.1 +@contentstack/cli-cm-branches/1.0.11 darwin-arm64 node-v20.3.1 $ csdx --help [COMMAND] USAGE $ csdx COMMAND diff --git a/packages/contentstack-branches/package.json b/packages/contentstack-branches/package.json index 7ed7022517..7d788ef48f 100644 --- a/packages/contentstack-branches/package.json +++ b/packages/contentstack-branches/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-branches", "description": "Contentstack CLI plugin to do branches operations", - "version": "1.0.10", + "version": "1.0.11", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { diff --git a/packages/contentstack-branches/src/branch/merge-handler.ts b/packages/contentstack-branches/src/branch/merge-handler.ts index 3e74f4474d..823da0743a 100644 --- a/packages/contentstack-branches/src/branch/merge-handler.ts +++ b/packages/contentstack-branches/src/branch/merge-handler.ts @@ -14,10 +14,10 @@ import { executeMerge, generateMergeScripts, selectCustomPreferences, + selectContentMergePreference, + selectContentMergeCustomPreferences, } from '../utils'; -const enableEntryExp = false; - export default class MergeHandler { private strategy: string; private strategySubOption?: string; @@ -66,6 +66,12 @@ export default class MergeHandler { await this.executeMerge(mergePayload); } else if (this.executeOption === 'export') { await this.exportSummary(mergePayload); + } else if (this.executeOption === 'merge_n_scripts') { + this.enableEntryExp = true; + await this.executeMerge(mergePayload); + } else if (this.executeOption === 'summary_n_scripts') { + this.enableEntryExp = true; + await this.exportSummary(mergePayload); } else { await this.exportSummary(mergePayload); await this.executeMerge(mergePayload); @@ -203,6 +209,10 @@ export default class MergeHandler { }; await writeFile(path.join(this.exportSummaryPath, 'merge-summary.json'), summary); cliux.success('Exported the summary successfully'); + + if (this.enableEntryExp) { + this.executeEntryExpFlow(this.stackAPIKey, mergePayload); + } } async executeMerge(mergePayload) { @@ -227,14 +237,63 @@ export default class MergeHandler { } } - executeEntryExpFlow(mergeJobUID: string, mergePayload) { - let scriptFolderPath = generateMergeScripts(this.mergeSettings.mergeContent, mergeJobUID); + async executeEntryExpFlow(mergeJobUID: string, mergePayload) { + const { mergeContent } = this.mergeSettings; + let mergePreference = await selectContentMergePreference(); + let selectedMergePreference; + + const updateEntryMergeStrategy = (items, mergeStrategy) => { + items && + items.forEach((item) => { + item.entry_merge_strategy = mergeStrategy; + }); + }; + + switch (mergePreference) { + case 'existing_new': + selectedMergePreference = 'merge_existing_new'; + updateEntryMergeStrategy(mergeContent.content_types.added, selectedMergePreference); + updateEntryMergeStrategy(mergeContent.content_types.modified, selectedMergePreference); + break; + + case 'new': + selectedMergePreference = 'merge_new'; + updateEntryMergeStrategy(mergeContent.content_types.added, selectedMergePreference); + break; + + case 'existing': + selectedMergePreference = 'merge_existing'; + updateEntryMergeStrategy(mergeContent.content_types.modified, selectedMergePreference); + break; + + case 'ask_preference': + selectedMergePreference = 'custom'; + const selectedMergeItems = await selectContentMergeCustomPreferences(mergeContent.content_types); + mergeContent.content_types = { + added: [], + modified: [], + deleted: [], + }; + + selectedMergeItems.forEach((item) => { + mergeContent.content_types[item.status].push(item.value); + }); + break; + + default: + cliux.error(`error: Invalid preference ${mergePreference}`); + process.exit(1); + } + + let scriptFolderPath = generateMergeScripts(mergeContent.content_types, mergeJobUID); if (scriptFolderPath !== undefined) { cliux.success(`\nSuccess! We have generated entry migration files in the folder ${scriptFolderPath}`); - + cliux.print('\nWARNING!!! Migration is not intended to be run more than once. Migrated(entries/assets) will be duplicated if run more than once', {color: 'yellow'}); + + const migrationCommand = `csdx cm:stacks:migration --multiple --file-path ./${scriptFolderPath} --config {compare-branch:${mergePayload.compare_branch},file-path:./${scriptFolderPath}} --branch ${mergePayload.base_branch} --stack-api-key ${this.stackAPIKey}`; cliux.print( - `\nKindly follow the steps in the guide "https://www.contentstack.com/docs/developers/cli/migrate-branch-entries" to update the migration scripts, and then run the command \n\ncsdx cm:stacks:migration --multiple --file-path ./${scriptFolderPath} --config compare-branch:${mergePayload.compare_branch} --branch ${mergePayload.base_branch} --stack-api-key ${this.stackAPIKey}`, + `\nKindly follow the steps in the guide "https://www.contentstack.com/docs/developers/cli/migrate-branch-entries" to update the migration scripts, and then run the command:\n\n${migrationCommand}`, { color: 'blue' }, ); } diff --git a/packages/contentstack-branches/src/commands/cm/branches/merge.ts b/packages/contentstack-branches/src/commands/cm/branches/merge.ts index 377a485ec6..2d65484670 100644 --- a/packages/contentstack-branches/src/commands/cm/branches/merge.ts +++ b/packages/contentstack-branches/src/commands/cm/branches/merge.ts @@ -91,7 +91,7 @@ export default class BranchMergeCommand extends Command { exportSummaryPath: branchMergeFlags['export-summary-path'], mergeSummary: branchMergeFlags.mergeSummary, host: this.cmaHost, - enableEntryExp: true, + enableEntryExp: false, }).start(); } catch (error) { console.log('Error in Merge operations', error); diff --git a/packages/contentstack-branches/src/utils/asset-folder-create-script.ts b/packages/contentstack-branches/src/utils/asset-folder-create-script.ts new file mode 100644 index 0000000000..30ad3ec1fe --- /dev/null +++ b/packages/contentstack-branches/src/utils/asset-folder-create-script.ts @@ -0,0 +1,157 @@ +export function assetFolderCreateScript(contentType) { + return ` + const fs = require('fs'); + const path = require('path'); + module.exports = async ({ migration, stackSDKInstance, managementAPIClient, config, branch, apiKey }) => { + let filePath = config['file-path'] || process.cwd(); + let compareBranch = config['compare-branch']; + let folderMapper = {}; + let folderBucket = []; + + const getAssetCount = async function (branchName, isDir = false) { + const queryParam = { + asc: 'created_at', + include_count: true, + skip: 10 ** 100, + }; + + if (isDir) queryParam.query = { is_dir: true }; + + return await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: branchName }) + .asset() + .query(queryParam) + .count() + .then(({ assets }) => assets) + .catch((error) => {}); + }; + + async function getFolderJSON(skip, fCount, branchName, folderData = []) { + const queryRequestObj = { + skip, + include_folders: true, + query: { is_dir: true }, + }; + + return await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: branchName }) + .asset() + .query(queryRequestObj) + .find() + .then(async (response) => { + skip += 100; + folderData = [...folderData, ...response.items]; + if (skip >= fCount) { + return folderData; + } + return await getFolderJSON(skip, fCount, branchName, folderData); + }) + .catch((error) => {}); + } + + function buildTree(coll) { + let tree = {}; + for (let i = 0; i < coll.length; i++) { + if (coll[i].parent_uid === null || !coll[i].hasOwnProperty('parent_uid')) { + tree[coll[i].uid] = {}; + } + } + findBranches(tree, Object.keys(tree), coll); + return tree; + } + + function findBranches(tree, branches, coll) { + branches.forEach((branch) => { + for (const element of coll) { + if (branch === element.parent_uid) { + let childUid = element.uid; + tree[branch][childUid] = {}; + return findBranches(tree[branch], [childUid], coll); + } + } + }); + } + + function buildFolderReqObjs(baseFolderUIDs, compareAssetsFolder, tree, parent_uid) { + for (let leaf in tree) { + //folder doesn't exists + if (baseFolderUIDs.indexOf(leaf) === -1) { + let folderObj = compareAssetsFolder.filter((folder) => folder.uid === leaf); + if (folderObj && folderObj.length) { + let requestOption = { + folderReq: { + asset: { + name: folderObj[0].name, + parent_uid: parent_uid || null, + }, + }, + oldUid: leaf, + }; + folderBucket.push(requestOption); + } + } + if (Object.keys(tree[leaf]).length > 0) { + buildFolderReqObjs(baseFolderUIDs, compareAssetsFolder, tree[leaf], leaf, folderBucket); + } + } + } + + async function createFolder(payload) { + if (folderMapper.hasOwnProperty(payload.folderReq.asset.parent_uid)) { + // replace old uid with new + payload.folderReq.asset.parent_uid = folderMapper[payload.folderReq.asset.parent_uid]; + } + await managementAPIClient + .stack({ api_key: apiKey, branch_uid: branch }) + .asset() + .folder() + .create(payload.folderReq) + .then((res) => { + folderMapper[payload.oldUid] = res.uid; + }) + .catch((err) => console.log(err)); + } + + const createAssetTask = () => { + return { + title: 'Create Assets Folder', + successTitle: 'Assets folder Created Successfully', + failedTitle: 'Failed to create assets folder', + task: async () => { + try { + const baseAssetsFolderCount = await getAssetCount(branch, true); + const compareAssetsFolderCount = await getAssetCount(compareBranch, true); + const baseAssetsFolder = await getFolderJSON(0, baseAssetsFolderCount, branch); + const compareAssetsFolder = await getFolderJSON(0, compareAssetsFolderCount, compareBranch); + if (Array.isArray(compareAssetsFolder) && Array.isArray(baseAssetsFolder)) { + const baseAssetUIDs = baseAssetsFolder.map((bAsset) => bAsset.uid); + //create new asset folder in base branch and update it in mapper + const tree = buildTree(compareAssetsFolder); + buildFolderReqObjs(baseAssetUIDs, compareAssetsFolder, tree, null); + for (let i = 0; i < folderBucket.length; i++) { + await createFolder(folderBucket[i]); + } + fs.writeFileSync(path.resolve(filePath, 'folder-mapper.json'), JSON.stringify(folderMapper)); + } + } catch (error) { + throw error; + } + }, + }; + }; + if (compareBranch && branch.length !== 0 && apiKey.length !== 0) { + migration.addTask(createAssetTask()); + } else { + if (apiKey.length === 0) { + console.error('Please provide api key using --stack-api-key flag'); + } + if (!compareBranch) { + console.error('Please provide compare branch through --config compare-branch: flag'); + } + if (branch.length === 0) { + console.error('Please provide branch name through --branch flag'); + } + } +} +`; +} diff --git a/packages/contentstack-branches/src/utils/create-merge-scripts.ts b/packages/contentstack-branches/src/utils/create-merge-scripts.ts index 1e8cc05bd8..9112abe615 100644 --- a/packages/contentstack-branches/src/utils/create-merge-scripts.ts +++ b/packages/contentstack-branches/src/utils/create-merge-scripts.ts @@ -1,29 +1,57 @@ import fs from 'fs'; +import { cliux } from '@contentstack/cli-utilities'; import { entryCreateScript } from './entry-create-script'; import { entryUpdateScript } from './entry-update-script'; +import { entryCreateUpdateScript } from './entry-create-update-script'; +import { assetFolderCreateScript } from './asset-folder-create-script'; type CreateMergeScriptsProps = { uid: string; - status: string; + entry_merge_strategy: string; + type?: string; }; export function generateMergeScripts(mergeSummary, mergeJobUID) { try { let scriptFolderPath; - if (mergeSummary.content_types.modified && mergeSummary.content_types.modified?.length !== 0) { - mergeSummary.content_types.modified.map((contentType) => { - let data = entryUpdateScript(contentType.uid); - scriptFolderPath = createMergeScripts(contentType, data, mergeJobUID); - }); - } + const processContentType = (contentType, scriptFunction) => { + let data: any; + if (contentType.uid) { + data = scriptFunction(contentType.uid); + } else { + data = scriptFunction(); + } + scriptFolderPath = createMergeScripts(contentType, mergeJobUID, data); + }; - if (mergeSummary.content_types.added && mergeSummary.content_types.added?.length !== 0) { - mergeSummary.content_types.added?.map((contentType) => { - let data = entryCreateScript(contentType.uid); - scriptFolderPath = createMergeScripts(contentType, data, mergeJobUID); - }); - } + const mergeStrategies = { + asset_create_folder: assetFolderCreateScript, + merge_existing_new: entryCreateUpdateScript, + merge_existing: entryUpdateScript, + merge_new: entryCreateScript, + ignore: entryCreateUpdateScript, + }; + + const processContentTypes = (contentTypes, messageType) => { + if (contentTypes && contentTypes.length > 0) { + processContentType( + { type: 'assets', uid: '', entry_merge_strategy: '' }, + mergeStrategies['asset_create_folder'], + ); + contentTypes.forEach((contentType) => { + const mergeStrategy = contentType.entry_merge_strategy; + if (mergeStrategies.hasOwnProperty(mergeStrategy)) { + processContentType(contentType, mergeStrategies[mergeStrategy]); + } + }); + } else { + cliux.print(`No ${messageType} entries selected for merge`, { color: 'yellow' }); + } + }; + + processContentTypes(mergeSummary.modified, 'Modified'); + processContentTypes(mergeSummary.added, 'New'); return scriptFolderPath; } catch (error) { @@ -31,17 +59,19 @@ export function generateMergeScripts(mergeSummary, mergeJobUID) { } } -export function getContentypeMergeStatus(status) { - if (status === 'modified') { +export function getContentTypeMergeStatus(status) { + if (status === 'merge_existing') { return 'updated'; - } else if (status === 'compare_only') { + } else if (status === 'merge_new') { return 'created'; + } else if (status === 'merge_existing_new') { + return 'created_updated'; } else { return ''; } } -export function createMergeScripts(contentType: CreateMergeScriptsProps, content, mergeJobUID) { +export function createMergeScripts(contentType: CreateMergeScriptsProps, mergeJobUID: string, content?: any) { const date = new Date(); const rootFolder = 'merge_scripts'; const fileCreatedAt = `${date.getFullYear()}${ @@ -63,11 +93,15 @@ export function createMergeScripts(contentType: CreateMergeScriptsProps, content if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath); } - fs.writeFileSync( - `${fullPath}/${fileCreatedAt}_${getContentypeMergeStatus(contentType.status)}_${contentType.uid}.js`, - content, - 'utf-8', - ); + let filePath: string; + if (contentType.type === 'assets') { + filePath = `${fullPath}/${fileCreatedAt}_create_assets_folder.js`; + } else { + filePath = `${fullPath}/${fileCreatedAt}_${getContentTypeMergeStatus(contentType.entry_merge_strategy)}_${ + contentType.uid + }.js`; + } + fs.writeFileSync(filePath, content, 'utf-8'); } return fullPath; } catch (error) { diff --git a/packages/contentstack-branches/src/utils/entry-create-script.ts b/packages/contentstack-branches/src/utils/entry-create-script.ts index fb1c1d6ce1..5739966094 100644 --- a/packages/contentstack-branches/src/utils/entry-create-script.ts +++ b/packages/contentstack-branches/src/utils/entry-create-script.ts @@ -1,69 +1,498 @@ export function entryCreateScript(contentType) { return ` -module.exports = async ({ migration, stackSDKInstance, managementAPIClient, config, branch, apiKey }) => { - const keysToRemove = [ - 'content_type_uid', - 'created_at', - 'updated_at', - 'created_by', - 'updated_by', - 'ACL', - 'stackHeaders', - 'urlPath', - '_version', - '_in_progress', - 'update', - 'delete', - 'fetch', - 'publish', - 'unpublish', - 'publishRequest', - 'setWorkflowStage', - 'import', - ]; + const fs = require('fs'); + const path = require('path'); + module.exports = async ({ migration, stackSDKInstance, managementAPIClient, config, branch, apiKey }) => { + const keysToRemove = [ + 'content_type_uid', + 'created_at', + 'updated_at', + 'created_by', + 'updated_by', + 'ACL', + 'stackHeaders', + 'urlPath', + '_version', + '_in_progress', + 'update', + 'delete', + 'fetch', + 'publish', + 'unpublish', + 'publishRequest', + 'setWorkflowStage', + 'import', + ]; - let compareBranch = config['compare-branch']; + let compareBranch = config['compare-branch']; + let filePath = config['file-path'] || process.cwd(); + let assetDirPath = path.resolve(filePath, 'assets'); + let cAssetDetails = []; + let assetUIDMapper = {}; + let assetUrlMapper = {}; + let assetRefPath = {}; + let isAssetDownload = false; - const createEntryTask = () => { - return { - title: 'Create Entries', - successTitle: 'Entries Created Successfully', - failedTitle: 'Failed to create entries', - task: async () => { - const compareBranchEntries = await managementAPIClient - .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) // - .contentType('${contentType}') - .entry() - .query() - .find(); - const compareFilteredProperties = compareBranchEntries.items.map((entry) => { - keysToRemove.map((key) => delete entry[key]); - return entry; - }); - try { - compareFilteredProperties.length !== 0 && - compareFilteredProperties.forEach(async (entryDetails) => { - await stackSDKInstance.contentType('${contentType}').entry().create({ entry: entryDetails }); - }); - } catch (error) { - throw error; + function getValueByPath(obj, path) { + return path.split('[').reduce((o, key) => o && o[key.replace(/\]$/, '')], obj); + } + + function updateValueByPath(obj, path, newValue, type, index) { + path.split('[').reduce((o, key, index, arr) => { + if (index === arr.length - 1) { + if (type === 'file') { + o[key.replace(/]$/, '')][index] = newValue; + } else { + o[key.replace(/]$/, '')][0].uid = newValue; + } + } else { + return o[key.replace(/\]$/, '')]; + } + }, obj); + } + + const findReference = function (schema, path, flag) { + let references = []; + + for (const i in schema) { + const currentPath = path ? path + '[' + schema[i].uid : schema[i].uid; + if (schema[i].data_type === 'group' || schema[i].data_type === 'global_field') { + references = references.concat(findReference(schema[i].schema, currentPath, flag)); + } else if (schema[i].data_type === 'blocks') { + for (const block in schema[i].blocks) { + references = references.concat( + findReference( + schema[i].blocks[block].schema, + currentPath + '[' + block + '][' + schema[i].blocks[block].uid + ']', + flag, + ), + ); + } + } else if (schema[i].data_type === 'reference') { + flag.references = true; + references.push(currentPath); } - }, + } + + return references; }; - }; - if (compareBranch && branch.length !== 0 && apiKey.length !== 0) { - migration.addTask(createEntryTask()); - } else { - if (apiKey.length === 0) { - console.error('Please provide api key using --stack-api-key flag'); + + const findAssets = function (schema, entry, refPath, path) { + for (const i in schema) { + const currentPath = path ? path + '[' + schema[i].uid : schema[i].uid; + if (schema[i].data_type === 'group' || schema[i].data_type === 'global_field') { + findAssets(schema[i].schema, entry, refPath, currentPath); + } else if (schema[i].data_type === 'blocks') { + for (const block in schema[i].blocks) { + { + if (schema[i].blocks[block].schema) { + findAssets( + schema[i].blocks[block].schema, + entry, + refPath, + currentPath + '[' + block + '][' + schema[i].blocks[block].uid + ']', + ); + } + } + } + } else if (schema[i].data_type === 'json' && schema[i].field_metadata.rich_text_type) { + findAssetIdsFromJsonRte(entry, schema, refPath, path); + } else if ( + schema[i].data_type === 'text' && + schema[i].field_metadata && + (schema[i].field_metadata.markdown || schema[i].field_metadata.rich_text_type) + ) { + findFileUrls(schema[i], entry); + } else if (schema[i].data_type === 'file') { + refPath.push(currentPath) + const imgDetails = getValueByPath(entry, currentPath); + if (schema[i].multiple) { + if (imgDetails && imgDetails.length) { + imgDetails.forEach((img) => { + const obj = { + uid: img.uid, + parent_uid: img.parent_uid, + description: img.description, + title: img.title, + filename: img.filename, + url: img.url, + }; + cAssetDetails.push(obj); + }); + } + } else { + if (imgDetails) { + const obj = { + uid: imgDetails.uid, + parent_uid: imgDetails.parent_uid, + description: imgDetails.description, + title: imgDetails.title, + filename: imgDetails.filename, + url: imgDetails.url, + }; + cAssetDetails.push(obj); + } + } + } + } + }; + + function findFileUrls(schema, _entry) { + let markdownRegEx; + let markdownMatch; + let text; + // Regex to detect v3 asset uri patterns + if (schema && schema.field_metadata && schema.field_metadata.markdown) { + text = marked(JSON.stringify(_entry)); + } else { + text = JSON.stringify(_entry); + } + markdownRegEx = new RegExp( + '(https://(assets|(eu-|azure-na-|azure-eu-)?images).contentstack.(io|com)/v3/assets/(.*?)/(.*?)/(.*?)/(.*?)(?="))', + 'g', + ); + while ((markdownMatch = markdownRegEx.exec(text)) !== null) { + if (markdownMatch && typeof markdownMatch[0] === 'string') { + const assetDetails = markdownMatch[0].split('/'); + //fetch assetUID from url + const assetUID = assetDetails && assetDetails[6]; + const obj = { + uid: assetUID, + url: markdownMatch[0], + }; + cAssetDetails.push(obj); + } + } } - if (!compareBranch) { - console.error('Please provide compare branch through --config compare-branch: flag'); + + function findAssetIdsFromJsonRte(entryObj, ctSchema) { + if(ctSchema !== undefined){ + for (const element of ctSchema) { + switch (element.data_type) { + case 'blocks': { + if (entryObj[element.uid]) { + if (element.multiple) { + entryObj[element.uid].forEach((e) => { + let key = Object.keys(e).pop(); + let subBlock = element.blocks.filter((block) => block.uid === key).pop(); + findAssetIdsFromJsonRte(e[key], subBlock.schema); + }); + } + } + break; + } + case 'global_field': + case 'group': { + if (entryObj[element.uid]) { + if (element.multiple) { + entryObj[element.uid].forEach((e) => { + findAssetIdsFromJsonRte(e, element.schema); + }); + } else { + findAssetIdsFromJsonRte(entryObj[element.uid], element.schema); + } + } + break; + } + case 'json': { + if (entryObj[element.uid] && element.field_metadata.rich_text_type) { + if (element.multiple) { + entryObj[element.uid].forEach((jsonRteData) => { + gatherJsonRteAssetIds(jsonRteData); + }); + } else { + gatherJsonRteAssetIds(entryObj[element.uid]); + } + } + break; + } + } + } + } } - if (branch.length === 0) { - console.error('Please provide branch name through --branch flag'); + + function gatherJsonRteAssetIds(jsonRteData) { + jsonRteData.children.forEach((element) => { + if (element.type) { + switch (element.type) { + case 'a': + case 'p': { + if (element.children && element.children.length > 0) { + gatherJsonRteAssetIds(element); + } + break; + } + case 'reference': { + if (Object.keys(element.attrs).length > 0 && element.attrs.type === 'asset') { + cAssetDetails.push({ uid: element.attrs['asset-uid'] }); + if (element.attrs['asset-link']) { + const assetDetails = element.attrs['asset-link'].split('/'); + //fetch assetUID from url + const assetUID = assetDetails && assetDetails[6]; + const obj = { + uid: assetUID, + url: element.attrs['asset-link'], + }; + cAssetDetails.push(obj); + } else if (element.attrs['href']) { + const assetDetails = element.attrs['href'].split('/'); + //fetch assetUID from url + const assetUID = assetDetails && assetDetails[6]; + const obj = { + uid: assetUID, + url: element.attrs['href'], + }; + cAssetDetails.push(obj); + } + } + if (element.children && element.children.length > 0) { + gatherJsonRteAssetIds(element); + } + break; + } + } + } + }); } - } -}; + + const updateAssetDetailsInEntries = function (entry) { + assetRefPath[entry.uid].forEach((refPath) => { + let imgDetails = entry[refPath]; + if (imgDetails !== undefined) { + if (imgDetails && !Array.isArray(imgDetails)) { + entry[refPath] = assetUIDMapper[imgDetails.uid]; + } else if (imgDetails && Array.isArray(imgDetails)) { + for (let i = 0; i < imgDetails.length; i++) { + const img = imgDetails[i]; + entry[refPath][i] = assetUIDMapper[img.uid]; + } + } + } else { + imgDetails = getValueByPath(entry, refPath); + if (imgDetails && !Array.isArray(imgDetails)) { + const imgUID = imgDetails?.uid; + updateValueByPath(entry, refPath, assetUIDMapper[imgUID], 'file', 0); + } else if (imgDetails && Array.isArray(imgDetails)) { + for (let i = 0; i < imgDetails.length; i++) { + const img = imgDetails[i]; + const imgUID = img?.uid; + updateValueByPath(entry, refPath, assetUIDMapper[imgUID], 'file', i); + } + } + } + }); + entry = JSON.stringify(entry); + const assetUrls = cAssetDetails.map((asset) => asset.url); + const assetUIDs = cAssetDetails.map((asset) => asset.uid); + assetUrls.forEach(function (assetUrl) { + let mappedAssetUrl = assetUrlMapper[assetUrl]; + if (typeof mappedAssetUrl !== 'undefined') { + entry = entry.replace(assetUrl, mappedAssetUrl); + } + }); + + assetUIDs.forEach(function (assetUid) { + let uid = assetUIDMapper[assetUid]; + if (typeof uid !== 'undefined') { + entry = entry.replace(new RegExp(assetUid, 'img'), uid); + } + }); + return JSON.parse(entry); + }; + + const checkAndDownloadAsset = async function (cAsset) { + const assetUID = cAsset?.uid; + if (cAsset && assetUID) { + const bAssetDetail = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: branch }) + .asset(assetUID) + .fetch() + .then((assets) => assets) + .catch((error) => {}); + if (bAssetDetail) { + assetUIDMapper[cAsset.uid] = bAssetDetail.uid; + assetUrlMapper[cAsset.url] = bAssetDetail.url; + return false; + } + else { + isAssetDownload = true; + const cAssetDetail = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .asset(assetUID) + .fetch() + .then((assets) => assets) + .catch((error) => {}); + const updatedObj = { + parent_uid: cAssetDetail.parent_uid, + description: cAssetDetail.description, + title: cAssetDetail.title, + filename: cAssetDetail.filename, + url: cAssetDetail.url, + }; + Object.assign(cAsset, updatedObj); + const url = cAssetDetail?.url; + if (url) { + const assetFolderPath = path.resolve(assetDirPath, assetUID); + if (!fs.existsSync(assetFolderPath)) fs.mkdirSync(assetFolderPath); + const assetFilePath = path.resolve(assetFolderPath, cAsset.filename); + const assetWriterStream = fs.createWriteStream(assetFilePath); + const data = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .asset(assetUID) + .download({ url, responseType: 'stream' }) + .then(({ data }) => data) + .catch((error) => { + throw error; + }); + assetWriterStream.on('error', (error) => { + throw error; + }); + data.pipe(assetWriterStream); + } + } + } + return cAsset; + }; + + const uploadAssets = async function () { + const assetFolderMap = JSON.parse(fs.readFileSync(path.resolve(filePath, 'folder-mapper.json'), 'utf8')); + const stackAPIClient = managementAPIClient.stack({ api_key: stackSDKInstance.api_key, branch_uid: branch }); + for (let i = 0; i < cAssetDetails?.length; i++) { + const asset = cAssetDetails[i]; + let requestOption = {}; + + requestOption.parent_uid = assetFolderMap[asset.parent_uid] || asset.parent_uid; + + if (asset.hasOwnProperty('description') && typeof asset.description === 'string') { + requestOption.description = asset.description; + } + + if (asset.hasOwnProperty('tags') && Array.isArray(asset.tags)) { + requestOption.tags = asset.tags; + } + + if (asset.hasOwnProperty('title') && typeof asset.title === 'string') { + requestOption.title = asset.title; + } + requestOption.upload = path.resolve(assetDirPath, asset.uid, asset.filename); + const res = await stackAPIClient + .asset() + .create(requestOption) + .then((asset) => asset) + .catch((error) => { + throw error; + }); + assetUIDMapper[asset.uid] = res && res.uid; + assetUrlMapper[asset.url] = res && res.url; + } + }; + + const createEntryTask = () => { + return { + title: 'Create Entries', + successTitle: 'Entries Created Successfully', + failedTitle: 'Failed to create entries', + task: async () => { + const compareBranchEntries = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .contentType('${contentType}') + .entry() + .query() + .find(); + + const compareFilteredProperties = compareBranchEntries.items.map((entry) => { + keysToRemove.map((key) => delete entry[key]); + return entry; + }); + + let contentType = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .contentType('${contentType}') + .fetch(); + + for (let i = 0; i < compareBranchEntries?.items?.length; i++) { + assetRefPath[compareBranchEntries.items[i].uid] = [] + findAssets(contentType.schema, compareBranchEntries.items[i], assetRefPath[compareBranchEntries.items[i].uid]); + cAssetDetails = [...new Map(cAssetDetails.map((item) => [item['uid'], item])).values()]; + } + if (cAssetDetails && cAssetDetails.length) { + if (!fs.existsSync(assetDirPath)) { + fs.mkdirSync(assetDirPath); + } + for (let i = 0; i < cAssetDetails.length; i++) { + const asset = cAssetDetails[i]; + const updatedCAsset = await checkAndDownloadAsset(asset); + if (updatedCAsset) { + cAssetDetails[i] = updatedCAsset; + } + } + if (isAssetDownload) await uploadAssets(); + } + + let flag = { + references: false, + }; + + const references = await findReference(contentType.schema, '', flag); + + async function updateEntry(entry, entryDetails) { + Object.assign(entry, { ...entryDetails }); + await entry.update(); + } + + async function updateReferences(entryDetails, baseEntry, references) { + for (let i in references) { + let compareEntryRef = getValueByPath(entryDetails, references[i]); + let baseEntryRef = getValueByPath(baseEntry, references[i]); + + if (compareEntryRef && compareEntryRef.length > 0 && baseEntryRef && baseEntryRef.length >= 0) { + let compareRefEntry = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .contentType(compareEntryRef[0]._content_type_uid) + .entry(compareEntryRef[0].uid) + .fetch(); + let baseRefEntry = await stackSDKInstance + .contentType(compareEntryRef[0]._content_type_uid) + .entry() + .query({ query: { title: compareRefEntry.title } }) + .find(); + + updateValueByPath(entryDetails, references[i], baseRefEntry.items[0].uid); + } + } + } + + try { + compareFilteredProperties.length !== 0 && + compareFilteredProperties.forEach(async (entryDetails) => { + entryDetails = updateAssetDetailsInEntries(entryDetails); + let createdEntry = await stackSDKInstance.contentType('${contentType}').entry().create({ entry: entryDetails }).catch(error => { + throw error; + }); + if (flag.references) { + await updateReferences(entryDetails, createdEntry, references); + } + await updateEntry(createdEntry, entryDetails); + }); + } catch (error) { + throw error; + } + }, + }; + }; + if (compareBranch && branch.length !== 0 && apiKey.length !== 0) { + migration.addTask(createEntryTask()); + } else { + if (apiKey.length === 0) { + console.error('Please provide api key using --stack-api-key flag'); + } + if (!compareBranch) { + console.error('Please provide compare branch through --config compare-branch: flag'); + } + if (branch.length === 0) { + console.error('Please provide branch name through --branch flag'); + } + } + }; `; } diff --git a/packages/contentstack-branches/src/utils/entry-create-update-script.ts b/packages/contentstack-branches/src/utils/entry-create-update-script.ts new file mode 100644 index 0000000000..64b188d925 --- /dev/null +++ b/packages/contentstack-branches/src/utils/entry-create-update-script.ts @@ -0,0 +1,567 @@ +export function entryCreateUpdateScript(contentType) { + return ` + const fs = require('fs'); + const path = require('path'); + module.exports = async ({ migration, stackSDKInstance, managementAPIClient, config, branch, apiKey }) => { + const keysToRemove = [ + 'content_type_uid', + 'created_at', + 'updated_at', + 'created_by', + 'updated_by', + 'ACL', + 'stackHeaders', + 'urlPath', + '_version', + '_in_progress', + 'update', + 'delete', + 'fetch', + 'publish', + 'unpublish', + 'publishRequest', + 'setWorkflowStage', + 'import', + ]; + + let compareBranch = config['compare-branch']; + let filePath = config['file-path'] || process.cwd(); + let assetDirPath = path.resolve(filePath, 'assets'); + let assetDetails = []; + let newAssetDetails = []; + let assetUIDMapper = {}; + let assetUrlMapper = {}; + let assetRefPath = {}; + let isAssetDownload = false; + + function converter(data) { + let arr = []; + for (const elm of data.entries()) { + // @ts-ignore + arr.push([elm[1].title, elm[1]]); + } + return arr; + } + + function deleteUnwantedKeysFromObject(obj, keysToRemove) { + if(obj){ + keysToRemove.map((key) => delete obj[key]); + return obj; + } + } + + function uniquelyConcatenateArrays(compareArr, baseArr) { + let uniqueArray = compareArr.concat(baseArr.filter((item) => compareArr.indexOf(item) < 0)); + return uniqueArray; + } + + function getValueByPath(obj, path) { + return path.split('[').reduce((o, key) => o && o[key.replace(/\]$/, '')], obj); + } + + function updateValueByPath(obj, path, newValue, type, index) { + path.split('[').reduce((o, key, index, arr) => { + if (index === arr.length - 1) { + if (type === 'file') { + o[key.replace(/]$/, '')][index] = newValue; + } else { + o[key.replace(/]$/, '')][0].uid = newValue; + } + } else { + return o[key.replace(/\]$/, '')]; + } + }, obj); + } + + const findReference = function (schema, path, flag) { + let references = []; + + for (const i in schema) { + const currentPath = path ? path + '[' + schema[i].uid : schema[i].uid; + if (schema[i].data_type === 'group' || schema[i].data_type === 'global_field') { + references = references.concat(findReference(schema[i].schema, currentPath, flag)); + } else if (schema[i].data_type === 'blocks') { + for (const block in schema[i].blocks) { + references = references.concat( + findReference( + schema[i].blocks[block].schema, + currentPath + '[' + block + '][' + schema[i].blocks[block].uid + ']', + flag, + ), + ); + } + } else if (schema[i].data_type === 'reference') { + flag.references = true; + references.push(currentPath); + } + } + + return references; + }; + + const findAssets = function (schema, entry, refPath, path) { + for (const i in schema) { + const currentPath = path ? path + '[' + schema[i].uid : schema[i].uid; + if (schema[i].data_type === 'group' || schema[i].data_type === 'global_field') { + findAssets(schema[i].schema, entry, refPath, currentPath); + } else if (schema[i].data_type === 'blocks') { + for (const block in schema[i].blocks) { + { + if (schema[i].blocks[block].schema) { + findAssets( + schema[i].blocks[block].schema, + entry, + refPath, + currentPath + '[' + block + '][' + schema[i].blocks[block].uid + ']', + ); + } + } + } + } else if (schema[i].data_type === 'json' && schema[i].field_metadata.rich_text_type) { + findAssetIdsFromJsonRte(entry, schema, refPath, path); + } else if ( + schema[i].data_type === 'text' && + schema[i].field_metadata && + (schema[i].field_metadata.markdown || schema[i].field_metadata.rich_text_type) + ) { + findFileUrls(schema[i], entry); + } else if (schema[i].data_type === 'file') { + refPath.push(currentPath) + const imgDetails = getValueByPath(entry, currentPath); + if (schema[i].multiple) { + if (imgDetails && imgDetails.length) { + imgDetails.forEach((img) => { + const obj = { + uid: img.uid, + parent_uid: img.parent_uid, + description: img.description, + title: img.title, + filename: img.filename, + url: img.url, + }; + assetDetails.push(obj); + }); + } + } else { + if (imgDetails) { + const obj = { + uid: imgDetails.uid, + parent_uid: imgDetails.parent_uid, + description: imgDetails.description, + title: imgDetails.title, + filename: imgDetails.filename, + url: imgDetails.url, + }; + assetDetails.push(obj); + } + } + } + } + }; + + function findFileUrls(schema, _entry) { + let markdownRegEx; + let markdownMatch; + let text; + // Regex to detect v3 asset uri patterns + if (schema && schema.field_metadata && schema.field_metadata.markdown) { + text = marked(JSON.stringify(_entry)); + } else { + text = JSON.stringify(_entry); + } + markdownRegEx = new RegExp( + '(https://(assets|(eu-|azure-na-|azure-eu-)?images).contentstack.(io|com)/v3/assets/(.*?)/(.*?)/(.*?)/(.*?)(?="))', + 'g', + ); + while ((markdownMatch = markdownRegEx.exec(text)) !== null) { + if (markdownMatch && typeof markdownMatch[0] === 'string') { + const assetDetails = markdownMatch[0].split('/'); + //fetch assetUID from url + const assetUID = assetDetails && assetDetails[6]; + const obj = { + uid: assetUID, + url: markdownMatch[0], + }; + assetDetails.push(obj); + } + } + } + + function findAssetIdsFromJsonRte(entryObj, ctSchema) { + if(ctSchema !== undefined){ + for (const element of ctSchema) { + switch (element.data_type) { + case 'blocks': { + if (entryObj[element.uid]) { + if (element.multiple) { + entryObj[element.uid].forEach((e) => { + let key = Object.keys(e).pop(); + let subBlock = element.blocks.filter((block) => block.uid === key).pop(); + findAssetIdsFromJsonRte(e[key], subBlock.schema); + }); + } + } + break; + } + case 'global_field': + case 'group': { + if (entryObj[element.uid]) { + if (element.multiple) { + entryObj[element.uid].forEach((e) => { + findAssetIdsFromJsonRte(e, element.schema); + }); + } else { + findAssetIdsFromJsonRte(entryObj[element.uid], element.schema); + } + } + break; + } + case 'json': { + if (entryObj[element.uid] && element.field_metadata.rich_text_type) { + if (element.multiple) { + entryObj[element.uid].forEach((jsonRteData) => { + gatherJsonRteAssetIds(jsonRteData); + }); + } else { + gatherJsonRteAssetIds(entryObj[element.uid]); + } + } + break; + } + } + } + } + } + + function gatherJsonRteAssetIds(jsonRteData) { + jsonRteData.children.forEach((element) => { + if (element.type) { + switch (element.type) { + case 'a': + case 'p': { + if (element.children && element.children.length > 0) { + gatherJsonRteAssetIds(element); + } + break; + } + case 'reference': { + if (Object.keys(element.attrs).length > 0 && element.attrs.type === 'asset') { + assetDetails.push({ uid: element.attrs['asset-uid'] }); + if (element.attrs['asset-link']) { + const assetDetails = element.attrs['asset-link'].split('/'); + //fetch assetUID from url + const assetUID = assetDetails && assetDetails[6]; + const obj = { + uid: assetUID, + url: element.attrs['asset-link'], + }; + assetDetails.push(obj); + } else if (element.attrs['href']) { + const assetDetails = element.attrs['href'].split('/'); + //fetch assetUID from url + const assetUID = assetDetails && assetDetails[6]; + const obj = { + uid: assetUID, + url: element.attrs['href'], + }; + assetDetails.push(obj); + } + } + if (element.children && element.children.length > 0) { + gatherJsonRteAssetIds(element); + } + break; + } + } + } + }); + } + + const updateAssetDetailsInEntries = function (entry) { + assetRefPath[entry.uid].forEach((refPath) => { + let imgDetails = entry[refPath]; + if (imgDetails !== undefined) { + if (imgDetails && !Array.isArray(imgDetails)) { + entry[refPath] = assetUIDMapper[imgDetails.uid]; + } else if (imgDetails && Array.isArray(imgDetails)) { + for (let i = 0; i < imgDetails.length; i++) { + const img = imgDetails[i]; + entry[refPath][i] = assetUIDMapper[img.uid]; + } + } + } else { + imgDetails = getValueByPath(entry, refPath); + if (imgDetails && !Array.isArray(imgDetails)) { + const imgUID = imgDetails?.uid; + updateValueByPath(entry, refPath, assetUIDMapper[imgUID], 'file', 0); + } else if (imgDetails && Array.isArray(imgDetails)) { + for (let i = 0; i < imgDetails.length; i++) { + const img = imgDetails[i]; + const imgUID = img?.uid; + updateValueByPath(entry, refPath, assetUIDMapper[imgUID], 'file', i); + } + } + } + }); + entry = JSON.stringify(entry); + const assetUrls = assetDetails.map((asset) => asset.url); + const assetUIDs = assetDetails.map((asset) => asset.uid); + assetUrls.forEach(function (assetUrl) { + let mappedAssetUrl = assetUrlMapper[assetUrl]; + if (typeof mappedAssetUrl !== 'undefined') { + entry = entry.replace(assetUrl, mappedAssetUrl); + } + }); + + assetUIDs.forEach(function (assetUid) { + let uid = assetUIDMapper[assetUid]; + if (typeof uid !== 'undefined') { + entry = entry.replace(new RegExp(assetUid, 'img'), uid); + } + }); + return JSON.parse(entry); + }; + + const checkAndDownloadAsset = async function (cAsset) { + if (cAsset) { + const assetUID = cAsset.uid; + const bAssetDetail = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: branch }) + .asset(assetUID) + .fetch() + .then((assets) => assets) + .catch((error) => {}); + if (bAssetDetail) { + assetUIDMapper[cAsset.uid] = bAssetDetail.uid; + assetUrlMapper[cAsset.url] = bAssetDetail.url; + return false; + } + else { + isAssetDownload = true; + const cAssetDetail = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .asset(assetUID) + .fetch() + .then((assets) => assets) + .catch((error) => {}); + const updatedObj = { + parent_uid: cAssetDetail.parent_uid, + description: cAssetDetail.description, + title: cAssetDetail.title, + filename: cAssetDetail.filename, + url: cAssetDetail.url, + }; + Object.assign(cAsset, updatedObj); + const url = cAssetDetail?.url; + if (url) { + const assetFolderPath = path.resolve(assetDirPath, assetUID); + if (!fs.existsSync(assetFolderPath)) fs.mkdirSync(assetFolderPath); + const assetFilePath = path.resolve(assetFolderPath, cAsset.filename); + const assetWriterStream = fs.createWriteStream(assetFilePath); + const data = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .asset(assetUID) + .download({ url, responseType: 'stream' }) + .then(({ data }) => data) + .catch((error) => { + throw error; + }); + assetWriterStream.on('error', (error) => { + throw error; + }); + data.pipe(assetWriterStream); + } + } + } + return cAsset; + }; + + const uploadAssets = async function () { + const assetFolderMap = JSON.parse(fs.readFileSync(path.resolve(filePath, 'folder-mapper.json'), 'utf8')); + const stackAPIClient = managementAPIClient.stack({ api_key: stackSDKInstance.api_key, branch_uid: branch }); + for (let i = 0; i < assetDetails?.length; i++) { + const asset = assetDetails[i]; + let requestOption = {}; + + requestOption.parent_uid = assetFolderMap[asset.parent_uid] || asset.parent_uid; + + if (asset.hasOwnProperty('description') && typeof asset.description === 'string') { + requestOption.description = asset.description; + } + + if (asset.hasOwnProperty('tags') && Array.isArray(asset.tags)) { + requestOption.tags = asset.tags; + } + + if (asset.hasOwnProperty('title') && typeof asset.title === 'string') { + requestOption.title = asset.title; + } + requestOption.upload = path.resolve(assetDirPath, asset.uid, asset.filename); + const res = await stackAPIClient + .asset() + .create(requestOption) + .then((asset) => asset) + .catch((error) => { + throw error; + }); + assetUIDMapper[asset.uid] = res && res.uid; + assetUrlMapper[asset.url] = res && res.url; + } + }; + + + const updateEntryTask = () => { + return { + title: 'Update Entries', + successMessage: 'Entries Updated Successfully', + failedMessage: 'Failed to update entries', + task: async () => { + let compareBranchEntries = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .contentType('${contentType}') + .entry() + .query() + .find(); + + let baseBranchEntries = await stackSDKInstance.contentType('${contentType}').entry().query().find(); + + let contentType = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .contentType('${contentType}') + .fetch(); + + for (let i = 0; i < compareBranchEntries?.items?.length; i++) { + assetRefPath[compareBranchEntries.items[i].uid] = [] + findAssets(contentType.schema, compareBranchEntries.items[i], assetRefPath[compareBranchEntries.items[i].uid]); + } + + for (let i = 0; i < baseBranchEntries?.items?.length; i++) { + assetRefPath[baseBranchEntries.items[i].uid] = [] + findAssets(contentType.schema, baseBranchEntries.items[i], assetRefPath[baseBranchEntries.items[i].uid]); + } + assetDetails = [...new Map(assetDetails.map((item) => [item['uid'], item])).values()]; + newAssetDetails = assetDetails; + + if (newAssetDetails && newAssetDetails.length) { + if (!fs.existsSync(assetDirPath)) { + fs.mkdirSync(assetDirPath); + } + for (let i = 0; i < newAssetDetails.length; i++) { + const asset = newAssetDetails[i]; + const updatedCAsset = await checkAndDownloadAsset(asset); + if(updatedCAsset){ + newAssetDetails[i] = updatedCAsset; + } + } + if (isAssetDownload) await uploadAssets(); + } + + let flag = { + references: false + }; + + const references = await findReference(contentType.schema, '', flag); + + async function updateEntry(entry, entryDetails) { + Object.assign(entry, { ...entryDetails }); + await entry.update(); + } + + async function updateReferences(entryDetails, baseEntry, references) { + for (let i in references) { + let compareEntryRef = getValueByPath(entryDetails, references[i]); + let baseEntryRef = getValueByPath(baseEntry, references[i]); + + if (compareEntryRef && compareEntryRef.length > 0 && baseEntryRef && baseEntryRef.length >= 0) { + let compareRefEntry = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .contentType(compareEntryRef[0]._content_type_uid) + .entry(compareEntryRef[0].uid) + .fetch(); + let baseRefEntry = await stackSDKInstance + .contentType(compareEntryRef[0]._content_type_uid) + .entry() + .query({ query: { title: compareRefEntry.title } }) + .find(); + + updateValueByPath(entryDetails, references[i], baseRefEntry.items[0].uid); + } + } + } + + try { + if (contentType.options.singleton) { + compareBranchEntries.items.map(async (el) => { + let entryDetails = deleteUnwantedKeysFromObject(el, keysToRemove); + entryDetails = updateAssetDetailsInEntries(entryDetails); + + if (baseBranchEntries && baseBranchEntries.items.length) { + let baseEntryUid = baseBranchEntries.items[0].uid; + let entry = await stackSDKInstance.contentType('${contentType}').entry(baseEntryUid); + + if (flag.references) { + await updateReferences(entryDetails, baseBranchEntries.items[0], references); + } + + await updateEntry(entry, entryDetails); + } else { + let createdEntry = await stackSDKInstance.contentType('${contentType}').entry().create({ entry: entryDetails }); + + if (flag.references) { + await updateReferences(entryDetails, createdEntry, references); + } + + await updateEntry(createdEntry, entryDetails); + } + }); + } else { + let compareMap = new Map(converter(compareBranchEntries.items)); + let baseMap = new Map(converter(baseBranchEntries.items)); + + let arr = uniquelyConcatenateArrays(Array.from(compareMap.keys()), Array.from(baseMap.keys())); + + arr.map(async (el) => { + let entryDetails = deleteUnwantedKeysFromObject(compareMap.get(el), keysToRemove); + entryDetails = updateAssetDetailsInEntries(entryDetails); + if (compareMap.get(el) && !baseMap.get(el)) { + let createdEntry = await stackSDKInstance.contentType('${contentType}').entry().create({ entry: entryDetails }); + + if (flag.references) { + await updateReferences(entryDetails, createdEntry, references); + } + + await updateEntry(createdEntry, entryDetails); + } else if (compareMap.get(el) && baseMap.get(el)) { + let baseEntry = baseMap.get(el); + let entry = await stackSDKInstance.contentType('${contentType}').entry(baseEntry.uid); + + if (flag.references) { + await updateReferences(entryDetails, baseEntry, references); + } + + await updateEntry(entry, entryDetails); + } + }); + } + } catch (error) { + throw error; + } + }, + }; + }; + + if (compareBranch && branch.length !== 0 && apiKey.length !== 0) { + migration.addTask(updateEntryTask()); + } else { + if (apiKey.length === 0) { + console.error('Please provide api key using --stack-api-key flag'); + } + if (!compareBranch) { + console.error('Please provide compare branch through --config compare-branch: flag'); + } + if (branch.length === 0) { + console.error('Please provide branch name through --branch flag'); + } + } + };`; +} diff --git a/packages/contentstack-branches/src/utils/entry-update-script.ts b/packages/contentstack-branches/src/utils/entry-update-script.ts index ebd4678681..0e085db968 100644 --- a/packages/contentstack-branches/src/utils/entry-update-script.ts +++ b/packages/contentstack-branches/src/utils/entry-update-script.ts @@ -1,122 +1,566 @@ export function entryUpdateScript(contentType) { return ` -module.exports = async ({ migration, stackSDKInstance, managementAPIClient, config, branch, apiKey }) => { - const keysToRemove = [ - 'content_type_uid', - 'created_at', - 'updated_at', - 'created_by', - 'updated_by', - 'ACL', - 'stackHeaders', - 'urlPath', - '_version', - '_in_progress', - 'update', - 'delete', - 'fetch', - 'publish', - 'unpublish', - 'publishRequest', - 'setWorkflowStage', - 'import', - ]; + const fs = require('fs'); + const path = require('path'); + module.exports = async ({ migration, stackSDKInstance, managementAPIClient, config, branch, apiKey }) => { + const keysToRemove = [ + 'content_type_uid', + 'created_at', + 'updated_at', + 'created_by', + 'updated_by', + 'ACL', + 'stackHeaders', + 'urlPath', + '_version', + '_in_progress', + 'update', + 'delete', + 'fetch', + 'publish', + 'unpublish', + 'publishRequest', + 'setWorkflowStage', + 'import', + ]; + + let compareBranch = config['compare-branch']; + let filePath = config['file-path'] || process.cwd(); + let assetDirPath = path.resolve(filePath, 'assets'); + let assetDetails = []; + let newAssetDetails = []; + let assetUIDMapper = {}; + let assetUrlMapper = {}; + let assetRefPath = {}; + let isAssetDownload = false; + + function converter(data) { + let arr = []; + for (const elm of data.entries()) { + // @ts-ignore + arr.push([elm[1].title, elm[1]]); + } + return arr; + } + + function deleteUnwantedKeysFromObject(obj, keysToRemove) { + if(obj){ + keysToRemove.map((key) => delete obj[key]); + return obj; + } + } + + function uniquelyConcatenateArrays(compareArr, baseArr) { + let uniqueArray = compareArr.concat(baseArr.filter((item) => compareArr.indexOf(item) < 0)); + return uniqueArray; + } + + function getValueByPath(obj, path) { + return path.split('[').reduce((o, key) => o && o[key.replace(/\]$/, '')], obj); + } + + function updateValueByPath(obj, path, newValue, type, index) { + path.split('[').reduce((o, key, index, arr) => { + if (index === arr.length - 1) { + if (type === 'file') { + o[key.replace(/]$/, '')][index] = newValue; + } else { + o[key.replace(/]$/, '')][0].uid = newValue; + } + } else { + return o[key.replace(/\]$/, '')]; + } + }, obj); + } - let compareBranch = config['compare-branch']; + const findReference = function (schema, path, flag) { + let references = []; + + for (const i in schema) { + const currentPath = path ? path + '[' + schema[i].uid : schema[i].uid; + if (schema[i].data_type === 'group' || schema[i].data_type === 'global_field') { + references = references.concat(findReference(schema[i].schema, currentPath, flag)); + } else if (schema[i].data_type === 'blocks') { + for (const block in schema[i].blocks) { + references = references.concat( + findReference( + schema[i].blocks[block].schema, + currentPath + '[' + block + '][' + schema[i].blocks[block].uid + ']', + flag, + ), + ); + } + } else if (schema[i].data_type === 'reference') { + flag.references = true; + references.push(currentPath); + } + } + + return references; + }; - function converter(data) { - let arr = []; - for (const elm of data.entries()) { - // @ts-ignore - arr.push([elm[1].title, elm[1]]); + const findAssets = function (schema, entry, refPath, path) { + for (const i in schema) { + const currentPath = path ? path + '[' + schema[i].uid : schema[i].uid; + if (schema[i].data_type === 'group' || schema[i].data_type === 'global_field') { + findAssets(schema[i].schema, entry, refPath, currentPath); + } else if (schema[i].data_type === 'blocks') { + for (const block in schema[i].blocks) { + { + if (schema[i].blocks[block].schema) { + findAssets( + schema[i].blocks[block].schema, + entry, + refPath, + currentPath + '[' + block + '][' + schema[i].blocks[block].uid + ']', + ); + } + } + } + } else if (schema[i].data_type === 'json' && schema[i].field_metadata.rich_text_type) { + findAssetIdsFromJsonRte(entry, schema, refPath, path); + } else if ( + schema[i].data_type === 'text' && + schema[i].field_metadata && + (schema[i].field_metadata.markdown || schema[i].field_metadata.rich_text_type) + ) { + findFileUrls(schema[i], entry); + } else if (schema[i].data_type === 'file') { + refPath.push(currentPath) + const imgDetails = getValueByPath(entry, currentPath); + if (schema[i].multiple) { + if (imgDetails && imgDetails.length) { + imgDetails.forEach((img) => { + const obj = { + uid: img.uid, + parent_uid: img.parent_uid, + description: img.description, + title: img.title, + filename: img.filename, + url: img.url, + }; + assetDetails.push(obj); + }); + } + } else { + if (imgDetails) { + const obj = { + uid: imgDetails.uid, + parent_uid: imgDetails.parent_uid, + description: imgDetails.description, + title: imgDetails.title, + filename: imgDetails.filename, + url: imgDetails.url, + }; + assetDetails.push(obj); + } + } + } + } + }; + + function findFileUrls(schema, _entry) { + let markdownRegEx; + let markdownMatch; + let text; + // Regex to detect v3 asset uri patterns + if (schema && schema.field_metadata && schema.field_metadata.markdown) { + text = marked(JSON.stringify(_entry)); + } else { + text = JSON.stringify(_entry); + } + markdownRegEx = new RegExp( + '(https://(assets|(eu-|azure-na-|azure-eu-)?images).contentstack.(io|com)/v3/assets/(.*?)/(.*?)/(.*?)/(.*?)(?="))', + 'g', + ); + while ((markdownMatch = markdownRegEx.exec(text)) !== null) { + if (markdownMatch && typeof markdownMatch[0] === 'string') { + const assetDetails = markdownMatch[0].split('/'); + //fetch assetUID from url + const assetUID = assetDetails && assetDetails[6]; + const obj = { + uid: assetUID, + url: markdownMatch[0], + }; + assetDetails.push(obj); + } + } } - return arr; - } - - function deleteUnwantedKeysFromObject(obj, keysToRemove) { - if(obj){ - keysToRemove.map((key) => delete obj[key]); - return obj; + + function findAssetIdsFromJsonRte(entryObj, ctSchema) { + if(ctSchema !== undefined){ + for (const element of ctSchema) { + switch (element.data_type) { + case 'blocks': { + if (entryObj[element.uid]) { + if (element.multiple) { + entryObj[element.uid].forEach((e) => { + let key = Object.keys(e).pop(); + let subBlock = element.blocks.filter((block) => block.uid === key).pop(); + findAssetIdsFromJsonRte(e[key], subBlock.schema); + }); + } + } + break; + } + case 'global_field': + case 'group': { + if (entryObj[element.uid]) { + if (element.multiple) { + entryObj[element.uid].forEach((e) => { + findAssetIdsFromJsonRte(e, element.schema); + }); + } else { + findAssetIdsFromJsonRte(entryObj[element.uid], element.schema); + } + } + break; + } + case 'json': { + if (entryObj[element.uid] && element.field_metadata.rich_text_type) { + if (element.multiple) { + entryObj[element.uid].forEach((jsonRteData) => { + gatherJsonRteAssetIds(jsonRteData); + }); + } else { + gatherJsonRteAssetIds(entryObj[element.uid]); + } + } + break; + } + } + } + } } - } - - function uniquelyConcatenateArrays(compareArr, baseArr) { - let uniqueArray = compareArr.concat(baseArr.filter((item) => compareArr.indexOf(item) < 0)); - return uniqueArray; - } + + function gatherJsonRteAssetIds(jsonRteData) { + jsonRteData.children.forEach((element) => { + if (element.type) { + switch (element.type) { + case 'a': + case 'p': { + if (element.children && element.children.length > 0) { + gatherJsonRteAssetIds(element); + } + break; + } + case 'reference': { + if (Object.keys(element.attrs).length > 0 && element.attrs.type === 'asset') { + assetDetails.push({ uid: element.attrs['asset-uid'] }); + if (element.attrs['asset-link']) { + const assetDetails = element.attrs['asset-link'].split('/'); + //fetch assetUID from url + const assetUID = assetDetails && assetDetails[6]; + const obj = { + uid: assetUID, + url: element.attrs['asset-link'], + }; + assetDetails.push(obj); + } else if (element.attrs['href']) { + const assetDetails = element.attrs['href'].split('/'); + //fetch assetUID from url + const assetUID = assetDetails && assetDetails[6]; + const obj = { + uid: assetUID, + url: element.attrs['href'], + }; + assetDetails.push(obj); + } + } + if (element.children && element.children.length > 0) { + gatherJsonRteAssetIds(element); + } + break; + } + } + } + }); + } + + const updateAssetDetailsInEntries = function (entry) { + assetRefPath[entry.uid].forEach((refPath) => { + let imgDetails = entry[refPath]; + if (imgDetails !== undefined) { + if (imgDetails && !Array.isArray(imgDetails)) { + entry[refPath] = assetUIDMapper[imgDetails.uid]; + } else if (imgDetails && Array.isArray(imgDetails)) { + for (let i = 0; i < imgDetails.length; i++) { + const img = imgDetails[i]; + entry[refPath][i] = assetUIDMapper[img.uid]; + } + } + } else { + imgDetails = getValueByPath(entry, refPath); + if (imgDetails && !Array.isArray(imgDetails)) { + const imgUID = imgDetails?.uid; + updateValueByPath(entry, refPath, assetUIDMapper[imgUID], 'file', 0); + } else if (imgDetails && Array.isArray(imgDetails)) { + for (let i = 0; i < imgDetails.length; i++) { + const img = imgDetails[i]; + const imgUID = img?.uid; + updateValueByPath(entry, refPath, assetUIDMapper[imgUID], 'file', i); + } + } + } + }); + entry = JSON.stringify(entry); + const assetUrls = assetDetails.map((asset) => asset.url); + const assetUIDs = assetDetails.map((asset) => asset.uid); + assetUrls.forEach(function (assetUrl) { + let mappedAssetUrl = assetUrlMapper[assetUrl]; + if (typeof mappedAssetUrl !== 'undefined') { + entry = entry.replace(assetUrl, mappedAssetUrl); + } + }); + + assetUIDs.forEach(function (assetUid) { + let uid = assetUIDMapper[assetUid]; + if (typeof uid !== 'undefined') { + entry = entry.replace(new RegExp(assetUid, 'img'), uid); + } + }); + return JSON.parse(entry); + }; + + const checkAndDownloadAsset = async function (cAsset) { + if (cAsset) { + const assetUID = cAsset.uid; + const bAssetDetail = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: branch }) + .asset(assetUID) + .fetch() + .then((assets) => assets) + .catch((error) => {}); + if (bAssetDetail) { + assetUIDMapper[cAsset.uid] = bAssetDetail.uid; + assetUrlMapper[cAsset.url] = bAssetDetail.url; + return false; + } + else { + isAssetDownload = true; + const cAssetDetail = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .asset(assetUID) + .fetch() + .then((assets) => assets) + .catch((error) => {}); + const updatedObj = { + parent_uid: cAssetDetail.parent_uid, + description: cAssetDetail.description, + title: cAssetDetail.title, + filename: cAssetDetail.filename, + url: cAssetDetail.url, + }; + Object.assign(cAsset, updatedObj); + const url = cAssetDetail?.url; + if (url) { + const assetFolderPath = path.resolve(assetDirPath, assetUID); + if (!fs.existsSync(assetFolderPath)) fs.mkdirSync(assetFolderPath); + const assetFilePath = path.resolve(assetFolderPath, cAsset.filename); + const assetWriterStream = fs.createWriteStream(assetFilePath); + const data = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .asset(assetUID) + .download({ url, responseType: 'stream' }) + .then(({ data }) => data) + .catch((error) => { + throw error; + }); + assetWriterStream.on('error', (error) => { + throw error; + }); + data.pipe(assetWriterStream); + } + } + } + return cAsset; + }; + + const uploadAssets = async function () { + const assetFolderMap = JSON.parse(fs.readFileSync(path.resolve(filePath, 'folder-mapper.json'), 'utf8')); + const stackAPIClient = managementAPIClient.stack({ api_key: stackSDKInstance.api_key, branch_uid: branch }); + for (let i = 0; i < assetDetails?.length; i++) { + const asset = assetDetails[i]; + let requestOption = {}; + + requestOption.parent_uid = assetFolderMap[asset.parent_uid] || asset.parent_uid; + + if (asset.hasOwnProperty('description') && typeof asset.description === 'string') { + requestOption.description = asset.description; + } + + if (asset.hasOwnProperty('tags') && Array.isArray(asset.tags)) { + requestOption.tags = asset.tags; + } + + if (asset.hasOwnProperty('title') && typeof asset.title === 'string') { + requestOption.title = asset.title; + } + requestOption.upload = path.resolve(assetDirPath, asset.uid, asset.filename); + const res = await stackAPIClient + .asset() + .create(requestOption) + .then((asset) => asset) + .catch((error) => { + throw error; + }); + assetUIDMapper[asset.uid] = res && res.uid; + assetUrlMapper[asset.url] = res && res.url; + } + }; - const updateEntryTask = () => { - return { - title: 'Update Entries', - successMessage: 'Entries Updated Successfully', - failedMessage: 'Failed to update entries', - task: async () => { - let compareBranchEntries = await managementAPIClient - .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) - .contentType('${contentType}') - .entry() - .query() - .find(); + const updateEntryTask = () => { + return { + title: 'Update Entries', + successMessage: 'Entries Updated Successfully', + failedMessage: 'Failed to update entries', + task: async () => { + let compareBranchEntries = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .contentType('${contentType}') + .entry() + .query() + .find(); + + let baseBranchEntries = await stackSDKInstance.contentType('${contentType}').entry().query().find(); + + let contentType = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .contentType('${contentType}') + .fetch(); - let baseBranchEntries = await stackSDKInstance.contentType('${contentType}').entry().query().find(); + for (let i = 0; i < compareBranchEntries?.items?.length; i++) { + assetRefPath[compareBranchEntries.items[i].uid] = [] + findAssets(contentType.schema, compareBranchEntries.items[i], assetRefPath[compareBranchEntries.items[i].uid]); + } - let contentType = await managementAPIClient - .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) - .contentType('${contentType}') - .fetch(); - try { - if (contentType.options.singleton) { - compareBranchEntries.items.map(async (el) => { - let entryDetails = deleteUnwantedKeysFromObject(el, keysToRemove); + for (let i = 0; i < baseBranchEntries?.items?.length; i++) { + assetRefPath[baseBranchEntries.items[i].uid] = [] + findAssets(contentType.schema, baseBranchEntries.items[i], assetRefPath[baseBranchEntries.items[i].uid]); + } + assetDetails = [...new Map(assetDetails.map((item) => [item['uid'], item])).values()]; + newAssetDetails = assetDetails; - if (baseBranchEntries.items.length) { - let baseEntryUid = baseBranchEntries.items[0].uid; - let entry = await stackSDKInstance.contentType('${contentType}').entry(baseEntryUid); - Object.assign(entry, { ...entryDetails }); - entry.update(); - } else { - await stackSDKInstance.contentType('${contentType}').entry().create({ entry: entryDetails }); + if (newAssetDetails && newAssetDetails.length) { + if (!fs.existsSync(assetDirPath)) { + fs.mkdirSync(assetDirPath); + } + for (let i = 0; i < newAssetDetails.length; i++) { + const asset = newAssetDetails[i]; + const updatedCAsset = await checkAndDownloadAsset(asset); + if(updatedCAsset){ + newAssetDetails[i] = updatedCAsset; } - }); - } else { - let compareMap = new Map(converter(compareBranchEntries.items)); - let baseMap = new Map(converter(baseBranchEntries.items)); + } + if (isAssetDownload) await uploadAssets(); + } - let arr = uniquelyConcatenateArrays(Array.from(compareMap.keys()), Array.from(baseMap.keys())); + let flag = { + references: false + }; + + const references = await findReference(contentType.schema, '', flag); + + async function updateEntry(entry, entryDetails) { + Object.assign(entry, { ...entryDetails }); + await entry.update(); + } - arr.map(async (el) => { - let entryDetails = deleteUnwantedKeysFromObject(compareMap.get(el), keysToRemove); + async function updateReferences(entryDetails, baseEntry, references) { + for (let i in references) { + let compareEntryRef = getValueByPath(entryDetails, references[i]); + let baseEntryRef = getValueByPath(baseEntry, references[i]); + + if (compareEntryRef && compareEntryRef.length > 0 && baseEntryRef && baseEntryRef.length >= 0) { + let compareRefEntry = await managementAPIClient + .stack({ api_key: stackSDKInstance.api_key, branch_uid: compareBranch }) + .contentType(compareEntryRef[0]._content_type_uid) + .entry(compareEntryRef[0].uid) + .fetch(); + let baseRefEntry = await stackSDKInstance + .contentType(compareEntryRef[0]._content_type_uid) + .entry() + .query({ query: { title: compareRefEntry.title } }) + .find(); + + updateValueByPath(entryDetails, references[i], baseRefEntry.items[0].uid); + } + } + } - if (compareMap.get(el) && !baseMap.get(el)) { - await stackSDKInstance.contentType('${contentType}').entry().create({ entry: entryDetails }); - } else if (compareMap.get(el) && baseMap.get(el)) { - let baseEntry = baseMap.get(el); + try { + if (contentType.options.singleton) { + compareBranchEntries.items.map(async (el) => { + let entryDetails = deleteUnwantedKeysFromObject(el, keysToRemove); + entryDetails = updateAssetDetailsInEntries(entryDetails); + + if (baseBranchEntries && baseBranchEntries.items.length) { + let baseEntryUid = baseBranchEntries.items[0].uid; + let entry = await stackSDKInstance.contentType('${contentType}').entry(baseEntryUid); + + if (flag.references) { + await updateReferences(entryDetails, baseBranchEntries.items[0], references); + } + + await updateEntry(entry, entryDetails); + } else { + let createdEntry = await stackSDKInstance.contentType('${contentType}').entry().create({ entry: entryDetails }); + + if (flag.references) { + await updateReferences(entryDetails, createdEntry, references); + } + + await updateEntry(createdEntry, entryDetails); + } + }); + } else { + let compareMap = new Map(converter(compareBranchEntries.items)); + let baseMap = new Map(converter(baseBranchEntries.items)); + + let arr = uniquelyConcatenateArrays(Array.from(compareMap.keys()), Array.from(baseMap.keys())); + + arr.map(async (el) => { + let entryDetails = deleteUnwantedKeysFromObject(compareMap.get(el), keysToRemove); + entryDetails = updateAssetDetailsInEntries(entryDetails); + if (compareMap.get(el) && !baseMap.get(el)) { + let createdEntry = await stackSDKInstance.contentType('${contentType}').entry().create({ entry: entryDetails }); + + if (flag.references) { + await updateReferences(entryDetails, createdEntry, references); + } + + await updateEntry(createdEntry, entryDetails); + } else if (compareMap.get(el) && baseMap.get(el)) { + let baseEntry = baseMap.get(el); + let entry = await stackSDKInstance.contentType('${contentType}').entry(baseEntry.uid); + + if (flag.references) { + await updateReferences(entryDetails, baseEntry, references); + } - let entry = await stackSDKInstance.contentType('${contentType}').entry(baseEntry.uid); - Object.assign(entry, { ...entryDetails }); - entry.update(); - } - }); + await updateEntry(entry, entryDetails); + } + }); + } + } catch (error) { + throw error; } - } catch (error) { - throw error; - } - }, + }, + }; }; - }; - - if (compareBranch && branch.length !== 0 && apiKey.length !== 0) { - migration.addTask(updateEntryTask()); - } else { - if (apiKey.length === 0) { - console.error('Please provide api key using --stack-api-key flag'); - } - if (!compareBranch) { - console.error('Please provide compare branch through --config compare-branch: flag'); - } - if (branch.length === 0) { - console.error('Please provide branch name through --branch flag'); + + if (compareBranch && branch.length !== 0 && apiKey.length !== 0) { + migration.addTask(updateEntryTask()); + } else { + if (apiKey.length === 0) { + console.error('Please provide api key using --stack-api-key flag'); + } + if (!compareBranch) { + console.error('Please provide compare branch through --config compare-branch: flag'); + } + if (branch.length === 0) { + console.error('Please provide branch name through --branch flag'); + } } - } -};`; + };`; } diff --git a/packages/contentstack-branches/src/utils/interactive.ts b/packages/contentstack-branches/src/utils/interactive.ts index cfe27e9f67..958eca36e5 100644 --- a/packages/contentstack-branches/src/utils/interactive.ts +++ b/packages/contentstack-branches/src/utils/interactive.ts @@ -134,6 +134,8 @@ export async function selectMergeExecution(): Promise { choices: [ { name: 'Execute Merge', value: 'both' }, { name: 'Export Merge Summary', value: 'export' }, + { name: 'Execute Merge and Generate Content Migration Scripts', value: 'merge_n_scripts' }, + { name: 'Export Summary and Generate Content Migration Scripts', value: 'summary_n_scripts' }, { name: 'Go Back', value: 'previous' }, { name: 'Start Over', value: 'restart' }, ], @@ -148,6 +150,28 @@ export async function selectMergeExecution(): Promise { return strategy; } +export async function selectContentMergePreference(): Promise { + const strategy = await cliux + .inquire({ + type: 'list', + name: 'module', + choices: [ + { name: 'Both existing and new', value: 'existing_new' }, + { name: 'New only', value: 'new' }, + { name: 'Existing only', value: 'existing' }, + { name: 'Ask for preference', value: 'ask_preference' }, + ], + message: 'What content entries do you want to migrate?', + }) + .then((name) => name as string) + .catch((err) => { + cliux.error('Failed to collect the preference'); + process.exit(1); + }); + + return strategy; +} + export async function askExportMergeSummaryPath(): Promise { return await cliux.inquire({ type: 'input', @@ -247,3 +271,66 @@ export async function askBranchNameConfirmation(): Promise { validate: inquireRequireFieldValidation, }); } + +export async function selectContentMergeCustomPreferences(payload) { + // parse rows + const tableRows = []; + if (payload.modified?.length || payload.added?.length) { + forEach(payload.added, (item: BranchDiffRes) => { + const row: any = {}; + row.name = `+ ${item.title}`; + row.status = 'added'; + row.value = item; + tableRows.push(row); + }); + + forEach(payload.modified, (item: BranchDiffRes) => { + const row: any = {}; + row.name = `± ${item.title}`; + row.status = 'modified'; + row.value = item; + tableRows.push(row); + }); + } else { + return; + } + + const selectedStrategies = await cliux.inquire({ + type: 'table', + message: `Select the Content Entry changes for merge`, + name: 'mergeContentEntriesPreferences', + selectAll: true, + pageSize: 10, + columns: [ + { + name: 'Merge New Only', + value: 'merge_new', + }, + { + name: 'Merge Modified Only', + value: 'merge_existing', + }, + { + name: 'Merge Both', + value: 'merge_existing_new', + }, + { + name: 'Ignore', + value: 'ignore', + }, + ], + rows: tableRows, + }); + + let updatedArray = []; + forEach(selectedStrategies, (strategy: string, index: number) => { + const selectedItem = tableRows[index]; + + if (strategy && selectedItem) { + selectedItem.value.entry_merge_strategy = strategy; + updatedArray.push(selectedItem); + } + }); + + return updatedArray; // selected items +} diff --git a/packages/contentstack-export-to-csv/package.json b/packages/contentstack-export-to-csv/package.json index 8dd894b7aa..349a41827d 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.3.12", + "version": "1.4.0", "author": "Abhinav Gupta @abhinav-from-contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { diff --git a/packages/contentstack-export-to-csv/src/util/index.js b/packages/contentstack-export-to-csv/src/util/index.js index 5bbc2ad453..cefdf6cb4f 100644 --- a/packages/contentstack-export-to-csv/src/util/index.js +++ b/packages/contentstack-export-to-csv/src/util/index.js @@ -380,7 +380,7 @@ function cleanEntries(entries, language, environments, contentTypeUid) { let workflow = ''; const envArr = []; entry.publish_details.forEach((env) => { - envArr.push(JSON.stringify([environments[env['environment']], env['locale']])); + envArr.push(JSON.stringify([environments[env['environment']], env['locale'], env['time']])); }); delete entry.publish_details; if ('_workflow' in entry) { diff --git a/packages/contentstack-import/README.md b/packages/contentstack-import/README.md index d4e8c90c41..20b53af987 100644 --- a/packages/contentstack-import/README.md +++ b/packages/contentstack-import/README.md @@ -47,7 +47,7 @@ $ npm install -g @contentstack/cli-cm-import $ csdx COMMAND running command... $ csdx (--version) -@contentstack/cli-cm-import/1.8.1 darwin-arm64 node-v20.3.1 +@contentstack/cli-cm-import/1.8.2 darwin-arm64 node-v20.3.1 $ csdx --help [COMMAND] USAGE $ csdx COMMAND diff --git a/packages/contentstack-import/package.json b/packages/contentstack-import/package.json index 0a6f07f064..0712d83cdc 100644 --- a/packages/contentstack-import/package.json +++ b/packages/contentstack-import/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-import", "description": "Contentstack CLI plugin to import content into stack", - "version": "1.8.1", + "version": "1.8.2", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { diff --git a/packages/contentstack-import/src/import/modules-js/global-fields.js b/packages/contentstack-import/src/import/modules-js/global-fields.js index 368b57803a..e830271aa2 100644 --- a/packages/contentstack-import/src/import/modules-js/global-fields.js +++ b/packages/contentstack-import/src/import/modules-js/global-fields.js @@ -83,7 +83,6 @@ module.exports = class ImportGlobalFields { log(self.config, chalk.green('Global field ' + global_field_uid + ' created successfully'), 'success'); }) .catch(function (err) { - console.log let error = JSON.parse(err.message); if (error.errors.title) { // eslint-disable-next-line no-undef diff --git a/packages/contentstack-import/src/import/modules-js/workflows.js b/packages/contentstack-import/src/import/modules-js/workflows.js index 89a6e0c38b..ca5dcadaea 100644 --- a/packages/contentstack-import/src/import/modules-js/workflows.js +++ b/packages/contentstack-import/src/import/modules-js/workflows.js @@ -4,12 +4,13 @@ * MIT Licensed */ -const mkdirp = require('mkdirp'); const fs = require('fs'); const path = require('path'); -const Promise = require('bluebird'); const chalk = require('chalk'); -const { isEmpty, merge } = require('lodash'); +const mkdirp = require('mkdirp'); +const Promise = require('bluebird'); +const { isEmpty, merge, filter, map, cloneDeep, find } = require('lodash'); + let { default: config } = require('../../config'); const { fileHelper, log, formatError } = require('../../utils'); @@ -58,6 +59,7 @@ module.exports = class importWorkflows { if (!self.workflowUidMapper.hasOwnProperty(workflowUid)) { const roleNameMap = {}; const workflowStages = workflow.workflow_stages; + const oldWorkflowStages = cloneDeep(workflow.workflow_stages); const roles = await self.stackAPIClient.role().fetchAll(); for (const role of roles.items) { @@ -65,6 +67,12 @@ module.exports = class importWorkflows { } for (const stage of workflowStages) { + delete stage.uid; + + if (!isEmpty(stage.next_available_stages)) { + stage.next_available_stages = ['$all']; + } + if (stage.SYS_ACL.users.uids.length && stage.SYS_ACL.users.uids[0] !== '$all') { stage.SYS_ACL.users.uids = ['$all']; } @@ -110,7 +118,20 @@ module.exports = class importWorkflows { return self.stackAPIClient .workflow() .create({ workflow }) - .then(function (response) { + .then(async function (response) { + if ( + !isEmpty(filter(oldWorkflowStages, ({ next_available_stages }) => !isEmpty(next_available_stages))) + ) { + let updateRresponse = await self + .updateNextAvailableStagesUid(response, response.workflow_stages, oldWorkflowStages) + .catch((error) => { + log(self.config, `Workflow '${workflow.name}' update failed.`, 'error'); + log(self.config, error, 'error'); + }); + + if (updateRresponse) response = updateRresponse; + } + self.workflowUidMapper[workflowUid] = response; fileHelper.writeFileSync(workflowUidMapperPath, self.workflowUidMapper); }) @@ -132,7 +153,7 @@ module.exports = class importWorkflows { // the workflow has already been created log( self.config, - chalk.white( `The Workflows ${workflow.name} already exists. Skipping it to avoid duplicates!`), + chalk.white(`The Workflows ${workflow.name} already exists. Skipping it to avoid duplicates!`), 'success', ); } @@ -152,4 +173,25 @@ module.exports = class importWorkflows { }); }); } + + updateNextAvailableStagesUid(workflow, newWorkflowStages, oldWorkflowStages) { + newWorkflowStages = map(newWorkflowStages, (newStage, index) => { + const oldStage = oldWorkflowStages[index]; + if (!isEmpty(oldStage.next_available_stages)) { + newStage.next_available_stages = map(oldStage.next_available_stages, (stageUid) => { + if (stageUid === '$all') return stageUid; + const stageName = find(oldWorkflowStages, { uid: stageUid })?.name; + return find(newWorkflowStages, { name: stageName })?.uid; + }).filter((val) => val); + } + + return newStage; + }); + + workflow.workflow_stages = newWorkflowStages; + + const updateWorkflow = this.stackAPIClient.workflow(workflow.uid); + Object.assign(updateWorkflow, workflow); + return updateWorkflow.update(); + } }; diff --git a/packages/contentstack-import/src/import/modules/base-class.ts b/packages/contentstack-import/src/import/modules/base-class.ts index f2efc86e3c..932e375235 100644 --- a/packages/contentstack-import/src/import/modules/base-class.ts +++ b/packages/contentstack-import/src/import/modules/base-class.ts @@ -54,8 +54,8 @@ export type ApiOptions = { url?: string; entity: ApiModuleType; apiData?: Record | any; - resolve: (value: any) => void; - reject: (error: any) => void; + resolve: (value: any) => Promise | void; + reject: (error: any) => Promise | void; additionalInfo?: Record; includeParamOnCompletion?: boolean; serializeData?: (input: ApiOptions) => any; diff --git a/packages/contentstack-import/src/import/modules/workflows.ts b/packages/contentstack-import/src/import/modules/workflows.ts index 4d2cb8e419..c1ab96975a 100644 --- a/packages/contentstack-import/src/import/modules/workflows.ts +++ b/packages/contentstack-import/src/import/modules/workflows.ts @@ -1,9 +1,12 @@ import chalk from 'chalk'; -import isEmpty from 'lodash/isEmpty'; -import values from 'lodash/values'; +import map from 'lodash/map'; import find from 'lodash/find'; +import { join } from 'node:path'; +import values from 'lodash/values'; +import filter from 'lodash/filter'; +import isEmpty from 'lodash/isEmpty'; +import cloneDeep from 'lodash/cloneDeep'; import findIndex from 'lodash/findIndex'; -import { join, resolve } from 'node:path'; import config from '../../config'; import BaseClass, { ApiOptions } from './base-class'; @@ -47,7 +50,10 @@ export default class ImportWorkflows extends BaseClass { //Step1 check folder exists or not if (fileHelper.fileExistsSync(this.workflowsFolderPath)) { - this.workflows = fsUtil.readFile(join(this.workflowsFolderPath, this.workflowsConfig.fileName), true) as Record; + this.workflows = fsUtil.readFile(join(this.workflowsFolderPath, this.workflowsConfig.fileName), true) as Record< + string, + unknown + >; } else { log(this.importConfig, `No such file or directory - '${this.workflowsFolderPath}'`, 'error'); return; @@ -86,13 +92,14 @@ export default class ImportWorkflows extends BaseClass { .then((data: any) => data) .catch((err: any) => log(this.importConfig, `Failed to fetch roles. ${formatError(err)}`, 'error')); - for (const role of roles?.items) { + for (const role of roles?.items || []) { this.roleNameMap[role.name] = role.uid; } } async importWorkflows() { const apiContent = values(this.workflows); + const oldWorkflows = cloneDeep(values(this.workflows)); //check and create custom roles if not exists for (const workflow of values(this.workflows)) { @@ -101,7 +108,21 @@ export default class ImportWorkflows extends BaseClass { } } - const onSuccess = ({ response, apiData: { uid, name } = { uid: null, name: '' } }: any) => { + const onSuccess = async ({ response, apiData: { uid, name } = { uid: null, name: '' } }: any) => { + const oldWorkflowStages = find(oldWorkflows, { uid })?.workflow_stages; + if (!isEmpty(filter(oldWorkflowStages, ({ next_available_stages }) => !isEmpty(next_available_stages)))) { + let updateRresponse = await this.updateNextAvailableStagesUid( + response, + response.workflow_stages, + oldWorkflowStages, + ).catch((error) => { + log(this.importConfig, `Workflow '${name}' update failed.`, 'error'); + log(this.importConfig, error, 'error'); + }); + + if (updateRresponse) response = updateRresponse; + } + this.createdWorkflows.push(response); this.workflowUidMapper[uid] = response.uid; log(this.importConfig, `Workflow '${name}' imported successfully`, 'success'); @@ -145,6 +166,31 @@ export default class ImportWorkflows extends BaseClass { ); } + updateNextAvailableStagesUid( + workflow: Record, + newWorkflowStages: Record[], + oldWorkflowStages: Record[], + ) { + newWorkflowStages = map(newWorkflowStages, (newStage, index) => { + const oldStage = oldWorkflowStages[index]; + if (!isEmpty(oldStage.next_available_stages)) { + newStage.next_available_stages = map(oldStage.next_available_stages, (stageUid) => { + if (stageUid === '$all') return stageUid; + const stageName = find(oldWorkflowStages, { uid: stageUid })?.name; + return find(newWorkflowStages, { name: stageName })?.uid; + }).filter((val) => val); + } + + return newStage; + }); + + workflow.workflow_stages = newWorkflowStages; + + const updateWorkflow = this.stack.workflow(workflow.uid); + Object.assign(updateWorkflow, workflow); + return updateWorkflow.update(); + } + /** * @method serializeWorkflows * @param {ApiOptions} apiOptions ApiOptions @@ -165,6 +211,14 @@ export default class ImportWorkflows extends BaseClass { if (!workflow.branches) { workflow.branches = ['main']; } + for (const stage of workflow.workflow_stages) { + delete stage.uid; + + if (!isEmpty(stage.next_available_stages)) { + stage.next_available_stages = ['$all']; + } + } + apiOptions.apiData = workflow; } return apiOptions; diff --git a/packages/contentstack-migration/README.md b/packages/contentstack-migration/README.md index fb6fdebcb9..23933f2703 100644 --- a/packages/contentstack-migration/README.md +++ b/packages/contentstack-migration/README.md @@ -21,7 +21,7 @@ $ npm install -g @contentstack/cli-migration $ csdx COMMAND running command... $ csdx (--version) -@contentstack/cli-migration/1.3.10 darwin-arm64 node-v20.3.1 +@contentstack/cli-migration/1.3.11 darwin-arm64 node-v20.3.1 $ csdx --help [COMMAND] USAGE $ csdx COMMAND diff --git a/packages/contentstack-migration/package.json b/packages/contentstack-migration/package.json index f0f02f3961..606e4204b8 100644 --- a/packages/contentstack-migration/package.json +++ b/packages/contentstack-migration/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/cli-migration", - "version": "1.3.10", + "version": "1.3.11", "author": "@contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { diff --git a/packages/contentstack-migration/src/commands/cm/stacks/migration.js b/packages/contentstack-migration/src/commands/cm/stacks/migration.js index 0b7482817c..631245743c 100644 --- a/packages/contentstack-migration/src/commands/cm/stacks/migration.js +++ b/packages/contentstack-migration/src/commands/cm/stacks/migration.js @@ -160,7 +160,13 @@ class MigrationCommand extends Command { requests.splice(0, requests.length); } catch (error) { // errorHandler(null, null, null, error) - this.log(error); + if (error.message) { + this.log(error.message); + } else if (error.errorMessage) { + this.log(error.errorMessage); + }else{ + this.log(error) + } } } diff --git a/packages/contentstack/README.md b/packages/contentstack/README.md index 2747f10302..71f2806b2b 100644 --- a/packages/contentstack/README.md +++ b/packages/contentstack/README.md @@ -18,7 +18,7 @@ $ npm install -g @contentstack/cli $ csdx COMMAND running command... $ csdx (--version|-v) -@contentstack/cli/1.8.1 darwin-arm64 node-v20.3.1 +@contentstack/cli/1.8.2 darwin-arm64 node-v20.3.1 $ csdx --help [COMMAND] USAGE $ csdx COMMAND diff --git a/packages/contentstack/package.json b/packages/contentstack/package.json index 601cb825da..14e61aab08 100755 --- a/packages/contentstack/package.json +++ b/packages/contentstack/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli", "description": "Command-line tool (CLI) to interact with Contentstack", - "version": "1.8.1", + "version": "1.8.2", "author": "Contentstack", "bin": { "csdx": "./bin/run" @@ -27,17 +27,17 @@ "@contentstack/cli-cm-bulk-publish": "~1.3.10", "@contentstack/cli-cm-clone": "~1.4.15", "@contentstack/cli-cm-export": "~1.8.0", - "@contentstack/cli-cm-export-to-csv": "~1.3.12", - "@contentstack/cli-cm-import": "~1.8.1", + "@contentstack/cli-cm-export-to-csv": "~1.4.0", + "@contentstack/cli-cm-import": "~1.8.2", "@contentstack/cli-cm-migrate-rte": "~1.4.10", "@contentstack/cli-cm-seed": "~1.4.14", "@contentstack/cli-command": "~1.2.11", "@contentstack/cli-config": "~1.4.10", "@contentstack/cli-launch": "~1.0.10", - "@contentstack/cli-migration": "~1.3.10", + "@contentstack/cli-migration": "~1.3.11", "@contentstack/cli-utilities": "~1.5.1", "@contentstack/management": "~1.10.0", - "@contentstack/cli-cm-branches": "~1.0.10", + "@contentstack/cli-cm-branches": "~1.0.11", "@oclif/plugin-help": "^5", "@oclif/plugin-not-found": "^2.3.9", "@oclif/plugin-plugins": "^2.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index baf5ef35e6..e2237033dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,18 +12,18 @@ importers: specifiers: '@contentstack/cli-auth': ~1.3.12 '@contentstack/cli-cm-bootstrap': ~1.4.14 - '@contentstack/cli-cm-branches': ~1.0.10 + '@contentstack/cli-cm-branches': ~1.0.11 '@contentstack/cli-cm-bulk-publish': ~1.3.10 '@contentstack/cli-cm-clone': ~1.4.15 '@contentstack/cli-cm-export': ~1.8.0 - '@contentstack/cli-cm-export-to-csv': ~1.3.12 - '@contentstack/cli-cm-import': ~1.8.1 + '@contentstack/cli-cm-export-to-csv': ~1.4.0 + '@contentstack/cli-cm-import': ~1.8.2 '@contentstack/cli-cm-migrate-rte': ~1.4.10 '@contentstack/cli-cm-seed': ~1.4.14 '@contentstack/cli-command': ~1.2.11 '@contentstack/cli-config': ~1.4.10 '@contentstack/cli-launch': ~1.0.10 - '@contentstack/cli-migration': ~1.3.10 + '@contentstack/cli-migration': ~1.3.11 '@contentstack/cli-utilities': ~1.5.1 '@contentstack/management': ~1.10.0 '@oclif/core': ^2.9.3