UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

381 lines (378 loc) 18.4 kB
/* jscpd:ignore-start */ // External Libraries and Node.js Modules import { glob } from 'glob'; import fs from 'fs-extra'; import * as path from 'path'; // Salesforce Specific import { SfCommand, Flags, optionalOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; // Project Specific Utilities import { uxLog } from '../../../common/utils/index.js'; import { NotifProvider } from '../../../common/notifProvider/index.js'; import { getBranchMarkdown, getNotificationButtons, getSeverityIcon } from '../../../common/utils/notifUtils.js'; import { generateCsvFile, generateReportPath } from '../../../common/utils/filesUtils.js'; import { GLOB_IGNORE_PATTERNS } from '../../../common/utils/projectUtils.js'; import { CONSTANTS } from '../../../config/index.js'; import { setConnectionVariables } from '../../../common/utils/orgUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); /* jscpd:ignore-end */ export default class LintMetadataStatus extends SfCommand { static title = 'check inactive metadatas'; static description = `Check if elements are inactive in the project: - Approval Processes - Assignment Rules - Auto Response Rules - Escalation Rules - Flows - Forecasting Types - Record Types - Validation Rules - Workflow Rules ![](https://github.com/hardisgroupcom/sfdx-hardis/raw/main/docs/assets/images/detect-inactive-metadata.gif) This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/salesforce-monitoring-inactive-metadata/) and can output Grafana, Slack and MsTeams Notifications. `; static examples = ['$ sf hardis:lint:metadatastatus']; /* jscpd:ignore-start */ static flags = { debug: Flags.boolean({ char: 'd', default: false, description: messages.getMessage('debugMode'), }), outputfile: Flags.string({ char: 'f', description: 'Force the path and name of output report file. Must end with .csv', }), websocket: Flags.string({ description: messages.getMessage('websocket'), }), skipauth: Flags.boolean({ description: 'Skip authentication check when a default username is required', }), 'target-org': optionalOrgFlagWithDeprecations, }; /* jscpd:ignore-end */ static supportsDevhubUsername = false; // Set this to true if your command requires a project workspace; 'requiresProject' is false by default static requiresProject = true; flowFilePattern = '**/flows/**/*.flow-meta.xml'; validationRuleFilePattern = '**/objects/**/validationRules/*.validationRule-meta.xml'; ignorePatterns = GLOB_IGNORE_PATTERNS; inactiveItems = []; outputFile; outputFilesRes = {}; async run() { const { flags } = await this.parse(LintMetadataStatus); const inactiveApprovalProcesses = await this.verifyApprovalProcesses(); const inactiveAssignmentRules = await this.verifyAssignmentRules(); const inactiveAutoResponseRules = await this.verifyAutoResponseRules(); const inactiveEscalationRules = await this.verifyEscalationRules(); const draftFlows = await this.verifyFlows(); const inactiveForecastingTypes = await this.verifyForecastingTypes(); const inactiveRecordTypes = await this.verifyRecordTypes(); const inactiveValidationRules = await this.verifyValidationRules(); const inactiveWorkflows = await this.verifyWorkflowRules(); this.inactiveItems = [ ...inactiveApprovalProcesses, ...inactiveAssignmentRules, ...inactiveAutoResponseRules, ...draftFlows, ...inactiveEscalationRules, ...inactiveForecastingTypes, ...inactiveRecordTypes, ...inactiveValidationRules, ...inactiveWorkflows, ]; // Prepare notifications const branchMd = await getBranchMarkdown(); const notifButtons = await getNotificationButtons(); let notifSeverity = 'log'; let notifText = `No inactive configuration elements has been found in ${branchMd}`; const attachments = []; if (this.inactiveItems.length > 0) { notifSeverity = 'warning'; if (inactiveApprovalProcesses.length > 0) { attachments.push({ text: `*Inactive Approval Processes*\n${inactiveApprovalProcesses.map((file) => `• ${file.name}`).join('\n')}`, }); } if (inactiveAssignmentRules.length > 0) { attachments.push({ text: `*Inactive Assignment Rules*\n${inactiveAssignmentRules.map((file) => `• ${file.name}`).join('\n')}`, }); } if (inactiveAutoResponseRules.length > 0) { attachments.push({ text: `*Inactive Auto Response Rules*\n${inactiveAutoResponseRules.map((file) => `• ${file.name}`).join('\n')}`, }); } if (inactiveEscalationRules.length > 0) { attachments.push({ text: `*Inactive Escalation Rules*\n${inactiveEscalationRules.map((file) => `• ${file.name}`).join('\n')}`, }); } if (draftFlows.length > 0) { attachments.push({ text: `*Inactive Flows*\n${draftFlows.map((file) => `• ${file.name}`).join('\n')}`, }); } if (inactiveForecastingTypes.length > 0) { attachments.push({ text: `*Inactive Forecasting Types*\n${inactiveForecastingTypes.map((file) => `• ${file.name}`).join('\n')}`, }); } if (inactiveRecordTypes.length > 0) { attachments.push({ text: `*Inactive Record Types*\n${inactiveRecordTypes.map((file) => `• ${file.name}`).join('\n')}`, }); } if (inactiveValidationRules.length > 0) { attachments.push({ text: `*Inactive Validation Rules*\n${inactiveValidationRules.map((file) => `• ${file.name}`).join('\n')}`, }); } if (inactiveWorkflows.length > 0) { attachments.push({ text: `*Inactive Workflow Rules*\n${inactiveWorkflows.map((file) => `• ${file.name}`).join('\n')}`, }); } notifText = `${this.inactiveItems.length} inactive configuration elements have been found in ${branchMd}`; // Build result file await this.buildCsvFile(); } else { uxLog(this, 'No draft flow or validation rule files detected.'); } // Post notifications await setConnectionVariables(flags['target-org']?.getConnection()); // Required for some notifications providers like Email await NotifProvider.postNotifications({ type: 'METADATA_STATUS', text: notifText, attachments: attachments, buttons: notifButtons, severity: notifSeverity, sideImage: 'flow', attachedFiles: this.outputFilesRes.xlsxFile ? [this.outputFilesRes.xlsxFile] : [], logElements: this.inactiveItems, data: { metric: this.inactiveItems.length }, metrics: { InactiveMetadatas: this.inactiveItems.length, }, }); return {}; } /** * This function verifies the status of flows by checking each flow file. * It reads each flow file and checks if the flow is in 'Draft' status. * If the flow is in 'Draft' status, it extracts the file name and adds it to the list of draft files. * * @returns {Promise<string[]>} - A Promise that resolves to an array of draft files. Each entry in the array is the name of a draft file. */ async verifyFlows() { const draftFiles = []; const flowFiles = await glob(this.flowFilePattern, { ignore: this.ignorePatterns }); const severityIcon = getSeverityIcon('warning'); for (const file of flowFiles) { const flowContent = await fs.readFile(file, 'utf-8'); if (flowContent.includes('<status>Draft</status>')) { const fileName = path.basename(file, '.flow-meta.xml'); draftFiles.push({ type: 'Flow (draft)', name: fileName, severity: 'warning', severityIcon: severityIcon }); } } return draftFiles.sort((a, b) => a.name.localeCompare(b.name)); } /** * This function verifies the validation rules by checking each rule file for inactive rules. * It reads each validation rule file and checks if the rule is active or not. * If the rule is inactive, it extracts the rule name and the object name and adds them to the list of inactive rules. * * @returns {Promise<string[]>} - A Promise that resolves to an array of inactive rules. Each entry in the array is a string in the format 'ObjectName - RuleName'. */ async verifyValidationRules() { const inactiveRules = []; const validationRuleFiles = await glob(this.validationRuleFilePattern, { ignore: this.ignorePatterns }); const severityIcon = getSeverityIcon('warning'); for (const file of validationRuleFiles) { // Skip if validation rule is from a managed package if (path.basename(file).includes('__')) { continue; } const ruleContent = await fs.readFile(file, 'utf-8'); if (ruleContent.includes('<active>false</active>')) { const ruleName = path.basename(file, '.validationRule-meta.xml'); const objectName = path.basename(path.dirname(path.dirname(file))); inactiveRules.push({ type: 'Validation Rule (inactive)', name: `${objectName} - ${ruleName}`, severity: 'warning', severityIcon: severityIcon, }); } } return inactiveRules.sort((a, b) => a.name.localeCompare(b.name)); } async verifyRecordTypes() { const inactiveRecordTypes = []; const recordTypeFiles = await glob('**/objects/**/recordTypes/*.recordType-meta.xml', { ignore: this.ignorePatterns, }); const severityIcon = getSeverityIcon('warning'); for (const file of recordTypeFiles) { const recordTypeName = path.basename(file, '.recordType-meta.xml'); const objectName = path.basename(path.dirname(path.dirname(file))); // Skip if record type is from a managed package if (path.basename(recordTypeName).includes('__')) { continue; } const recordTypeXml = await fs.readFile(file, 'utf-8'); if (recordTypeXml.includes('<active>false</active>')) { inactiveRecordTypes.push({ type: 'Record Type (inactive)', name: `${objectName} - ${recordTypeName}`, severity: 'warning', severityIcon: severityIcon, }); } } return inactiveRecordTypes.sort((a, b) => a.name.localeCompare(b.name)); } async verifyApprovalProcesses() { const inactiveApprovalProcesses = []; const approvalProcessFiles = await glob('**/approvalProcesses/**/*.approvalProcess-meta.xml', { ignore: this.ignorePatterns, }); const severityIcon = getSeverityIcon('warning'); for (const file of approvalProcessFiles) { const approvalProcessFullName = path.basename(file, '.approvalProcess-meta.xml'); const [objectName, approvalProcessName] = approvalProcessFullName.split('.'); // Skip if approval process is from a managed package if (path.basename(approvalProcessName).includes('__')) { continue; } const approvalProcessXml = await fs.readFile(file, 'utf-8'); if (approvalProcessXml.includes('<active>false</active>')) { inactiveApprovalProcesses.push({ type: 'Approval Process (inactive)', name: `${objectName} - ${approvalProcessName}`, severity: 'warning', severityIcon: severityIcon, }); } } return inactiveApprovalProcesses.sort((a, b) => a.name.localeCompare(b.name)); } async verifyForecastingTypes() { const inactiveForecastTypes = []; const forecastTypeFiles = await glob('**/forecastingTypes/**/*.forecastingType-meta.xml', { ignore: this.ignorePatterns, }); const severityIcon = getSeverityIcon('warning'); for (const file of forecastTypeFiles) { const forecastingTypeName = path.basename(file, '.forecastingType-meta.xml'); const forecastTypeXml = await fs.readFile(file, 'utf-8'); if (forecastTypeXml.includes('<active>false</active>')) { inactiveForecastTypes.push({ type: 'Forecasting Type (inactive)', name: forecastingTypeName, severity: 'warning', severityIcon: severityIcon, }); } } return inactiveForecastTypes.sort((a, b) => a.name.localeCompare(b.name)); } async verifyWorkflowRules() { const inactiveWorkflowRules = []; const workflowRuleFiles = await glob('**/workflows/**/*.workflow-meta.xml', { ignore: this.ignorePatterns, }); const severityIcon = getSeverityIcon('warning'); for (const file of workflowRuleFiles) { const workflowRuleName = path.basename(file, '.workflow-meta.xml'); const workflowRuleXml = await fs.readFile(file, 'utf-8'); if (workflowRuleXml.includes('<active>false</active>')) { inactiveWorkflowRules.push({ type: 'Workflow Rule (inactive)', name: workflowRuleName, severity: 'warning', severityIcon: severityIcon, }); } } return inactiveWorkflowRules.sort((a, b) => a.name.localeCompare(b.name)); } async verifyAssignmentRules() { const inactiveAssignmentRules = []; const assignmentRuleFiles = await glob('**/assignmentRules/**/*.assignmentRules-meta.xml', { ignore: this.ignorePatterns, }); const severityIcon = getSeverityIcon('warning'); for (const file of assignmentRuleFiles) { const assignmentRuleName = path.basename(file, '.assignmentRules-meta.xml'); const assignmentRuleXml = await fs.readFile(file, 'utf-8'); if (assignmentRuleXml.includes('<active>false</active>')) { inactiveAssignmentRules.push({ type: 'Assignment Rule (inactive)', name: assignmentRuleName, severity: 'warning', severityIcon: severityIcon, }); } } return inactiveAssignmentRules.sort((a, b) => a.name.localeCompare(b.name)); } async verifyAutoResponseRules() { const inactiveAutoResponseRules = []; const autoResponseRuleFiles = await glob('**/autoResponseRules/**/*.autoResponseRules-meta.xml', { ignore: this.ignorePatterns, }); const severityIcon = getSeverityIcon('warning'); for (const file of autoResponseRuleFiles) { const autoResponseRuleName = path.basename(file, '.autoResponseRules-meta.xml'); const autoResponseRuleXml = await fs.readFile(file, 'utf-8'); if (autoResponseRuleXml.includes('<active>false</active>')) { inactiveAutoResponseRules.push({ type: 'Auto Response Rule (inactive)', name: autoResponseRuleName, severity: 'warning', severityIcon: severityIcon, }); } } return inactiveAutoResponseRules.sort((a, b) => a.name.localeCompare(b.name)); } async verifyEscalationRules() { const inactiveEscalationRules = []; const escalationRuleFiles = await glob('**/escalationRules/**/*.escalationRules-meta.xml', { ignore: this.ignorePatterns, }); const severityIcon = getSeverityIcon('warning'); for (const file of escalationRuleFiles) { const escalationRuleName = path.basename(file, '.escalationRules-meta.xml'); const escalationRuleXml = await fs.readFile(file, 'utf-8'); if (escalationRuleXml.includes('<active>false</active>')) { inactiveEscalationRules.push({ type: 'Escalation Rule (inactive)', name: escalationRuleName, severity: 'warning', severityIcon: severityIcon, }); } } return inactiveEscalationRules.sort((a, b) => a.name.localeCompare(b.name)); } /** * This function builds a CSV file from arrays of draft flows and inactive validation rules. * It first ensures that the output file path is generated. * It then maps the draft flows and inactive validation rules into an array of objects, each with a 'type' property set to either "Draft Flow" or "Inactive VR" and a 'name' property set to the file or rule name. * Finally, it generates a CSV file from this array and writes it to the output file. * * @returns {Promise<void>} - A Promise that resolves when the CSV file has been successfully generated. */ async buildCsvFile() { this.outputFile = await generateReportPath('lint-metadatastatus', this.outputFile); this.outputFilesRes = await generateCsvFile(this.inactiveItems, this.outputFile); } } //# sourceMappingURL=metadatastatus.js.map