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

208 lines (201 loc) 10.4 kB
/* jscpd:ignore-start */ // External Libraries and Node.js Modules import fs from 'fs-extra'; import * as xml2js from 'xml2js'; import { glob } from 'glob'; 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 { setConnectionVariables } from '../../../common/utils/orgUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); /* jscpd:ignore-end */ export default class MetadataStatus extends SfCommand { static title = 'check missing description on custom fields'; static description = ` ## Command Behavior **Checks for missing descriptions on custom fields within your Salesforce DX project.** This command helps enforce documentation standards by identifying custom fields that lack a descriptive explanation. Comprehensive field descriptions are crucial for: - **Maintainability:** Making it easier for developers and administrators to understand the purpose and usage of each field. - **Data Governance:** Ensuring data quality and consistency. - **User Adoption:** Providing clear guidance to end-users on how to interact with fields. It specifically targets custom fields (ending with \`__c\`) and excludes standard fields, managed package fields, and fields on Custom Settings or Data Cloud objects. <details markdown="1"> <summary>Technical explanations</summary> The command's technical implementation involves: - **File Discovery:** It uses \`glob\` to find all custom field metadata files (\`.field-meta.xml\`) within your project. - **Custom Setting Exclusion:** It first filters out fields belonging to Custom Settings by reading the corresponding object metadata files (\`.object-meta.xml\`) and checking for the \`<customSettingsType>\` tag. It also excludes Data Cloud objects (\`__dlm\`, \`__dll\`) and managed package fields. - **XML Parsing:** For each remaining custom field file, it reads the XML content and parses it using \`xml2js\` to extract the \`fullName\` and \`description\` attributes. - **Description Check:** It verifies if the \`description\` attribute is present and not empty for each custom field. - **Data Aggregation:** All custom fields found to be missing a description are collected into a list, along with their object and field names. - **Report Generation:** It generates a CSV report (\`lint-missingattributes.csv\`) containing details of all fields with missing descriptions. - **Notification Integration:** It integrates with the \`NotifProvider\` to send notifications (e.g., to Slack, MS Teams, Grafana) about the presence and count of fields with missing descriptions, making it suitable for automated quality checks in CI/CD pipelines. </details> `; static examples = ['$ sf hardis:lint:missingattributes']; /* 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; objectFileDirectory = '**/objects/**/fields/*.*'; fieldsWithoutDescription = []; outputFile; outputFilesRes = {}; nonCustomSettingsFieldDirectories = []; ignorePatterns = GLOB_IGNORE_PATTERNS; async run() { const { flags } = await this.parse(MetadataStatus); await this.filterOutCustomSettings(); this.fieldsWithoutDescription = await this.verifyFieldDescriptions(); // Build notifications const branchMd = await getBranchMarkdown(); const notifButtons = await getNotificationButtons(); let notifSeverity = 'log'; let notifText = `No missing descriptions on fields were found in ${branchMd}.`; let attachments = []; if (this.fieldsWithoutDescription.length > 0) { notifSeverity = 'warning'; notifText = `${this.fieldsWithoutDescription.length} fields with missing descriptions were found in ${branchMd}`; await this.buildCsvFile(this.fieldsWithoutDescription); attachments = [ { text: `*Missing descriptions*\n${this.fieldsWithoutDescription.map((file) => `• ${file.name}`).join('\n')}`, }, ]; } else { uxLog("other", this, 'No missing descriptions on fields were found.'); } // Post notifications await setConnectionVariables(flags['target-org']?.getConnection()); // Required for some notifications providers like Email await NotifProvider.postNotifications({ type: 'MISSING_ATTRIBUTES', text: notifText, attachments: attachments, buttons: notifButtons, severity: notifSeverity, sideImage: 'flow', logElements: this.fieldsWithoutDescription, data: { metric: this.fieldsWithoutDescription.length }, metrics: { MetadatasWithoutDescription: this.fieldsWithoutDescription.length, }, }); return {}; } async filterOutCustomSettings() { const parserCS = new xml2js.Parser(); const objectDirectories = await glob(this.objectFileDirectory, { ignore: this.ignorePatterns }); for (const directory of objectDirectories) { const objectName = path.basename(path.dirname(path.dirname(directory))); // Filter Data Cloud & managed items if (objectName.endsWith("__dlm") || objectName.endsWith("__dll") || objectName.split('__').length > 2) { continue; } const objectMetaFilePath = path.join(path.dirname(path.dirname(directory)), `${objectName}.object-meta.xml`); if (fs.existsSync(objectMetaFilePath)) { try { const objectMetaFileContent = fs.readFileSync(objectMetaFilePath, 'utf8'); let isCustomSettingsObject = false; const result = await parserCS.parseStringPromise(objectMetaFileContent); if (result && result.CustomObject && result.CustomObject.customSettingsType) { isCustomSettingsObject = true; } if (!isCustomSettingsObject) { this.nonCustomSettingsFieldDirectories.push(directory); } } catch (err) { console.error(err); } } else { this.nonCustomSettingsFieldDirectories.push(directory); } } } async verifyFieldDescriptions() { const fieldsWithoutDescription = []; const fieldResults = await Promise.all(this.nonCustomSettingsFieldDirectories.map(async (fieldFile) => { const fieldContent = await this.readFileAsync(fieldFile); return await this.parseXmlStringAsync(fieldContent); })); const severityIconInfo = getSeverityIcon('info'); for (let i = 0; i < fieldResults.length; i++) { const fieldResult = fieldResults[i]; if (fieldResult && fieldResult.CustomField) { const fieldName = fieldResult.CustomField.fullName[0]; // Skip standard and managed fields if (fieldName.endsWith('__c') && !fieldResult.CustomField.description && (fieldName.match(/__/g) || []).length < 2) { const fieldFile = this.nonCustomSettingsFieldDirectories[i].replace(/\\/g, '/'); const objectName = fieldFile.split('/').slice(-3, -2)[0]; const fullFieldName = `${objectName}.${fieldName}`; fieldsWithoutDescription.push({ name: fullFieldName, object: objectName, field: fieldName, severity: 'info', severityIcon: severityIconInfo, }); } } } return fieldsWithoutDescription; } parseXmlStringAsync(xmlString) { return new Promise((resolve, reject) => { xml2js.parseString(xmlString, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } readFileAsync(filePath) { return new Promise((resolve, reject) => { fs.readFile(filePath, 'utf8', (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); } async buildCsvFile(fieldsWithoutDescription) { this.outputFile = await generateReportPath('lint-missingattributes', this.outputFile); const csvData = fieldsWithoutDescription.map((field) => ({ type: 'Field', name: field.name })); this.outputFilesRes = await generateCsvFile(csvData, this.outputFile, { fileTitle: 'Missing Attributes' }); } } //# sourceMappingURL=missingattributes.js.map