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
JavaScript
/* 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