Skip to content

Commit ed7524b

Browse files
authored
Merge pull request #2178 from contentstack/feat/DX-3638
feat: Added taxonomy localization support in export-to-csv
2 parents 55869f1 + 9d13906 commit ed7524b

File tree

4 files changed

+297
-151
lines changed

4 files changed

+297
-151
lines changed

packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js

Lines changed: 80 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const {
77
cliux,
88
doesBranchExist,
99
isManagementTokenValid,
10-
log
1110
} = require('@contentstack/cli-utilities');
1211
const util = require('../../util');
1312
const config = require('../../util/config');
@@ -18,7 +17,8 @@ class ExportToCsvCommand extends Command {
1817
required: false,
1918
multiple: false,
2019
options: ['entries', 'users', 'teams', 'taxonomies'],
21-
description: 'Option to export data (entries, users, teams, taxonomies). <options: entries|users|teams|taxonomies>',
20+
description:
21+
'Option to export data (entries, users, teams, taxonomies). <options: entries|users|teams|taxonomies>',
2222
}),
2323
alias: flags.string({
2424
char: 'a',
@@ -67,12 +67,22 @@ class ExportToCsvCommand extends Command {
6767
'taxonomy-uid': flags.string({
6868
description: 'Provide the taxonomy UID of the related terms you want to export.',
6969
}),
70+
'include-fallback': flags.boolean({
71+
description:
72+
"[Optional] Include fallback locale data when exporting taxonomies. When enabled, if a taxonomy term doesn't exist in the specified locale, it will fallback to the hierarchy defined in the branch settings.",
73+
default: false,
74+
}),
75+
'fallback-locale': flags.string({
76+
description:
77+
"[Optional] Specify a specific fallback locale for taxonomy export. This locale will be used when a taxonomy term doesn't exist in the primary locale. Takes priority over branch fallback hierarchy when both are specified.",
78+
required: false,
79+
}),
7080
delimiter: flags.string({
71-
description: '[optional] Provide a delimiter to separate individual data fields within the CSV file. For example: cm:export-to-csv --delimiter \'|\'',
81+
description:
82+
"[optional] Provide a delimiter to separate individual data fields within the CSV file. For example: cm:export-to-csv --delimiter '|'",
7283
default: ',',
7384
}),
74-
};
75-
85+
};
7686
async run() {
7787
try {
7888
let action, managementAPIClient;
@@ -87,9 +97,11 @@ class ExportToCsvCommand extends Command {
8797
'content-type': contentTypesFlag,
8898
alias: managementTokenAlias,
8999
branch: branchUid,
90-
"team-uid": teamUid,
100+
'team-uid': teamUid,
91101
'taxonomy-uid': taxonomyUID,
92-
delimiter
102+
'include-fallback': includeFallback,
103+
'fallback-locale': fallbackLocale,
104+
delimiter,
93105
},
94106
} = await this.parse(ExportToCsvCommand);
95107

@@ -127,7 +139,12 @@ class ExportToCsvCommand extends Command {
127139
}
128140

129141
stackAPIClient = this.getStackClient(managementAPIClient, stack);
130-
stackAPIClient = await this.checkAndUpdateBranchDetail(branchUid, stack, stackAPIClient, managementAPIClient);
142+
stackAPIClient = await this.checkAndUpdateBranchDetail(
143+
branchUid,
144+
stack,
145+
stackAPIClient,
146+
managementAPIClient,
147+
);
131148

132149
const contentTypeCount = await util.getContentTypeCount(stackAPIClient);
133150

@@ -223,15 +240,15 @@ class ExportToCsvCommand extends Command {
223240
}
224241
case config.exportTeams:
225242
case 'teams': {
226-
try{
243+
try {
227244
let organization;
228245
if (org) {
229246
organization = { uid: org, name: orgName || org };
230247
} else {
231248
organization = await util.chooseOrganization(managementAPIClient, action); // prompt for organization
232249
}
233-
234-
await util.exportTeams(managementAPIClient,organization,teamUid, delimiter);
250+
251+
await util.exportTeams(managementAPIClient, organization, teamUid, delimiter);
235252
} catch (error) {
236253
if (error.message || error.errorMessage) {
237254
cliux.error(util.formatError(error));
@@ -242,7 +259,11 @@ class ExportToCsvCommand extends Command {
242259
case config.exportTaxonomies:
243260
case 'taxonomies': {
244261
let stack;
262+
let language;
245263
let stackAPIClient;
264+
let finalIncludeFallback = includeFallback;
265+
let finalFallbackLocale = fallbackLocale;
266+
246267
if (managementTokenAlias) {
247268
const { stackDetails, apiClient } = await this.getAliasDetails(managementTokenAlias, stackName);
248269
managementAPIClient = apiClient;
@@ -252,7 +273,29 @@ class ExportToCsvCommand extends Command {
252273
}
253274

254275
stackAPIClient = this.getStackClient(managementAPIClient, stack);
255-
await this.createTaxonomyAndTermCsvFile(stackAPIClient, stackName, stack, taxonomyUID, delimiter);
276+
if (locale) {
277+
language = { code: locale };
278+
} else {
279+
language = await util.chooseLanguage(stackAPIClient);
280+
}
281+
282+
if (includeFallback === undefined || fallbackLocale === undefined) {
283+
const fallbackOptions = await util.chooseFallbackOptions(stackAPIClient);
284+
285+
if (includeFallback === undefined) {
286+
finalIncludeFallback = fallbackOptions.includeFallback;
287+
}
288+
if (fallbackLocale === undefined && fallbackOptions.fallbackLocale) {
289+
finalFallbackLocale = fallbackOptions.fallbackLocale;
290+
}
291+
}
292+
293+
await this.createTaxonomyAndTermCsvFile(stackAPIClient, stackName, stack, taxonomyUID, delimiter, {
294+
locale: language.code,
295+
branch: branchUid,
296+
include_fallback: finalIncludeFallback,
297+
fallback_locale: finalFallbackLocale,
298+
});
256299
break;
257300
}
258301
}
@@ -287,7 +330,7 @@ class ExportToCsvCommand extends Command {
287330
.query()
288331
.find()
289332
.then(({ items }) => (items !== undefined ? items : []))
290-
.catch((_err) => {});
333+
.catch(() => {});
291334
}
292335

293336
/**
@@ -335,9 +378,14 @@ class ExportToCsvCommand extends Command {
335378
let apiClient, stackDetails;
336379
const listOfTokens = configHandler.get('tokens');
337380
if (managementTokenAlias && listOfTokens[managementTokenAlias]) {
338-
const checkManagementTokenValidity = await isManagementTokenValid((listOfTokens[managementTokenAlias].apiKey) ,listOfTokens[managementTokenAlias].token);
339-
if(checkManagementTokenValidity.hasOwnProperty('message')) {
340-
throw checkManagementTokenValidity.valid==='failedToCheck'?checkManagementTokenValidity.message:(`error: Management token or stack API key is invalid. ${checkManagementTokenValidity.message}`);
381+
const checkManagementTokenValidity = await isManagementTokenValid(
382+
listOfTokens[managementTokenAlias].apiKey,
383+
listOfTokens[managementTokenAlias].token,
384+
);
385+
if (Object.prototype.hasOwnProperty.call(checkManagementTokenValidity, 'message')) {
386+
throw checkManagementTokenValidity.valid === 'failedToCheck'
387+
? checkManagementTokenValidity.message
388+
: `error: Management token or stack API key is invalid. ${checkManagementTokenValidity.message}`;
341389
}
342390
apiClient = await managementSDKClient({
343391
host: this.cmaHost,
@@ -393,13 +441,12 @@ class ExportToCsvCommand extends Command {
393441
* @param {object} stack
394442
* @param {string} taxUID
395443
*/
396-
async createTaxonomyAndTermCsvFile(stackAPIClient, stackName, stack, taxUID, delimiter) {
397-
//TODO: Temp variable to export taxonomies in importable format will replaced with flag once decided
398-
const importableCSV = true;
444+
async createTaxonomyAndTermCsvFile(stackAPIClient, stackName, stack, taxUID, delimiter, localeOptions = {}) {
399445
const payload = {
400446
stackAPIClient,
401447
type: '',
402448
limit: config.limit || 100,
449+
...localeOptions, // Spread locale, branch, include_fallback, fallback_locale
403450
};
404451
//check whether the taxonomy is valid or not
405452
let taxonomies = [];
@@ -410,37 +457,14 @@ class ExportToCsvCommand extends Command {
410457
} else {
411458
taxonomies = await util.getAllTaxonomies(payload);
412459
}
413-
414-
if (!importableCSV) {
415-
const formattedTaxonomiesData = util.formatTaxonomiesData(taxonomies);
416-
if (formattedTaxonomiesData?.length) {
417-
const fileName = `${stackName ? stackName : stack.name}_taxonomies.csv`;
418-
util.write(this, formattedTaxonomiesData, fileName, 'taxonomies', delimiter);
419-
} else {
420-
cliux.print('info: No taxonomies found! Please provide a valid stack.', { color: 'blue' });
421-
}
422-
423-
for (let index = 0; index < taxonomies?.length; index++) {
424-
const taxonomy = taxonomies[index];
425-
const taxonomyUID = taxonomy?.uid;
426-
if (taxonomyUID) {
427-
payload['taxonomyUID'] = taxonomyUID;
428-
const terms = await util.getAllTermsOfTaxonomy(payload);
429-
const formattedTermsData = util.formatTermsOfTaxonomyData(terms, taxonomyUID);
430-
const taxonomyName = taxonomy?.name ?? '';
431-
const termFileName = `${stackName ?? stack.name}_${taxonomyName}_${taxonomyUID}_terms.csv`;
432-
if (formattedTermsData?.length) {
433-
util.write(this, formattedTermsData, termFileName, 'terms', delimiter);
434-
} else {
435-
cliux.print(`info: No terms found for the taxonomy UID - '${taxonomyUID}'!`, { color: 'blue' });
436-
}
437-
}
438-
}
460+
461+
if (!taxonomies?.length) {
462+
cliux.print('info: No taxonomies found!', { color: 'blue' });
439463
} else {
440464
const fileName = `${stackName ?? stack.name}_taxonomies.csv`;
441465
const { taxonomiesData, headers } = await util.createImportableCSV(payload, taxonomies);
442466
if (taxonomiesData?.length) {
443-
util.write(this, taxonomiesData, fileName, 'taxonomies',delimiter, headers);
467+
util.write(this, taxonomiesData, fileName, 'taxonomies', delimiter, headers);
444468
}
445469
}
446470
}
@@ -486,6 +510,16 @@ ExportToCsvCommand.examples = [
486510
'',
487511
'Exporting taxonomies and respective terms to a .CSV file with a delimiter',
488512
'csdx cm:export-to-csv --action <taxonomies> --alias <management-token-alias> --delimiter <delimiter>',
513+
'',
514+
'Exporting taxonomies with specific locale',
515+
'csdx cm:export-to-csv --action <taxonomies> --alias <management-token-alias> --locale <locale>',
516+
'',
517+
'Exporting taxonomies with fallback locale support',
518+
'csdx cm:export-to-csv --action <taxonomies> --alias <management-token-alias> --locale <locale> --include-fallback',
519+
'',
520+
'Exporting taxonomies with custom fallback locale',
521+
'csdx cm:export-to-csv --action <taxonomies> --alias <management-token-alias> --locale <locale> --include-fallback --fallback-locale <fallback-locale>',
522+
'',
489523
];
490524

491525
module.exports = ExportToCsvCommand;

packages/contentstack-export-to-csv/src/util/index.js

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,50 @@ function startupQuestions() {
485485
});
486486
}
487487

488+
function chooseFallbackOptions(stackAPIClient) {
489+
return new Promise(async (resolve, reject) => {
490+
try {
491+
const questions = [
492+
{
493+
type: 'confirm',
494+
name: 'includeFallback',
495+
message: 'Include fallback locale data when exporting taxonomies?',
496+
default: false,
497+
},
498+
];
499+
500+
const { includeFallback } = await inquirer.prompt(questions);
501+
502+
let fallbackLocale = null;
503+
504+
if (includeFallback) {
505+
// Get available languages for fallback locale selection
506+
const languages = await getLanguages(stackAPIClient);
507+
const languagesList = Object.keys(languages);
508+
509+
const fallbackQuestion = [
510+
{
511+
type: 'list',
512+
name: 'selectedFallbackLocale',
513+
message: 'Choose fallback locale',
514+
choices: languagesList,
515+
},
516+
];
517+
518+
const { selectedFallbackLocale } = await inquirer.prompt(fallbackQuestion);
519+
fallbackLocale = languages[selectedFallbackLocale];
520+
}
521+
522+
resolve({
523+
includeFallback,
524+
fallbackLocale,
525+
});
526+
} catch (error) {
527+
reject(error);
528+
}
529+
});
530+
}
531+
488532
function getOrgUsers(managementAPIClient, orgUid) {
489533
return new Promise((resolve, reject) => {
490534
managementAPIClient
@@ -1080,11 +1124,17 @@ async function getTaxonomy(payload) {
10801124
* @returns {*} Promise<any>
10811125
*/
10821126
async function taxonomySDKHandler(payload, skip) {
1083-
const { stackAPIClient, taxonomyUID, type, format } = payload;
1127+
const { stackAPIClient, taxonomyUID, type, format, locale, branch, include_fallback, fallback_locale } = payload;
10841128

10851129
const queryParams = { include_count: true, limit: payload.limit };
10861130
if (skip >= 0) queryParams['skip'] = skip || 0;
10871131

1132+
// Add locale and branch parameters if provided
1133+
if (locale) queryParams['locale'] = locale;
1134+
if (branch) queryParams['branch'] = branch;
1135+
if (include_fallback !== undefined) queryParams['include_fallback'] = include_fallback;
1136+
if (fallback_locale) queryParams['fallback_locale'] = fallback_locale;
1137+
10881138
switch (type) {
10891139
case 'taxonomies':
10901140
return await stackAPIClient
@@ -1109,9 +1159,15 @@ async function taxonomySDKHandler(payload, skip) {
11091159
.then((data) => data)
11101160
.catch((err) => handleTaxonomyErrorMsg(err));
11111161
case 'export-taxonomies':
1162+
const exportParams = { format };
1163+
if (locale) exportParams.locale = locale;
1164+
if (branch) exportParams.branch = branch;
1165+
if (include_fallback !== undefined) exportParams.include_fallback = include_fallback;
1166+
if (fallback_locale) exportParams.fallback_locale = fallback_locale;
1167+
11121168
return await stackAPIClient
11131169
.taxonomy(taxonomyUID)
1114-
.export({ format })
1170+
.export(exportParams)
11151171
.then((data) => data)
11161172
.catch((err) => handleTaxonomyErrorMsg(err));
11171173
default:
@@ -1176,20 +1232,20 @@ function handleTaxonomyErrorMsg(err) {
11761232
* @returns
11771233
*/
11781234
async function createImportableCSV(payload, taxonomies) {
1179-
let taxonomiesData = [];
1180-
let headers = [];
1181-
payload['type'] = 'export-taxonomies';
1182-
payload['format'] = 'csv';
1183-
for (const taxonomy of taxonomies) {
1184-
if (taxonomy?.uid) {
1185-
payload['taxonomyUID'] = taxonomy?.uid;
1186-
const data = await taxonomySDKHandler(payload);
1187-
const taxonomies = await csvParse(data, headers);
1188-
taxonomiesData.push(...taxonomies);
1189-
}
1235+
let taxonomiesData = [];
1236+
let headers = [];
1237+
payload['type'] = 'export-taxonomies';
1238+
payload['format'] = 'csv';
1239+
for (const taxonomy of taxonomies) {
1240+
if (taxonomy?.uid) {
1241+
payload['taxonomyUID'] = taxonomy?.uid;
1242+
const data = await taxonomySDKHandler(payload);
1243+
const taxonomies = await csvParse(data, headers);
1244+
taxonomiesData.push(...taxonomies);
11901245
}
1246+
}
11911247

1192-
return { taxonomiesData, headers };
1248+
return { taxonomiesData, headers };
11931249
}
11941250

11951251
/**
@@ -1224,6 +1280,7 @@ module.exports = {
12241280
chooseBranch: chooseBranch,
12251281
chooseContentType: chooseContentType,
12261282
chooseLanguage: chooseLanguage,
1283+
chooseFallbackOptions: chooseFallbackOptions,
12271284
getEntries: getEntries,
12281285
getEnvironments: getEnvironments,
12291286
cleanEntries: cleanEntries,

packages/contentstack-export-to-csv/test/mock-data/common.mock.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,26 @@
261261
"updated_at": "2023-09-11T10:44:40.213Z",
262262
"ACL": [],
263263
"_version": 1
264+
},
265+
{
266+
"code": "en-us",
267+
"name": "English - United States",
268+
"locale": null,
269+
"uid": "en-us-uid",
270+
"created_at": "2023-09-11T10:44:40.213Z",
271+
"updated_at": "2023-09-11T10:44:40.213Z",
272+
"ACL": [],
273+
"_version": 1
274+
},
275+
{
276+
"code": "fr-fr",
277+
"name": "French - France",
278+
"fallback_locale": "en-us",
279+
"uid": "fr-fr-uid",
280+
"created_at": "2023-09-11T10:44:40.213Z",
281+
"updated_at": "2023-09-11T10:44:40.213Z",
282+
"ACL": [],
283+
"_version": 1
264284
}
265285
],
266286
"Teams": {

0 commit comments

Comments
 (0)