@contentstack/cli-cm-export-to-csv 1.4.4 → 1.5.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.5.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,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'],
19
+ description: `Option to export data (entries, users, teams)`,
20
20
  }),
21
21
  alias: flags.string({
22
22
  char: 'a',
@@ -59,6 +59,9 @@ 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
+ })
62
65
  };
63
66
 
64
67
  async run() {
@@ -75,11 +78,18 @@ class ExportToCsvCommand extends Command {
75
78
  'content-type': contentTypesFlag,
76
79
  alias: managementTokenAlias,
77
80
  branch: branchUid,
81
+ "team-uid": teamUid
78
82
  },
79
83
  } = await this.parse(ExportToCsvCommand);
80
84
 
81
85
  if (!managementTokenAlias) {
82
86
  managementAPIClient = await managementSDKClient({ host: this.cmaHost });
87
+ if (!isAuthenticated()) {
88
+ this.error(config.CLI_EXPORT_CSV_ENTRIES_ERROR, {
89
+ exit: 2,
90
+ suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'],
91
+ });
92
+ }
83
93
  }
84
94
 
85
95
  if (actionFlag) {
@@ -113,14 +123,6 @@ class ExportToCsvCommand extends Command {
113
123
  this.error('Provided management token alias not found in your config.!');
114
124
  } else {
115
125
  let organization;
116
-
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
126
  if (org) {
125
127
  organization = { uid: org };
126
128
  } else {
@@ -227,12 +229,6 @@ class ExportToCsvCommand extends Command {
227
229
  case config.exportUsers:
228
230
  case 'users': {
229
231
  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
232
  let organization;
237
233
 
238
234
  if (org) {
@@ -258,6 +254,24 @@ class ExportToCsvCommand extends Command {
258
254
  }
259
255
  break;
260
256
  }
257
+ case config.exportTeams:
258
+ case 'teams': {
259
+ try{
260
+ let organization;
261
+ if (org) {
262
+ organization = { uid: org, name: orgName || org };
263
+ } else {
264
+ organization = await util.chooseOrganization(managementAPIClient, action); // prompt for organization
265
+ }
266
+
267
+ await util.exportTeams(managementAPIClient,organization,teamUid);
268
+ } catch (error) {
269
+ if (error.message || error.errorMessage) {
270
+ cliux.error(util.formatError(error));
271
+ }
272
+ }
273
+ }
274
+ break;
261
275
  }
262
276
  } catch (error) {
263
277
  if (error.message || error.errorMessage) {
@@ -310,6 +324,21 @@ ExportToCsvCommand.examples = [
310
324
  '',
311
325
  'Exporting organization users to csv with organization name provided',
312
326
  'csdx cm:export-to-csv --action <users> --org <org-uid> --org-name <org-name>',
327
+ '',
328
+ 'Exporting Organizations Teams to CSV',
329
+ 'csdx cm:export-to-csv --action <teams>',
330
+ '',
331
+ 'Exporting Organizations Teams to CSV with org-uid',
332
+ 'csdx cm:export-to-csv --action <teams> --org <org-uid>',
333
+ '',
334
+ 'Exporting Organizations Teams to CSV with team uid',
335
+ 'csdx cm:export-to-csv --action <teams> --team-uid <team-uid>',
336
+ '',
337
+ 'Exporting Organizations Teams to CSV with org-uid and team uid',
338
+ 'csdx cm:export-to-csv --action <teams> --org <org-uid> --team-uid <team-uid>',
339
+ '',
340
+ 'Exporting Organizations Teams to CSV with org-uid and team uid',
341
+ 'csdx cm:export-to-csv --action <teams> --org <org-uid> --team-uid <team-uid> --org-name <org-name>',
313
342
  ];
314
343
 
315
344
  module.exports = ExportToCsvCommand;
@@ -1,10 +1,11 @@
1
1
  module.exports = {
2
2
  cancelString: 'Cancel and Exit',
3
3
  exportEntries: 'Export entries to a .CSV file',
4
- exportUsers: "Export organization users' data to a .CSV file",
4
+ exportUsers: "Export organization user's data to a .CSV file",
5
+ exportTeams: "Export organization team's data to a .csv file",
5
6
  adminError: "Unable to export data. Make sure you're an admin or owner of this organization",
6
7
  organizationNameRegex: /\'/,
7
8
  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
-
9
+ 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!'
10
11
  };
package/src/util/index.js CHANGED
@@ -2,14 +2,14 @@ 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');
5
7
  const fastcsv = require('fast-csv');
6
8
  const inquirer = require('inquirer');
7
9
  const debug = require('debug')('export-to-csv');
8
10
  const checkboxPlus = require('inquirer-checkbox-plus-prompt');
9
-
10
11
  const config = require('./config.js');
11
- const { cliux, configHandler } = require('@contentstack/cli-utilities');
12
-
12
+ const { cliux, configHandler, HttpClient } = require('@contentstack/cli-utilities');
13
13
  const directory = './data';
14
14
  const delimeter = os.platform() === 'win32' ? '\\' : '/';
15
15
 
@@ -20,7 +20,7 @@ function chooseOrganization(managementAPIClient, action) {
20
20
  return new Promise(async (resolve, reject) => {
21
21
  try {
22
22
  let organizations;
23
- if (action === config.exportUsers) {
23
+ if (action === config.exportUsers || action === config.exportTeams || action === 'teams') {
24
24
  organizations = await getOrganizationsWhereUserIsAdmin(managementAPIClient);
25
25
  } else {
26
26
  organizations = await getOrganizations(managementAPIClient);
@@ -105,7 +105,7 @@ async function getOrganizationsWhereUserIsAdmin(managementAPIClient) {
105
105
  organizations.forEach((org) => {
106
106
  result[org.name] = org.uid;
107
107
  });
108
- }
108
+ }
109
109
 
110
110
  return result;
111
111
  } catch (error) {
@@ -321,7 +321,13 @@ function getEntries(stackAPIClient, contentType, language, skip, limit) {
321
321
  stackAPIClient
322
322
  .contentType(contentType)
323
323
  .entry()
324
- .query({ include_publish_details: true, locale: language, skip: skip * 100, limit: limit, include_workflow: true })
324
+ .query({
325
+ include_publish_details: true,
326
+ locale: language,
327
+ skip: skip * 100,
328
+ limit: limit,
329
+ include_workflow: true,
330
+ })
325
331
  .find()
326
332
  .then((entries) => resolve(entries))
327
333
  .catch((error) => reject(error));
@@ -373,11 +379,11 @@ function exitProgram() {
373
379
 
374
380
  function sanitizeEntries(flatEntry) {
375
381
  // sanitize against CSV Injections
376
- const CSVRegex = /^[\\+\\=@\\-]/
382
+ const CSVRegex = /^[\\+\\=@\\-]/;
377
383
  for (key in flatEntry) {
378
384
  if (typeof flatEntry[key] === 'string' && flatEntry[key].match(CSVRegex)) {
379
385
  flatEntry[key] = flatEntry[key].replace(/\"/g, "\"\"");
380
- flatEntry[key] = `"'${flatEntry[key]}"`
386
+ flatEntry[key] = `"'${flatEntry[key]}"`;
381
387
  } else if (typeof flatEntry[key] === 'object') {
382
388
  // convert any objects or arrays to string
383
389
  // to store this data correctly in csv
@@ -394,7 +400,7 @@ function cleanEntries(entries, language, environments, contentTypeUid) {
394
400
  return filteredEntries.map((entry) => {
395
401
  let workflow = '';
396
402
  const envArr = [];
397
- if(entry.publish_details.length) {
403
+ if (entry.publish_details.length) {
398
404
  entry.publish_details.forEach((env) => {
399
405
  envArr.push(JSON.stringify([environments[env['environment']], env['locale'], env['time']]));
400
406
  });
@@ -403,10 +409,10 @@ function cleanEntries(entries, language, environments, contentTypeUid) {
403
409
  delete entry.publish_details;
404
410
  delete entry.setWorkflowStage;
405
411
  if ('_workflow' in entry) {
406
- if(entry._workflow?.name) {
407
- workflow = entry['_workflow']['name'];
408
- delete entry['_workflow'];
409
- }
412
+ if (entry._workflow?.name) {
413
+ workflow = entry['_workflow']['name'];
414
+ delete entry['_workflow'];
415
+ }
410
416
  }
411
417
  entry = flatten(entry);
412
418
  entry = sanitizeEntries(entry);
@@ -459,7 +465,7 @@ function startupQuestions() {
459
465
  type: 'list',
460
466
  name: 'action',
461
467
  message: 'Choose Action',
462
- choices: [config.exportEntries, config.exportUsers, 'Exit'],
468
+ choices: [config.exportEntries, config.exportUsers, config.exportTeams, 'Exit'],
463
469
  },
464
470
  ];
465
471
  inquirer
@@ -678,6 +684,271 @@ function wait(time) {
678
684
  });
679
685
  }
680
686
 
687
+ function handleErrorMsg(err) {
688
+ cliux.print(`Error: ${(err?.errorMessage || err?.message) ? err?.errorMessage || err?.message : messageHandler.parse('CLI_EXPORT_CSV_API_FAILED')}`, { color: 'red' })
689
+ process.exit(1);
690
+ }
691
+
692
+ async function apiRequestHandler(org, queryParam = {}) {
693
+ const headers = {
694
+ authtoken: configHandler.get('authtoken'),
695
+ organization_uid: org.uid,
696
+ 'Content-Type': 'application/json',
697
+ api_version: 1.1,
698
+ };
699
+
700
+ return await new HttpClient()
701
+ .headers(headers)
702
+ .queryParams(queryParam)
703
+ .get(`${configHandler.get('region')?.cma}/organizations/${org?.uid}/teams`)
704
+ .then((res) => {
705
+ const { status, data } = res;
706
+ if (status === 200) {
707
+ return data;
708
+ } else {
709
+ cliux.print(`${data?.error_message || data?.message || data?.errorMessage}`, { color: 'red' });
710
+ process.exit(1);
711
+ }
712
+ })
713
+ .catch((error) => {
714
+ handleErrorMsg(error);
715
+ });
716
+ }
717
+
718
+ async function exportOrgTeams(managementAPIClient, org) {
719
+ let teamsObjectArray = [];
720
+ let skip = 0;
721
+ let limit = config?.limit || 100;
722
+ do {
723
+ const data = await apiRequestHandler(org, { skip: skip, limit: limit, includeUserDetails: true });
724
+ skip += limit;
725
+ teamsObjectArray.push(...data?.teams);
726
+ if (skip >= data?.count) break;
727
+ } while (1);
728
+ teamsObjectArray = await cleanTeamsData(teamsObjectArray, managementAPIClient, org);
729
+ return teamsObjectArray;
730
+ }
731
+
732
+ async function getOrgRolesForTeams(managementAPIClient, org) {
733
+ let roleMap = {}; // for org level there are two roles only admin and member
734
+
735
+ // SDK call to get the role uids
736
+ await managementAPIClient
737
+ .organization(org.uid)
738
+ .roles()
739
+ .then((roles) => {
740
+ roles.items.forEach((item) => {
741
+ if (item.name === 'member' || item.name === 'admin') {
742
+ roleMap[item.name] = item.uid;
743
+ }
744
+ });
745
+ })
746
+ .catch((err) => {
747
+ handleErrorMsg(err);
748
+ });
749
+ return roleMap;
750
+ }
751
+
752
+ async function cleanTeamsData(data, managementAPIClient, org) {
753
+ const roleMap = await getOrgRolesForTeams(managementAPIClient, org);
754
+ const fieldToBeDeleted = [
755
+ '_id',
756
+ 'createdAt',
757
+ 'createdBy',
758
+ 'updatedAt',
759
+ 'updatedBy',
760
+ '__v',
761
+ 'createdByUserName',
762
+ 'updatedByUserName',
763
+ 'organizationUid',
764
+ ];
765
+ if (data?.length) {
766
+ return data.map((team) => {
767
+ team = omit(team, fieldToBeDeleted);
768
+
769
+ team.organizationRole = (team.organizationRole === roleMap["member"]) ? "member" : "admin";
770
+
771
+ if (!team.hasOwnProperty("description")) {
772
+ team.description = "";
773
+ }
774
+ team.Total_Members = team?.users?.length || 0;
775
+
776
+ return team;
777
+ });
778
+ } else {
779
+ return [];
780
+ }
781
+ }
782
+
783
+ async function exportTeams(managementAPIClient, organization, teamUid) {
784
+ cliux.print(
785
+ `info: Exporting the ${
786
+ teamUid && organization?.name
787
+ ? `team with uid ${teamUid} in Organisation ${organization?.name} `
788
+ : `teams of Organisation ` + organization?.name
789
+ }`,
790
+ { color: 'blue' },
791
+ );
792
+ const allTeamsData = await exportOrgTeams(managementAPIClient, organization);
793
+ if (!allTeamsData?.length) {
794
+ cliux.print(`info: The organization ${organization?.name} does not have any teams associated with it. Please verify and provide the correct organization name.`);
795
+ } else {
796
+ const modifiedTeam = cloneDeep(allTeamsData);
797
+ modifiedTeam.forEach((team) => {
798
+ delete team['users'];
799
+ delete team['stackRoleMapping'];
800
+ });
801
+ const fileName = `${kebabize(organization.name.replace(config.organizationNameRegex, ''))}_teams_export.csv`;
802
+ write(this, modifiedTeam, fileName, ' organization Team details');
803
+ // exporting teams user data or a single team user data
804
+ cliux.print(
805
+ `info: Exporting the teams user data for ${teamUid ? `team ` + teamUid : `organisation ` + organization?.name}`,
806
+ { color: 'blue' },
807
+ );
808
+ await getTeamsDetail(allTeamsData, organization, teamUid);
809
+ cliux.print(
810
+ `info: Exporting the stack role details for ${
811
+ teamUid ? `team ` + teamUid : `organisation ` + organization?.name
812
+ }`,
813
+ { color: 'blue' },
814
+ );
815
+ // Exporting the stack Role data for all the teams or exporting stack role data for a single team
816
+ await exportRoleMappings(managementAPIClient, allTeamsData, teamUid);
817
+ }
818
+ }
819
+
820
+ async function getTeamsDetail(allTeamsData, organization, teamUid) {
821
+ if (!teamUid) {
822
+ const userData = await getTeamsUserDetails(allTeamsData);
823
+ const fileName = `${kebabize(
824
+ organization.name.replace(config.organizationNameRegex, ''),
825
+ )}_team_User_Details_export.csv`;
826
+
827
+ write(this, userData, fileName, 'Team User details');
828
+ } else {
829
+ const team = allTeamsData.filter((team) => team.uid === teamUid)[0];
830
+
831
+ team.users.forEach((user) => {
832
+ user['team-name'] = team.name;
833
+ user['team-uid'] = team.uid;
834
+ delete user['active'];
835
+ delete user['orgInvitationStatus'];
836
+ });
837
+
838
+ const fileName = `${kebabize(
839
+ organization.name.replace(config.organizationNameRegex, ''),
840
+ )}_team_${teamUid}_User_Details_export.csv`;
841
+
842
+ write(this, team.users, fileName, 'Team User details');
843
+ }
844
+ }
845
+
846
+ async function exportRoleMappings(managementAPIClient, allTeamsData, teamUid) {
847
+ let stackRoleWithTeamData = [];
848
+ let flag = false;
849
+ const stackNotAdmin = [];
850
+ if (teamUid) {
851
+ const team = find(allTeamsData,function(teamObject) { return teamObject?.uid===teamUid });
852
+ for (const stack of team?.stackRoleMapping) {
853
+ const roleData = await mapRoleWithTeams(managementAPIClient, stack, team?.name, team?.uid);
854
+ stackRoleWithTeamData.push(...roleData);
855
+ if(roleData[0]['Stack Name']==='') {
856
+ flag = true;
857
+ stackNotAdmin.push(stack.stackApiKey);
858
+ }
859
+ }
860
+ } else {
861
+ for (const team of allTeamsData ?? []) {
862
+ for (const stack of team?.stackRoleMapping ?? []) {
863
+ const roleData = await mapRoleWithTeams(managementAPIClient, stack, team?.name, team?.uid);
864
+ stackRoleWithTeamData.push(...roleData);
865
+ if(roleData[0]['Stack Name']==='') {
866
+ flag = true;
867
+ stackNotAdmin.push(stack.stackApiKey);
868
+ }
869
+ }
870
+ }
871
+ }
872
+ if(stackNotAdmin?.length) {
873
+ 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"});
874
+ cliux.print(`${stackNotAdmin.join(' , ')}`,{color:"yellow"});
875
+ }
876
+ if(flag) {
877
+ let export_stack_role = [
878
+ {
879
+ type: 'list',
880
+ name: 'chooseExport',
881
+ message: `Access denied: Please confirm if you still want to continue exporting the data without the { Stack Name, Stack Uid, Role Name } fields.`,
882
+ choices: ['yes', 'no'],
883
+ loop: false,
884
+ }]
885
+ const exportStackRole = await inquirer
886
+ .prompt(export_stack_role)
887
+ .then(( chosenOrg ) => {
888
+ return chosenOrg
889
+ })
890
+ .catch((error) => {
891
+ cliux.print(error, {color:'red'});
892
+ process.exit(1);
893
+ });
894
+ if(exportStackRole.chooseExport === 'no') {
895
+ process.exit(1);
896
+ }
897
+ }
898
+
899
+ const fileName = `${kebabize('Stack_Role_Mapping'.replace(config.organizationNameRegex, ''))}${
900
+ teamUid ? `_${teamUid}` : ''
901
+ }.csv`;
902
+
903
+ write(this, stackRoleWithTeamData, fileName, 'Team Stack Role details');
904
+ }
905
+
906
+ async function mapRoleWithTeams(managementAPIClient, stackRoleMapping, teamName, teamUid) {
907
+ const roles = await getRoleData(managementAPIClient, stackRoleMapping.stackApiKey);
908
+ const stackRole = {};
909
+ roles?.items?.forEach((role) => {
910
+ if (!stackRole.hasOwnProperty(role?.uid)) {
911
+ stackRole[role?.uid] = role?.name;
912
+ stackRole[role?.stack?.api_key] = {name: role?.stack?.name, uid: role?.stack?.uid }
913
+ }
914
+ });
915
+ const stackRoleMapOfTeam = stackRoleMapping?.roles.map((role) => {
916
+ return {
917
+ 'Team Name': teamName,
918
+ 'Team Uid': teamUid,
919
+ 'Stack Name': stackRole[stackRoleMapping?.stackApiKey]?.name || '',
920
+ 'Stack Uid': stackRole[stackRoleMapping?.stackApiKey]?.uid || '',
921
+ 'Role Name': stackRole[role] || '',
922
+ 'Role Uid': role || '',
923
+ };
924
+ });
925
+ return stackRoleMapOfTeam;
926
+ }
927
+
928
+ async function getRoleData(managementAPIClient, stackApiKey) {
929
+ try {
930
+ return await managementAPIClient.stack({ api_key: stackApiKey }).role().fetchAll();
931
+ } catch (error) {
932
+ return {}
933
+ }
934
+ }
935
+
936
+ async function getTeamsUserDetails(teamsObject) {
937
+ const allTeamUsers = [];
938
+ teamsObject.forEach((team) => {
939
+ if (team?.users?.length) {
940
+ team.users.forEach((user) => {
941
+ user['team-name'] = team.name;
942
+ user['team-uid'] = team.uid;
943
+ delete user['active'];
944
+ delete user['orgInvitationStatus'];
945
+ allTeamUsers.push(user);
946
+ });
947
+ }
948
+ });
949
+ return allTeamUsers;
950
+ }
951
+
681
952
  module.exports = {
682
953
  chooseOrganization: chooseOrganization,
683
954
  chooseStack: chooseStack,
@@ -704,4 +975,6 @@ module.exports = {
704
975
  chooseInMemContentTypes: chooseInMemContentTypes,
705
976
  getEntriesCount: getEntriesCount,
706
977
  formatError: formatError,
978
+ exportOrgTeams: exportOrgTeams,
979
+ exportTeams: exportTeams,
707
980
  };