diff --git a/package-lock.json b/package-lock.json index b6c22e5fa3..687a373cac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22868,7 +22868,7 @@ "@contentstack/cli-cm-bulk-publish": "~1.3.13", "@contentstack/cli-cm-clone": "~1.6.0", "@contentstack/cli-cm-export": "~1.9.2", - "@contentstack/cli-cm-export-to-csv": "~1.4.4", + "@contentstack/cli-cm-export-to-csv": "~1.5.0", "@contentstack/cli-cm-import": "~1.10.0", "@contentstack/cli-cm-migrate-rte": "~1.4.13", "@contentstack/cli-cm-seed": "~1.6.0", @@ -24279,7 +24279,7 @@ }, "packages/contentstack-export-to-csv": { "name": "@contentstack/cli-cm-export-to-csv", - "version": "1.4.4", + "version": "1.5.0", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.2.14", @@ -26990,7 +26990,7 @@ "@contentstack/cli-cm-bulk-publish": "~1.3.13", "@contentstack/cli-cm-clone": "~1.6.0", "@contentstack/cli-cm-export": "~1.9.2", - "@contentstack/cli-cm-export-to-csv": "~1.4.4", + "@contentstack/cli-cm-export-to-csv": "~1.5.0", "@contentstack/cli-cm-import": "~1.10.0", "@contentstack/cli-cm-migrate-rte": "~1.4.13", "@contentstack/cli-cm-seed": "~1.6.0", diff --git a/packages/contentstack-export-to-csv/package.json b/packages/contentstack-export-to-csv/package.json index 0f5c36b25b..4de33b158a 100644 --- a/packages/contentstack-export-to-csv/package.json +++ b/packages/contentstack-export-to-csv/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-export-to-csv", "description": "Export entities to csv", - "version": "1.4.4", + "version": "1.5.0", "author": "Abhinav Gupta @abhinav-from-contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { diff --git a/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js b/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js index 8fa579906c..7dfe314abc 100644 --- a/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js +++ b/packages/contentstack-export-to-csv/src/commands/cm/export-to-csv.js @@ -15,8 +15,8 @@ class ExportToCsvCommand extends Command { action: flags.string({ required: false, multiple: false, - options: ['entries', 'users'], - description: `Option to export data (entries, users)`, + options: ['entries', 'users', 'teams'], + description: `Option to export data (entries, users, teams)`, }), alias: flags.string({ char: 'a', @@ -59,6 +59,9 @@ class ExportToCsvCommand extends Command { multiple: false, required: false, }), + "team-uid": flags.string({ + description: 'Uid of the team whose user data and stack roles are required' + }) }; async run() { @@ -75,11 +78,18 @@ class ExportToCsvCommand extends Command { 'content-type': contentTypesFlag, alias: managementTokenAlias, branch: branchUid, + "team-uid": teamUid }, } = await this.parse(ExportToCsvCommand); if (!managementTokenAlias) { managementAPIClient = await managementSDKClient({ host: this.cmaHost }); + if (!isAuthenticated()) { + this.error(config.CLI_EXPORT_CSV_ENTRIES_ERROR, { + exit: 2, + suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'], + }); + } } if (actionFlag) { @@ -113,14 +123,6 @@ class ExportToCsvCommand extends Command { this.error('Provided management token alias not found in your config.!'); } else { let organization; - - if (!isAuthenticated()) { - this.error(config.CLI_EXPORT_CSV_ENTRIES_ERROR, { - exit: 2, - suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'], - }); - } - if (org) { organization = { uid: org }; } else { @@ -227,12 +229,6 @@ class ExportToCsvCommand extends Command { case config.exportUsers: case 'users': { try { - if (!isAuthenticated()) { - this.error(config.CLI_EXPORT_CSV_LOGIN_FAILED, { - exit: 2, - suggestions: ['https://www.contentstack.com/docs/developers/cli/authentication/'], - }); - } let organization; if (org) { @@ -258,6 +254,24 @@ class ExportToCsvCommand extends Command { } break; } + case config.exportTeams: + case 'teams': { + try{ + let organization; + if (org) { + organization = { uid: org, name: orgName || org }; + } else { + organization = await util.chooseOrganization(managementAPIClient, action); // prompt for organization + } + + await util.exportTeams(managementAPIClient,organization,teamUid); + } catch (error) { + if (error.message || error.errorMessage) { + cliux.error(util.formatError(error)); + } + } + } + break; } } catch (error) { if (error.message || error.errorMessage) { @@ -310,6 +324,21 @@ ExportToCsvCommand.examples = [ '', 'Exporting organization users to csv with organization name provided', 'csdx cm:export-to-csv --action --org --org-name ', + '', + 'Exporting Organizations Teams to CSV', + 'csdx cm:export-to-csv --action ', + '', + 'Exporting Organizations Teams to CSV with org-uid', + 'csdx cm:export-to-csv --action --org ', + '', + 'Exporting Organizations Teams to CSV with team uid', + 'csdx cm:export-to-csv --action --team-uid ', + '', + 'Exporting Organizations Teams to CSV with org-uid and team uid', + 'csdx cm:export-to-csv --action --org --team-uid ', + '', + 'Exporting Organizations Teams to CSV with org-uid and team uid', + 'csdx cm:export-to-csv --action --org --team-uid --org-name ', ]; module.exports = ExportToCsvCommand; diff --git a/packages/contentstack-export-to-csv/src/util/config.js b/packages/contentstack-export-to-csv/src/util/config.js index 6a424cc3a5..92c83aa1d7 100644 --- a/packages/contentstack-export-to-csv/src/util/config.js +++ b/packages/contentstack-export-to-csv/src/util/config.js @@ -1,10 +1,11 @@ module.exports = { cancelString: 'Cancel and Exit', exportEntries: 'Export entries to a .CSV file', - exportUsers: "Export organization users' data to a .CSV file", + exportUsers: "Export organization user's data to a .CSV file", + exportTeams: "Export organization team's data to a .csv file", adminError: "Unable to export data. Make sure you're an admin or owner of this organization", organizationNameRegex: /\'/, CLI_EXPORT_CSV_LOGIN_FAILED: "You need to login to execute this command. See: auth:login --help", - CLI_EXPORT_CSV_ENTRIES_ERROR: "You need to either login or provide a management token to execute this command" - + CLI_EXPORT_CSV_ENTRIES_ERROR: "You need to either login or provide a management token to execute this command", + CLI_EXPORT_CSV_API_FAILED: 'Something went wrong. Please try again!' }; diff --git a/packages/contentstack-export-to-csv/src/util/index.js b/packages/contentstack-export-to-csv/src/util/index.js index daeee46058..1d28cf06c5 100644 --- a/packages/contentstack-export-to-csv/src/util/index.js +++ b/packages/contentstack-export-to-csv/src/util/index.js @@ -2,14 +2,14 @@ const os = require('os'); const fs = require('fs'); const mkdirp = require('mkdirp'); const find = require('lodash/find'); +const cloneDeep = require('lodash/cloneDeep'); +const omit = require('lodash/omit'); const fastcsv = require('fast-csv'); const inquirer = require('inquirer'); const debug = require('debug')('export-to-csv'); const checkboxPlus = require('inquirer-checkbox-plus-prompt'); - const config = require('./config.js'); -const { cliux, configHandler } = require('@contentstack/cli-utilities'); - +const { cliux, configHandler, HttpClient } = require('@contentstack/cli-utilities'); const directory = './data'; const delimeter = os.platform() === 'win32' ? '\\' : '/'; @@ -20,7 +20,7 @@ function chooseOrganization(managementAPIClient, action) { return new Promise(async (resolve, reject) => { try { let organizations; - if (action === config.exportUsers) { + if (action === config.exportUsers || action === config.exportTeams || action === 'teams') { organizations = await getOrganizationsWhereUserIsAdmin(managementAPIClient); } else { organizations = await getOrganizations(managementAPIClient); @@ -105,7 +105,7 @@ async function getOrganizationsWhereUserIsAdmin(managementAPIClient) { organizations.forEach((org) => { result[org.name] = org.uid; }); - } + } return result; } catch (error) { @@ -321,7 +321,13 @@ function getEntries(stackAPIClient, contentType, language, skip, limit) { stackAPIClient .contentType(contentType) .entry() - .query({ include_publish_details: true, locale: language, skip: skip * 100, limit: limit, include_workflow: true }) + .query({ + include_publish_details: true, + locale: language, + skip: skip * 100, + limit: limit, + include_workflow: true, + }) .find() .then((entries) => resolve(entries)) .catch((error) => reject(error)); @@ -373,11 +379,11 @@ function exitProgram() { function sanitizeEntries(flatEntry) { // sanitize against CSV Injections - const CSVRegex = /^[\\+\\=@\\-]/ + const CSVRegex = /^[\\+\\=@\\-]/; for (key in flatEntry) { if (typeof flatEntry[key] === 'string' && flatEntry[key].match(CSVRegex)) { flatEntry[key] = flatEntry[key].replace(/\"/g, "\"\""); - flatEntry[key] = `"'${flatEntry[key]}"` + flatEntry[key] = `"'${flatEntry[key]}"`; } else if (typeof flatEntry[key] === 'object') { // convert any objects or arrays to string // to store this data correctly in csv @@ -394,7 +400,7 @@ function cleanEntries(entries, language, environments, contentTypeUid) { return filteredEntries.map((entry) => { let workflow = ''; const envArr = []; - if(entry.publish_details.length) { + if (entry.publish_details.length) { entry.publish_details.forEach((env) => { envArr.push(JSON.stringify([environments[env['environment']], env['locale'], env['time']])); }); @@ -403,10 +409,10 @@ function cleanEntries(entries, language, environments, contentTypeUid) { delete entry.publish_details; delete entry.setWorkflowStage; if ('_workflow' in entry) { - if(entry._workflow?.name) { - workflow = entry['_workflow']['name']; - delete entry['_workflow']; - } + if (entry._workflow?.name) { + workflow = entry['_workflow']['name']; + delete entry['_workflow']; + } } entry = flatten(entry); entry = sanitizeEntries(entry); @@ -459,7 +465,7 @@ function startupQuestions() { type: 'list', name: 'action', message: 'Choose Action', - choices: [config.exportEntries, config.exportUsers, 'Exit'], + choices: [config.exportEntries, config.exportUsers, config.exportTeams, 'Exit'], }, ]; inquirer @@ -678,6 +684,271 @@ function wait(time) { }); } +function handleErrorMsg(err) { + cliux.print(`Error: ${(err?.errorMessage || err?.message) ? err?.errorMessage || err?.message : messageHandler.parse('CLI_EXPORT_CSV_API_FAILED')}`, { color: 'red' }) + process.exit(1); +} + +async function apiRequestHandler(org, queryParam = {}) { + const headers = { + authtoken: configHandler.get('authtoken'), + organization_uid: org.uid, + 'Content-Type': 'application/json', + api_version: 1.1, + }; + + return await new HttpClient() + .headers(headers) + .queryParams(queryParam) + .get(`${configHandler.get('region')?.cma}/organizations/${org?.uid}/teams`) + .then((res) => { + const { status, data } = res; + if (status === 200) { + return data; + } else { + cliux.print(`${data?.error_message || data?.message || data?.errorMessage}`, { color: 'red' }); + process.exit(1); + } + }) + .catch((error) => { + handleErrorMsg(error); + }); +} + +async function exportOrgTeams(managementAPIClient, org) { + let teamsObjectArray = []; + let skip = 0; + let limit = config?.limit || 100; + do { + const data = await apiRequestHandler(org, { skip: skip, limit: limit, includeUserDetails: true }); + skip += limit; + teamsObjectArray.push(...data?.teams); + if (skip >= data?.count) break; + } while (1); + teamsObjectArray = await cleanTeamsData(teamsObjectArray, managementAPIClient, org); + return teamsObjectArray; +} + +async function getOrgRoles(managementAPIClient, org) { + let roleMap = {}; // for org level there are two roles only admin and member + + // SDK call to get the role uids + await managementAPIClient + .organization(org.uid) + .roles() + .then((roles) => { + roles.items.forEach((item) => { + if (item.name === 'member' || item.name === 'admin') { + roleMap[item.name] = item.uid; + } + }); + }) + .catch((err) => { + handleErrorMsg(err); + }); + return roleMap; +} + +async function cleanTeamsData(data, managementAPIClient, org) { + const roleMap = await getOrgRoles(managementAPIClient, org); + const fieldToBeDeleted = [ + '_id', + 'createdAt', + 'createdBy', + 'updatedAt', + 'updatedBy', + '__v', + 'createdByUserName', + 'updatedByUserName', + 'organizationUid', + ]; + if (data?.length) { + return data.map((team) => { + team = omit(team, fieldToBeDeleted); + + team.organizationRole = (team.organizationRole === roleMap["member"]) ? "member" : "admin"; + + if (!team.hasOwnProperty("description")) { + team.description = ""; + } + team.Total_Members = team?.users?.length || 0; + + return team; + }); + } else { + return []; + } +} + +async function exportTeams(managementAPIClient, organization, teamUid) { + cliux.print( + `info: Exporting the ${ + teamUid && organization?.name + ? `team with uid ${teamUid} in Organisation ${organization?.name} ` + : `teams of Organisation ` + organization?.name + }`, + { color: 'blue' }, + ); + const allTeamsData = await exportOrgTeams(managementAPIClient, organization); + if (!allTeamsData?.length) { + cliux.print(`info: The organization ${organization?.name} does not have any teams associated with it. Please verify and provide the correct organization name.`); + } else { + const modifiedTeam = cloneDeep(allTeamsData); + modifiedTeam.forEach((team) => { + delete team['users']; + delete team['stackRoleMapping']; + }); + const fileName = `${kebabize(organization.name.replace(config.organizationNameRegex, ''))}_teams_export.csv`; + write(this, modifiedTeam, fileName, ' organization Team details'); + // exporting teams user data or a single team user data + cliux.print( + `info: Exporting the teams user data for ${teamUid ? `team ` + teamUid : `organisation ` + organization?.name}`, + { color: 'blue' }, + ); + await getTeamsDetail(allTeamsData, organization, teamUid); + cliux.print( + `info: Exporting the stack role details for ${ + teamUid ? `team ` + teamUid : `organisation ` + organization?.name + }`, + { color: 'blue' }, + ); + // Exporting the stack Role data for all the teams or exporting stack role data for a single team + await exportRoleMappings(managementAPIClient, allTeamsData, teamUid); + } +} + +async function getTeamsDetail(allTeamsData, organization, teamUid) { + if (!teamUid) { + const userData = await getTeamsUserDetails(allTeamsData); + const fileName = `${kebabize( + organization.name.replace(config.organizationNameRegex, ''), + )}_team_User_Details_export.csv`; + + write(this, userData, fileName, 'Team User details'); + } else { + const team = allTeamsData.filter((team) => team.uid === teamUid)[0]; + + team.users.forEach((user) => { + user['team-name'] = team.name; + user['team-uid'] = team.uid; + delete user['active']; + delete user['orgInvitationStatus']; + }); + + const fileName = `${kebabize( + organization.name.replace(config.organizationNameRegex, ''), + )}_team_${teamUid}_User_Details_export.csv`; + + write(this, team.users, fileName, 'Team User details'); + } +} + +async function exportRoleMappings(managementAPIClient, allTeamsData, teamUid) { + let stackRoleWithTeamData = []; + let flag = false; + const stackNotAdmin = []; + if (teamUid) { + const team = find(allTeamsData,function(teamObject) { return teamObject?.uid===teamUid }); + for (const stack of team?.stackRoleMapping) { + const roleData = await mapRoleWithTeams(managementAPIClient, stack, team?.name, team?.uid); + stackRoleWithTeamData.push(...roleData); + if(roleData[0]['Stack Name']==='') { + flag = true; + stackNotAdmin.push(stack.stackApiKey); + } + } + } else { + for (const team of allTeamsData ?? []) { + for (const stack of team?.stackRoleMapping ?? []) { + const roleData = await mapRoleWithTeams(managementAPIClient, stack, team?.name, team?.uid); + stackRoleWithTeamData.push(...roleData); + if(roleData[0]['Stack Name']==='') { + flag = true; + stackNotAdmin.push(stack.stackApiKey); + } + } + } + } + if(stackNotAdmin?.length) { + 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"}); + cliux.print(`${stackNotAdmin.join(' , ')}`,{color:"yellow"}); + } + if(flag) { + let export_stack_role = [ + { + type: 'list', + name: 'chooseExport', + message: `Access denied: Please confirm if you still want to continue exporting the data without the { Stack Name, Stack Uid, Role Name } fields.`, + choices: ['yes', 'no'], + loop: false, + }] + const exportStackRole = await inquirer + .prompt(export_stack_role) + .then(( chosenOrg ) => { + return chosenOrg + }) + .catch((error) => { + cliux.print(error, {color:'red'}); + process.exit(1); + }); + if(exportStackRole.chooseExport === 'no') { + process.exit(1); + } + } + + const fileName = `${kebabize('Stack_Role_Mapping'.replace(config.organizationNameRegex, ''))}${ + teamUid ? `_${teamUid}` : '' + }.csv`; + + write(this, stackRoleWithTeamData, fileName, 'Team Stack Role details'); +} + +async function mapRoleWithTeams(managementAPIClient, stackRoleMapping, teamName, teamUid) { + const roles = await getRoleData(managementAPIClient, stackRoleMapping.stackApiKey); + const stackRole = {}; + roles?.items?.forEach((role) => { + if (!stackRole.hasOwnProperty(role?.uid)) { + stackRole[role?.uid] = role?.name; + stackRole[role?.stack?.api_key] = {name: role?.stack?.name, uid: role?.stack?.uid } + } + }); + const stackRoleMapOfTeam = stackRoleMapping?.roles.map((role) => { + return { + 'Team Name': teamName, + 'Team Uid': teamUid, + 'Stack Name': stackRole[stackRoleMapping?.stackApiKey]?.name || '', + 'Stack Uid': stackRole[stackRoleMapping?.stackApiKey]?.uid || '', + 'Role Name': stackRole[role] || '', + 'Role Uid': role || '', + }; + }); + return stackRoleMapOfTeam; +} + +async function getRoleData(managementAPIClient, stackApiKey) { + try { + return await managementAPIClient.stack({ api_key: stackApiKey }).role().fetchAll(); + } catch (error) { + return {} + } +} + +async function getTeamsUserDetails(teamsObject) { + const allTeamUsers = []; + teamsObject.forEach((team) => { + if (team?.users?.length) { + team.users.forEach((user) => { + user['team-name'] = team.name; + user['team-uid'] = team.uid; + delete user['active']; + delete user['orgInvitationStatus']; + allTeamUsers.push(user); + }); + } + }); + return allTeamUsers; +} + module.exports = { chooseOrganization: chooseOrganization, chooseStack: chooseStack, @@ -704,4 +975,6 @@ module.exports = { chooseInMemContentTypes: chooseInMemContentTypes, getEntriesCount: getEntriesCount, formatError: formatError, + exportOrgTeams: exportOrgTeams, + exportTeams: exportTeams, }; diff --git a/packages/contentstack-export-to-csv/test/mock-data/common.mock.json b/packages/contentstack-export-to-csv/test/mock-data/common.mock.json new file mode 100644 index 0000000000..48d9f7630a --- /dev/null +++ b/packages/contentstack-export-to-csv/test/mock-data/common.mock.json @@ -0,0 +1,422 @@ +{ + "taxonomiesResp": { + "taxonomies": [ + { + "uid": "taxonomy_uid_1", + "name": "taxonomy uid 1", + "description": "", + "created_at": "2023-09-01T06:09:44.934Z", + "created_by": "user1", + "updated_at": "2023-09-01T06:44:16.604Z", + "updated_by": "user1" + }, + { + "uid": "taxonomy_uid_2", + "name": "taxonomy uid 2", + "description": "", + "created_at": "2023-09-01T06:09:44.934Z", + "created_by": "user1", + "updated_at": "2023-09-01T06:44:16.604Z", + "updated_by": "user1" + } + ], + "count": 2 + }, + "termsResp": { + "terms": [ + { + "uid": "wsq", + "name": "wsq", + "created_at": "2023-08-30T09:51:12.043Z", + "created_by": "user1", + "updated_at": "2023-08-30T09:51:12.043Z", + "updated_by": "user1", + "parent_uid": null, + "depth": 1 + }, + { + "uid": "term2", + "name": "term2", + "created_at": "2023-08-30T09:45:11.963Z", + "created_by": "user2", + "updated_at": "2023-08-30T09:45:11.963Z", + "updated_by": "user2", + "parent_uid": null, + "depth": 1 + } + ], + "count": 2 + }, + "organizations": [ + { + "uid": "test-uid-1", + "name": "test org 1" + }, + { + "uid": "test-uid-2", + "name": "test org 2" + }, + { + "uid": "org_uid_1_teams", + "name": "Teams Org" + } + ], + "stacks": [ + { + "name": "Stack 1", + "uid": "stack-uid-1", + "api_key": "stack_api_key_1" + }, + { + "name": "Stack 2", + "uid": "stack-uid-2", + "api_key": "stack_api_key_2" + } + ], + "users": [ + { + "uid": "uid1", + "email": "test@gmail.abc", + "user_uid": "user1", + "org_uid": "test-uid-1", + "invited_by": "user2", + "invited_at": "2023-08-21T11:08:41.038Z", + "status": "accepted", + "acceptance_token": "dfghdfgd", + "created_at": "2023-08-21T11:08:41.036Z", + "updated_at": "2023-08-21T11:09:11.342Z", + "urlPath": "/user", + "organizations": [ + { + "uid": "test-uid-1", + "name": "test org 1", + "org_roles": [ + { + "uid": "role1", + "name": "Admin", + "description": "Admin Role", + "org_uid": "test-uid-1", + "admin": true, + "default": true + } + ] + } + ] + }, + { + "uid": "test-uid-2", + "name": "test org 2" + }, + { + "uid": "uid2", + "email": "test@gmail.abc", + "user_uid": "user2", + "org_uid": "test-uid-2", + "invited_by": "user3", + "invited_at": "2023-08-21T11:08:41.038Z", + "status": "accepted", + "acceptance_token": "thor", + "created_at": "2023-08-21T11:08:41.036Z", + "updated_at": "2023-08-21T11:09:11.342Z", + "urlPath": "/user", + "organizations": [ + { + "uid": "org_uid_1_teams", + "name": "Teams Org", + "org_roles": [ + { + "uid": "role1", + "name": "Admin", + "description": "Admin Role", + "org_uid": "test-uid-1", + "admin": true, + "default": true + } + ] + } + ] + } + ], + "roles": [ + { + "urlPath": "/roles/role1", + "uid": "role1", + "name": "admin", + "description": "The Admin role has rights to add/remove users from an organization, and has access to stacks created by self or shared by others.", + "org_uid": "test-uid-1", + "owner_uid": "user1", + "admin": true, + "default": true, + "users": ["user2", "user3"], + "created_at": "2023-08-08T10:09:43.445Z", + "updated_at": "2023-08-21T11:08:41.042Z" + } + ], + "contentTypes": [ + { + "stackHeaders": { + "api_key": "stack_api_key_1" + }, + "urlPath": "/content_types/ct1", + "created_at": "2023-08-08T13:52:31.980Z", + "updated_at": "2023-08-08T13:52:34.265Z", + "title": "CT 1", + "uid": "ct_1", + "_version": 2, + "inbuilt_class": false, + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { + "_default": true, + "version": 3 + }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false + } + ] + }, + { + "stackHeaders": { + "api_key": "stack_api_key_1" + }, + "urlPath": "/content_types/ct2", + "created_at": "2023-08-08T13:52:31.980Z", + "updated_at": "2023-08-08T13:52:34.265Z", + "title": "CT 2", + "uid": "ct_2", + "_version": 2, + "inbuilt_class": false, + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { + "_default": true, + "version": 3 + }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false + } + ] + } + ], + "branch": { + "stackHeaders": { + "api_key": "stack_api_key_1" + }, + "urlPath": "/stacks/branches/test_branch1", + "uid": "test_branch1", + "source": "", + "created_by": "user1", + "updated_by": "user1", + "created_at": "2023-08-08T13:51:43.217Z", + "updated_at": "2023-08-08T13:51:43.217Z", + "deleted_at": false + }, + "entry": [ + { + "stackHeaders": { + "api_key": "stack_api_key_1" + }, + "content_type_uid": "home", + "urlPath": "/content_types/ct1/entries/test_entry1", + "title": "Test Entry1", + "url": "/", + "tags": [], + "locale": "en1", + "uid": "test_entry1", + "created_by": "user1", + "updated_by": "user1", + "created_at": "2023-08-08T13:52:46.592Z", + "updated_at": "2023-08-08T13:52:46.592Z", + "_version": 1 + } + ], + "environments": [ + { + "stackHeaders": { + "api_key": "stack_api_key_1" + }, + "urlPath": "/environments/development", + "urls": [ + { + "url": "http://localhost:3000/", + "locale": "en1" + } + ], + "name": "development", + "_version": 3, + "uid": "env1", + "created_by": "user1", + "updated_by": "user1", + "created_at": "2023-06-12T18:59:56.853Z", + "updated_at": "2023-06-12T18:59:56.853Z" + } + ], + "locales": [ + { + "code": "en1", + "name": "English - En", + "fallback_locale": "en-us", + "uid": "gsfdasgdf", + "created_at": "2023-09-11T10:44:40.213Z", + "updated_at": "2023-09-11T10:44:40.213Z", + "ACL": [], + "_version": 1 + } + ], + "Teams": { + "emptyTeam": [], + "allTeams": { + "count": 1, + "teams": [ + { + "_id": "team_1_uid", + "name": "Test_Team_1", + + + + + "organizationUid": "org_uid_1_teams", + "users": [], + "stackRoleMapping": [ + { + "stackApiKey": "stack_api_key", + "roles": ["role_uid_1"] + } + ], + "organizationRole": "org_role_uid_1", + + "uid": "team_1_uid", + "createdByUserName": "team_creator", + "updatedByUserName": "team_creator" + } + ] + } + }, + "roless": { + "roles": [{ + "name": "Developer", + "description": "Developer can perform all Content Manager's actions, view audit logs, create roles, invite users, manage content types, languages, and environments.", + "uid": "stack_role_uid_3", + "users": [], + "owner": "ownerEmail@domain.com", + "stack": { + + + "uid": "stack_uid", + "name": "CLI Test", + "org_uid": "org_uid_1_teams", + "api_key": "stack_api_key", + "master_locale": "en-us", + + "owner_uid": "team_owner_uid", + "user_uids": ["team_owner_uid"] + }, + "SYS_ACL": {} + }, + { + "name": "Content Manager", + "description": "Content Managers can view all content types, manage entries and assets. They cannot edit content types or access stack settings.", + "uid": "stack_role_uid_1", + + "updated_by": "team_owner_uid", + + + "owner": "ownerEmail@domain.com", + "stack": { + + + "uid": "stack_uid", + "name": "CLI Test", + "org_uid": "org_uid_1_teams", + "api_key": "stack_api_key", + "master_locale": "en-us", + + "owner_uid": "team_owner_uid", + "user_uids": ["team_owner_uid"] + }, + "SYS_ACL": {} + }, + { + "name": "Admin", + "description": "Admin can perform all actions and manage all settings of the stack, except the ability to delete or transfer ownership of the stack.", + "uid": "role_uid_1", + + + + "updated_at": "2023-10-26T04:44:51.529Z", + "users": [], + "owner": "ownerEmail@domain.com", + "stack": { + + + "uid": "stack_uid", + "name": "CLI Test", + "org_uid": "org_uid_1_teams", + "api_key": "stack_api_key", + "master_locale": "en-us", + + "owner_uid": "team_owner_uid", + "user_uids": ["team_owner_uid"] + }, + "SYS_ACL": {} + }, + { + "name": "Custom_Role_1", + "description": "", + "users": [], + "uid": "stack_role_uid_2", + + "updated_by": "team_owner_uid", + + + "owner": "ownerEmail@domain.com", + "stack": { + + + "uid": "stack_uid", + "name": "CLI Test", + "org_uid": "org_uid_1_teams", + "api_key": "stack_api_key", + "master_locale": "en-us", + + "owner_uid": "team_owner_uid", + "user_uids": ["team_owner_uid"] + }, + "SYS_ACL": {} + } +]}, + "org_roles": { + "roles": [ + { + "uid": "org_role_uid_1", + "name": "admin", + "description": "The Admin role has rights to add/remove users from an organization, and has access to stacks created by self or shared by others.", + "org_uid": "org_uid_1_teams", + "owner_uid": "org_owner_uid", + "admin": true, + "default": true, + "users": ["user_1_uid", "org_owner_uid", "user_2_uid"] + }, + { + "uid": "org_role_uid_2", + "name": "member", + "description": "The Member role has access only to the stacks created by or shared with him/her, and does not have access to organization settings.", + "org_uid": "org_uid_1_teams", + "owner_uid": "org_owner_uid", + "admin": false, + "default": true, + "users": ["user_1_uid_member", "user_2_uid_member", "user_3_uid_member", "user_4_uid_member"] + } + ] + } +} diff --git a/packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.js b/packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.js index d2415f7179..26f421b4d6 100644 --- a/packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.js +++ b/packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.js @@ -5,215 +5,208 @@ const { stub, assert } = require('sinon'); const { config } = require('dotenv'); const inquirer = require('inquirer'); const { cliux } = require('@contentstack/cli-utilities'); - -config(); - -describe('Export to csv command with action = entries', () => { - let inquireStub; - let errorStub; - let consoleLogStub; - - beforeEach(() => { - inquireStub = stub(inquirer, 'prompt'); - errorStub = stub(cliux, 'error'); - consoleLogStub = stub(cliux, 'print'); - }); - - afterEach(() => { - inquireStub.restore(); - errorStub.restore(); - consoleLogStub.restore(); - }); - - it('Should ask for action when action is not passed (entries or users)', async () => { - await ExportToCsvCommand.run([]); - assert.calledOnce(inquireStub); - }); - - it('Should ask for org when org is not passed', async () => { - const args = ['--action', 'entries']; - await ExportToCsvCommand.run(args); - assert.calledOnce(inquireStub); - }); - - it('Should ask for stack when stack api key flag is not passed', async (done) => { - const args = ['--action', 'entries', '--org', process.env.ORG]; - done(); - await ExportToCsvCommand.run(args); - assert.calledOnce(inquireStub); - }); - - it('Should ask for branch when branch flag is not passed', async () => { - const args = ['--action', 'entries', '--org', process.env.ORG, '--stack-api-key', process.env.STACK]; - await ExportToCsvCommand.run(args); - assert.calledTwice(inquireStub); - }); - - it('Should throw an error if stack does not have branches enabled', async () => { - const args = [ - '--action', - 'entries', - '--org', - process.env.ORG_WITH_NO_BRANCHES, - '--stack-api-key', - process.env.STACK_WITH_ORG_WITH_NO_BRANCHES, - '--branch', - 'invalid', - ]; - await ExportToCsvCommand.run(args); - assert.calledWith( - errorStub, - 'Branches are not part of your plan. Please contact support@contentstack.com to upgrade your plan.', - ); - }); - - it('Should ask for content type when content type flag is not passed', async () => { - const args = [ - '--action', - 'entries', - '--org', - process.env.ORG, - '--stack-api-key', - process.env.STACK, - '--branch', - process.env.BRANCH, - ]; - await ExportToCsvCommand.run(args); - assert.calledOnce(inquireStub); - }); - - it('Should create a file starting with the name passed as stack-name flag', async () => { - const args = [ - '--action', - 'entries', - '--org', - process.env.ORG, - '--stack-api-key', - process.env.STACK, - '--branch', - process.env.BRANCH, - '--content-type', - 'page', - '--locale', - 'en-us', - '--stack-name', - 'okok', - ]; - await ExportToCsvCommand.run(args); - assert.calledWith(consoleLogStub, `Writing entries to file: ${process.cwd()}/okok_page_en-us_entries_export.csv`); - }); - - it('Should throw an error when invalid org is passed', async () => { - const args = ['--action', 'entries', '--org', 'invalid']; - await ExportToCsvCommand.run(args); - assert.calledWith(errorStub, `Couldn't find the organization. Please check input parameters.`); - }); - - it('Should throw an error when invalid stack is passed', async () => { - const args = ['--action', 'entries', '--org', process.env.ORG, '--stack-api-key', 'invalid']; - await ExportToCsvCommand.run(args); - assert.calledWith(errorStub, 'Could not find stack'); - }); - - it('Should throw an error when invalid branch is passed', async () => { - const args = [ - '--action', - 'entries', - '--org', - process.env.ORG, - '--stack-api-key', - process.env.STACK, - '--branch', - process.env.INVALID_BRANCH, - ]; - await ExportToCsvCommand.run(args); - assert.calledWith(errorStub, 'Failed to fetch Branch. Please try again with valid parameters.'); - }); - - it('Should throw an error when invalid contenttype is passed', async () => { - const args = [ - '--action', - 'entries', - '--org', - process.env.ORG, - '--stack-api-key', - process.env.STACK, - '--branch', - process.env.BRANCH, - '--content-type', - 'invalid', - '--locale', - 'en-us', - ]; - await ExportToCsvCommand.run(args); - assert.calledWith( - errorStub, - `The Content Type invalid was not found. Please try again. Content Type is not valid.`, - ); - }); - - it('Should throw an error when invalid locale is passed', async () => { - const args = [ - '--action', - 'entries', - '--org', - process.env.ORG, - '--stack-api-key', - process.env.STACK, - '--branch', - process.env.BRANCH, - '--content-type', - 'header', - '--locale', - 'invalid', - ]; - await ExportToCsvCommand.run(args); - assert.calledWith(errorStub, 'Language was not found. Please try again.'); - }); +const mockData = require('../../mock-data/common.mock.json'); +const { test, expect } = require('@oclif/test'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const { configHandler } = require('@contentstack/cli-utilities'); +const { kebabize } = require('../../../src/util/index'); +const { cma } = configHandler.get('region'); +const { PassThrough } = require('stream'); + +describe('Testing the teams support in cli export-to-csv',()=>{ + + describe('Testing Teams Command with using org flag and team flag', () => { + test + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .nock(cma, (api) => { + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api + .get(`/v3/organizations/org_uid_1_teams/roles`) + .reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api + .get(`/v3/roles`) + .reply(200, {roles: mockData.roless.roles}); + }) + .command([ + 'cm:export-to-csv', + '--action', + 'teams', + '--org', + 'org_uid_1_teams', + '--team-uid', + 'team_1_uid' + ]) + .it('CSV file should be created'); + }); + describe('Testing Teams Command with using org flag and team flag and there are no teams', () => { + test + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .nock(cma, (api) => { + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api + .get(`/v3/organizations/org_uid_1_teams/roles`) + .reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api + .get(`/v3/roles`) + .reply(200, {roles: mockData.roless.roles}); + }) + .command([ + 'cm:export-to-csv', + '--action', + 'teams', + '--org', + 'org_uid_1_teams', + '--team-uid', + 'team_1_uid' + ]) + .it('CSV file should be created'); + }); + describe('Testing Teams Command with using org flag', () => { + test + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .nock(cma, (api) => { + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api + .get(`/v3/organizations/org_uid_1_teams/roles`) + .reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api + .get(`/v3/roles`) + .reply(200, {roles: mockData.roless.roles}); + }) + .command([ + 'cm:export-to-csv', + '--action', + 'teams', + '--org', + 'org_uid_1_teams' + ]) + .it('CSV file should be created'); + }); + describe('Testing Teams Command with prompt', () => { + test + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .stub(inquirer, 'registerPrompt', () => {}) + .stub(inquirer, 'prompt', () => { + return Promise.resolve({ action: 'teams', chosenOrg: mockData.organizations[2].name }); + }) + .nock(cma, (api) => { + api.get('/v3/user?include_orgs_roles=true').reply(200, { user: mockData.users[2] }); + }) + .nock(cma, (api) => { + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api + .get(`/v3/organizations/org_uid_1_teams/roles`) + .reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api + .get(`/v3/roles`) + .reply(200, {roles: mockData.roless.roles}); + }) + .command([ + 'cm:export-to-csv' + ]) + .it('CSV file should be created'); + }); + describe('Testing Teams Command with prompt and no stack role data', () => { + test + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .stub(inquirer, 'registerPrompt', () => {}) + .stub(inquirer, 'prompt', () => { + return Promise.resolve({ action: 'teams', chosenOrg: mockData.organizations[2].name, chooseExport: 'yes'}); + }) + .nock(cma, (api) => { + api.get('/v3/user?include_orgs_roles=true').reply(200, { user: mockData.users[2] }); + }) + .nock(cma, (api) => { + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api + .get(`/v3/organizations/org_uid_1_teams/roles`) + .reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api + .get(`/v3/roles`) + .reply(200, {roles: {}}); + }) + .command([ + 'cm:export-to-csv' + ]) + .it('CSV file should be created'); }); - -describe('Export to csv command with action = users', () => { - let inquireStub; - let errorStub; - let consoleLogStub; - - beforeEach(() => { - inquireStub = stub(inquirer, 'prompt'); - errorStub = stub(cliux, 'error'); - consoleLogStub = stub(cliux, 'print'); - }); - - afterEach(() => { - inquireStub.restore(); - errorStub.restore(); - consoleLogStub.restore(); - }); - - it('Should ask for org when org is not passed', async () => { - const args = ['--action', 'entries']; - await ExportToCsvCommand.run(args); - assert.calledOnce(inquireStub); - }); - - it('Should write users data to file if the user has permissions', async () => { - const args = ['--action', 'users', '--org', process.env.ORG]; - - await ExportToCsvCommand.run(args); - assert.calledWith( - consoleLogStub, - `Writing organization details to file: ${process.cwd()}/${process.env.ORG}_users_export.csv`, - ); - }); - - it('Should show an error that user does not have org permissions to perform the operation if user enters such org', async () => { - const args = ['--action', 'users', '--org', process.env.ORG_WITH_NO_PERMISSION]; - await ExportToCsvCommand.run(args); - assert.calledWith(errorStub, `You don't have the permission to do this operation.`); - }); - - it('Should create a file starting with the name passed as org-name flag', async () => { - const args = ['--action', 'users', '--org', process.env.ORG, '--org-name', 'okok']; - await ExportToCsvCommand.run(args); - assert.calledWith(consoleLogStub, `Writing organization details to file: ${process.cwd()}/okok_users_export.csv`); - }); +describe('Testing Teams Command with prompt and no stack role data', () => { + test + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(fs, 'createWriteStream', () => new PassThrough()) + .stub(mkdirp, 'sync', () => {}) + .stub(process, 'chdir', () => {}) + .stub(inquirer, 'registerPrompt', () => {}) + .stub(inquirer, 'prompt', () => { + return Promise.resolve({ action: 'teams', chosenOrg: mockData.organizations[2].name, chooseExport: 'no'}); + }) + .nock(cma, (api) => { + api.get('/v3/user?include_orgs_roles=true').reply(200, { user: mockData.users[2] }); + }) + .nock(cma, (api) => { + api + .get(`/organizations/org_uid_1_teams/teams?skip=0&limit=100&includeUserDetails=true`) + .reply(200, mockData.Teams.allTeams); + }) + .nock(cma, (api) => { + api + .get(`/v3/organizations/org_uid_1_teams/roles`) + .reply(200, mockData.org_roles); + }) + .nock(cma, (api) => { + api + .get(`/v3/roles`) + .reply(200, {roles: {}}); + }) + .command([ + 'cm:export-to-csv' + ]) + .it('No CSV file should be created'); }); +}); \ No newline at end of file diff --git a/packages/contentstack/package.json b/packages/contentstack/package.json index cf9adf2984..d6db98a054 100755 --- a/packages/contentstack/package.json +++ b/packages/contentstack/package.json @@ -29,7 +29,7 @@ "@contentstack/cli-cm-bulk-publish": "~1.3.13", "@contentstack/cli-cm-clone": "~1.6.0", "@contentstack/cli-cm-export": "~1.9.2", - "@contentstack/cli-cm-export-to-csv": "~1.4.4", + "@contentstack/cli-cm-export-to-csv": "~1.5.0", "@contentstack/cli-cm-import": "~1.10.0", "@contentstack/cli-cm-migrate-rte": "~1.4.13", "@contentstack/cli-cm-seed": "~1.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56bb2419ac..bfe04441e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ importers: '@contentstack/cli-cm-bulk-publish': ~1.3.13 '@contentstack/cli-cm-clone': ~1.6.0 '@contentstack/cli-cm-export': ~1.9.2 - '@contentstack/cli-cm-export-to-csv': ~1.4.4 + '@contentstack/cli-cm-export-to-csv': ~1.5.0 '@contentstack/cli-cm-import': ~1.10.0 '@contentstack/cli-cm-migrate-rte': ~1.4.13 '@contentstack/cli-cm-seed': ~1.6.0