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
193 lines (185 loc) • 10.3 kB
JavaScript
/* jscpd:ignore-start */
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import c from 'chalk';
import fs from 'fs-extra';
import { glob } from 'glob';
import { mergeObjectPropertyLists, uxLog } from '../../../../common/utils/index.js';
import { buildOrgManifest } from '../../../../common/utils/deployUtils.js';
import { promptOrg } from '../../../../common/utils/orgUtils.js';
import { parsePackageXmlFile, parseXmlFile, writeXmlFile } from '../../../../common/utils/xmlUtils.js';
import { GLOB_IGNORE_PATTERNS } from '../../../../common/utils/projectUtils.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
export default class OrgMissingItems extends SfCommand {
static title = 'Clean SFDX items using target org definition';
static description = `
## Command Behavior
**Cleans Salesforce DX project sources by removing metadata components that are not present in a target Salesforce org or the local \`package.xml\` file.**
This command helps maintain a lean and accurate codebase by identifying and removing metadata that is either obsolete in the target org or not explicitly included in your project's \`package.xml\`. This is particularly useful for:
- **Reducing Deployment Size:** Eliminating unnecessary metadata reduces the size of deployments, leading to faster deployments and fewer conflicts.
- **Ensuring Consistency:** Synchronizing your local codebase with the actual state of a Salesforce org.
- **Cleaning Up Orphaned Metadata:** Removing components that might have been deleted from the org but still exist in your local project.
Key features:
- **Target Org Integration:** Connects to a specified Salesforce org (or prompts for one) to retrieve its metadata manifest.
- **\`package.xml\` Comparison:** Compares your local project's metadata with the target org's metadata and your local \`package.xml\` to identify missing items.
- **Report Type Cleaning:** Specifically targets and cleans \`reportType-meta.xml\` files by removing references to fields or objects that are not present in the target org or your \`package.xml\`.
<details markdown="1">
<summary>Technical explanations</summary>
The command's technical implementation involves several steps:
- **Org Manifest Generation:** If not provided, it generates a full \`package.xml\` from the target Salesforce org using \`buildOrgManifest\`.
- **XML Parsing and Merging:** It parses the generated org manifest and merges it with the local \`package.xml\` and \`destructiveChanges.xml\` files to create a comprehensive list of existing and deleted metadata.
- **Metadata Analysis:** It iterates through specific metadata types (currently \`reportType-meta.xml\` files) within the configured source folder.
- **Field and Object Validation:** For each \`reportType-meta.xml\` file, it examines the columns and filters out references to custom fields or objects that are not found in the merged \`package.xml\` content or are marked for destruction.
- **XML Modification:** If changes are detected, it updates the \`reportType-meta.xml\` file by writing the modified XML content back to the file using \`writeXmlFile\`.
- **File System Operations:** It uses \`fs-extra\` for file system operations and \`glob\` for pattern matching to find relevant metadata files.
- **SOQL Queries:** The \`buildOrgManifest\` utility (used internally) performs SOQL queries to retrieve metadata information from the Salesforce org.
</details>
`;
static examples = ['$ sf hardis:project:clean:orgmissingitems'];
static flags = {
folder: Flags.string({
char: 'f',
default: 'force-app',
description: 'Root folder',
}),
packagexmlfull: Flags.string({
char: 'p',
description: 'Path to packagexml used for cleaning.\nMust contain also standard CustomObject and CustomField elements.\nIf not provided, it will be generated from a remote org',
}),
packagexmltargetorg: Flags.string({
char: 't',
description: 'Target org username or alias to build package.xml (SF CLI must be authenticated).\nIf not provided, will be prompted to the user.',
}),
debug: Flags.boolean({
char: 'd',
default: false,
description: messages.getMessage('debugMode'),
}),
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;
/* jscpd:ignore-end */
folder;
targetOrgUsernameAlias;
packageXmlFull;
debugMode = false;
standardFields = [
'Id',
'Name',
'Parent',
'IsActive',
'Alias',
'Owner',
'CreatedBy',
'CreatedDate',
'LastActivityDate',
'LastModifiedBy',
'LastModifiedDate',
'RecordType',
];
standardSuffixes = [
'Street',
'City',
'State',
'PostalCode',
'Country',
'Latitude',
'Longitude',
'GeocodeAccuracy',
];
async run() {
const { flags } = await this.parse(OrgMissingItems);
this.folder = flags.folder || './force-app';
this.debugMode = flags.debug || false;
this.targetOrgUsernameAlias = flags.packagexmltargetorg || null;
this.packageXmlFull = flags.packagexmlfull || null;
if (this.packageXmlFull === null) {
// Request user to select an org if not provided
if (this.targetOrgUsernameAlias == null) {
const targetOrg = await promptOrg(this, { devHub: false, setDefault: false, defaultOrgUsername: flags['target-org']?.getUsername() });
this.targetOrgUsernameAlias = targetOrg.username;
}
this.packageXmlFull = await buildOrgManifest(this.targetOrgUsernameAlias);
}
let packageXmlContent = await parsePackageXmlFile(this.packageXmlFull);
// Merge with local package.xml content
if (fs.existsSync('./manifest/package.xml')) {
const localPackageXmlContent = await parsePackageXmlFile('./manifest/package.xml');
packageXmlContent = mergeObjectPropertyLists(packageXmlContent, localPackageXmlContent, { sort: true });
}
// Build destructiveChanges
let destructiveChangesContent = {};
if (fs.existsSync('./manifest/destructiveChanges.xml')) {
destructiveChangesContent = await parsePackageXmlFile('./manifest/destructiveChanges.xml');
}
// Build additional lists
const packageXmlAllFields = packageXmlContent['CustomField'].map((customField) => customField.split('.')[1]);
// const destructiveChangesAllFields = (destructiveChangesContent["CustomField"] || []).map(customField => customField.split('.')[1]);
// Clean report types
let counterItems = 0;
const patternReportType = this.folder + `/**/reportTypes/*.reportType-meta.xml`;
const matchFilesPattern = await glob(patternReportType, {
cwd: process.cwd(),
ignore: GLOB_IGNORE_PATTERNS
});
uxLog("log", this, `Processing reportTypes...`);
for (const reportTypeFile of matchFilesPattern) {
if (this.debugMode) {
uxLog("other", this, `Processing ${reportTypeFile}...`);
}
let changed = false;
const reportType = await parseXmlFile(reportTypeFile);
for (let iSection = 0; iSection < reportType.ReportType.sections.length; iSection++) {
const section = reportType.ReportType.sections[iSection];
const prevLen = section.columns.length;
// Filter columns referring to fields not in package.xml of target org + local package.xml
section.columns = section.columns.filter((column) => {
const object = column.table[0];
const field = column.field[0].split('.')[0];
const objectField = `${object}.${field}`;
if ((destructiveChangesContent['CustomObject'] || []).includes(object)) {
return false;
}
const objectFound = (packageXmlContent['CustomObject'] || []).includes(object);
const fieldFound = (packageXmlContent['CustomField'] || []).includes(objectField);
const isStandardTechField = this.standardFields.includes(field);
const isStandardSubField = this.standardSuffixes.filter((suffix) => field.endsWith(suffix)).length > 0;
if ((objectFound && (fieldFound || isStandardTechField || isStandardSubField)) ||
(object.includes('__r') && (isStandardTechField || isStandardSubField)) ||
(object.includes('__r') && packageXmlAllFields.includes(field))) {
return true;
}
else {
if (this.debugMode) {
uxLog("log", this, `-- filtered ${objectField}`);
}
return false;
}
});
if (section.columns.length !== prevLen) {
reportType.ReportType.sections[iSection] = section;
changed = true;
}
}
// Update source file if content has been updated
if (changed) {
await writeXmlFile(reportTypeFile, reportType);
uxLog("log", this, `Updated ${reportTypeFile}`);
counterItems++;
}
}
// Summary
const msg = `Updated ${c.green(c.bold(counterItems))} items`;
uxLog("action", this, c.cyan(msg));
// Return an object to be displayed with --json
return { outputString: msg };
}
}
//# sourceMappingURL=orgmissingitems.js.map