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

190 lines (188 loc) • 8.28 kB
/* jscpd:ignore-start */ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Logger, LoggerLevel, Messages } from '@salesforce/core'; import { uxLog } from '../../../../common/utils/index.js'; import { parseXmlFile } from '../../../../common/utils/xmlUtils.js'; import { getConfig } from '../../../../config/index.js'; import { glob } from 'glob'; import c from 'chalk'; import { GLOB_IGNORE_PATTERNS } from '../../../../common/utils/projectUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); // 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. function getCommonPermissionPatterns(rootTagName) { return [ `${rootTagName}.fieldPermissions.field`, `${rootTagName}.objectPermissions.object`, `${rootTagName}.classAccesses.apexClass`, ]; } export default class Find extends SfCommand { static metadataDuplicateFindKeys = { layout: ['Layout.layoutSections.layoutColumns.layoutItems.field', 'Layout.quickActionListItems.quickActionName'], profile: getCommonPermissionPatterns('Profile'), labels: ['CustomLabels.labels.fullName'], permissionset: getCommonPermissionPatterns('PermissionSet'), }; static title = 'XML duplicate values finder'; static description = `find duplicate values in XML file(s). Find duplicate values in XML file(s). Keys to be checked can be configured in \`config/sfdx-hardis.yml\` using property metadataDuplicateFindKeys. Default config : metadataDuplicateFindKeys : ${Find.metadataDuplicateFindKeys} `; static examples = [ ` <?xml version="1.0" encoding="UTF-8"?> <Layout xmlns="http://soap.sforce.com/2006/04/metadata"> <layoutSections> ... <layoutColumns> <layoutItems> <behavior>Required</behavior> <field>Name</field> </layoutItems> <layoutItems> <behavior>Required</behavior> <field>Name</field> </layoutItems> </layoutColumns> </layoutSections> </Layout> `, ` $ sf hardis:project:metadata:findduplicates --file layout.layout-meta.xml [sfdx-hardis] Duplicate values in layout.layout-meta.xml - Key : Layout.layoutSections.layoutColumns.layoutItems.field - Values : Name `, ` $ sf hardis:project.metadata:findduplicates -f "force-app/main/default/**/*.xml" [sfdx-hardis] hardis:project:metadata:findduplicates execution time 0:00:00.397 [sfdx-hardis] Duplicate values in layout1.layout-meta.xml - Key : Layout.layoutSections.layoutColumns.layoutItems.field - Values : CreatedById [sfdx-hardis] Duplicate values in layout2.layout-meta.xml - Key : Layout.layoutSections.layoutColumns.layoutItems.field - Values : LastModifiedById, Name `, ]; configInfo; logLevel; static flags = { files: Flags.string({ char: 'f', description: 'XML metadata files path', multiple: true, }), websocket: Flags.string({ description: messages.getMessage('websocket'), }), skipauth: Flags.boolean({ description: 'Skip authentication check when a default username is required', }), }; // Set this to true if your command requires a project workspace; 'requiresProject' is false by default static requiresProject = true; async run() { const { flags } = await this.parse(Find); uxLog("action", this, c.cyan(`Start finding duplicate values in XML metadata files.`)); await this.initConfig(); const filesWithDuplicates = await this.findDuplicates(flags); uxLog("action", this, c.cyan("Summary")); if (filesWithDuplicates.length > 0) { const duplicatesString = filesWithDuplicates .map((file) => { return `${file.file}\n - Key : ${file.key}\n - Values : ${file.duplicates.join(', ')}`; }) .join('\n'); uxLog("error", this, c.red(`Found ${filesWithDuplicates.length} files with duplicate values\n${duplicatesString}`)); process.exitCode = 1; } else { uxLog("success", this, c.green('No duplicate values found.')); } return filesWithDuplicates; } async initConfig() { this.configInfo = await getConfig('user'); if (this.configInfo.metadataDuplicateFindKeys) { Find.metadataDuplicateFindKeys = this.configInfo.metadataDuplicateFindKeys; } // Gets the root SF CLI logger level this.logLevel = (await Logger.root()).getLevel(); } async findDuplicates(flags) { // Collect input parameters const inputFiles = []; if (flags.files) { const files = await glob('./' + flags.files, { cwd: process.cwd(), ignore: GLOB_IGNORE_PATTERNS }); inputFiles.push(...files); } const foundFilesWithDuplicates = []; for (const inputFile of inputFiles) { // Extract given metadata type based on filename using type-meta.xml // For example PersonAccount.layout-meta.xml returns layout and Admin.profile-meta.xml returns profile const filenameRegex = /\w*\.(\w*)-meta.xml/; if (!inputFile.match(filenameRegex)) { uxLog("warning", this, c.yellow(`Filename ${inputFile} does not match expected pattern, skipping.`)); continue; } const type = inputFile.match(filenameRegex)[1]; // Check if given metadata type has unicity rules const uniqueKeys = Find.metadataDuplicateFindKeys[type]; if (!uniqueKeys) { if (this.logLevel === LoggerLevel.DEBUG) { uxLog("error", this, c.grey(`No unicity rule found for metadata type ${type} (processing ${inputFile})`)); } continue; } // Read metadata file const file = await parseXmlFile(inputFile); uniqueKeys.forEach((key) => { // Traverse the file down to the key based on the fragments separated by . (dots), abort if not found const allProps = key.split('.'); if (!file || !allProps[0] || !file[allProps[0]]) { uxLog("warning", this, c.yellow(`Key ${key} not found in file ${inputFile}`)); return; } const valuesFound = this.traverseDown(file, allProps[0], allProps, []); // https://stackoverflow.com/a/840808 const duplicates = valuesFound.filter((e, i, a) => a.indexOf(e) !== i); if (duplicates.length) { foundFilesWithDuplicates.push({ file: inputFile, key, duplicates, }); } }); } return foundFilesWithDuplicates; } /** * Traverse down a XML tree, allProps containing all the properties to be traversed, currentProp being updated as we * descend. */ traverseDown(parent, currentProp, allProps, results) { const nextProp = allProps[allProps.indexOf(currentProp) + 1]; // If we're at the end of property path (A.B.C -> parent = A.B, currentProp = C, nextProp = undefined) we add the // value contained in A.B.C if (nextProp === undefined && parent[currentProp]) { results.push(parent[currentProp][0]); } // If A.B is an array, we'll traverse A.B.C1, A.B.C2, etc... if (Array.isArray(parent[currentProp])) { parent[currentProp].forEach((childProp) => { this.traverseDown(childProp, nextProp, allProps, results); }); } // If A.B is an object, we simply set it as the new "parent" else if (parent[currentProp]) { this.traverseDown(parent[currentProp], nextProp, allProps, results); } return results; } } //# sourceMappingURL=findduplicates.js.map