@contentstack/cli-cm-export-to-csv 1.4.4 → 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,12 +1,12 @@
1
1
  {
2
2
  "name": "@contentstack/cli-cm-export-to-csv",
3
3
  "description": "Export entities to csv",
4
- "version": "1.4.4",
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": {
8
- "@contentstack/cli-command": "~1.2.14",
9
- "@contentstack/cli-utilities": "~1.5.4",
8
+ "@contentstack/cli-command": "~1.2.15",
9
+ "@contentstack/cli-utilities": "~1.5.5",
10
10
  "chalk": "^4.1.0",
11
11
  "fast-csv": "^4.3.6",
12
12
  "inquirer": "8.2.4",
@@ -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'],
19
- description: `Option to export data (entries, users)`,
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',
@@ -59,6 +59,16 @@ class ExportToCsvCommand extends Command {
59
59
  multiple: false,
60
60
  required: false,
61
61
  }),
62
+ "team-uid": flags.string({
63
+ description: 'Uid of the team whose user data and stack roles are required'
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
+ }),
62
72
  };
63
73
 
64
74
  async run() {
@@ -75,11 +85,20 @@ class ExportToCsvCommand extends Command {
75
85
  'content-type': contentTypesFlag,
76
86
  alias: managementTokenAlias,
77
87
  branch: branchUid,
88
+ "team-uid": teamUid,
89
+ 'taxonomy-uid': taxonomyUID,
90
+ delimiter
78
91
  },
79
92
  } = await this.parse(ExportToCsvCommand);
80
93
 
81
94
  if (!managementTokenAlias) {
82
95
  managementAPIClient = await managementSDKClient({ host: this.cmaHost });
96
+ if (!isAuthenticated()) {
97
+ this.error(config.CLI_EXPORT_CSV_ENTRIES_ERROR, {
98
+ exit: 2,
99
+ suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'],
100
+ });
101
+ }
83
102
  }
84
103
 
85
104
  if (actionFlag) {
@@ -96,69 +115,17 @@ class ExportToCsvCommand extends Command {
96
115
  let stackAPIClient;
97
116
  let language;
98
117
  let contentTypes = [];
99
- let stackBranches;
100
- const listOfTokens = configHandler.get('tokens');
101
-
102
- if (managementTokenAlias && listOfTokens[managementTokenAlias]) {
103
- managementAPIClient = await managementSDKClient({
104
- host: this.cmaHost,
105
- management_token: listOfTokens[managementTokenAlias].token,
106
- });
107
- stack = {
108
- name: stackName || managementTokenAlias,
109
- apiKey: listOfTokens[managementTokenAlias].apiKey,
110
- token: listOfTokens[managementTokenAlias].token,
111
- };
112
- } else if (managementTokenAlias) {
113
- this.error('Provided management token alias not found in your config.!');
114
- } else {
115
- let organization;
116
118
 
117
- if (!isAuthenticated()) {
118
- this.error(config.CLI_EXPORT_CSV_ENTRIES_ERROR, {
119
- exit: 2,
120
- suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'],
121
- });
122
- }
123
-
124
- if (org) {
125
- organization = { uid: org };
126
- } else {
127
- organization = await util.chooseOrganization(managementAPIClient); // prompt for organization
128
- }
129
- if (!stackAPIKey) {
130
- stack = await util.chooseStack(managementAPIClient, organization.uid); // prompt for stack
131
- } else {
132
- stack = await util.chooseStack(managementAPIClient, organization.uid, stackAPIKey);
133
- }
119
+ if (managementTokenAlias) {
120
+ const { stackDetails, apiClient } = await this.getAliasDetails(managementTokenAlias, stackName);
121
+ managementAPIClient = apiClient;
122
+ stack = stackDetails;
123
+ } else {
124
+ stack = await this.getStackDetails(managementAPIClient, stackAPIKey, org);
134
125
  }
135
126
 
136
127
  stackAPIClient = this.getStackClient(managementAPIClient, stack);
137
-
138
- if (branchUid) {
139
- try {
140
- const branchExists = await doesBranchExist(stackAPIClient, branchUid);
141
- if (branchExists?.errorCode) {
142
- throw new Error(branchExists.errorMessage);
143
- }
144
- stack.branch_uid = branchUid;
145
- stackAPIClient = this.getStackClient(managementAPIClient, stack);
146
- } catch (error) {
147
- if (error.message || error.errorMessage) {
148
- cliux.error(util.formatError(error));
149
- this.exit();
150
- }
151
- }
152
- } else {
153
- stackBranches = await this.getStackBranches(stackAPIClient);
154
- if (stackBranches === undefined) {
155
- stackAPIClient = this.getStackClient(managementAPIClient, stack);
156
- } else {
157
- const { branch } = await util.chooseBranch(stackBranches);
158
- stack.branch_uid = branch;
159
- stackAPIClient = this.getStackClient(managementAPIClient, stack);
160
- }
161
- }
128
+ await this.checkAndUpdateBranchDetail(branchUid, stack, stackAPIClient, managementAPIClient);
162
129
 
163
130
  const contentTypeCount = await util.getContentTypeCount(stackAPIClient);
164
131
 
@@ -217,7 +184,7 @@ class ExportToCsvCommand extends Command {
217
184
  flatEntries = flatEntries.concat(flatEntriesResult);
218
185
  }
219
186
  let fileName = `${stackName ? stackName : stack.name}_${contentType}_${language.code}_entries_export.csv`;
220
- util.write(this, flatEntries, fileName, 'entries'); // write to file
187
+ util.write(this, flatEntries, fileName, 'entries', delimiter); // write to file
221
188
  }
222
189
  } catch (error) {
223
190
  cliux.error(util.formatError(error));
@@ -227,12 +194,6 @@ class ExportToCsvCommand extends Command {
227
194
  case config.exportUsers:
228
195
  case 'users': {
229
196
  try {
230
- if (!isAuthenticated()) {
231
- this.error(config.CLI_EXPORT_CSV_LOGIN_FAILED, {
232
- exit: 2,
233
- suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'],
234
- });
235
- }
236
197
  let organization;
237
198
 
238
199
  if (org) {
@@ -250,7 +211,25 @@ class ExportToCsvCommand extends Command {
250
211
  (orgName ? orgName : organization.name).replace(config.organizationNameRegex, ''),
251
212
  )}_users_export.csv`;
252
213
 
253
- util.write(this, listOfUsers, fileName, 'organization details');
214
+ util.write(this, listOfUsers, fileName, 'organization details', delimiter);
215
+ } catch (error) {
216
+ if (error.message || error.errorMessage) {
217
+ cliux.error(util.formatError(error));
218
+ }
219
+ }
220
+ break;
221
+ }
222
+ case config.exportTeams:
223
+ case 'teams': {
224
+ try{
225
+ let organization;
226
+ if (org) {
227
+ organization = { uid: org, name: orgName || org };
228
+ } else {
229
+ organization = await util.chooseOrganization(managementAPIClient, action); // prompt for organization
230
+ }
231
+
232
+ await util.exportTeams(managementAPIClient,organization,teamUid, delimiter);
254
233
  } catch (error) {
255
234
  if (error.message || error.errorMessage) {
256
235
  cliux.error(util.formatError(error));
@@ -258,6 +237,22 @@ class ExportToCsvCommand extends Command {
258
237
  }
259
238
  break;
260
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;
255
+ }
261
256
  }
262
257
  } catch (error) {
263
258
  if (error.message || error.errorMessage) {
@@ -273,8 +268,8 @@ class ExportToCsvCommand extends Command {
273
268
  getStackClient(managementAPIClient, stack) {
274
269
  const stackInit = {
275
270
  api_key: stack.apiKey,
276
- branch_uid: stack.branch_uid,
277
271
  };
272
+ if (stack?.branch_uid) stackInit['branch_uid'] = stack.branch_uid;
278
273
  if (stack.token) {
279
274
  return managementAPIClient.stack({
280
275
  ...stackInit,
@@ -292,9 +287,159 @@ class ExportToCsvCommand extends Command {
292
287
  .then(({ items }) => (items !== undefined ? items : []))
293
288
  .catch((_err) => {});
294
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
+ }
295
440
  }
296
441
 
297
- 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`;
298
443
 
299
444
  ExportToCsvCommand.examples = [
300
445
  'csdx cm:export-to-csv',
@@ -310,6 +455,30 @@ ExportToCsvCommand.examples = [
310
455
  '',
311
456
  'Exporting organization users to csv with organization name provided',
312
457
  'csdx cm:export-to-csv --action <users> --org <org-uid> --org-name <org-name>',
458
+ '',
459
+ 'Exporting Organizations Teams to CSV',
460
+ 'csdx cm:export-to-csv --action <teams>',
461
+ '',
462
+ 'Exporting Organizations Teams to CSV with org-uid',
463
+ 'csdx cm:export-to-csv --action <teams> --org <org-uid>',
464
+ '',
465
+ 'Exporting Organizations Teams to CSV with team uid',
466
+ 'csdx cm:export-to-csv --action <teams> --team-uid <team-uid>',
467
+ '',
468
+ 'Exporting Organizations Teams to CSV with org-uid and team uid',
469
+ 'csdx cm:export-to-csv --action <teams> --org <org-uid> --team-uid <team-uid>',
470
+ '',
471
+ 'Exporting Organizations Teams to CSV with org-uid and team uid',
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>',
313
482
  ];
314
483
 
315
484
  module.exports = ExportToCsvCommand;
@@ -1,10 +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
- exportUsers: "Export organization users' data to a .CSV file",
5
+ exportUsers: "Export organization user's data to a .CSV file",
6
+ exportTeams: "Export organization team's data to a .csv file",
7
+ exportTaxonomies: 'Export taxonomies to a .CSV file',
5
8
  adminError: "Unable to export data. Make sure you're an admin or owner of this organization",
6
9
  organizationNameRegex: /\'/,
7
10
  CLI_EXPORT_CSV_LOGIN_FAILED: "You need to login to execute this command. See: auth:login --help",
8
- CLI_EXPORT_CSV_ENTRIES_ERROR: "You need to either login or provide a management token to execute this command"
9
-
11
+ CLI_EXPORT_CSV_ENTRIES_ERROR: "You need to either login or provide a management token to execute this command",
12
+ CLI_EXPORT_CSV_API_FAILED: 'Something went wrong! Please try again'
10
13
  };
package/src/util/index.js CHANGED
@@ -2,13 +2,22 @@ const os = require('os');
2
2
  const fs = require('fs');
3
3
  const mkdirp = require('mkdirp');
4
4
  const find = require('lodash/find');
5
+ const cloneDeep = require('lodash/cloneDeep');
6
+ const omit = require('lodash/omit');
7
+ const flat = require('lodash/flatten');
5
8
  const fastcsv = require('fast-csv');
6
9
  const inquirer = require('inquirer');
7
10
  const debug = require('debug')('export-to-csv');
8
11
  const checkboxPlus = require('inquirer-checkbox-plus-prompt');
9
-
10
12
  const config = require('./config.js');
11
- const { cliux, configHandler } = require('@contentstack/cli-utilities');
13
+ const {
14
+ cliux,
15
+ configHandler,
16
+ HttpClient,
17
+ messageHandler,
18
+ managementSDKClient,
19
+ ContentstackClient,
20
+ } = require('@contentstack/cli-utilities');
12
21
 
13
22
  const directory = './data';
14
23
  const delimeter = os.platform() === 'win32' ? '\\' : '/';
@@ -20,7 +29,7 @@ function chooseOrganization(managementAPIClient, action) {
20
29
  return new Promise(async (resolve, reject) => {
21
30
  try {
22
31
  let organizations;
23
- if (action === config.exportUsers) {
32
+ if (action === config.exportUsers || action === config.exportTeams || action === 'teams') {
24
33
  organizations = await getOrganizationsWhereUserIsAdmin(managementAPIClient);
25
34
  } else {
26
35
  organizations = await getOrganizations(managementAPIClient);
@@ -105,7 +114,7 @@ async function getOrganizationsWhereUserIsAdmin(managementAPIClient) {
105
114
  organizations.forEach((org) => {
106
115
  result[org.name] = org.uid;
107
116
  });
108
- }
117
+ }
109
118
 
110
119
  return result;
111
120
  } catch (error) {
@@ -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
  {
@@ -321,7 +328,13 @@ function getEntries(stackAPIClient, contentType, language, skip, limit) {
321
328
  stackAPIClient
322
329
  .contentType(contentType)
323
330
  .entry()
324
- .query({ include_publish_details: true, locale: language, skip: skip * 100, limit: limit, include_workflow: true })
331
+ .query({
332
+ include_publish_details: true,
333
+ locale: language,
334
+ skip: skip * 100,
335
+ limit: limit,
336
+ include_workflow: true,
337
+ })
325
338
  .find()
326
339
  .then((entries) => resolve(entries))
327
340
  .catch((error) => reject(error));
@@ -371,20 +384,20 @@ function exitProgram() {
371
384
  process.exit();
372
385
  }
373
386
 
374
- function sanitizeEntries(flatEntry) {
387
+ function sanitizeData(flatData) {
375
388
  // sanitize against CSV Injections
376
- const CSVRegex = /^[\\+\\=@\\-]/
377
- for (key in flatEntry) {
378
- if (typeof flatEntry[key] === 'string' && flatEntry[key].match(CSVRegex)) {
379
- flatEntry[key] = flatEntry[key].replace(/\"/g, "\"\"");
380
- flatEntry[key] = `"'${flatEntry[key]}"`
381
- } else if (typeof flatEntry[key] === 'object') {
389
+ const CSVRegex = /^[\\+\\=@\\-]/;
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') {
382
395
  // convert any objects or arrays to string
383
396
  // to store this data correctly in csv
384
- flatEntry[key] = JSON.stringify(flatEntry[key]);
397
+ flatData[key] = JSON.stringify(flatData[key]);
385
398
  }
386
399
  }
387
- return flatEntry;
400
+ return flatData;
388
401
  }
389
402
 
390
403
  function cleanEntries(entries, language, environments, contentTypeUid) {
@@ -394,7 +407,7 @@ function cleanEntries(entries, language, environments, contentTypeUid) {
394
407
  return filteredEntries.map((entry) => {
395
408
  let workflow = '';
396
409
  const envArr = [];
397
- if(entry.publish_details.length) {
410
+ if (entry?.publish_details?.length) {
398
411
  entry.publish_details.forEach((env) => {
399
412
  envArr.push(JSON.stringify([environments[env['environment']], env['locale'], env['time']]));
400
413
  });
@@ -403,13 +416,13 @@ function cleanEntries(entries, language, environments, contentTypeUid) {
403
416
  delete entry.publish_details;
404
417
  delete entry.setWorkflowStage;
405
418
  if ('_workflow' in entry) {
406
- if(entry._workflow?.name) {
407
- workflow = entry['_workflow']['name'];
408
- delete entry['_workflow'];
409
- }
419
+ if (entry._workflow?.name) {
420
+ workflow = entry['_workflow']['name'];
421
+ delete entry['_workflow'];
422
+ }
410
423
  }
411
424
  entry = flatten(entry);
412
- entry = sanitizeEntries(entry);
425
+ entry = sanitizeData(entry);
413
426
  entry['publish_details'] = envArr;
414
427
  entry['_workflow'] = workflow;
415
428
  entry['ACL'] = JSON.stringify({}); // setting ACL to empty obj
@@ -437,7 +450,7 @@ function getDateTime() {
437
450
  return dateTime.join('_');
438
451
  }
439
452
 
440
- function write(command, entries, fileName, message) {
453
+ function write(command, entries, fileName, message, delimiter, headers) {
441
454
  // eslint-disable-next-line no-undef
442
455
  if (process.cwd().split(delimeter).pop() !== 'data' && !fs.existsSync(directory)) {
443
456
  mkdirp.sync(directory);
@@ -449,7 +462,8 @@ function write(command, entries, fileName, message) {
449
462
  }
450
463
  // eslint-disable-next-line no-undef
451
464
  cliux.print(`Writing ${message} to file: ${process.cwd()}${delimeter}${fileName}`);
452
- 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 });
453
467
  }
454
468
 
455
469
  function startupQuestions() {
@@ -459,7 +473,7 @@ function startupQuestions() {
459
473
  type: 'list',
460
474
  name: 'action',
461
475
  message: 'Choose Action',
462
- choices: [config.exportEntries, config.exportUsers, 'Exit'],
476
+ choices: [config.exportEntries, config.exportUsers, config.exportTeams, config.exportTaxonomies, 'Exit'],
463
477
  },
464
478
  ];
465
479
  inquirer
@@ -678,6 +692,479 @@ function wait(time) {
678
692
  });
679
693
  }
680
694
 
695
+ function handleErrorMsg(err) {
696
+ cliux.print(`Error: ${(err?.errorMessage || err?.message) ? err?.errorMessage || err?.message : messageHandler.parse('CLI_EXPORT_CSV_API_FAILED')}`, { color: 'red' })
697
+ process.exit(1);
698
+ }
699
+
700
+ async function apiRequestHandler(org, queryParam = {}) {
701
+ const headers = {
702
+ authtoken: configHandler.get('authtoken'),
703
+ organization_uid: org.uid,
704
+ 'Content-Type': 'application/json',
705
+ api_version: 1.1,
706
+ };
707
+
708
+ return await new HttpClient()
709
+ .headers(headers)
710
+ .queryParams(queryParam)
711
+ .get(`${configHandler.get('region')?.cma}/organizations/${org?.uid}/teams`)
712
+ .then((res) => {
713
+ const { status, data } = res;
714
+ if (status === 200) {
715
+ return data;
716
+ } else {
717
+ cliux.print(`${data?.error_message || data?.message || data?.errorMessage}`, { color: 'red' });
718
+ process.exit(1);
719
+ }
720
+ })
721
+ .catch((error) => {
722
+ handleErrorMsg(error);
723
+ });
724
+ }
725
+
726
+ async function exportOrgTeams(managementAPIClient, org) {
727
+ let teamsObjectArray = [];
728
+ let skip = 0;
729
+ let limit = config?.limit || 100;
730
+ do {
731
+ const data = await apiRequestHandler(org, { skip: skip, limit: limit, includeUserDetails: true });
732
+ skip += limit;
733
+ teamsObjectArray.push(...data?.teams);
734
+ if (skip >= data?.count) break;
735
+ } while (1);
736
+ teamsObjectArray = await cleanTeamsData(teamsObjectArray, managementAPIClient, org);
737
+ return teamsObjectArray;
738
+ }
739
+
740
+ async function getOrgRolesForTeams(managementAPIClient, org) {
741
+ let roleMap = {}; // for org level there are two roles only admin and member
742
+
743
+ // SDK call to get the role uids
744
+ await managementAPIClient
745
+ .organization(org.uid)
746
+ .roles()
747
+ .then((roles) => {
748
+ roles.items.forEach((item) => {
749
+ if (item.name === 'member' || item.name === 'admin') {
750
+ roleMap[item.name] = item.uid;
751
+ }
752
+ });
753
+ })
754
+ .catch((err) => {
755
+ handleErrorMsg(err);
756
+ });
757
+ return roleMap;
758
+ }
759
+
760
+ async function cleanTeamsData(data, managementAPIClient, org) {
761
+ const roleMap = await getOrgRolesForTeams(managementAPIClient, org);
762
+ const fieldToBeDeleted = [
763
+ '_id',
764
+ 'createdAt',
765
+ 'createdBy',
766
+ 'updatedAt',
767
+ 'updatedBy',
768
+ '__v',
769
+ 'createdByUserName',
770
+ 'updatedByUserName',
771
+ 'organizationUid',
772
+ ];
773
+ if (data?.length) {
774
+ return data.map((team) => {
775
+ team = omit(team, fieldToBeDeleted);
776
+
777
+ team.organizationRole = (team.organizationRole === roleMap["member"]) ? "member" : "admin";
778
+
779
+ if (!team.hasOwnProperty("description")) {
780
+ team.description = "";
781
+ }
782
+ team.Total_Members = team?.users?.length || 0;
783
+
784
+ return team;
785
+ });
786
+ } else {
787
+ return [];
788
+ }
789
+ }
790
+
791
+ async function exportTeams(managementAPIClient, organization, teamUid, delimiter) {
792
+ cliux.print(
793
+ `info: Exporting the ${
794
+ teamUid && organization?.name
795
+ ? `team with uid ${teamUid} in Organisation ${organization?.name} `
796
+ : `teams of Organisation ` + organization?.name
797
+ }`,
798
+ { color: 'blue' },
799
+ );
800
+ const allTeamsData = await exportOrgTeams(managementAPIClient, organization);
801
+ if (!allTeamsData?.length) {
802
+ cliux.print(`info: The organization ${organization?.name} does not have any teams associated with it. Please verify and provide the correct organization name.`);
803
+ } else {
804
+ const modifiedTeam = cloneDeep(allTeamsData);
805
+ modifiedTeam.forEach((team) => {
806
+ delete team['users'];
807
+ delete team['stackRoleMapping'];
808
+ });
809
+ const fileName = `${kebabize(organization.name.replace(config.organizationNameRegex, ''))}_teams_export.csv`;
810
+ write(this, modifiedTeam, fileName, ' organization Team details', delimiter);
811
+ // exporting teams user data or a single team user data
812
+ cliux.print(
813
+ `info: Exporting the teams user data for ${teamUid ? `team ` + teamUid : `organisation ` + organization?.name}`,
814
+ { color: 'blue' },
815
+ );
816
+ await getTeamsDetail(allTeamsData, organization, teamUid, delimiter);
817
+ cliux.print(
818
+ `info: Exporting the stack role details for ${
819
+ teamUid ? `team ` + teamUid : `organisation ` + organization?.name
820
+ }`,
821
+ { color: 'blue' },
822
+ );
823
+ // Exporting the stack Role data for all the teams or exporting stack role data for a single team
824
+ await exportRoleMappings(managementAPIClient, allTeamsData, teamUid, delimiter);
825
+ }
826
+ }
827
+
828
+ async function getTeamsDetail(allTeamsData, organization, teamUid, delimiter) {
829
+ if (!teamUid) {
830
+ const userData = await getTeamsUserDetails(allTeamsData);
831
+ const fileName = `${kebabize(
832
+ organization.name.replace(config.organizationNameRegex, ''),
833
+ )}_team_User_Details_export.csv`;
834
+
835
+ write(this, userData, fileName, 'Team User details', delimiter);
836
+ } else {
837
+ const team = allTeamsData.filter((team) => team.uid === teamUid)[0];
838
+
839
+ team.users.forEach((user) => {
840
+ user['team-name'] = team.name;
841
+ user['team-uid'] = team.uid;
842
+ delete user['active'];
843
+ delete user['orgInvitationStatus'];
844
+ });
845
+
846
+ const fileName = `${kebabize(
847
+ organization.name.replace(config.organizationNameRegex, ''),
848
+ )}_team_${teamUid}_User_Details_export.csv`;
849
+
850
+ write(this, team.users, fileName, 'Team User details', delimiter);
851
+ }
852
+ }
853
+
854
+ async function exportRoleMappings(managementAPIClient, allTeamsData, teamUid, delimiter) {
855
+ let stackRoleWithTeamData = [];
856
+ let flag = false;
857
+ const stackNotAdmin = [];
858
+ if (teamUid) {
859
+ const team = find(allTeamsData,function(teamObject) { return teamObject?.uid===teamUid });
860
+ for (const stack of team?.stackRoleMapping) {
861
+ const roleData = await mapRoleWithTeams(managementAPIClient, stack, team?.name, team?.uid);
862
+ stackRoleWithTeamData.push(...roleData);
863
+ if(roleData[0]['Stack Name']==='') {
864
+ flag = true;
865
+ stackNotAdmin.push(stack.stackApiKey);
866
+ }
867
+ }
868
+ } else {
869
+ for (const team of allTeamsData ?? []) {
870
+ for (const stack of team?.stackRoleMapping ?? []) {
871
+ const roleData = await mapRoleWithTeams(managementAPIClient, stack, team?.name, team?.uid);
872
+ stackRoleWithTeamData.push(...roleData);
873
+ if(roleData[0]['Stack Name']==='') {
874
+ flag = true;
875
+ stackNotAdmin.push(stack.stackApiKey);
876
+ }
877
+ }
878
+ }
879
+ }
880
+ if(stackNotAdmin?.length) {
881
+ cliux.print(`warning: Admin access denied to the following stacks using the provided API keys. Please get in touch with the stack owner to request access.`,{color:"yellow"});
882
+ cliux.print(`${stackNotAdmin.join(' , ')}`,{color:"yellow"});
883
+ }
884
+ if(flag) {
885
+ let export_stack_role = [
886
+ {
887
+ type: 'list',
888
+ name: 'chooseExport',
889
+ message: `Access denied: Please confirm if you still want to continue exporting the data without the { Stack Name, Stack Uid, Role Name } fields.`,
890
+ choices: ['yes', 'no'],
891
+ loop: false,
892
+ }]
893
+ const exportStackRole = await inquirer
894
+ .prompt(export_stack_role)
895
+ .then(( chosenOrg ) => {
896
+ return chosenOrg
897
+ })
898
+ .catch((error) => {
899
+ cliux.print(error, {color:'red'});
900
+ process.exit(1);
901
+ });
902
+ if(exportStackRole.chooseExport === 'no') {
903
+ process.exit(1);
904
+ }
905
+ }
906
+
907
+ const fileName = `${kebabize('Stack_Role_Mapping'.replace(config.organizationNameRegex, ''))}${
908
+ teamUid ? `_${teamUid}` : ''
909
+ }.csv`;
910
+
911
+ write(this, stackRoleWithTeamData, fileName, 'Team Stack Role details', delimiter);
912
+ }
913
+
914
+ async function mapRoleWithTeams(managementAPIClient, stackRoleMapping, teamName, teamUid) {
915
+ const roles = await getRoleData(managementAPIClient, stackRoleMapping.stackApiKey);
916
+ const stackRole = {};
917
+ roles?.items?.forEach((role) => {
918
+ if (!stackRole.hasOwnProperty(role?.uid)) {
919
+ stackRole[role?.uid] = role?.name;
920
+ stackRole[role?.stack?.api_key] = {name: role?.stack?.name, uid: role?.stack?.uid }
921
+ }
922
+ });
923
+ const stackRoleMapOfTeam = stackRoleMapping?.roles.map((role) => {
924
+ return {
925
+ 'Team Name': teamName,
926
+ 'Team Uid': teamUid,
927
+ 'Stack Name': stackRole[stackRoleMapping?.stackApiKey]?.name || '',
928
+ 'Stack Uid': stackRole[stackRoleMapping?.stackApiKey]?.uid || '',
929
+ 'Role Name': stackRole[role] || '',
930
+ 'Role Uid': role || '',
931
+ };
932
+ });
933
+ return stackRoleMapOfTeam;
934
+ }
935
+
936
+ async function getRoleData(managementAPIClient, stackApiKey) {
937
+ try {
938
+ return await managementAPIClient.stack({ api_key: stackApiKey }).role().fetchAll();
939
+ } catch (error) {
940
+ return {}
941
+ }
942
+ }
943
+
944
+ async function getTeamsUserDetails(teamsObject) {
945
+ const allTeamUsers = [];
946
+ teamsObject.forEach((team) => {
947
+ if (team?.users?.length) {
948
+ team.users.forEach((user) => {
949
+ user['team-name'] = team.name;
950
+ user['team-uid'] = team.uid;
951
+ delete user['active'];
952
+ delete user['orgInvitationStatus'];
953
+ allTeamUsers.push(user);
954
+ });
955
+ }
956
+ });
957
+ return allTeamUsers;
958
+ }
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
+
681
1168
  module.exports = {
682
1169
  chooseOrganization: chooseOrganization,
683
1170
  chooseStack: chooseStack,
@@ -704,4 +1191,13 @@ module.exports = {
704
1191
  chooseInMemContentTypes: chooseInMemContentTypes,
705
1192
  getEntriesCount: getEntriesCount,
706
1193
  formatError: formatError,
1194
+ exportOrgTeams: exportOrgTeams,
1195
+ exportTeams: exportTeams,
1196
+ getAllTaxonomies,
1197
+ getAllTermsOfTaxonomy,
1198
+ formatTaxonomiesData,
1199
+ formatTermsOfTaxonomyData,
1200
+ getTaxonomy,
1201
+ getStacks,
1202
+ createImportableCSV,
707
1203
  };