diff --git a/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js b/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js index 8fa579906c..26cbe7fe2e 100644 --- a/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js +++ b/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js @@ -15,7 +15,7 @@ class ExportToCsvCommand extends Command { action: flags.string({ required: false, multiple: false, - options: ['entries', 'users'], + options: ['entries', 'users', 'taxonomies'], description: `Option to export data (entries, users)`, }), alias: flags.string({ @@ -59,6 +59,9 @@ class ExportToCsvCommand extends Command { multiple: false, required: false, }), + 'taxonomy-uid': flags.string({ + description: 'Provide taxonomy UID for which terms need to be exported', + }), }; async run() { @@ -75,6 +78,7 @@ class ExportToCsvCommand extends Command { 'content-type': contentTypesFlag, alias: managementTokenAlias, branch: branchUid, + 'taxonomy-uid': taxonomyUID, }, } = await this.parse(ExportToCsvCommand); @@ -96,69 +100,17 @@ class ExportToCsvCommand extends Command { let stackAPIClient; let language; let contentTypes = []; - let stackBranches; - const listOfTokens = configHandler.get('tokens'); - if (managementTokenAlias && listOfTokens[managementTokenAlias]) { - managementAPIClient = await managementSDKClient({ - host: this.cmaHost, - management_token: listOfTokens[managementTokenAlias].token, - }); - stack = { - name: stackName || managementTokenAlias, - apiKey: listOfTokens[managementTokenAlias].apiKey, - token: listOfTokens[managementTokenAlias].token, - }; - } else if (managementTokenAlias) { - this.error('Provided management token alias not found in your config.!'); + if (managementTokenAlias) { + const { stackDetails, apiClient } = await this.getAliasDetails(managementTokenAlias, stackName); + managementAPIClient = apiClient; + stack = stackDetails; } else { - let organization; - - if (!isAuthenticated()) { - this.error(config.CLI_EXPORT_CSV_ENTRIES_ERROR, { - exit: 2, - suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'], - }); - } - - if (org) { - organization = { uid: org }; - } else { - organization = await util.chooseOrganization(managementAPIClient); // prompt for organization - } - if (!stackAPIKey) { - stack = await util.chooseStack(managementAPIClient, organization.uid); // prompt for stack - } else { - stack = await util.chooseStack(managementAPIClient, organization.uid, stackAPIKey); - } + stack = await this.getStackDetails(managementAPIClient, stackAPIKey, org); } stackAPIClient = this.getStackClient(managementAPIClient, stack); - - if (branchUid) { - try { - const branchExists = await doesBranchExist(stackAPIClient, branchUid); - if (branchExists?.errorCode) { - throw new Error(branchExists.errorMessage); - } - stack.branch_uid = branchUid; - stackAPIClient = this.getStackClient(managementAPIClient, stack); - } catch (error) { - if (error.message || error.errorMessage) { - cliux.error(util.formatError(error)); - this.exit(); - } - } - } else { - stackBranches = await this.getStackBranches(stackAPIClient); - if (stackBranches === undefined) { - stackAPIClient = this.getStackClient(managementAPIClient, stack); - } else { - const { branch } = await util.chooseBranch(stackBranches); - stack.branch_uid = branch; - stackAPIClient = this.getStackClient(managementAPIClient, stack); - } - } + await this.checkAndUpdateBranchDetail(branchUid, stack, stackAPIClient, managementAPIClient); const contentTypeCount = await util.getContentTypeCount(stackAPIClient); @@ -258,6 +210,23 @@ class ExportToCsvCommand extends Command { } break; } + case config.exportTaxonomies: + case 'taxonomies': { + let stack; + let stackAPIClient; + let taxUID; + if (managementTokenAlias) { + const { stackDetails, apiClient } = await this.getAliasDetails(managementTokenAlias, stackName); + managementAPIClient = apiClient; + stack = stackDetails; + } else { + stack = await this.getStackDetails(managementAPIClient, stackAPIKey, org); + } + + stackAPIClient = this.getStackClient(managementAPIClient, stack); + await this.createTaxonomyAndTermCsvFile(stackName, stack, taxonomyUID); + break; + } } } catch (error) { if (error.message || error.errorMessage) { @@ -292,9 +261,150 @@ class ExportToCsvCommand extends Command { .then(({ items }) => (items !== undefined ? items : [])) .catch((_err) => {}); } + + /** + * check whether branch enabled org or not and update branch details + * @param {string} branchUid + * @param {object} stack + * @param {*} stackAPIClient + * @param {*} managementAPIClient + */ + async checkAndUpdateBranchDetail(branchUid, stack, stackAPIClient, managementAPIClient) { + if (branchUid) { + try { + const branchExists = await doesBranchExist(stackAPIClient, branchUid); + if (branchExists?.errorCode) { + throw new Error(branchExists.errorMessage); + } + stack.branch_uid = branchUid; + stackAPIClient = getStackClient(managementAPIClient, stack); + } catch (error) { + if (error?.message || error?.errorMessage) { + cliux.error(util.formatError(error)); + this.exit(); + } + } + } else { + const stackBranches = await this.getStackBranches(stackAPIClient); + if (stackBranches === undefined) { + stackAPIClient = this.getStackClient(managementAPIClient, stack); + } else { + const { branch } = await util.chooseBranch(stackBranches); + stack.branch_uid = branch; + stackAPIClient = this.getStackClient(managementAPIClient, stack); + } + } + } + + /** + * fetch stack details from alias token + * @param {string} managementTokenAlias + * @param {string} stackName + * @returns + */ + async getAliasDetails(managementTokenAlias, stackName) { + let apiClient, stackDetails; + const listOfTokens = configHandler.get('tokens'); + if (managementTokenAlias && listOfTokens[managementTokenAlias]) { + apiClient = await managementSDKClient({ + host: this.cmaHost, + management_token: listOfTokens[managementTokenAlias].token, + }); + stackDetails = { + name: stackName || managementTokenAlias, + apiKey: listOfTokens[managementTokenAlias].apiKey, + token: listOfTokens[managementTokenAlias].token, + }; + } else if (managementTokenAlias) { + this.error('Provided management token alias not found in your config.!'); + } + return { + apiClient, + stackDetails, + }; + } + + /** + * fetch stack details on basis of the selected org and stack + * @param {*} managementAPIClient + * @param {string} stackAPIKey + * @param {string} org + * @returns + */ + async getStackDetails(managementAPIClient, stackAPIKey, org) { + let organization, stackDetails; + + if (!isAuthenticated()) { + this.error(config.CLI_EXPORT_CSV_ENTRIES_ERROR, { + exit: 2, + suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'], + }); + } + + if (org) { + organization = { uid: org }; + } else { + organization = await util.chooseOrganization(managementAPIClient); // prompt for organization + } + if (!stackAPIKey) { + stackDetails = await util.chooseStack(managementAPIClient, organization.uid); // prompt for stack + } else { + stackDetails = await util.chooseStack(managementAPIClient, organization.uid, stackAPIKey); + } + return stackDetails; + } + + /** + * Create a taxonomies csv file for stack and a terms csv file for associated taxonomies + * @param {string} stackName + * @param {object} stack + * @param {string} taxUID + */ + async createTaxonomyAndTermCsvFile(stackName, stack, taxUID) { + const { cma } = configHandler.get('region') || {}; + const payload = { + baseUrl: `${cma}/v3/taxonomies`, + apiKey: stack.apiKey, + mgToken: stack?.token, + }; + //check whether the taxonomy is valid or not + let taxonomies = []; + if (taxUID) { + const taxonomy = await util.getTaxonomy(payload, taxUID); + taxonomies.push(taxonomy); + } else { + payload['url'] = payload.baseUrl; + taxonomies = await util.getAllTaxonomies(payload); + } + + const formattedTaxonomiesData = util.formatTaxonomiesData(taxonomies); + if (formattedTaxonomiesData?.length) { + const fileName = `${stackName ? stackName : stack.name}_taxonomies.csv`; + util.write(this, formattedTaxonomiesData, fileName, 'taxonomies'); + } else { + cliux.print('info: No taxonomies found. Please provide a valid stack!', { color: 'blue' }); + } + + for (let index = 0; index < taxonomies?.length; index++) { + const taxonomy = taxonomies[index]; + const taxonomyUID = taxonomy?.uid; + if (taxonomyUID) { + payload['url'] = `${payload.baseUrl}/${taxonomyUID}/terms`; + const terms = await util.getAllTermsOfTaxonomy(payload); + const formattedTermsData = util.formatTermsOfTaxonomyData(terms, taxonomyUID); + const taxonomyName = taxonomy?.name ? taxonomy.name : ''; + const termFileName = `${stackName ? stackName : stack.name}_${taxonomyName}_${taxonomyUID}_terms.csv`; + if (formattedTermsData?.length) { + util.write(this, formattedTermsData, termFileName, 'terms'); + } else { + cliux.print(`info: No terms found for the taxonomy UID - '${taxonomyUID}'`, { color: 'blue' }); + } + } + } + } } -ExportToCsvCommand.description = `Export entries or organization users to csv using this command`; +ExportToCsvCommand.description = `Export entries, taxonomies, terms or organization users to csv using this command`; ExportToCsvCommand.examples = [ 'csdx cm:export-to-csv', @@ -310,6 +420,10 @@ ExportToCsvCommand.examples = [ '', 'Exporting organization users to csv with organization name provided', 'csdx cm:export-to-csv --action --org --org-name ', + 'Exporting taxonomies and related terms to csv with taxonomy uid provided', + 'csdx cm:export-to-csv --action --alias --taxonomy-uid ', + 'Exporting taxonomies and respective terms to csv', + 'csdx cm:export-to-csv --action --alias ', ]; module.exports = ExportToCsvCommand; diff --git a/packages/contentstack-export-to-csv/src/util/config.js b/packages/contentstack-export-to-csv/src/util/config.js index 6a424cc3a5..64f986352c 100644 --- a/packages/contentstack-export-to-csv/src/util/config.js +++ b/packages/contentstack-export-to-csv/src/util/config.js @@ -1,10 +1,12 @@ module.exports = { + limit:100, cancelString: 'Cancel and Exit', exportEntries: 'Export entries to a .CSV file', exportUsers: "Export organization users' data to a .CSV file", + exportTaxonomies: 'Export taxonomies to a .CSV file', adminError: "Unable to export data. Make sure you're an admin or owner of this organization", organizationNameRegex: /\'/, CLI_EXPORT_CSV_LOGIN_FAILED: "You need to login to execute this command. See: auth:login --help", - CLI_EXPORT_CSV_ENTRIES_ERROR: "You need to either login or provide a management token to execute this command" - + CLI_EXPORT_CSV_ENTRIES_ERROR: "You need to either login or provide a management token to execute this command", + CLI_EXPORT_CSV_API_FAILED: 'Something went wrong. Please try again' }; diff --git a/packages/contentstack-export-to-csv/src/util/index.js b/packages/contentstack-export-to-csv/src/util/index.js index 73a232f0dc..a760ce6722 100644 --- a/packages/contentstack-export-to-csv/src/util/index.js +++ b/packages/contentstack-export-to-csv/src/util/index.js @@ -2,13 +2,14 @@ const os = require('os'); const fs = require('fs'); const mkdirp = require('mkdirp'); const find = require('lodash/find'); +const flat = require('lodash/flatten'); const fastcsv = require('fast-csv'); const inquirer = require('inquirer'); const debug = require('debug')('export-to-csv'); const checkboxPlus = require('inquirer-checkbox-plus-prompt'); const config = require('./config.js'); -const { cliux, configHandler } = require('@contentstack/cli-utilities'); +const { cliux, configHandler, HttpClient, messageHandler } = require('@contentstack/cli-utilities'); const directory = './data'; const delimeter = os.platform() === 'win32' ? '\\' : '/'; @@ -105,7 +106,7 @@ async function getOrganizationsWhereUserIsAdmin(managementAPIClient) { organizations.forEach((org) => { result[org.name] = org.uid; }); - } + } return result; } catch (error) { @@ -321,7 +322,13 @@ function getEntries(stackAPIClient, contentType, language, skip, limit) { stackAPIClient .contentType(contentType) .entry() - .query({ include_publish_details: true, locale: language, skip: skip * 100, limit: limit, include_workflow: true }) + .query({ + include_publish_details: true, + locale: language, + skip: skip * 100, + limit: limit, + include_workflow: true, + }) .find() .then((entries) => resolve(entries)) .catch((error) => reject(error)); @@ -378,7 +385,7 @@ function cleanEntries(entries, language, environments, contentTypeUid) { return filteredEntries.map((entry) => { let workflow = ''; const envArr = []; - if(entry.publish_details.length) { + if (entry.publish_details.length) { entry.publish_details.forEach((env) => { envArr.push(JSON.stringify([environments[env['environment']], env['locale'], env['time']])); }); @@ -387,10 +394,10 @@ function cleanEntries(entries, language, environments, contentTypeUid) { delete entry.publish_details; delete entry.setWorkflowStage; if ('_workflow' in entry) { - if(entry._workflow?.name) { - workflow = entry['_workflow']['name']; - delete entry['_workflow']; - } + if (entry._workflow?.name) { + workflow = entry['_workflow']['name']; + delete entry['_workflow']; + } } entry = flatten(entry); entry['publish_details'] = envArr; @@ -443,7 +450,7 @@ function startupQuestions() { type: 'list', name: 'action', message: 'Choose Action', - choices: [config.exportEntries, config.exportUsers, 'Exit'], + choices: [config.exportEntries, config.exportUsers, config.exportTaxonomies, 'Exit'], }, ]; inquirer @@ -662,6 +669,152 @@ function wait(time) { }); } +/** + * fetch all taxonomies in the provided stack + * @param {object} payload + * @param {number} skip + * @param {number} limit + * @param {array} taxonomies + * @returns + */ +async function getAllTaxonomies(payload, skip = 0, limit = 100, taxonomies = []) { + const response = await apiRequestHandler(payload, skip, limit); + if(response){ + skip += config.limit || 100; + taxonomies = [...taxonomies, ...response.taxonomies]; + if (skip >= response?.count) { + return taxonomies; + } else { + return getAllTaxonomies(payload, skip, limit, taxonomies); + } + } + return taxonomies; +} + +/** + * fetch terms of related taxonomy + * @param {object} payload + * @param {number} skip + * @param {number} limit + * @param {array} terms + * @returns + */ +async function getAllTermsOfTaxonomy(payload, skip = 0, limit = 100, terms = []) { + const response = await apiRequestHandler(payload, skip, limit); + if(response){ + skip += config.limit || 100; + terms = [...terms, ...response.terms]; + if (skip >= response?.count) { + return terms; + } else { + return getAllTermsOfTaxonomy(payload, skip, limit, terms); + } + } + return terms; +} + +/** + * Verify the existence of a taxonomy. Obtain its details if it exists and return + * @param {object} payload + * @param {string} taxonomyUID + * @returns + */ +async function getTaxonomy(payload, taxonomyUID) { + payload['url'] = `${payload.baseUrl}/${taxonomyUID}`; + const resp = await apiRequestHandler(payload); + return resp?.taxonomy || ''; +} + +async function apiRequestHandler(payload, skip, limit) { + const headers = { + api_key: payload.apiKey, + 'Content-Type': 'application/json', + }; + + if (payload?.mgToken) headers['authorization'] = payload.mgToken; + else headers['authToken'] = configHandler.get('authtoken'); + + const params = { + include_count: true, + skip: 0, + limit: 30, + }; + + if (skip >= 0) params['skip'] = skip; + if (limit >= 0) params['limit'] = limit; + + return await new HttpClient() + .headers(headers) + .queryParams(params) + .get(payload.url) + .then((res) => { + //NOTE - temporary code for handling api errors response + const { status, data } = res; + if ([200, 201, 202].includes(status)) return data; + else { + let errorMsg; + if ([500, 503, 502].includes(status)) errorMsg = data?.message || data; + else errorMsg = data?.error_message; + if(errorMsg === undefined){ + errorMsg = Object.values(data?.errors) && flat(Object.values(data.errors)); + } + cliux.print(`Error: ${errorMsg}`, { color: 'red' }); + process.exit(1); + } + }) + .catch((err) => handleErrorMsg(err)); +} + +/** + * Change taxonomies data in required CSV headers format + * @param {array} taxonomies + * @returns + */ +function formatTaxonomiesData(taxonomies) { + if(taxonomies?.length){ + const formattedTaxonomies = taxonomies.map((taxonomy) => { + return { + 'Taxonomy UID': taxonomy.uid, + Name: taxonomy.name, + Description: taxonomy.description, + }; + }); + return formattedTaxonomies; + } +} + +/** + * Modify the linked taxonomy data's terms in required CSV headers format + * @param {array} terms + * @param {string} taxonomyUID + * @returns + */ +function formatTermsOfTaxonomyData(terms, taxonomyUID) { + if(terms?.length){ + const formattedTerms = terms.map((term) => { + return { + 'Taxonomy UID': taxonomyUID, + UID: term.uid, + Name: term.name, + 'Parent UID': term.parent_uid, + }; + }); + return formattedTerms; + } +} + +function handleErrorMsg(err) { + if (err?.errorMessage) { + cliux.print(`Error: ${err.errorMessage}`, { color: 'red' }); + } else if (err?.message) { + cliux.print(`Error: ${err.message}`, { color: 'red' }); + } else { + console.log(err); + cliux.print(`Error: ${messageHandler.parse('CLI_EXPORT_CSV_API_FAILED')}`, { color: 'red' }); + } + process.exit(1); +} + module.exports = { chooseOrganization: chooseOrganization, chooseStack: chooseStack, @@ -688,4 +841,9 @@ module.exports = { chooseInMemContentTypes: chooseInMemContentTypes, getEntriesCount: getEntriesCount, formatError: formatError, + getAllTaxonomies, + getAllTermsOfTaxonomy, + formatTaxonomiesData, + formatTermsOfTaxonomyData, + getTaxonomy, };