From d78304186b704e376df335e908230bb4e7caa6a3 Mon Sep 17 00:00:00 2001 From: Ethan Sargent Date: Wed, 20 Sep 2023 19:10:56 +1000 Subject: [PATCH] feat(profiles): add profile merge and retrieve to sfp (#1353) * move from sfpowerkit * fix: remove useWorkspaces from lerna removing deprecated configuration value for lerna 7 compat * feat(profiles): merge * feat(profiles): include merge and retrieve * Revert "fix: remove useWorkspaces from lerna" This reverts commit a090f91bcd154692b55d151e14ecc79d640235ff. * fix(profiles): update profile commands to latest libs --------- Co-authored-by: Azlam <43767972+azlam-abdulsalam@users.noreply.github.com> Co-authored-by: azlam --- .../messages/profile_retrieve.json | 3 +- .../src/SfpowerscriptsCommand.ts | 2 +- .../src/commands/profile/merge.ts | 157 ++++++++++++++++++ .../src/commands/profile/reconcile.ts | 4 +- .../src/commands/profile/retrieve.ts | 128 ++++++++++++++ 5 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 packages/sfpowerscripts-cli/src/commands/profile/merge.ts create mode 100644 packages/sfpowerscripts-cli/src/commands/profile/retrieve.ts diff --git a/packages/sfpowerscripts-cli/messages/profile_retrieve.json b/packages/sfpowerscripts-cli/messages/profile_retrieve.json index 9622a4950..e1e31933a 100644 --- a/packages/sfpowerscripts-cli/messages/profile_retrieve.json +++ b/packages/sfpowerscripts-cli/messages/profile_retrieve.json @@ -2,5 +2,6 @@ "commandDescription": "Retrieve profiles from the salesforce org with all its associated permissions. Common use case for this command is to migrate profile changes from a integration environment to other higher environments [overcomes SFDX CLI Profile retrieve issue where it doesnt fetch the full profile unless the entire metadata is present in source], or retrieving profiles from production to lower environments for testing.", "folderFlagDescription": "retrieve only updated versions of profiles found in this directory, If ignored, all profiles will be retrieved.", "profileListFlagDescription": "comma separated list of profiles to be retrieved. Use it for selectively retrieving an existing profile or retrieving a new profile", - "deleteFlagDescription": "set this flag to delete profile files that does not exist in the org, when retrieving in bulk" + "deleteFlagDescription": "set this flag to delete profile files that does not exist in the org, when retrieving in bulk", + "retriveDelayWarning":"Retrieving profiles may take a significant time depending on the number of profiles \nand managed package components installed in your org,Please be patient" } diff --git a/packages/sfpowerscripts-cli/src/SfpowerscriptsCommand.ts b/packages/sfpowerscripts-cli/src/SfpowerscriptsCommand.ts index 90327e678..78c3b1ab3 100644 --- a/packages/sfpowerscripts-cli/src/SfpowerscriptsCommand.ts +++ b/packages/sfpowerscripts-cli/src/SfpowerscriptsCommand.ts @@ -89,7 +89,7 @@ export default abstract class SfpowerscriptsCommand extends Command { SFPLogger.printHeaderLine('',COLOR_HEADER,LoggerLevel.INFO); SFPLogger.log( COLOR_HEADER( - `sfpowerscripts -- The DX@Scale CI/CD Orchestrator -Version:${this.config.version} -Release:${this.config.pjson.release}` + `sfp -- The DX@Scale CLI -Version:${this.config.version} -Release:${this.config.pjson.release}` ) ); diff --git a/packages/sfpowerscripts-cli/src/commands/profile/merge.ts b/packages/sfpowerscripts-cli/src/commands/profile/merge.ts new file mode 100644 index 000000000..01d93482d --- /dev/null +++ b/packages/sfpowerscripts-cli/src/commands/profile/merge.ts @@ -0,0 +1,157 @@ +import { Messages, Org } from '@salesforce/core'; +import { isNil } from 'lodash'; +import { Sfpowerkit } from '@dxatscale/sfprofiles/lib/utils/sfpowerkit'; +import SFPLogger, { LoggerLevel } from '@dxatscale/sfp-logger'; +import ProfileRetriever from '@dxatscale/sfprofiles/lib/impl/metadata/retriever/profileRetriever'; +import ProfileMerge from '@dxatscale/sfprofiles/lib/impl/source/profileMerge'; +import SfpowerscriptsCommand from '../../SfpowerscriptsCommand'; +import Table from 'cli-table'; +import { ZERO_BORDER_TABLE } from '../../ui/TableConstants'; +import { arrayFlagSfdxStyle, loglevel, orgApiVersionFlagSfdxStyle, requiredUserNameFlag } from '../../flags/sfdxflags'; +import { Flags } from '@oclif/core'; + +Messages.importMessagesDirectory(__dirname); + +const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'profile_merge'); + +export default class Merge extends SfpowerscriptsCommand { + public static description = messages.getMessage('commandDescription'); + + public static examples = [ + `$ sfp profile:merge -u sandbox`, + `$ sfp profile:merge -f force-app -n "My Profile" -u sandbox`, + `$ sfp profile:merge -f "module1, module2, module3" -n "My Profile1, My profile2" -u sandbox`, + ]; + + public static flags = { + folder: arrayFlagSfdxStyle({ + char: 'f', + description: messages.getMessage('folderFlagDescription'), + required: false, + }), + profilelist: arrayFlagSfdxStyle({ + char: 'n', + description: messages.getMessage('profileListFlagDescription'), + required: false, + }), + metadata: arrayFlagSfdxStyle({ + char: 'm', + description: messages.getMessage('metadataFlagDescription'), + required: false, + }), + delete: Flags.boolean({ + char: 'd', + description: messages.getMessage('deleteFlagDescription'), + required: false, + }), + targetorg: requiredUserNameFlag, + 'apiversion': orgApiVersionFlagSfdxStyle, + loglevel, + }; + + // Comment this out if your command does not require an org username + protected static requiresUsername = true + + // Set this to true if your command requires a project workspace; 'requiresProject' is false by default + protected static requiresProject = true; + + public async execute(): Promise { + let argFolder = this.flags.folder; + let argProfileList = this.flags.profilelist; + let argMetadatas = this.flags.metadata; + + // argMetadatas = (val: string) => { + // let parts = val.split(':'); + // return { + // MetadataType: parts[0].trim(), + // ApiName: parts.length >= 2 ? parts[1].trim() : '*', + // }; + // }; + + Sfpowerkit.initCache(); + + let metadatas = undefined; + let invalidArguments = []; + + if (argMetadatas !== undefined) { + metadatas = {}; + ProfileRetriever.supportedMetadataTypes.forEach((val) => { + metadatas[val] = []; + }); + for (let i = 0; i < argMetadatas.length; i++) { + if (ProfileRetriever.supportedMetadataTypes.includes(argMetadatas[i].MetadataType)) { + metadatas[argMetadatas[i].MetadataType].push(argMetadatas[i].ApiName); + } else { + invalidArguments.push(argMetadatas[i].MetadataType); + } + } + if (invalidArguments.length > 0) { + throw new Error( + 'Metadata(s) ' + invalidArguments.join(', ') + ' is/are not supported.' + ); + } + } + + if (!isNil(argFolder) && argFolder.length !== 0) { + Sfpowerkit.setDefaultFolder(argFolder[0]); + } + ``; + + + this.org = await Org.create({ aliasOrUsername: this.flags.targetorg }); + const profileUtils = new ProfileMerge(this.org); + + let mergedProfiles = await profileUtils.merge(argFolder, argProfileList || [], metadatas, this.flags.delete); + + const table = new Table({ + head: ['State', 'Full Name', 'Type', 'Path'], + chars: ZERO_BORDER_TABLE, + }); + if (mergedProfiles.added) { + mergedProfiles.added.forEach((profile) => { + table.push({ + state: 'Add', + fullName: profile.name, + type: 'Profile', + path: profile.path, + }); + }); + } + if (mergedProfiles.updated) { + mergedProfiles.updated.forEach((profile) => { + table.push({ + state: 'Merged', + fullName: profile.name, + type: 'Profile', + path: profile.path, + }); + }); + } + if (this.flags.delete) { + if (mergedProfiles.deleted) { + mergedProfiles.deleted.forEach((profile) => { + table.push({ + state: 'Deleted', + fullName: profile.name, + type: 'Profile', + path: profile.path, + }); + }); + } + } else { + if (mergedProfiles.deleted) { + mergedProfiles.deleted.forEach((profile) => { + table.push({ + state: 'Skipped', + fullName: profile.name, + type: 'Profile', + path: profile.path, + }); + }); + } + } + SFPLogger.log(table.toString(), LoggerLevel.INFO); + + return mergedProfiles; + } +} diff --git a/packages/sfpowerscripts-cli/src/commands/profile/reconcile.ts b/packages/sfpowerscripts-cli/src/commands/profile/reconcile.ts index b9132fdd5..cb9e85402 100644 --- a/packages/sfpowerscripts-cli/src/commands/profile/reconcile.ts +++ b/packages/sfpowerscripts-cli/src/commands/profile/reconcile.ts @@ -12,11 +12,9 @@ import { ZERO_BORDER_TABLE } from '../../ui/TableConstants'; import { Flags } from '@oclif/core'; import { arrayFlagSfdxStyle, loglevel, orgApiVersionFlagSfdxStyle, requiredUserNameFlag } from '../../flags/sfdxflags'; -// Initialize Messages with the current plugin directory Messages.importMessagesDirectory(__dirname); -// Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core, -// or any library that is using the messages framework can also be loaded this way. + const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'profile_reconcile'); export default class Reconcile extends SfpowerscriptsCommand { diff --git a/packages/sfpowerscripts-cli/src/commands/profile/retrieve.ts b/packages/sfpowerscripts-cli/src/commands/profile/retrieve.ts new file mode 100644 index 000000000..0f55d6be9 --- /dev/null +++ b/packages/sfpowerscripts-cli/src/commands/profile/retrieve.ts @@ -0,0 +1,128 @@ +import { Messages, Org } from '@salesforce/core'; +import * as fs from 'fs-extra'; +import { isNil } from 'lodash'; +import { Sfpowerkit } from '@dxatscale/sfprofiles/lib/utils/sfpowerkit'; +import ProfileSync from '@dxatscale/sfprofiles/lib/impl/source/profileSync'; +import SfpowerscriptsCommand from '../../SfpowerscriptsCommand'; +import Table from 'cli-table'; +import { ZERO_BORDER_TABLE } from '../../ui/TableConstants'; +import { arrayFlagSfdxStyle, loglevel, orgApiVersionFlagSfdxStyle, requiredUserNameFlag } from '../../flags/sfdxflags'; +import { Flags } from '@oclif/core'; +import SFPLogger, { COLOR_KEY_MESSAGE, COLOR_WARNING, LoggerLevel } from '@dxatscale/sfp-logger'; + + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'profile_retrieve'); + +export default class Retrieve extends SfpowerscriptsCommand { + public static description = messages.getMessage('commandDescription'); + + public static examples = [ + `$ sfp profile:retrieve -u prod`, + `$ sfp profile:retrieve -f force-app -n "My Profile" -u prod`, + `$ sfp profile:retrieve -f "module1, module2, module3" -n "My Profile1, My profile2" -u prod`, + ]; + + + public static flags = { + folder: arrayFlagSfdxStyle({ + char: 'f', + description: messages.getMessage('folderFlagDescription'), + required: false, + }), + profilelist: arrayFlagSfdxStyle({ + char: 'n', + description: messages.getMessage('profileListFlagDescription'), + required: false, + }), + delete: Flags.boolean({ + char: 'd', + description: messages.getMessage('deleteFlagDescription'), + required: false, + }), + targetorg: requiredUserNameFlag, + 'apiversion': orgApiVersionFlagSfdxStyle, + loglevel, + }; + + // Comment this out if your command does not require an org username + protected static requiresUsername = true; + + // Set this to true if your command requires a project workspace; 'requiresProject' is false by default + protected static requiresProject = true; + + public async execute(): Promise { + let argFolder: string = this.flags.folder; + let argProfileList: string[] = this.flags.profilelist; + + let folders: string[] = []; + if (!isNil(argFolder) && argFolder.length !== 0) { + for (let dir of argFolder) { + if (!fs.existsSync(dir)) { + throw new Error(`The profile path ${dir} does not exist.`); + } + } + folders.push(...argFolder); + } + + Sfpowerkit.initCache(); + + SFPLogger.log(COLOR_WARNING(messages.getMessage('retriveDelayWarning')),LoggerLevel.INFO); + SFPLogger.log(COLOR_KEY_MESSAGE(`Retrieving profiles from ${this.flags.targetorg}`),LoggerLevel.INFO ); + + this.org = await Org.create({ aliasOrUsername: this.flags.targetorg }); + const profileUtils = new ProfileSync(this.org); + + let syncProfiles = await profileUtils.sync(folders, argProfileList || [], this.flags.delete); + + const table = new Table({ + head: ['State', 'Full Name', 'Type', 'Path'], + chars: ZERO_BORDER_TABLE, + }); + if (syncProfiles.added) { + syncProfiles.added.forEach((profile) => { + table.push({ + state: 'Add', + fullName: profile.name, + type: 'Profile', + path: profile.path, + }); + }); + } + if (syncProfiles.updated) { + syncProfiles.updated.forEach((profile) => { + table.push({ + state: 'Updated', + fullName: profile.name, + type: 'Profile', + path: profile.path, + }); + }); + } + if (this.flags.delete) { + if (syncProfiles.deleted) { + syncProfiles.deleted.forEach((profile) => { + table.push({ + state: 'Deleted', + fullName: profile.name, + type: 'Profile', + path: profile.path, + }); + }); + } + } else { + if (syncProfiles.deleted) { + syncProfiles.deleted.forEach((profile) => { + table.push({ + state: 'Skipped', + fullName: profile.name, + type: 'Profile', + path: profile.path, + }); + }); + } + } + + return syncProfiles; + } +}