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

193 lines (185 loc) • 10.3 kB
/* 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