@contentstack/cli-cm-export-to-csv 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@contentstack/cli-cm-export-to-csv",
3
3
  "description": "Export entities to csv",
4
- "version": "1.5.0",
4
+ "version": "1.6.0",
5
5
  "author": "Abhinav Gupta @abhinav-from-contentstack",
6
6
  "bugs": "https://github.com/contentstack/cli/issues",
7
7
  "dependencies": {
@@ -15,12 +15,15 @@
15
15
  },
16
16
  "devDependencies": {
17
17
  "@oclif/test": "^2.2.10",
18
- "chai": "^4.2.0",
18
+ "@types/chai": "^4.3.6",
19
+ "@types/mocha": "^10.0.1",
20
+ "chai": "^4.3.8",
19
21
  "debug": "^4.3.1",
22
+ "dotenv": "^16.3.1",
20
23
  "eslint": "^7.32.0",
21
24
  "eslint-config-oclif": "^4.0.0",
22
25
  "globby": "^10.0.2",
23
- "mocha": "^10.0.0",
26
+ "mocha": "^10.2.0",
24
27
  "nyc": "^15.1.0",
25
28
  "oclif": "^3.8.1"
26
29
  },
@@ -44,7 +47,8 @@
44
47
  "postpack": "rm -f oclif.manifest.json",
45
48
  "prepack": "oclif manifest && oclif readme",
46
49
  "test": "nyc mocha --forbid-only \"test/**/*.test.js\"",
47
- "test:unit": "nyc mocha --timeout 10000 --forbid-only \"test/unit/**/*.test.js\"",
50
+ "test:unit": "mocha --timeout 10000 --forbid-only \"test/unit/**/*.test.js\" \"test/util/common-utils.test.js\"",
51
+ "test:unit:report": "nyc --extension .js mocha --forbid-only \"test/unit/**/*.test.js\" \"test/util/common-utils.test.js\"",
48
52
  "version": "oclif readme && git add README.md",
49
53
  "clean": "rm -rf ./node_modules tsconfig.build.tsbuildinfo"
50
54
  },
@@ -61,4 +65,4 @@
61
65
  }
62
66
  },
63
67
  "repository": "https://github.com/contentstack/cli"
64
- }
68
+ }
@@ -15,8 +15,8 @@ class ExportToCsvCommand extends Command {
15
15
  action: flags.string({
16
16
  required: false,
17
17
  multiple: false,
18
- options: ['entries', 'users', 'teams'],
19
- description: `Option to export data (entries, users, teams)`,
18
+ options: ['entries', 'users', 'teams', 'taxonomies'],
19
+ description: `Option to export data (entries, users, teams, taxonomies)`,
20
20
  }),
21
21
  alias: flags.string({
22
22
  char: 'a',
@@ -61,7 +61,14 @@ class ExportToCsvCommand extends Command {
61
61
  }),
62
62
  "team-uid": flags.string({
63
63
  description: 'Uid of the team whose user data and stack roles are required'
64
- })
64
+ }),
65
+ 'taxonomy-uid': flags.string({
66
+ description: 'Provide the taxonomy UID of the related terms you want to export',
67
+ }),
68
+ delimiter: flags.string({
69
+ description: '[optional] Provide a delimiter to separate individual data fields within the CSV file.',
70
+ default: ',',
71
+ }),
65
72
  };
66
73
 
67
74
  async run() {
@@ -78,7 +85,9 @@ class ExportToCsvCommand extends Command {
78
85
  'content-type': contentTypesFlag,
79
86
  alias: managementTokenAlias,
80
87
  branch: branchUid,
81
- "team-uid": teamUid
88
+ "team-uid": teamUid,
89
+ 'taxonomy-uid': taxonomyUID,
90
+ delimiter
82
91
  },
83
92
  } = await this.parse(ExportToCsvCommand);
84
93
 
@@ -106,61 +115,17 @@ class ExportToCsvCommand extends Command {
106
115
  let stackAPIClient;
107
116
  let language;
108
117
  let contentTypes = [];
109
- let stackBranches;
110
- const listOfTokens = configHandler.get('tokens');
111
-
112
- if (managementTokenAlias && listOfTokens[managementTokenAlias]) {
113
- managementAPIClient = await managementSDKClient({
114
- host: this.cmaHost,
115
- management_token: listOfTokens[managementTokenAlias].token,
116
- });
117
- stack = {
118
- name: stackName || managementTokenAlias,
119
- apiKey: listOfTokens[managementTokenAlias].apiKey,
120
- token: listOfTokens[managementTokenAlias].token,
121
- };
122
- } else if (managementTokenAlias) {
123
- this.error('Provided management token alias not found in your config.!');
118
+
119
+ if (managementTokenAlias) {
120
+ const { stackDetails, apiClient } = await this.getAliasDetails(managementTokenAlias, stackName);
121
+ managementAPIClient = apiClient;
122
+ stack = stackDetails;
124
123
  } else {
125
- let organization;
126
- if (org) {
127
- organization = { uid: org };
128
- } else {
129
- organization = await util.chooseOrganization(managementAPIClient); // prompt for organization
130
- }
131
- if (!stackAPIKey) {
132
- stack = await util.chooseStack(managementAPIClient, organization.uid); // prompt for stack
133
- } else {
134
- stack = await util.chooseStack(managementAPIClient, organization.uid, stackAPIKey);
135
- }
124
+ stack = await this.getStackDetails(managementAPIClient, stackAPIKey, org);
136
125
  }
137
126
 
138
127
  stackAPIClient = this.getStackClient(managementAPIClient, stack);
139
-
140
- if (branchUid) {
141
- try {
142
- const branchExists = await doesBranchExist(stackAPIClient, branchUid);
143
- if (branchExists?.errorCode) {
144
- throw new Error(branchExists.errorMessage);
145
- }
146
- stack.branch_uid = branchUid;
147
- stackAPIClient = this.getStackClient(managementAPIClient, stack);
148
- } catch (error) {
149
- if (error.message || error.errorMessage) {
150
- cliux.error(util.formatError(error));
151
- this.exit();
152
- }
153
- }
154
- } else {
155
- stackBranches = await this.getStackBranches(stackAPIClient);
156
- if (stackBranches === undefined) {
157
- stackAPIClient = this.getStackClient(managementAPIClient, stack);
158
- } else {
159
- const { branch } = await util.chooseBranch(stackBranches);
160
- stack.branch_uid = branch;
161
- stackAPIClient = this.getStackClient(managementAPIClient, stack);
162
- }
163
- }
128
+ await this.checkAndUpdateBranchDetail(branchUid, stack, stackAPIClient, managementAPIClient);
164
129
 
165
130
  const contentTypeCount = await util.getContentTypeCount(stackAPIClient);
166
131
 
@@ -219,7 +184,7 @@ class ExportToCsvCommand extends Command {
219
184
  flatEntries = flatEntries.concat(flatEntriesResult);
220
185
  }
221
186
  let fileName = `${stackName ? stackName : stack.name}_${contentType}_${language.code}_entries_export.csv`;
222
- util.write(this, flatEntries, fileName, 'entries'); // write to file
187
+ util.write(this, flatEntries, fileName, 'entries', delimiter); // write to file
223
188
  }
224
189
  } catch (error) {
225
190
  cliux.error(util.formatError(error));
@@ -246,7 +211,7 @@ class ExportToCsvCommand extends Command {
246
211
  (orgName ? orgName : organization.name).replace(config.organizationNameRegex, ''),
247
212
  )}_users_export.csv`;
248
213
 
249
- util.write(this, listOfUsers, fileName, 'organization details');
214
+ util.write(this, listOfUsers, fileName, 'organization details', delimiter);
250
215
  } catch (error) {
251
216
  if (error.message || error.errorMessage) {
252
217
  cliux.error(util.formatError(error));
@@ -264,14 +229,30 @@ class ExportToCsvCommand extends Command {
264
229
  organization = await util.chooseOrganization(managementAPIClient, action); // prompt for organization
265
230
  }
266
231
 
267
- await util.exportTeams(managementAPIClient,organization,teamUid);
232
+ await util.exportTeams(managementAPIClient,organization,teamUid, delimiter);
268
233
  } catch (error) {
269
234
  if (error.message || error.errorMessage) {
270
235
  cliux.error(util.formatError(error));
271
236
  }
272
237
  }
238
+ break;
239
+ }
240
+ case config.exportTaxonomies:
241
+ case 'taxonomies': {
242
+ let stack;
243
+ let stackAPIClient;
244
+ if (managementTokenAlias) {
245
+ const { stackDetails, apiClient } = await this.getAliasDetails(managementTokenAlias, stackName);
246
+ managementAPIClient = apiClient;
247
+ stack = stackDetails;
248
+ } else {
249
+ stack = await this.getStackDetails(managementAPIClient, stackAPIKey, org);
250
+ }
251
+
252
+ stackAPIClient = this.getStackClient(managementAPIClient, stack);
253
+ await this.createTaxonomyAndTermCsvFile(stackAPIClient, stackName, stack, taxonomyUID, delimiter);
254
+ break;
273
255
  }
274
- break;
275
256
  }
276
257
  } catch (error) {
277
258
  if (error.message || error.errorMessage) {
@@ -287,8 +268,8 @@ class ExportToCsvCommand extends Command {
287
268
  getStackClient(managementAPIClient, stack) {
288
269
  const stackInit = {
289
270
  api_key: stack.apiKey,
290
- branch_uid: stack.branch_uid,
291
271
  };
272
+ if (stack?.branch_uid) stackInit['branch_uid'] = stack.branch_uid;
292
273
  if (stack.token) {
293
274
  return managementAPIClient.stack({
294
275
  ...stackInit,
@@ -306,9 +287,159 @@ class ExportToCsvCommand extends Command {
306
287
  .then(({ items }) => (items !== undefined ? items : []))
307
288
  .catch((_err) => {});
308
289
  }
290
+
291
+ /**
292
+ * check whether branch enabled org or not and update branch details
293
+ * @param {string} branchUid
294
+ * @param {object} stack
295
+ * @param {*} stackAPIClient
296
+ * @param {*} managementAPIClient
297
+ */
298
+ async checkAndUpdateBranchDetail(branchUid, stack, stackAPIClient, managementAPIClient) {
299
+ if (branchUid) {
300
+ try {
301
+ const branchExists = await doesBranchExist(stackAPIClient, branchUid);
302
+ if (branchExists?.errorCode) {
303
+ throw new Error(branchExists.errorMessage);
304
+ }
305
+ stack.branch_uid = branchUid;
306
+ stackAPIClient = this.getStackClient(managementAPIClient, stack);
307
+ } catch (error) {
308
+ if (error?.message || error?.errorMessage) {
309
+ cliux.error(util.formatError(error));
310
+ this.exit();
311
+ }
312
+ }
313
+ } else {
314
+ const stackBranches = await this.getStackBranches(stackAPIClient);
315
+ if (stackBranches === undefined) {
316
+ stackAPIClient = this.getStackClient(managementAPIClient, stack);
317
+ } else {
318
+ const { branch } = await util.chooseBranch(stackBranches);
319
+ stack.branch_uid = branch;
320
+ stackAPIClient = this.getStackClient(managementAPIClient, stack);
321
+ }
322
+ }
323
+ }
324
+
325
+ /**
326
+ * fetch stack details from alias token
327
+ * @param {string} managementTokenAlias
328
+ * @param {string} stackName
329
+ * @returns
330
+ */
331
+ async getAliasDetails(managementTokenAlias, stackName) {
332
+ let apiClient, stackDetails;
333
+ const listOfTokens = configHandler.get('tokens');
334
+ if (managementTokenAlias && listOfTokens[managementTokenAlias]) {
335
+ apiClient = await managementSDKClient({
336
+ host: this.cmaHost,
337
+ management_token: listOfTokens[managementTokenAlias].token,
338
+ });
339
+ stackDetails = {
340
+ name: stackName || managementTokenAlias,
341
+ apiKey: listOfTokens[managementTokenAlias].apiKey,
342
+ token: listOfTokens[managementTokenAlias].token,
343
+ };
344
+ } else if (managementTokenAlias) {
345
+ this.error('Provided management token alias not found in your config.!');
346
+ }
347
+ return {
348
+ apiClient,
349
+ stackDetails,
350
+ };
351
+ }
352
+
353
+ /**
354
+ * fetch stack details on basis of the selected org and stack
355
+ * @param {*} managementAPIClient
356
+ * @param {string} stackAPIKey
357
+ * @param {string} org
358
+ * @returns
359
+ */
360
+ async getStackDetails(managementAPIClient, stackAPIKey, org) {
361
+ let organization, stackDetails;
362
+
363
+ if (!isAuthenticated()) {
364
+ this.error(config.CLI_EXPORT_CSV_ENTRIES_ERROR, {
365
+ exit: 2,
366
+ suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'],
367
+ });
368
+ }
369
+
370
+ if (org) {
371
+ organization = { uid: org };
372
+ } else {
373
+ organization = await util.chooseOrganization(managementAPIClient); // prompt for organization
374
+ }
375
+ if (!stackAPIKey) {
376
+ stackDetails = await util.chooseStack(managementAPIClient, organization.uid); // prompt for stack
377
+ } else {
378
+ stackDetails = await util.chooseStack(managementAPIClient, organization.uid, stackAPIKey);
379
+ }
380
+ return stackDetails;
381
+ }
382
+
383
+ /**
384
+ * Create a taxonomies csv file for stack and a terms csv file for associated taxonomies
385
+ * @param {string} stackName
386
+ * @param {object} stack
387
+ * @param {string} taxUID
388
+ */
389
+ async createTaxonomyAndTermCsvFile(stackAPIClient, stackName, stack, taxUID, delimiter) {
390
+ //TODO: Temp variable to export taxonomies in importable format will replaced with flag once decided
391
+ const importableCSV = true;
392
+ const payload = {
393
+ stackAPIClient,
394
+ type: '',
395
+ limit: config.limit || 100,
396
+ };
397
+ //check whether the taxonomy is valid or not
398
+ let taxonomies = [];
399
+ if (taxUID) {
400
+ payload['taxonomyUID'] = taxUID;
401
+ const taxonomy = await util.getTaxonomy(payload);
402
+ taxonomies.push(taxonomy);
403
+ } else {
404
+ taxonomies = await util.getAllTaxonomies(payload);
405
+ }
406
+
407
+ if (!importableCSV) {
408
+ const formattedTaxonomiesData = util.formatTaxonomiesData(taxonomies);
409
+ if (formattedTaxonomiesData?.length) {
410
+ const fileName = `${stackName ? stackName : stack.name}_taxonomies.csv`;
411
+ util.write(this, formattedTaxonomiesData, fileName, 'taxonomies', delimiter);
412
+ } else {
413
+ cliux.print('info: No taxonomies found! Please provide a valid stack.', { color: 'blue' });
414
+ }
415
+
416
+ for (let index = 0; index < taxonomies?.length; index++) {
417
+ const taxonomy = taxonomies[index];
418
+ const taxonomyUID = taxonomy?.uid;
419
+ if (taxonomyUID) {
420
+ payload['taxonomyUID'] = taxonomyUID;
421
+ const terms = await util.getAllTermsOfTaxonomy(payload);
422
+ const formattedTermsData = util.formatTermsOfTaxonomyData(terms, taxonomyUID);
423
+ const taxonomyName = taxonomy?.name ?? '';
424
+ const termFileName = `${stackName ?? stack.name}_${taxonomyName}_${taxonomyUID}_terms.csv`;
425
+ if (formattedTermsData?.length) {
426
+ util.write(this, formattedTermsData, termFileName, 'terms', delimiter);
427
+ } else {
428
+ cliux.print(`info: No terms found for the taxonomy UID - '${taxonomyUID}'!`, { color: 'blue' });
429
+ }
430
+ }
431
+ }
432
+ } else {
433
+ const fileName = `${stackName ?? stack.name}_taxonomies.csv`;
434
+ const { taxonomiesData, headers } = await util.createImportableCSV(payload, taxonomies);
435
+ if (taxonomiesData?.length) {
436
+ util.write(this, taxonomiesData, fileName, 'taxonomies',delimiter, headers);
437
+ }
438
+ }
439
+ }
309
440
  }
310
441
 
311
- ExportToCsvCommand.description = `Export entries or organization users to csv using this command`;
442
+ ExportToCsvCommand.description = `Export entries, taxonomies, terms or organization users to csv using this command`;
312
443
 
313
444
  ExportToCsvCommand.examples = [
314
445
  'csdx cm:export-to-csv',
@@ -339,6 +470,15 @@ ExportToCsvCommand.examples = [
339
470
  '',
340
471
  'Exporting Organizations Teams to CSV with org-uid and team uid',
341
472
  'csdx cm:export-to-csv --action <teams> --org <org-uid> --team-uid <team-uid> --org-name <org-name>',
473
+ '',
474
+ 'Exporting taxonomies and related terms to a .CSV file with the provided taxonomy UID',
475
+ 'csdx cm:export-to-csv --action <taxonomies> --alias <management-token-alias> --taxonomy-uid <taxonomy-uid>',
476
+ '',
477
+ 'Exporting taxonomies and respective terms to a .CSV file',
478
+ 'csdx cm:export-to-csv --action <taxonomies> --alias <management-token-alias>',
479
+ '',
480
+ 'Exporting taxonomies and respective terms to a .CSV file with a delimiter',
481
+ 'csdx cm:export-to-csv --action <taxonomies> --alias <management-token-alias> --delimiter <delimiter>',
342
482
  ];
343
483
 
344
484
  module.exports = ExportToCsvCommand;
@@ -1,11 +1,13 @@
1
1
  module.exports = {
2
+ limit:100,
2
3
  cancelString: 'Cancel and Exit',
3
4
  exportEntries: 'Export entries to a .CSV file',
4
5
  exportUsers: "Export organization user's data to a .CSV file",
5
6
  exportTeams: "Export organization team's data to a .csv file",
7
+ exportTaxonomies: 'Export taxonomies to a .CSV file',
6
8
  adminError: "Unable to export data. Make sure you're an admin or owner of this organization",
7
9
  organizationNameRegex: /\'/,
8
10
  CLI_EXPORT_CSV_LOGIN_FAILED: "You need to login to execute this command. See: auth:login --help",
9
11
  CLI_EXPORT_CSV_ENTRIES_ERROR: "You need to either login or provide a management token to execute this command",
10
- CLI_EXPORT_CSV_API_FAILED: 'Something went wrong. Please try again!'
12
+ CLI_EXPORT_CSV_API_FAILED: 'Something went wrong! Please try again'
11
13
  };
package/src/util/index.js CHANGED
@@ -4,12 +4,21 @@ const mkdirp = require('mkdirp');
4
4
  const find = require('lodash/find');
5
5
  const cloneDeep = require('lodash/cloneDeep');
6
6
  const omit = require('lodash/omit');
7
+ const flat = require('lodash/flatten');
7
8
  const fastcsv = require('fast-csv');
8
9
  const inquirer = require('inquirer');
9
10
  const debug = require('debug')('export-to-csv');
10
11
  const checkboxPlus = require('inquirer-checkbox-plus-prompt');
11
12
  const config = require('./config.js');
12
- const { cliux, configHandler, HttpClient } = require('@contentstack/cli-utilities');
13
+ const {
14
+ cliux,
15
+ configHandler,
16
+ HttpClient,
17
+ messageHandler,
18
+ managementSDKClient,
19
+ ContentstackClient,
20
+ } = require('@contentstack/cli-utilities');
21
+
13
22
  const directory = './data';
14
23
  const delimeter = os.platform() === 'win32' ? '\\' : '/';
15
24
 
@@ -155,9 +164,7 @@ function chooseStack(managementAPIClient, orgUid, stackApiKey) {
155
164
 
156
165
  async function chooseBranch(branchList) {
157
166
  try {
158
- const branches = await branchList;
159
-
160
- const branchesArray = branches.map((branch) => branch.uid);
167
+ const branchesArray = branchList.map((branch) => branch.uid);
161
168
 
162
169
  let _chooseBranch = [
163
170
  {
@@ -377,20 +384,20 @@ function exitProgram() {
377
384
  process.exit();
378
385
  }
379
386
 
380
- function sanitizeEntries(flatEntry) {
387
+ function sanitizeData(flatData) {
381
388
  // sanitize against CSV Injections
382
389
  const CSVRegex = /^[\\+\\=@\\-]/;
383
- for (key in flatEntry) {
384
- if (typeof flatEntry[key] === 'string' && flatEntry[key].match(CSVRegex)) {
385
- flatEntry[key] = flatEntry[key].replace(/\"/g, "\"\"");
386
- flatEntry[key] = `"'${flatEntry[key]}"`;
387
- } else if (typeof flatEntry[key] === 'object') {
390
+ for (key in flatData) {
391
+ if (typeof flatData[key] === 'string' && flatData[key].match(CSVRegex)) {
392
+ flatData[key] = flatData[key].replace(/\"/g, "\"\"");
393
+ flatData[key] = `"'${flatData[key]}"`;
394
+ } else if (typeof flatData[key] === 'object') {
388
395
  // convert any objects or arrays to string
389
396
  // to store this data correctly in csv
390
- flatEntry[key] = JSON.stringify(flatEntry[key]);
397
+ flatData[key] = JSON.stringify(flatData[key]);
391
398
  }
392
399
  }
393
- return flatEntry;
400
+ return flatData;
394
401
  }
395
402
 
396
403
  function cleanEntries(entries, language, environments, contentTypeUid) {
@@ -400,7 +407,7 @@ function cleanEntries(entries, language, environments, contentTypeUid) {
400
407
  return filteredEntries.map((entry) => {
401
408
  let workflow = '';
402
409
  const envArr = [];
403
- if (entry.publish_details.length) {
410
+ if (entry?.publish_details?.length) {
404
411
  entry.publish_details.forEach((env) => {
405
412
  envArr.push(JSON.stringify([environments[env['environment']], env['locale'], env['time']]));
406
413
  });
@@ -415,7 +422,7 @@ function cleanEntries(entries, language, environments, contentTypeUid) {
415
422
  }
416
423
  }
417
424
  entry = flatten(entry);
418
- entry = sanitizeEntries(entry);
425
+ entry = sanitizeData(entry);
419
426
  entry['publish_details'] = envArr;
420
427
  entry['_workflow'] = workflow;
421
428
  entry['ACL'] = JSON.stringify({}); // setting ACL to empty obj
@@ -443,7 +450,7 @@ function getDateTime() {
443
450
  return dateTime.join('_');
444
451
  }
445
452
 
446
- function write(command, entries, fileName, message) {
453
+ function write(command, entries, fileName, message, delimiter, headers) {
447
454
  // eslint-disable-next-line no-undef
448
455
  if (process.cwd().split(delimeter).pop() !== 'data' && !fs.existsSync(directory)) {
449
456
  mkdirp.sync(directory);
@@ -455,7 +462,8 @@ function write(command, entries, fileName, message) {
455
462
  }
456
463
  // eslint-disable-next-line no-undef
457
464
  cliux.print(`Writing ${message} to file: ${process.cwd()}${delimeter}${fileName}`);
458
- fastcsv.writeToPath(fileName, entries, { headers: true });
465
+ if (headers?.length) fastcsv.writeToPath(fileName, entries, { headers, delimiter });
466
+ else fastcsv.writeToPath(fileName, entries, { headers: true, delimiter });
459
467
  }
460
468
 
461
469
  function startupQuestions() {
@@ -465,7 +473,7 @@ function startupQuestions() {
465
473
  type: 'list',
466
474
  name: 'action',
467
475
  message: 'Choose Action',
468
- choices: [config.exportEntries, config.exportUsers, config.exportTeams, 'Exit'],
476
+ choices: [config.exportEntries, config.exportUsers, config.exportTeams, config.exportTaxonomies, 'Exit'],
469
477
  },
470
478
  ];
471
479
  inquirer
@@ -780,7 +788,7 @@ async function cleanTeamsData(data, managementAPIClient, org) {
780
788
  }
781
789
  }
782
790
 
783
- async function exportTeams(managementAPIClient, organization, teamUid) {
791
+ async function exportTeams(managementAPIClient, organization, teamUid, delimiter) {
784
792
  cliux.print(
785
793
  `info: Exporting the ${
786
794
  teamUid && organization?.name
@@ -799,13 +807,13 @@ async function exportTeams(managementAPIClient, organization, teamUid) {
799
807
  delete team['stackRoleMapping'];
800
808
  });
801
809
  const fileName = `${kebabize(organization.name.replace(config.organizationNameRegex, ''))}_teams_export.csv`;
802
- write(this, modifiedTeam, fileName, ' organization Team details');
810
+ write(this, modifiedTeam, fileName, ' organization Team details', delimiter);
803
811
  // exporting teams user data or a single team user data
804
812
  cliux.print(
805
813
  `info: Exporting the teams user data for ${teamUid ? `team ` + teamUid : `organisation ` + organization?.name}`,
806
814
  { color: 'blue' },
807
815
  );
808
- await getTeamsDetail(allTeamsData, organization, teamUid);
816
+ await getTeamsDetail(allTeamsData, organization, teamUid, delimiter);
809
817
  cliux.print(
810
818
  `info: Exporting the stack role details for ${
811
819
  teamUid ? `team ` + teamUid : `organisation ` + organization?.name
@@ -813,18 +821,18 @@ async function exportTeams(managementAPIClient, organization, teamUid) {
813
821
  { color: 'blue' },
814
822
  );
815
823
  // Exporting the stack Role data for all the teams or exporting stack role data for a single team
816
- await exportRoleMappings(managementAPIClient, allTeamsData, teamUid);
824
+ await exportRoleMappings(managementAPIClient, allTeamsData, teamUid, delimiter);
817
825
  }
818
826
  }
819
827
 
820
- async function getTeamsDetail(allTeamsData, organization, teamUid) {
828
+ async function getTeamsDetail(allTeamsData, organization, teamUid, delimiter) {
821
829
  if (!teamUid) {
822
830
  const userData = await getTeamsUserDetails(allTeamsData);
823
831
  const fileName = `${kebabize(
824
832
  organization.name.replace(config.organizationNameRegex, ''),
825
833
  )}_team_User_Details_export.csv`;
826
834
 
827
- write(this, userData, fileName, 'Team User details');
835
+ write(this, userData, fileName, 'Team User details', delimiter);
828
836
  } else {
829
837
  const team = allTeamsData.filter((team) => team.uid === teamUid)[0];
830
838
 
@@ -839,11 +847,11 @@ async function getTeamsDetail(allTeamsData, organization, teamUid) {
839
847
  organization.name.replace(config.organizationNameRegex, ''),
840
848
  )}_team_${teamUid}_User_Details_export.csv`;
841
849
 
842
- write(this, team.users, fileName, 'Team User details');
850
+ write(this, team.users, fileName, 'Team User details', delimiter);
843
851
  }
844
852
  }
845
853
 
846
- async function exportRoleMappings(managementAPIClient, allTeamsData, teamUid) {
854
+ async function exportRoleMappings(managementAPIClient, allTeamsData, teamUid, delimiter) {
847
855
  let stackRoleWithTeamData = [];
848
856
  let flag = false;
849
857
  const stackNotAdmin = [];
@@ -900,7 +908,7 @@ async function exportRoleMappings(managementAPIClient, allTeamsData, teamUid) {
900
908
  teamUid ? `_${teamUid}` : ''
901
909
  }.csv`;
902
910
 
903
- write(this, stackRoleWithTeamData, fileName, 'Team Stack Role details');
911
+ write(this, stackRoleWithTeamData, fileName, 'Team Stack Role details', delimiter);
904
912
  }
905
913
 
906
914
  async function mapRoleWithTeams(managementAPIClient, stackRoleMapping, teamName, teamUid) {
@@ -949,6 +957,214 @@ async function getTeamsUserDetails(teamsObject) {
949
957
  return allTeamUsers;
950
958
  }
951
959
 
960
+ /**
961
+ * fetch all taxonomies in the provided stack
962
+ * @param {object} payload
963
+ * @param {number} skip
964
+ * @param {array} taxonomies
965
+ * @returns
966
+ */
967
+ async function getAllTaxonomies(payload, skip = 0, taxonomies = []) {
968
+ payload['type'] = 'taxonomies';
969
+ const { items, count } = await taxonomySDKHandler(payload, skip);
970
+ if (items) {
971
+ skip += payload.limit;
972
+ taxonomies.push(...items);
973
+ if (skip >= count) {
974
+ return taxonomies;
975
+ } else {
976
+ return getAllTaxonomies(payload, skip, taxonomies);
977
+ }
978
+ }
979
+ return taxonomies;
980
+ }
981
+
982
+ /**
983
+ * fetch taxonomy related terms
984
+ * @param {object} payload
985
+ * @param {number} skip
986
+ * @param {number} limit
987
+ * @param {array} terms
988
+ * @returns
989
+ */
990
+ async function getAllTermsOfTaxonomy(payload, skip = 0, terms = []) {
991
+ payload['type'] = 'terms';
992
+ const { items, count } = await taxonomySDKHandler(payload, skip);
993
+ if (items) {
994
+ skip += payload.limit;
995
+ terms.push(...items);
996
+ if (skip >= count) {
997
+ return terms;
998
+ } else {
999
+ return getAllTermsOfTaxonomy(payload, skip, terms);
1000
+ }
1001
+ }
1002
+ return terms;
1003
+ }
1004
+
1005
+ /**
1006
+ * Verify the existence of a taxonomy. Obtain its details if it exists and return
1007
+ * @param {object} payload
1008
+ * @param {string} taxonomyUID
1009
+ * @returns
1010
+ */
1011
+ async function getTaxonomy(payload) {
1012
+ payload['type'] = 'taxonomy';
1013
+ const resp = await taxonomySDKHandler(payload);
1014
+ return resp;
1015
+ }
1016
+
1017
+ /**
1018
+ * taxonomy & term sdk handler
1019
+ * @async
1020
+ * @method
1021
+ * @param payload
1022
+ * @param skip
1023
+ * @param limit
1024
+ * @returns {*} Promise<any>
1025
+ */
1026
+ async function taxonomySDKHandler(payload, skip) {
1027
+ const { stackAPIClient, taxonomyUID, type } = payload;
1028
+
1029
+ const queryParams = { include_count: true, limit: payload.limit };
1030
+ if (skip >= 0) queryParams['skip'] = skip || 0;
1031
+
1032
+ switch (type) {
1033
+ case 'taxonomies':
1034
+ return await stackAPIClient
1035
+ .taxonomy()
1036
+ .query(queryParams)
1037
+ .find()
1038
+ .then((data) => data)
1039
+ .catch((err) => handleErrorMsg(err));
1040
+ case 'taxonomy':
1041
+ return await stackAPIClient
1042
+ .taxonomy(taxonomyUID)
1043
+ .fetch()
1044
+ .then((data) => data)
1045
+ .catch((err) => handleErrorMsg(err));
1046
+ case 'terms':
1047
+ queryParams['depth'] = 0;
1048
+ return await stackAPIClient
1049
+ .taxonomy(taxonomyUID)
1050
+ .terms()
1051
+ .query(queryParams)
1052
+ .find()
1053
+ .then((data) => data)
1054
+ .catch((err) => handleErrorMsg(err));
1055
+ default:
1056
+ handleErrorMsg({ errorMessage: 'Invalid module!' });
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * Change taxonomies data in required CSV headers format
1062
+ * @param {array} taxonomies
1063
+ * @returns
1064
+ */
1065
+ function formatTaxonomiesData(taxonomies) {
1066
+ if (taxonomies?.length) {
1067
+ const formattedTaxonomies = taxonomies.map((taxonomy) => {
1068
+ return sanitizeData({
1069
+ 'Taxonomy UID': taxonomy.uid,
1070
+ Name: taxonomy.name,
1071
+ Description: taxonomy.description,
1072
+ });
1073
+ });
1074
+ return formattedTaxonomies;
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * Modify the linked taxonomy data's terms in required CSV headers format
1080
+ * @param {array} terms
1081
+ * @param {string} taxonomyUID
1082
+ * @returns
1083
+ */
1084
+ function formatTermsOfTaxonomyData(terms, taxonomyUID) {
1085
+ if (terms?.length) {
1086
+ const formattedTerms = terms.map((term) => {
1087
+ return sanitizeData({
1088
+ 'Taxonomy UID': taxonomyUID,
1089
+ UID: term.uid,
1090
+ Name: term.name,
1091
+ 'Parent UID': term.parent_uid,
1092
+ Depth: term.depth,
1093
+ });
1094
+ });
1095
+ return formattedTerms;
1096
+ }
1097
+ }
1098
+
1099
+ function handleErrorMsg(err) {
1100
+ if (err?.errorMessage) {
1101
+ cliux.print(`Error: ${err.errorMessage}`, { color: 'red' });
1102
+ } else if (err?.message) {
1103
+ const errorMsg = err?.errors?.taxonomy || err?.errors?.term || err?.message;
1104
+ cliux.print(`Error: ${errorMsg}`, { color: 'red' });
1105
+ } else {
1106
+ console.log(err);
1107
+ cliux.print(`Error: ${messageHandler.parse('CLI_EXPORT_CSV_API_FAILED')}`, { color: 'red' });
1108
+ }
1109
+ process.exit(1);
1110
+ }
1111
+
1112
+ /**
1113
+ * create an importable CSV file, to utilize with the migration script.
1114
+ * @param {*} payload api request payload
1115
+ * @param {*} taxonomies taxonomies data
1116
+ * @returns
1117
+ */
1118
+ async function createImportableCSV(payload, taxonomies) {
1119
+ let taxonomiesData = [];
1120
+ let headers = ['Taxonomy Name','Taxonomy UID','Taxonomy Description'];
1121
+ for (let index = 0; index < taxonomies?.length; index++) {
1122
+ const taxonomy = taxonomies[index];
1123
+ const taxonomyUID = taxonomy?.uid;
1124
+ if (taxonomyUID) {
1125
+ const sanitizedTaxonomy = sanitizeData({
1126
+ 'Taxonomy Name': taxonomy?.name,
1127
+ 'Taxonomy UID': taxonomyUID,
1128
+ 'Taxonomy Description': taxonomy?.description,
1129
+ });
1130
+ taxonomiesData.push(sanitizedTaxonomy);
1131
+ payload['taxonomyUID'] = taxonomyUID;
1132
+ const terms = await getAllTermsOfTaxonomy(payload);
1133
+ //fetch all parent terms
1134
+ const parentTerms = terms.filter((term) => term?.parent_uid === null);
1135
+ const termsData = getParentAndChildTerms(parentTerms, terms, headers);
1136
+ taxonomiesData.push(...termsData)
1137
+ }
1138
+ }
1139
+
1140
+ return {taxonomiesData, headers};
1141
+ }
1142
+
1143
+ /**
1144
+ * Get the parent and child terms, then arrange them hierarchically in a CSV file.
1145
+ * @param {*} parentTerms list of parent terms
1146
+ * @param {*} terms respective terms of taxonomies
1147
+ * @param {*} headers list of csv headers include taxonomy and terms column
1148
+ * @param {*} termsData parent and child terms
1149
+ */
1150
+ function getParentAndChildTerms(parentTerms, terms, headers, termsData=[]) {
1151
+ for (let i = 0; i < parentTerms?.length; i++) {
1152
+ const parentTerm = parentTerms[i];
1153
+ const levelUID = `Term Level${parentTerm.depth} UID`;
1154
+ const levelName = `Term Level${parentTerm.depth} Name`;
1155
+ if (headers.indexOf(levelName) === -1) headers.push(levelName);
1156
+ if (headers.indexOf(levelUID) === -1) headers.push(levelUID);
1157
+ const sanitizedTermData = sanitizeData({ [levelName]: parentTerm.name, [levelUID]: parentTerm.uid });
1158
+ termsData.push(sanitizedTermData);
1159
+ //fetch all sibling terms
1160
+ const newParents = terms.filter((term) => term.parent_uid === parentTerm.uid);
1161
+ if (newParents?.length) {
1162
+ getParentAndChildTerms(newParents, terms, headers, termsData);
1163
+ }
1164
+ }
1165
+ return termsData;
1166
+ }
1167
+
952
1168
  module.exports = {
953
1169
  chooseOrganization: chooseOrganization,
954
1170
  chooseStack: chooseStack,
@@ -977,4 +1193,11 @@ module.exports = {
977
1193
  formatError: formatError,
978
1194
  exportOrgTeams: exportOrgTeams,
979
1195
  exportTeams: exportTeams,
1196
+ getAllTaxonomies,
1197
+ getAllTermsOfTaxonomy,
1198
+ formatTaxonomiesData,
1199
+ formatTermsOfTaxonomyData,
1200
+ getTaxonomy,
1201
+ getStacks,
1202
+ createImportableCSV,
980
1203
  };