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
925 lines (917 loc) • 46.2 kB
JavaScript
import { requiredOrgFlagWithDeprecations, SfCommand, } from "@salesforce/sf-plugins-core";
import { Flags } from "@salesforce/sf-plugins-core";
import { SfError, Messages } from "@salesforce/core";
import { soqlQuery, soqlQueryTooling, } from "../../../../common/utils/apiUtils.js";
import { execCommand, uxLog, uxLogTable } from "../../../../common/utils/index.js";
import { prompts } from "../../../../common/utils/prompts.js";
import c from "chalk";
import path from "path";
import fs from "fs";
import * as fsExtra from "fs-extra";
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages("sfdx-hardis", "org");
import { parseXmlFile, writeXmlFile, } from "../../../../common/utils/xmlUtils.js";
import { MetadataUtils } from "../../../../common/metadata-utils/index.js";
import { generateCsvFile, generateReportPath } from "../../../../common/utils/filesUtils.js";
import { WebSocketClient } from "../../../../common/websocketClient.js";
import { listOrgSObjectsFiltered } from "../../../../common/utils/orgUtils.js";
// Constants
const ALLOWED_AUTOMATIONS = ["Flow", "Trigger", "VR"]; // TODO: type and remove hardcoded
const CREDITS_TEXT = "by sfdx-hardis : https://sfdx-hardis.cloudity.com/hardis/project/generate/bypass/";
const IMPLEMENTATION_OUTCOME = {
ADDED: "added",
SKIPPED: "skipped",
IGNORED: "ignored",
FAILED: "failed",
};
const METADATA_GENERATION_OUTCOME = {
GENERATED: "generated",
OVERRIDDEN: "overridden",
FAILED: "failed",
};
export default class HardisProjectGenerateBypass extends SfCommand {
skipCredits = false;
retrieveFromOrg;
outputFile;
reports = {
metadataGeneration: [],
implementation: [],
};
static flags = {
"target-org": requiredOrgFlagWithDeprecations,
outputfile: Flags.string({
char: 'f',
description: 'Force the path and name of output report file. Must end with .csv',
}),
// TODO: detect sObjects from folder and use them instead of asking the user
objects: Flags.string({
aliases: ["sObjects"],
char: "s",
description: "Comma-separated list of sObjects to bypass (e.g., Account,Contact,Opportunity). If omitted, you will be prompted to select.",
required: false,
}),
automations: Flags.string({
char: "a",
description: `Comma-separated automations to bypass: ${ALLOWED_AUTOMATIONS.join(", ")}`,
required: false,
}),
websocket: Flags.string({
description: messages.getMessage("websocket"),
}),
skipauth: Flags.boolean({
description: "Skip authentication check when a default username is required",
}),
"skip-credits": Flags.boolean({
aliases: ["skipCredits"],
char: "k",
description: 'Omit the "Generated by" line in the XML files',
required: false,
default: false,
}),
"apply-to-vrs": Flags.boolean({
aliases: ["applyToVrs"],
description: "Apply bypass to Validation Rules",
required: false,
default: false,
}),
"apply-to-triggers": Flags.boolean({
aliases: ["applyToTriggers"],
description: "Apply bypass to Triggers",
required: false,
default: false,
}),
"apply-to-flows": Flags.boolean({
aliases: ["applyToFlows"],
description: "Apply bypass to Flows",
required: false,
default: false,
}),
"metadata-source": Flags.string({
char: "r",
aliases: ["metadataSource"],
description: "Source of metadata elements to apply bypass to. Options: 'org' or 'local'.",
required: false,
}),
};
static description = `
## Command Behavior
**Generates custom permissions and permission sets to bypass specified Salesforce automations (Flows, Triggers, and Validation Rules) for specific sObjects, with optional automatic implementation of bypass logic.**
This command provides a controlled mechanism to temporarily or permanently disable automations for certain sObjects, which is invaluable for:
- **Data Loading:** Bypassing validation rules or triggers during large data imports.
- **Troubleshooting:** Isolating automation issues by temporarily disabling them.
- **Development:** Allowing developers to work on specific sObjects without triggering complex automations.
Key functionalities:
- **Global Bypass Generation:** Automatically creates global bypass permissions (\`BypassAllFlows\`, \`BypassAllTriggers\`, \`BypassAllVRs\`) that work across all sObjects.
- **sObject Selection:** Specify a comma-separated list of sObjects via \`--objects\` flag (e.g., \`Account,Contact\`), or use interactive prompts to select from available triggerable and customizable sObjects.
- **Automation Type Selection:** Choose which automation types to bypass via \`--automations\` flag: \`Flow\`, \`Trigger\`, or \`VR\` (Validation Rules), or select interactively.
- **Automatic Bypass Implementation:** Optionally inject bypass logic directly into automation metadata using:
- \`--apply-to-flows\`: Adds a decision node at the start of record-triggered flows to check bypass permissions.
- \`--apply-to-triggers\`: Inserts a bypass check at the beginning of Apex trigger bodies using \`FeatureManagement.checkPermission()\`.
- \`--apply-to-vrs\`: Wraps validation rule error conditions with bypass permission checks.
- **Metadata Source Control:** Choose where to retrieve automation metadata from via \`--metadata-source\`:
- \`org\`: Retrieves the latest metadata from the connected org (recommended for accuracy).
- \`local\`: Uses local metadata files from the project (faster but may be outdated).
- **Metadata Generation:** For each selected sObject and automation type, generates:
- **Custom Permission** (e.g., \`BypassAccountFlows\`) - The bypass switch that can be assigned via Permission Sets.
- **Permission Set** (e.g., \`BypassAccountFlows\`) - Grants the corresponding Custom Permission to users.
- **Comprehensive Reporting:** Generates two detailed CSV reports:
- **Metadata Generation Report:** Lists all custom permissions and permission sets created.
- **Implementation Report:** Shows which automations had bypass logic added, skipped, ignored, or failed.
- **Credits Control:** Use \`--skip-credits\` to omit the "Generated by sfdx-hardis" attribution in XML files.
<details markdown="1">
<summary>Technical explanations</summary>
The command's technical implementation involves:
- **SOQL Queries:**
- Queries \`EntityDefinition\` to list all triggerable and customizable sObjects.
- Queries \`ValidationRule\` (Tooling API) to find validation rules for selected sObjects.
- Queries \`ApexTrigger\` (Tooling API) to find triggers related to selected sObjects.
- Queries \`FlowDefinitionView\` to find record-triggered flows for selected sObjects.
- **Interactive Prompts:** Uses the \`prompts\` library for user-friendly selection of sObjects, automation types, and implementation options when flags are not provided.
- **XML Generation:** Dynamically generates Custom Permission and Permission Set XML files with descriptive labels and comments indicating their purpose.
- **File System Operations:** Uses \`fs-extra\` to create directory structures and write metadata files to \`force-app/main/default/customPermissions/\` and \`force-app/main/default/permissionsets/\`.
- **Metadata Retrieval:** When \`--metadata-source org\` is used, executes \`sf project retrieve start --metadata\` commands in chunks of 25 records to retrieve current automation metadata from the org.
- **Smart Bypass Implementation:**
- **Validation Rules:** Modifies \`errorConditionFormula\` XML nodes to wrap existing formulas with \`AND(NOT($Permission.Bypass...), ...)\` checks.
- **Triggers:** Injects \`if(FeatureManagement.checkPermission('Bypass...')) { return; }\` at the start of trigger bodies.
- **Flows:** Adds a decision node named \`SFDX_HARDIS_FLOW_BYPASS_DO_NOT_RENAME\` as the first node, checking both specific and global bypass permissions.
- **Duplicate Detection:** Skips implementation if bypass logic is already present, preventing duplicate additions.
- **Error Handling:**
- Validates sObject and automation selections.
- Handles missing files gracefully when using local metadata source.
- Reports errors for each automation that fails processing.
- Continues processing remaining items even when individual items fail (unless critical errors occur).
- **Reporting:** Generates timestamped CSV reports showing outcomes for both metadata generation and bypass implementation operations.
</details>
`;
static examples = [
"$ sf hardis:project:generate:bypass",
"$ sf hardis:project:generate:bypass --sObjects Account,Contact,Opportunity",
"$ sf hardis:project:generate:bypass --automations Flow,Trigger,VR",
"$ sf hardis:project:generate:bypass --sObjects Account,Opportunity --automations Flow,Trigger",
"$ sf hardis:project:generate:bypass --skipCredits",
"$ sf hardis:project:generate:bypass --apply-to-vrs",
"$ sf hardis:project:generate:bypass --apply-to-triggers",
"$ sf hardis:project:generate:bypass --metadata-source org",
];
// Main run method
async run() {
// Collect options
const { flags } = await this.parse(HardisProjectGenerateBypass);
const connection = flags["target-org"].getConnection();
if (flags["metadata-source"] !== undefined &&
flags["metadata-source"] !== null) {
this.retrieveFromOrg =
String(flags["metadata-source"]).trim().toLowerCase() === "org";
}
this.skipCredits = flags["skip-credits"] || false;
let applyToTriggers = flags["apply-to-triggers"] || null;
let applyToVrs = flags["apply-to-vrs"] || null;
let applyToFlows = flags["apply-to-flows"] || null;
const sObjects = flags.objects || null;
const automations = flags.automations || null;
this.outputFile = flags.outputfile || null;
const availableSObjects = await listOrgSObjectsFiltered(connection);
let targetSObjects = {};
let targetAutomations = [];
// Filter objects
if (sObjects) {
const sObjectsFromFlag = sObjects.split(",").map((s) => s.trim());
targetSObjects = Object.fromEntries(Object.entries(availableSObjects).filter(([key]) => {
const res = sObjectsFromFlag.includes(key);
if (!res) {
uxLog("warning", this, c.yellow(`Warning: sObject "${key}" is not available or not customizable. Skipping.`));
}
return res;
}));
}
if (automations) {
targetAutomations = automations
.split(",")
.map((s) => s.trim())
.filter((s) => ALLOWED_AUTOMATIONS.includes(s));
}
// Generate global bypasses
uxLog("action", this, c.cyan(`Generating global bypasses...`));
this.generateFiles({ All: "All" }, ALLOWED_AUTOMATIONS);
// Handle prompts if needed
const promptsNeeded = [];
if (!Object.keys(targetSObjects).length) {
promptsNeeded.push({
type: "multiselect",
name: "sobjects",
message: "Select sObjects for bypass",
description: "Choose which sObjects should have automation bypass functionality",
choices: Object.entries(availableSObjects).map(([devName, label]) => ({
title: label,
value: devName,
})),
});
}
if (!targetAutomations.length) {
promptsNeeded.push({
type: "multiselect",
name: "automations",
message: "Select which automations to bypass",
description: "This will generate bypass custom permissions and permission sets for the selected automation types and sObjects",
choices: [
{ title: "Flows", value: "Flow" },
{ title: "Triggers", value: "Trigger" },
{ title: "Validation Rules", value: "VR" },
],
});
}
if (applyToVrs == null && applyToTriggers == null && applyToFlows == null) {
promptsNeeded.push({
type: "multiselect",
name: "applyTo",
message: "Where do you wish to have the bypass applied?",
description: "Choose which automation types should have the bypass logic applied automatically. The metadata files will be modified accordingly.",
choices: [
{ title: "Flows (as a decision node)", value: "applyToFlows" },
{ title: "Triggers (within the .trigger file)", value: "applyToTriggers" },
{ title: "Validation Rules (encapsulating the existing validation logic)", value: "applyToVrs" },
],
});
}
if (this.retrieveFromOrg == undefined || this.retrieveFromOrg == null) {
promptsNeeded.push({
type: "select",
name: "elementSource",
message: "Where do you want to get the elements to apply bypass to?",
description: "Choose the source for retrieving automation elements",
placeholder: "Select source",
choices: [
{ title: "Retrieve from org (recommended)", value: "org" },
{ title: "Use local elements in the project", value: "local" },
],
});
}
if (promptsNeeded.length) {
const promptResults = await prompts(promptsNeeded);
if (promptResults.sobjects) {
targetSObjects = Object.fromEntries(Object.entries(availableSObjects).filter(([key]) => promptResults.sobjects.includes(key)));
}
if (promptResults.automations) {
targetAutomations = promptResults.automations;
}
if (!applyToTriggers) {
applyToTriggers = promptResults.applyTo?.includes("applyToTriggers");
}
if (!applyToVrs) {
applyToVrs = promptResults.applyTo?.includes("applyToVrs");
}
if (!applyToFlows) {
applyToFlows = promptResults.applyTo?.includes("applyToFlows");
}
if (promptResults.elementSource) {
this.retrieveFromOrg = promptResults.elementSource === "org";
}
}
// Validate selections
if (!Object.keys(targetSObjects).length) {
throw new SfError(c.red("ERROR: You must select at least one sObject."));
}
if (!targetAutomations.length) {
throw new SfError(c.red("ERROR: You must select at least one automation type."));
}
// Generate files and apply bypasses
this.generateFiles(targetSObjects, targetAutomations);
if (applyToVrs) {
uxLog("action", this, c.cyan(`Implementing the bypass logic to Validation Rules...`));
await this.applyBypassToValidationRules(connection, targetSObjects);
}
if (applyToTriggers) {
uxLog("action", this, c.cyan(`Implementing the bypass logic to Triggers...`));
await this.applyBypassToTriggers(connection, targetSObjects);
}
if (applyToFlows) {
uxLog("action", this, c.cyan(`Implementing the bypass logic to Flows...`));
await this.applyBypassToFlows(connection, targetSObjects);
}
uxLog("action", this, c.cyan(`Bypass generation and implementation is completed.`));
if (applyToVrs || applyToTriggers || applyToFlows) {
uxLog("action", this, c.cyan(`Bypass implementation report:`));
uxLogTable(this, this.reports.implementation);
}
uxLog("action", this, c.cyan(`Generating report files...`));
await this.generateReports();
return {
outputString: "Bypass generation and implementation completed.",
};
}
async generateReports() {
const baseFilePath = await generateReportPath('project-generate-bypass-<REPLACEME>', this.outputFile, { withDate: true, withBranchName: false });
let metadataGenerationReportFilePath = baseFilePath;
let implementationReportFilePath = baseFilePath;
if (baseFilePath.includes('<REPLACEME>')) {
metadataGenerationReportFilePath = baseFilePath.replace('<REPLACEME>', 'generation');
implementationReportFilePath = baseFilePath.replace('<REPLACEME>', 'implementation');
}
else {
metadataGenerationReportFilePath = baseFilePath.replace('.csv', '-generation.csv');
implementationReportFilePath = baseFilePath.replace('.csv', '-implementation.csv');
}
await generateCsvFile(this.reports.metadataGeneration, metadataGenerationReportFilePath, { fileTitle: 'Bypass Metadata Generation Report' });
await generateCsvFile(this.reports.implementation, implementationReportFilePath, { fileTitle: 'Bypass Implementation Report' });
}
async queryTriggers(connection) {
const query = `SELECT Id, Name, Status, IsValid, Body, BodyCrc, TableEnumOrId, ManageableState From ApexTrigger WHERE ManageableState != 'installed'`;
const results = await soqlQueryTooling(query, connection);
uxLog("log", this, c.grey(`Found ${results.records.length} Triggers.`));
return results;
}
filterTriggerResults(triggerResults, sObjects) {
return triggerResults.records.filter((trigger) => {
const sObjectApiNameWithoutC = trigger.TableEnumOrId?.replace("__c", "");
return (sObjectApiNameWithoutC &&
Object.keys(sObjects).includes(sObjectApiNameWithoutC) &&
trigger.Body != "(hidden)");
});
}
async queryValidationRules(connection, sObjects) {
const query = `SELECT ValidationName, EntityDefinition.QualifiedApiName, ManageableState FROM ValidationRule
WHERE ManageableState != 'installed' AND EntityDefinition.DeveloperName IN (${Object.keys(sObjects)
.map((s) => `'${s}'`)
.join(", ")})`;
const results = await soqlQueryTooling(query, connection);
uxLog("log", this, c.grey(`Found ${results.records.length} Validation Rules.`));
return results;
}
async queryFlows(connection) {
const query = `SELECT Id, ApiName, Label, TriggerObjectOrEvent.QualifiedApiName FROM FlowDefinitionView WHERE ManageableState = 'unmanaged'`;
const results = await soqlQuery(query, connection);
uxLog("log", this, c.grey(`Found ${results.records.length} Flows.`));
return results;
}
filterFlowResults(flowResults, sObjects) {
return flowResults.records.filter((flow) => {
const triggerObject = flow.TriggerObjectOrEvent?.QualifiedApiName;
return triggerObject && Object.keys(sObjects).includes(triggerObject.replace("__c", ""));
});
}
// XML Generation
generateXML(type, sObject, automation) {
const creditsText = this.skipCredits ? "" : `Generated ${CREDITS_TEXT}`;
if (type === "customPermission") {
return `<?xml version="1.0" encoding="UTF-8"?>
<CustomPermission xmlns="http://soap.sforce.com/2006/04/metadata">
<isLicensed>false</isLicensed>
<label>Bypass ${automation}s for ${sObject}</label>
<description>If assigned (through a Permission Set), this Custom Permission will disable the execution of ${automation}s defined on the ${sObject} sObject.${creditsText}</description>
</CustomPermission>`;
}
else {
return `<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<customPermissions>
<enabled>true</enabled>
<name>Bypass${sObject}${automation}s</name>
</customPermissions>
<hasActivationRequired>false</hasActivationRequired>
<label>Bypass ${automation}s for ${sObject}</label>
<description>If assigned, this Permission Set will disable the execution of ${automation}s defined on the ${sObject} sObject.${creditsText}</description>
</PermissionSet>`;
}
}
generateXMLFiles(sObject, automation) {
// TODO: use current folder path from sf project
const customPermissionFilePath = path.join(`force-app/main/default/customPermissions/Bypass${sObject}${automation}s.customPermission-meta.xml`);
const permissionSetFilePath = path.join(`force-app/main/default/permissionsets/Bypass${sObject}${automation}s.permissionset-meta.xml`);
const baseReportItem = {
sObject,
automation,
outcome: METADATA_GENERATION_OUTCOME.FAILED,
};
try {
fsExtra.ensureDirSync(path.dirname(customPermissionFilePath));
fs.writeFileSync(customPermissionFilePath, this.generateXML("customPermission", sObject, automation), "utf-8");
fsExtra.ensureDirSync(path.dirname(permissionSetFilePath));
fs.writeFileSync(permissionSetFilePath, this.generateXML("permissionSet", sObject, automation), "utf-8");
const createdMessages = [
`Created: ${path.basename(customPermissionFilePath)} for ${sObject}`,
`Created: ${path.basename(permissionSetFilePath)} for ${sObject}`,
];
// In VS Code UI, prefer sending details through the websocket so they appear within the progress widget.
if (WebSocketClient.isAliveWithLwcUI()) {
for (const message of createdMessages) {
WebSocketClient.sendCommandLogLineMessage(message, 'log');
}
}
else {
for (const message of createdMessages) {
uxLog("log", this, c.grey(message));
}
}
baseReportItem.outcome = METADATA_GENERATION_OUTCOME.GENERATED;
baseReportItem.customPermissionFilePath = customPermissionFilePath;
baseReportItem.permissionSetFilePath = permissionSetFilePath;
this.reports.metadataGeneration.push(baseReportItem);
}
catch (error) {
uxLog("error", this, c.red(`Error generating XML files for ${sObject} and ${automation}: ${error}`));
this.reports.metadataGeneration.push(baseReportItem);
}
}
generateFiles(targetSObjects, targetAutomations) {
let counter = 0;
const totalSteps = Object.keys(targetSObjects).length * targetAutomations.length;
WebSocketClient.sendProgressStartMessage("Generating bypass metadata files...", totalSteps);
for (const developerName of Object.keys(targetSObjects)) {
counter++;
WebSocketClient.sendProgressStepMessage(counter, totalSteps);
for (const automation of targetAutomations) {
this.generateXMLFiles(developerName, automation);
counter++;
WebSocketClient.sendProgressStepMessage(counter, totalSteps);
}
}
WebSocketClient.sendProgressEndMessage(totalSteps);
}
// Metadata handling
async retrieveMetadataFiles(records, metadataType) {
const recordsChunks = this.chunkArray(records);
const results = [];
for (const chunk of recordsChunks) {
let command = `sf project retrieve start --metadata`;
command += chunk.map((record) => {
if (metadataType === "Flow") {
return ` Flow:${record.ApiName}`;
}
else if (metadataType === "ValidationRule") {
return ` ValidationRule:${record.EntityDefinition.QualifiedApiName}.${record.ValidationName}`;
}
else {
return ` ApexTrigger:${record.Name}`;
}
}).join(" ");
try {
const result = await execCommand(`${command} --ignore-conflicts --json`, this, {
debug: false,
retry: {
retryDelay: 30,
retryStringConstraint: "error",
retryMaxAttempts: 3,
},
});
results.push(result);
}
catch (error) {
uxLog("error", this, c.red(`Error retrieving ${metadataType}: ${error}`));
}
}
return results;
}
chunkArray(array, chunkSize = 25) {
return Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => array.slice(i * chunkSize, (i + 1) * chunkSize));
}
// Validation Rules
async handleValidationRuleFile(filePath, sObject, name) {
try {
const fileContent = await parseXmlFile(filePath);
if (!fileContent?.ValidationRule?.errorConditionFormula?.[0] ||
typeof fileContent.ValidationRule.errorConditionFormula[0] !== "string") {
return {
sObject,
automation: "ValidationRule",
name,
outcome: IMPLEMENTATION_OUTCOME.FAILED,
comment: "Invalid validation rule format or missing error condition formula",
};
}
const validationRuleContent = fileContent.ValidationRule.errorConditionFormula[0];
const bypassPermissionName = `$Permission.Bypass${sObject}VRs`;
if (typeof validationRuleContent === "string" &&
(validationRuleContent.includes(bypassPermissionName) || validationRuleContent.includes('BypassAllVRs'))) {
return {
sObject,
automation: "ValidationRule",
name,
outcome: IMPLEMENTATION_OUTCOME.IGNORED,
comment: "SFDX-Hardis Bypass already implemented",
};
}
if (typeof validationRuleContent === "string" && /bypass/i.test(validationRuleContent)) {
return {
sObject,
automation: "ValidationRule",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "Another bypass mechanism exists",
};
}
fileContent.ValidationRule.errorConditionFormula[0] = `AND( AND(NOT(${bypassPermissionName}), NOT($Permission.BypassAllVRs)), ${validationRuleContent})`;
await writeXmlFile(filePath, fileContent);
return {
sObject,
automation: "ValidationRule",
name,
outcome: IMPLEMENTATION_OUTCOME.ADDED,
comment: "SFDX-Hardis Bypass implemented",
};
}
catch (error) {
return {
sObject,
automation: "ValidationRule",
name,
outcome: IMPLEMENTATION_OUTCOME.FAILED,
comment: `Error processing file : ${error}`,
};
}
}
async applyBypassToValidationRules(connection, sObjects) {
const validationRuleRecords = await this.queryValidationRules(connection, sObjects);
if (!validationRuleRecords || validationRuleRecords.records.length === 0) {
uxLog("log", this, c.grey("No validation rules found for the specified sObjects."));
}
uxLog("log", this, c.grey(`Processing ${validationRuleRecords.records.length} Validation Rules.`));
const eligibleMetadataFilePaths = [];
if (this.retrieveFromOrg) {
const retrievedValidationRulesChunks = await this.retrieveMetadataFiles(validationRuleRecords.records, "ValidationRule");
for (const retrievedValidationRules of retrievedValidationRulesChunks) {
if (retrievedValidationRules?.status !== 1 &&
retrievedValidationRules?.result?.files &&
Array.isArray(retrievedValidationRules.result.files) &&
retrievedValidationRules.result.files.length > 0) {
for (const metadataFile of retrievedValidationRules.result.files) {
if (metadataFile?.type !== "ValidationRule" ||
metadataFile?.problemType === "Error") {
continue;
}
const [sObject, name] = metadataFile.fullName.split(".");
const filePath = metadataFile.filePath;
eligibleMetadataFilePaths.push({ filePath, sObject, name });
}
}
else {
uxLog("log", this, c.grey("No Validation Rule files found in the retrieved metadata chunk."));
}
}
}
else {
if (validationRuleRecords?.records) {
for (const record of validationRuleRecords.records) {
const sObject = record.EntityDefinition.QualifiedApiName;
const name = record.ValidationName;
const filePath = await MetadataUtils.findMetaFileFromTypeAndName("ValidationRule", name);
if (filePath === null) {
this.reports.implementation.push({
sObject,
automation: "ValidationRule",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: `File not found locally.`,
});
}
else {
eligibleMetadataFilePaths.push({ filePath, sObject, name });
}
}
}
}
for (const eligibleMetadataFilePath of eligibleMetadataFilePaths) {
this.reports.implementation.push(await this.handleValidationRuleFile(eligibleMetadataFilePath.filePath, eligibleMetadataFilePath.sObject, eligibleMetadataFilePath.name));
}
}
// Triggers
async handleTriggerFile(filePath, name) {
try {
if (!fs.existsSync(filePath)) {
return {
sObject: null,
automation: "Trigger",
name,
outcome: IMPLEMENTATION_OUTCOME.FAILED,
comment: "File not found locally.",
};
}
const fileContent = fs.readFileSync(filePath, "utf-8");
if (typeof fileContent !== "string") {
return {
sObject: null,
automation: "Trigger",
name,
outcome: IMPLEMENTATION_OUTCOME.FAILED,
comment: "Invalid file content format",
};
}
const match = fileContent.match(/trigger\s+\w+\s+on\s+(\w+)\s*\([^)]*\)\s*{\s*/i);
if (!match) {
return {
sObject: null,
automation: "Trigger",
name,
outcome: IMPLEMENTATION_OUTCOME.FAILED,
comment: "Unable to detect sObject",
};
}
const sObject = match[1].replace(/__c$/, "");
const bypassCheckLine = `if(FeatureManagement.checkPermission('Bypass${sObject}Triggers') || FeatureManagement.checkPermission('BypassAllTriggers')) { return; }`;
if (fileContent.includes(bypassCheckLine)) {
return {
sObject,
automation: "Trigger",
name,
outcome: IMPLEMENTATION_OUTCOME.IGNORED,
comment: "Bypass already implemented",
};
}
if (/bypass|PAD\.can/i.test(fileContent)) {
return {
sObject,
automation: "Trigger",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "Another bypass exists",
};
}
const fullBypassLine = `${bypassCheckLine}${this.skipCredits ? "" : "// Updated " + CREDITS_TEXT}`;
const openBraceIndex = fileContent.indexOf("{");
const beforeBrace = fileContent.substring(0, openBraceIndex + 1);
const afterBrace = fileContent.substring(openBraceIndex + 1).trimStart();
fsExtra.ensureDirSync(path.dirname(filePath));
fs.writeFileSync(filePath, `${beforeBrace}\n\t${fullBypassLine}\n\t${afterBrace}`, "utf-8");
return {
sObject,
automation: "Trigger",
name,
outcome: IMPLEMENTATION_OUTCOME.ADDED,
comment: "Bypass implemented",
};
}
catch (error) {
return {
sObject: null,
automation: "Trigger",
name,
outcome: IMPLEMENTATION_OUTCOME.FAILED,
comment: `Error processing file : ${error}`,
};
}
}
async applyBypassToTriggers(connection, sObjects) {
const triggerResults = await this.queryTriggers(connection);
const filteredTriggersResults = this.filterTriggerResults(triggerResults, sObjects);
if (!filteredTriggersResults || filteredTriggersResults?.length === 0) {
uxLog("log", this, c.grey("No triggers found for the specified sObjects."));
}
const eligibleMetadataFilePaths = [];
if (this.retrieveFromOrg) {
const retrievedTriggersChunks = await this.retrieveMetadataFiles(filteredTriggersResults, "ApexTrigger");
for (const retrievedTriggers of retrievedTriggersChunks) {
if (retrievedTriggers?.status !== 1 &&
retrievedTriggers?.result?.files &&
Array.isArray(retrievedTriggers.result.files) &&
retrievedTriggers.result.files.length > 0) {
for (const metadataFile of retrievedTriggers.result.files) {
if (metadataFile?.type !== "ApexTrigger" ||
!metadataFile?.filePath?.endsWith(".trigger") ||
metadataFile?.problemType === "Error") {
continue;
}
const name = metadataFile.fullName;
const filePath = metadataFile.filePath;
eligibleMetadataFilePaths.push({ filePath, name });
}
}
else {
uxLog("log", this, c.grey("No Trigger files found in the retrieved metadata chunk."));
}
}
}
else {
if (filteredTriggersResults) {
for (const record of filteredTriggersResults) {
const name = record.Name;
const filePath = await MetadataUtils.findMetaFileFromTypeAndName("ApexTrigger", name);
if (filePath === null) {
this.reports.implementation.push({
sObject: null,
automation: "Trigger",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: `File not found locally.`,
});
}
else {
eligibleMetadataFilePaths.push({ filePath, name });
}
}
}
}
for (const eligibleMetadataFilePath of eligibleMetadataFilePaths) {
this.reports.implementation.push(await this.handleTriggerFile(eligibleMetadataFilePath.filePath, eligibleMetadataFilePath.name));
}
}
// Flows
async handleFlowFile(filePath, name) {
try {
if (!fs.existsSync(filePath)) {
return {
sObject: null,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.FAILED,
comment: "File not found",
};
}
const fileContent = await parseXmlFile(filePath);
// Validate Flow structure - ensure we have a start element
if (!fileContent?.Flow?.start || !Array.isArray(fileContent.Flow.start) || fileContent.Flow.start.length === 0) {
return {
sObject: null,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "Flow does not have a start element (may be scheduled, autolaunched, or screen flow)",
};
}
const startElement = fileContent.Flow.start[0];
// Validate that start element has an object (record-triggered flow)
const sObject = startElement?.object?.[0] ?? null;
if (sObject == null) {
return {
sObject: null,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "No sObject found (may be scheduled, autolaunched, or screen flow)",
};
}
// Validate that start element has connector and targetReference
if (!startElement.connector || !Array.isArray(startElement.connector) || startElement.connector.length === 0) {
return {
sObject,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "Flow start element does not have a connector",
};
}
const connector = startElement.connector[0];
if (!connector.targetReference || !Array.isArray(connector.targetReference) || connector.targetReference.length === 0) {
return {
sObject,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "Flow start connector does not have a targetReference",
};
}
const filterFormula = startElement?.filterFormula?.[0] ?? null;
// Check if a bypass already exists in formula mode
if (filterFormula && typeof filterFormula === "string" && /bypass/i.test(filterFormula)) {
return {
sObject,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "Another bypass mechanism exists",
};
}
const firstNodeName = connector.targetReference?.[0] ?? null;
if (firstNodeName === null) {
return {
sObject,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "Flow has no start connector or target reference",
};
}
if (firstNodeName === 'SFDX_HARDIS_FLOW_BYPASS_DO_NOT_RENAME') {
return {
sObject,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.IGNORED,
comment: "SFDX-Hardis Bypass already implemented",
};
}
if (!Object.keys(fileContent.Flow ?? {}).includes('decisions')) {
fileContent.Flow.decisions = [];
}
fileContent.Flow.decisions.push({
"name": ["SFDX_HARDIS_FLOW_BYPASS_DO_NOT_RENAME"],
"label": ["Is Bypass Activated?"],
"description": ["Check if the bypass custom permission is assigned to the running user." + (this.skipCredits ? "" : " " + CREDITS_TEXT)],
"locationX": ["0"],
"locationY": ["0"],
"defaultConnectorLabel": ["No"],
"rules": [
{
"name": ["SFDX_HARDIS_BypassYes"],
"conditionLogic": ["or"],
"conditions": [
{
"leftValueReference": [`$Permission.Bypass${sObject}Flows`],
"operator": ["EqualTo"],
"rightValue": [{ "booleanValue": ["true"] }]
},
{
"leftValueReference": ["$Permission.BypassAllFlows"],
"operator": ["EqualTo"],
"rightValue": [{ "booleanValue": ["true"] }]
}
],
"label": ["Yes"],
"connector": [{ "targetReference": [firstNodeName] }]
}
]
});
// Ensure the start connector and its targetReference array exist before assignment
const startArray = fileContent.Flow.start;
if (!Array.isArray(startArray) ||
startArray.length === 0 ||
!Array.isArray(startArray[0].connector) ||
startArray[0].connector.length === 0 ||
!Array.isArray(startArray[0].connector[0].targetReference)) {
return {
sObject,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "Flow start connector not found; cannot attach bypass decision",
};
}
connector.targetReference[0] = 'SFDX_HARDIS_FLOW_BYPASS_DO_NOT_RENAME';
await writeXmlFile(filePath, fileContent);
return {
sObject,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.ADDED,
comment: "Bypass implemented",
};
}
catch (error) {
return {
sObject: null,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.FAILED,
comment: `Error processing file : ${error}`,
};
}
}
async applyBypassToFlows(connection, sObjects) {
const flowResults = await this.queryFlows(connection);
const filteredFlowResults = this.filterFlowResults(flowResults, sObjects);
if (!filteredFlowResults || filteredFlowResults?.length === 0) {
uxLog("log", this, c.grey("No flows found for the specified sObjects."));
}
const eligibleMetadataFilePaths = [];
if (this.retrieveFromOrg) {
const retrievedFlowChunks = await this.retrieveMetadataFiles(filteredFlowResults, "Flow");
for (const retrievedFlows of retrievedFlowChunks) {
if (retrievedFlows?.status !== 1 &&
retrievedFlows?.result?.files &&
Array.isArray(retrievedFlows.result.files) &&
retrievedFlows.result.files.length > 0) {
for (const metadataFile of retrievedFlows.result.files) {
if (metadataFile?.type !== "Flow" ||
metadataFile?.problemType === "Error") {
continue;
}
const name = metadataFile.fullName;
const filePath = metadataFile.filePath;
eligibleMetadataFilePaths.push({ filePath, name });
}
}
else {
uxLog("log", this, c.grey("No Flow files found in the retrieved metadata chunk."));
}
}
}
else {
if (filteredFlowResults) {
for (const record of filteredFlowResults) {
const name = record.ApiName;
const filePath = await MetadataUtils.findMetaFileFromTypeAndName("Flow", name);
if (filePath === null) {
this.reports.implementation.push({
sObject: null,
automation: "Flow",
name,
outcome: IMPLEMENTATION_OUTCOME.SKIPPED,
comment: "File not found locally",
});
}
else {
eligibleMetadataFilePaths.push({ filePath, name });
}
}
}
}
for (const eligibleMetadataFilePath of eligibleMetadataFilePaths) {
this.reports.implementation.push(await this.handleFlowFile(eligibleMetadataFilePath.filePath, eligibleMetadataFilePath.name));
}
}
}
/**
* Command TODO:
* - Add option to determine sobjects directly from current project instead of calling API
* - While generating, check if the metadata file already exists, if yes, warning
* - Before implementing, check if the permission set file exists, if not, warning
*/
//# sourceMappingURL=bypass.js.map