sfdx-hardis 5.43.5 → 5.44.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/lib/commands/hardis/org/diagnose/audittrail.js +14 -1
- package/lib/commands/hardis/org/diagnose/audittrail.js.map +1 -1
- package/lib/commands/hardis/project/generate/bypass.d.ts +30 -7
- package/lib/commands/hardis/project/generate/bypass.js +487 -113
- package/lib/commands/hardis/project/generate/bypass.js.map +1 -1
- package/lib/common/utils/branchStrategyMermaidBuilder.js +9 -2
- package/lib/common/utils/branchStrategyMermaidBuilder.js.map +1 -1
- package/oclif.lock +101 -115
- package/oclif.manifest.json +765 -736
- package/package.json +15 -15
|
@@ -1,89 +1,260 @@
|
|
|
1
|
-
import { requiredOrgFlagWithDeprecations, SfCommand } from
|
|
2
|
-
import { Flags } from
|
|
3
|
-
import { SfError, Messages } from
|
|
4
|
-
import { soqlQuery } from
|
|
5
|
-
import { uxLog } from
|
|
6
|
-
import { prompts } from
|
|
7
|
-
import c from
|
|
8
|
-
import path from
|
|
9
|
-
import fs from
|
|
1
|
+
import { requiredOrgFlagWithDeprecations, SfCommand, } from "@salesforce/sf-plugins-core";
|
|
2
|
+
import { Flags } from "@salesforce/sf-plugins-core";
|
|
3
|
+
import { SfError, Messages } from "@salesforce/core";
|
|
4
|
+
import { soqlQuery, soqlQueryTooling, } from "../../../../common/utils/apiUtils.js";
|
|
5
|
+
import { execCommand, uxLog } from "../../../../common/utils/index.js";
|
|
6
|
+
import { prompts } from "../../../../common/utils/prompts.js";
|
|
7
|
+
import c from "chalk";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import * as fsExtra from "fs-extra";
|
|
10
11
|
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
|
|
11
|
-
const messages = Messages.loadMessages(
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const messages = Messages.loadMessages("sfdx-hardis", "org");
|
|
13
|
+
import { parseXmlFile, writeXmlFile, } from "../../../../common/utils/xmlUtils.js";
|
|
14
|
+
import { MetadataUtils } from "../../../../common/metadata-utils/index.js";
|
|
15
|
+
// Constants
|
|
16
|
+
const ALLOWED_AUTOMATIONS = ["Flow", "Trigger", "VR"];
|
|
17
|
+
const CREDITS_TEXT = "by sfdx-hardis : https://sfdx-hardis.cloudity.com/hardis/project/generate/bypass/";
|
|
18
|
+
const STATUS = {
|
|
19
|
+
ADDED: "added",
|
|
20
|
+
SKIPPED: "skipped",
|
|
21
|
+
IGNORED: "ignored",
|
|
22
|
+
FAILED: "failed",
|
|
23
|
+
};
|
|
14
24
|
export default class HardisProjectGenerateBypass extends SfCommand {
|
|
25
|
+
skipCredits = false;
|
|
26
|
+
retrieveFromOrg;
|
|
15
27
|
static flags = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
"target-org": requiredOrgFlagWithDeprecations,
|
|
29
|
+
objects: Flags.string({
|
|
30
|
+
aliases: ["sObjects"],
|
|
31
|
+
char: "s",
|
|
32
|
+
description: "Comma-separated list of sObjects to bypass (e.g., Account,Contact,Opportunity). If omitted, you will be prompted to select.",
|
|
20
33
|
required: false,
|
|
21
34
|
}),
|
|
22
|
-
|
|
23
|
-
char:
|
|
24
|
-
description: `Comma-separated
|
|
35
|
+
automations: Flags.string({
|
|
36
|
+
char: "a",
|
|
37
|
+
description: `Comma-separated automations to bypass: ${ALLOWED_AUTOMATIONS.join(", ")}`,
|
|
25
38
|
required: false,
|
|
26
39
|
}),
|
|
27
|
-
'global': Flags.boolean({
|
|
28
|
-
char: 'g',
|
|
29
|
-
description: 'Generate global bypasses for all automations (Flow, Trigger, VR) without selecting sObjects.',
|
|
30
|
-
required: false,
|
|
31
|
-
default: false
|
|
32
|
-
}),
|
|
33
40
|
websocket: Flags.string({
|
|
34
|
-
description: messages.getMessage(
|
|
41
|
+
description: messages.getMessage("websocket"),
|
|
35
42
|
}),
|
|
36
43
|
skipauth: Flags.boolean({
|
|
37
|
-
description:
|
|
44
|
+
description: "Skip authentication check when a default username is required",
|
|
45
|
+
}),
|
|
46
|
+
"skip-credits": Flags.boolean({
|
|
47
|
+
aliases: ["skipCredits"],
|
|
48
|
+
char: "k",
|
|
49
|
+
description: 'Omit the "Generated by" line in the XML files',
|
|
50
|
+
required: false,
|
|
51
|
+
default: false,
|
|
52
|
+
}),
|
|
53
|
+
"apply-to-vrs": Flags.boolean({
|
|
54
|
+
aliases: ["applyToVrs"],
|
|
55
|
+
description: "Apply bypass to Validation Rules",
|
|
56
|
+
required: false,
|
|
57
|
+
default: false,
|
|
38
58
|
}),
|
|
39
|
-
|
|
40
|
-
aliases: [
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
"apply-to-triggers": Flags.boolean({
|
|
60
|
+
aliases: ["applyToTriggers"],
|
|
61
|
+
description: "Apply bypass to Triggers",
|
|
62
|
+
required: false,
|
|
63
|
+
default: false,
|
|
64
|
+
}),
|
|
65
|
+
"metadata-source": Flags.string({
|
|
66
|
+
char: "r",
|
|
67
|
+
aliases: ["metadataSource"],
|
|
68
|
+
description: "Source of metadata elements to apply bypass to. Options: 'org' or 'local'.",
|
|
43
69
|
required: false,
|
|
44
|
-
default: false
|
|
45
70
|
}),
|
|
46
71
|
};
|
|
47
72
|
static description = `
|
|
48
73
|
Generates bypass custom permissions and permission sets for specified sObjects and automations (Flows, Triggers, and Validation Rules). If no parameters are provided, it prompts for user selection.
|
|
49
74
|
`;
|
|
50
75
|
static examples = [
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
"$ sf hardis:project:generate:bypass",
|
|
77
|
+
"$ sf hardis:project:generate:bypass --sObjects Account,Contact,Opportunity",
|
|
78
|
+
"$ sf hardis:project:generate:bypass --automations Flow,Trigger,VR",
|
|
79
|
+
"$ sf hardis:project:generate:bypass --sObjects Account,Opportunity --automations Flow,Trigger",
|
|
80
|
+
"$ sf hardis:project:generate:bypass --skipCredits",
|
|
81
|
+
"$ sf hardis:project:generate:bypass --apply-to-vrs",
|
|
82
|
+
"$ sf hardis:project:generate:bypass --apply-to-triggers",
|
|
83
|
+
"$ sf hardis:project:generate:bypass --metadata-source org",
|
|
57
84
|
];
|
|
85
|
+
// Main run method
|
|
86
|
+
async run() {
|
|
87
|
+
// Collect options
|
|
88
|
+
const { flags } = await this.parse(HardisProjectGenerateBypass);
|
|
89
|
+
const connection = flags["target-org"].getConnection();
|
|
90
|
+
if (flags["metadata-source"] !== undefined &&
|
|
91
|
+
flags["metadata-source"] !== null) {
|
|
92
|
+
this.retrieveFromOrg =
|
|
93
|
+
String(flags["metadata-source"]).trim().toLowerCase() === "org";
|
|
94
|
+
}
|
|
95
|
+
this.skipCredits = flags["skip-credits"] || false;
|
|
96
|
+
let applyToTriggers = flags["apply-to-triggers"] || null;
|
|
97
|
+
let applyToVrs = flags["apply-to-vrs"] || null;
|
|
98
|
+
const sObjects = flags.objects || null;
|
|
99
|
+
const automations = flags.automations || null;
|
|
100
|
+
const availableSObjects = await this.getFilteredSObjects(connection);
|
|
101
|
+
let targetSObjects = {};
|
|
102
|
+
let targetAutomations = [];
|
|
103
|
+
// Filter objects
|
|
104
|
+
if (sObjects) {
|
|
105
|
+
const sObjectsFromFlag = flags.sObjects.split(",").map((s) => s.trim());
|
|
106
|
+
targetSObjects = Object.fromEntries(Object.entries(availableSObjects).filter(([key]) => {
|
|
107
|
+
const res = sObjectsFromFlag.includes(key);
|
|
108
|
+
if (!res) {
|
|
109
|
+
uxLog(this, c.yellow(`Warning: sObject "${key}" is not available or not customizable. Skipping.`));
|
|
110
|
+
}
|
|
111
|
+
return res;
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
if (automations) {
|
|
115
|
+
targetAutomations = automations
|
|
116
|
+
.split(",")
|
|
117
|
+
.map((s) => s.trim())
|
|
118
|
+
.filter((s) => ALLOWED_AUTOMATIONS.includes(s));
|
|
119
|
+
}
|
|
120
|
+
// Generate global bypasses
|
|
121
|
+
this.generateFiles({ All: "All" }, ALLOWED_AUTOMATIONS);
|
|
122
|
+
// Handle prompts if needed
|
|
123
|
+
const promptsNeeded = [];
|
|
124
|
+
if (!Object.keys(targetSObjects).length) {
|
|
125
|
+
promptsNeeded.push({
|
|
126
|
+
type: "multiselect",
|
|
127
|
+
name: "sobjects",
|
|
128
|
+
message: "Select sObjects for bypass",
|
|
129
|
+
choices: Object.entries(availableSObjects).map(([devName, label]) => ({
|
|
130
|
+
title: label,
|
|
131
|
+
value: devName,
|
|
132
|
+
})),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (!targetAutomations.length) {
|
|
136
|
+
promptsNeeded.push({
|
|
137
|
+
type: "multiselect",
|
|
138
|
+
name: "automations",
|
|
139
|
+
message: "Select automations to bypass",
|
|
140
|
+
choices: ALLOWED_AUTOMATIONS.map((a) => ({ title: a, value: a })),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (applyToVrs == null && applyToTriggers == null) {
|
|
144
|
+
promptsNeeded.push({
|
|
145
|
+
type: "multiselect",
|
|
146
|
+
name: "applyTo",
|
|
147
|
+
message: "To which automations do you want to automatically apply the bypass?",
|
|
148
|
+
choices: [
|
|
149
|
+
{ title: "Validation Rules", value: "applyToVrs" },
|
|
150
|
+
{ title: "Triggers", value: "applyToTriggers" },
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (this.retrieveFromOrg == undefined || this.retrieveFromOrg == null) {
|
|
155
|
+
promptsNeeded.push({
|
|
156
|
+
type: "select",
|
|
157
|
+
name: "elementSource",
|
|
158
|
+
message: "Where do you want to get the elements to apply bypass to?",
|
|
159
|
+
choices: [
|
|
160
|
+
{ title: "Retrieve from org (recommended)", value: "org" },
|
|
161
|
+
{ title: "Use local elements in the project", value: "local" },
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (promptsNeeded.length) {
|
|
166
|
+
const promptResults = await prompts(promptsNeeded);
|
|
167
|
+
if (promptResults.sobjects) {
|
|
168
|
+
targetSObjects = Object.fromEntries(Object.entries(availableSObjects).filter(([key]) => promptResults.sobjects.includes(key)));
|
|
169
|
+
}
|
|
170
|
+
if (promptResults.automations) {
|
|
171
|
+
targetAutomations = promptResults.automations;
|
|
172
|
+
}
|
|
173
|
+
if (!applyToTriggers) {
|
|
174
|
+
applyToTriggers = promptResults.applyTo?.includes("applyToTriggers");
|
|
175
|
+
}
|
|
176
|
+
if (!applyToVrs) {
|
|
177
|
+
applyToVrs = promptResults.applyTo?.includes("applyToVrs");
|
|
178
|
+
}
|
|
179
|
+
if (promptResults.elementSource) {
|
|
180
|
+
this.retrieveFromOrg = promptResults.elementSource === "org";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Validate selections
|
|
184
|
+
if (!Object.keys(targetSObjects).length) {
|
|
185
|
+
throw new SfError(c.red("ERROR: You must select at least one sObject."));
|
|
186
|
+
}
|
|
187
|
+
if (!targetAutomations.length) {
|
|
188
|
+
throw new SfError(c.red("ERROR: You must select at least one automation type."));
|
|
189
|
+
}
|
|
190
|
+
// Generate files and apply bypasses
|
|
191
|
+
this.generateFiles(targetSObjects, targetAutomations);
|
|
192
|
+
if (applyToVrs) {
|
|
193
|
+
await this.applyBypassToValidationRules(connection, targetSObjects);
|
|
194
|
+
}
|
|
195
|
+
if (applyToTriggers) {
|
|
196
|
+
await this.applyBypassToTriggers(connection, targetSObjects);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
outputString: "Generated bypass custom permissions and permission sets",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// Query methods
|
|
58
203
|
async querySObjects(connection) {
|
|
59
204
|
const sObjectsQuery = `
|
|
60
|
-
Select Id, Label, DeveloperName, DurableId,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
205
|
+
Select Id, Label, DeveloperName, QualifiedApiName, DurableId, IsTriggerable, IsCustomizable, IsApexTriggerable
|
|
206
|
+
FROM EntityDefinition WHERE IsTriggerable = true AND IsCustomizable = true and IsCustomSetting = false ORDER BY DeveloperName`;
|
|
207
|
+
const results = await soqlQuery(sObjectsQuery, connection);
|
|
208
|
+
uxLog(this, `Found ${results.records.length} sObjects.`);
|
|
209
|
+
return results;
|
|
64
210
|
}
|
|
65
211
|
async getFilteredSObjects(connection) {
|
|
66
212
|
const sObjectResults = await this.querySObjects(connection);
|
|
67
213
|
const sObjectsDict = {};
|
|
68
|
-
sObjectResults.records
|
|
69
|
-
if (!record.DeveloperName.endsWith(
|
|
70
|
-
|
|
214
|
+
for (const record of sObjectResults.records) {
|
|
215
|
+
if (!record.DeveloperName.endsWith("__Share") &&
|
|
216
|
+
!record.DeveloperName.endsWith("__ChangeEvent")) {
|
|
217
|
+
sObjectsDict[record.DeveloperName] = `${record.Label} (${record.QualifiedApiName})`;
|
|
71
218
|
}
|
|
72
|
-
}
|
|
219
|
+
}
|
|
73
220
|
return sObjectsDict;
|
|
74
221
|
}
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
222
|
+
async queryTriggers(connection) {
|
|
223
|
+
const query = `SELECT Id, Name, Status, IsValid, Body, BodyCrc, TableEnumOrId, ManageableState From ApexTrigger WHERE ManageableState != 'installed'`;
|
|
224
|
+
const results = await soqlQueryTooling(query, connection);
|
|
225
|
+
uxLog(this, `Found ${results.records.length} Triggers.`);
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
228
|
+
filterTriggerResults(triggerResults, sObjects) {
|
|
229
|
+
return triggerResults.records.filter((trigger) => {
|
|
230
|
+
const sObjectApiNameWithoutC = trigger.TableEnumOrId?.replace("__c", "");
|
|
231
|
+
return (sObjectApiNameWithoutC &&
|
|
232
|
+
Object.keys(sObjects).includes(sObjectApiNameWithoutC) &&
|
|
233
|
+
trigger.Body != "(hidden)");
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async queryValidationRules(connection, sObjects) {
|
|
237
|
+
const query = `SELECT ValidationName, EntityDefinition.QualifiedApiName, ManageableState FROM ValidationRule
|
|
238
|
+
WHERE ManageableState != 'installed' AND EntityDefinition.DeveloperName IN (${Object.keys(sObjects)
|
|
239
|
+
.map((s) => `'${s}'`)
|
|
240
|
+
.join(", ")})`;
|
|
241
|
+
const results = await soqlQueryTooling(query, connection);
|
|
242
|
+
uxLog(this, `Found ${results.records.length} Validation Rules.`);
|
|
243
|
+
return results;
|
|
244
|
+
}
|
|
245
|
+
// XML Generation
|
|
246
|
+
generateXML(type, sObject, automation) {
|
|
247
|
+
const creditsText = this.skipCredits ? "" : `Generated ${CREDITS_TEXT}`;
|
|
248
|
+
if (type === "customPermission") {
|
|
249
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
78
250
|
<CustomPermission xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
79
251
|
<isLicensed>false</isLicensed>
|
|
80
252
|
<label>Bypass ${automation}s for ${sObject}</label>
|
|
81
253
|
<description>If assigned (through a Permission Set), this Custom Permission will disable the execution of ${automation}s defined on the ${sObject} sObject.${creditsText}</description>
|
|
82
254
|
</CustomPermission>`;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
87
258
|
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
88
259
|
<customPermissions>
|
|
89
260
|
<enabled>true</enabled>
|
|
@@ -93,83 +264,286 @@ export default class HardisProjectGenerateBypass extends SfCommand {
|
|
|
93
264
|
<label>Bypass ${automation}s for ${sObject}</label>
|
|
94
265
|
<description>If assigned, this Permission Set will disable the execution of ${automation}s defined on the ${sObject} sObject.${creditsText}</description>
|
|
95
266
|
</PermissionSet>`;
|
|
267
|
+
}
|
|
96
268
|
}
|
|
97
|
-
generateXMLFiles(sObject, automation
|
|
269
|
+
generateXMLFiles(sObject, automation) {
|
|
98
270
|
const customPermissionFile = path.join(`force-app/main/default/customPermissions/Bypass${sObject}${automation}s.customPermission-meta.xml`);
|
|
99
271
|
const permissionSetFile = path.join(`force-app/main/default/permissionsets/Bypass${sObject}${automation}s.permissionset-meta.xml`);
|
|
100
|
-
|
|
101
|
-
fs.
|
|
102
|
-
|
|
103
|
-
fs.writeFileSync(permissionSetFile, this.
|
|
272
|
+
fsExtra.ensureDirSync(path.dirname(customPermissionFile));
|
|
273
|
+
fs.writeFileSync(customPermissionFile, this.generateXML("customPermission", sObject, automation), "utf-8");
|
|
274
|
+
fsExtra.ensureDirSync(path.dirname(permissionSetFile));
|
|
275
|
+
fs.writeFileSync(permissionSetFile, this.generateXML("permissionSet", sObject, automation), "utf-8");
|
|
104
276
|
uxLog(this, `Created: ${path.basename(customPermissionFile)} for ${sObject}`);
|
|
105
277
|
uxLog(this, `Created: ${path.basename(permissionSetFile)} for ${sObject}`);
|
|
106
278
|
}
|
|
107
|
-
generateFiles(targetSObjects, targetAutomations
|
|
108
|
-
Object.
|
|
109
|
-
targetAutomations.forEach(automation => {
|
|
110
|
-
this.generateXMLFiles(developerName, automation
|
|
279
|
+
generateFiles(targetSObjects, targetAutomations) {
|
|
280
|
+
Object.keys(targetSObjects).forEach((developerName) => {
|
|
281
|
+
targetAutomations.forEach((automation) => {
|
|
282
|
+
this.generateXMLFiles(developerName, automation);
|
|
111
283
|
});
|
|
112
284
|
});
|
|
113
285
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
286
|
+
// Metadata handling
|
|
287
|
+
async retrieveMetadataFiles(records, metadataType) {
|
|
288
|
+
const recordsChunks = this.chunkArray(records);
|
|
289
|
+
const results = [];
|
|
290
|
+
for (const chunk of recordsChunks) {
|
|
291
|
+
let command = `sf project retrieve start --metadata`;
|
|
292
|
+
command += chunk
|
|
293
|
+
.map((record) => {
|
|
294
|
+
return metadataType === "ValidationRule"
|
|
295
|
+
? ` ValidationRule:${record.EntityDefinition.QualifiedApiName}.${record.ValidationName}`
|
|
296
|
+
: ` ApexTrigger:${record.Name}`;
|
|
297
|
+
})
|
|
298
|
+
.join(" ");
|
|
299
|
+
try {
|
|
300
|
+
const result = await execCommand(`${command} --ignore-conflicts --json`, this, {
|
|
301
|
+
debug: false,
|
|
302
|
+
retry: {
|
|
303
|
+
retryDelay: 30,
|
|
304
|
+
retryStringConstraint: "error",
|
|
305
|
+
retryMaxAttempts: 3,
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
results.push(result);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
uxLog(this, c.red(`Error retrieving ${metadataType}: ${error}`));
|
|
312
|
+
}
|
|
139
313
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
chunkArray(array, chunkSize = 25) {
|
|
317
|
+
return Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => array.slice(i * chunkSize, (i + 1) * chunkSize));
|
|
318
|
+
}
|
|
319
|
+
// Validation Rules
|
|
320
|
+
async handleValidationRuleFile(filePath, sObject, name) {
|
|
321
|
+
try {
|
|
322
|
+
const fileContent = await parseXmlFile(filePath);
|
|
323
|
+
if (!fileContent?.ValidationRule?.errorConditionFormula?.[0] ||
|
|
324
|
+
typeof fileContent.ValidationRule.errorConditionFormula[0] !== "string") {
|
|
325
|
+
return {
|
|
326
|
+
sObject,
|
|
327
|
+
name,
|
|
328
|
+
action: STATUS.FAILED,
|
|
329
|
+
comment: "Invalid validation rule format or missing error condition formula",
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const validationRuleContent = fileContent.ValidationRule.errorConditionFormula[0];
|
|
333
|
+
const bypassPermissionName = `$Permission.Bypass${sObject}VRs`;
|
|
334
|
+
if (typeof validationRuleContent === "string" &&
|
|
335
|
+
validationRuleContent.includes(bypassPermissionName)) {
|
|
336
|
+
return {
|
|
337
|
+
sObject,
|
|
338
|
+
name,
|
|
339
|
+
action: STATUS.IGNORED,
|
|
340
|
+
comment: "SFDX-Hardis Bypass already implemented",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (typeof validationRuleContent === "string" &&
|
|
344
|
+
/bypass/i.test(validationRuleContent)) {
|
|
345
|
+
return {
|
|
346
|
+
sObject,
|
|
347
|
+
name,
|
|
348
|
+
action: STATUS.SKIPPED,
|
|
349
|
+
comment: "Another bypass mechanism exists",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const creditsText = this.skipCredits
|
|
353
|
+
? ""
|
|
354
|
+
: `/* Updated ${CREDITS_TEXT} */
|
|
355
|
+
`;
|
|
356
|
+
fileContent.ValidationRule.errorConditionFormula[0] = `${creditsText}
|
|
357
|
+
AND( AND(NOT(${bypassPermissionName}), NOT($Permission.BypassAllVRs)), ${validationRuleContent})`;
|
|
358
|
+
await writeXmlFile(filePath, fileContent);
|
|
359
|
+
return {
|
|
360
|
+
sObject,
|
|
361
|
+
name,
|
|
362
|
+
action: STATUS.ADDED,
|
|
363
|
+
comment: "SFDX-Hardis Bypass implemented",
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
return {
|
|
368
|
+
sObject,
|
|
369
|
+
name,
|
|
370
|
+
action: STATUS.FAILED,
|
|
371
|
+
comment: `Error processing file : ${error}`,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async applyBypassToValidationRules(connection, sObjects) {
|
|
376
|
+
const validationRuleRecords = await this.queryValidationRules(connection, sObjects);
|
|
377
|
+
if (!validationRuleRecords || validationRuleRecords.records.length === 0) {
|
|
378
|
+
uxLog(this, "No validation rules found for the specified sObjects.");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
uxLog(this, `Processing ${validationRuleRecords.records.length} Validation Rules.`);
|
|
382
|
+
const validationRulesTableReport = [];
|
|
383
|
+
const eligibleMetadataFilePaths = [];
|
|
384
|
+
if (this.retrieveFromOrg) {
|
|
385
|
+
const retrievedValidationRulesChunks = await this.retrieveMetadataFiles(validationRuleRecords.records, "ValidationRule");
|
|
386
|
+
for (const retrievedValidationRules of retrievedValidationRulesChunks) {
|
|
387
|
+
if (retrievedValidationRules?.status !== 1 &&
|
|
388
|
+
retrievedValidationRules?.result?.files &&
|
|
389
|
+
Array.isArray(retrievedValidationRules.result.files) &&
|
|
390
|
+
retrievedValidationRules.result.files.length > 0) {
|
|
391
|
+
for (const metadataFile of retrievedValidationRules.result.files) {
|
|
392
|
+
if (metadataFile?.type !== "ValidationRule" ||
|
|
393
|
+
metadataFile?.problemType === "Error") {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const [sObject, name] = metadataFile.fullName.split(".");
|
|
397
|
+
const filePath = metadataFile.filePath;
|
|
398
|
+
eligibleMetadataFilePaths.push({ filePath, sObject, name });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
uxLog(this, "No Validation Rule files found in the retrieved metadata chunk.");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
151
405
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
406
|
+
else {
|
|
407
|
+
if (validationRuleRecords?.records) {
|
|
408
|
+
for (const record of validationRuleRecords.records) {
|
|
409
|
+
const sObject = record.EntityDefinition.QualifiedApiName;
|
|
410
|
+
const name = record.ValidationName;
|
|
411
|
+
const filePath = await MetadataUtils.findMetaFileFromTypeAndName("ValidationRule", name);
|
|
412
|
+
if (filePath === null) {
|
|
413
|
+
// TODO: add to report instead of log
|
|
414
|
+
uxLog(this, `The validation rule ${name} for sObject ${sObject} does not have a corresponding metadata file locally. Skipping.`);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
eligibleMetadataFilePaths.push({ filePath, sObject, name });
|
|
418
|
+
}
|
|
157
419
|
}
|
|
158
|
-
targetSObjects = Object.fromEntries(Object.entries(availableSObjects).filter(([key]) => promptSelection.sobjects.includes(key)));
|
|
159
420
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
421
|
+
}
|
|
422
|
+
for (const eligibleMetadataFilePath of eligibleMetadataFilePaths) {
|
|
423
|
+
validationRulesTableReport.push(await this.handleValidationRuleFile(eligibleMetadataFilePath.filePath, eligibleMetadataFilePath.sObject, eligibleMetadataFilePath.name));
|
|
424
|
+
}
|
|
425
|
+
console.table(validationRulesTableReport);
|
|
426
|
+
}
|
|
427
|
+
// Triggers
|
|
428
|
+
async handleTriggerFile(filePath, name) {
|
|
429
|
+
try {
|
|
430
|
+
if (!fs.existsSync(filePath)) {
|
|
431
|
+
return {
|
|
432
|
+
sObject: null,
|
|
433
|
+
name,
|
|
434
|
+
action: STATUS.FAILED,
|
|
435
|
+
comment: "File not found",
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
439
|
+
if (typeof fileContent !== "string") {
|
|
440
|
+
return {
|
|
441
|
+
sObject: null,
|
|
442
|
+
name,
|
|
443
|
+
action: STATUS.FAILED,
|
|
444
|
+
comment: "Invalid file content format",
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const match = fileContent.match(/trigger\s+\w+\s+on\s+(\w+)\s*\([^)]*\)\s*{\s*/i);
|
|
448
|
+
if (!match) {
|
|
449
|
+
return {
|
|
450
|
+
sObject: null,
|
|
451
|
+
name,
|
|
452
|
+
action: STATUS.FAILED,
|
|
453
|
+
comment: "Unable to detect sObject",
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const sObject = match[1].replace(/__c$/, "");
|
|
457
|
+
const bypassCheckLine = `if(FeatureManagement.checkPermission('Bypass${sObject}Triggers') || FeatureManagement.checkPermission('BypassAllTriggers')) { return; }`;
|
|
458
|
+
if (fileContent.includes(bypassCheckLine)) {
|
|
459
|
+
return {
|
|
460
|
+
sObject,
|
|
461
|
+
name,
|
|
462
|
+
action: STATUS.IGNORED,
|
|
463
|
+
comment: "Bypass already implemented",
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (/bypass|PAD\.can/i.test(fileContent)) {
|
|
467
|
+
return {
|
|
468
|
+
sObject,
|
|
469
|
+
name,
|
|
470
|
+
action: STATUS.SKIPPED,
|
|
471
|
+
comment: "Another bypass exists",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const fullBypassLine = `${bypassCheckLine}${this.skipCredits ? "" : "// Updated " + CREDITS_TEXT}`;
|
|
475
|
+
const openBraceIndex = fileContent.indexOf("{");
|
|
476
|
+
const beforeBrace = fileContent.substring(0, openBraceIndex + 1);
|
|
477
|
+
const afterBrace = fileContent.substring(openBraceIndex + 1).trimStart();
|
|
478
|
+
fsExtra.ensureDirSync(path.dirname(filePath));
|
|
479
|
+
fs.writeFileSync(filePath, `${beforeBrace}\n\t${fullBypassLine}\n\t${afterBrace}`, "utf-8");
|
|
480
|
+
return {
|
|
481
|
+
sObject,
|
|
482
|
+
name,
|
|
483
|
+
action: STATUS.ADDED,
|
|
484
|
+
comment: "Bypass implemented",
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
return {
|
|
489
|
+
sObject: null,
|
|
490
|
+
name,
|
|
491
|
+
action: STATUS.FAILED,
|
|
492
|
+
comment: `Error processing file : ${error}`,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async applyBypassToTriggers(connection, sObjects) {
|
|
497
|
+
const triggerResults = await this.queryTriggers(connection);
|
|
498
|
+
const filteredTriggersResults = this.filterTriggerResults(triggerResults, sObjects);
|
|
499
|
+
if (!filteredTriggersResults || filteredTriggersResults?.length === 0) {
|
|
500
|
+
uxLog(this, "No triggers found for the specified sObjects.");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const triggerReport = [];
|
|
504
|
+
const eligibleMetadataFilePaths = [];
|
|
505
|
+
if (this.retrieveFromOrg) {
|
|
506
|
+
const retrievedTriggersChunks = await this.retrieveMetadataFiles(filteredTriggersResults, "ApexTrigger");
|
|
507
|
+
for (const retrievedTriggers of retrievedTriggersChunks) {
|
|
508
|
+
if (retrievedTriggers?.status !== 1 &&
|
|
509
|
+
retrievedTriggers?.result?.files &&
|
|
510
|
+
Array.isArray(retrievedTriggers.result.files) &&
|
|
511
|
+
retrievedTriggers.result.files.length > 0) {
|
|
512
|
+
for (const metadataFile of retrievedTriggers.result.files) {
|
|
513
|
+
if (metadataFile?.type !== "ApexTrigger" ||
|
|
514
|
+
!metadataFile?.filePath?.endsWith(".trigger") ||
|
|
515
|
+
metadataFile?.problemType === "Error") {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const name = metadataFile.fullName;
|
|
519
|
+
const filePath = metadataFile.filePath;
|
|
520
|
+
eligibleMetadataFilePaths.push({ filePath, name });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
uxLog(this, "No Trigger files found in the retrieved metadata chunk.");
|
|
163
525
|
}
|
|
164
|
-
targetAutomations = promptSelection.automations;
|
|
165
526
|
}
|
|
166
527
|
}
|
|
167
|
-
|
|
168
|
-
|
|
528
|
+
else {
|
|
529
|
+
if (filteredTriggersResults) {
|
|
530
|
+
for (const record of filteredTriggersResults) {
|
|
531
|
+
const name = record.Name;
|
|
532
|
+
const filePath = await MetadataUtils.findMetaFileFromTypeAndName("ApexTrigger", name);
|
|
533
|
+
if (filePath === null) {
|
|
534
|
+
// TODO: add to report instead of log
|
|
535
|
+
uxLog(this, `The trigger ${name} does not have a corresponding metadata file locally. Skipping.`);
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
eligibleMetadataFilePaths.push({ filePath, name });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
169
542
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
543
|
+
for (const eligibleMetadataFilePath of eligibleMetadataFilePaths) {
|
|
544
|
+
triggerReport.push(await this.handleTriggerFile(eligibleMetadataFilePath.filePath, eligibleMetadataFilePath.name));
|
|
545
|
+
}
|
|
546
|
+
console.table(triggerReport);
|
|
173
547
|
}
|
|
174
548
|
}
|
|
175
549
|
//# sourceMappingURL=bypass.js.map
|