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.
@@ -1,89 +1,260 @@
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 } from '../../../../common/utils/apiUtils.js';
5
- import { 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-extra';
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('sfdx-hardis', 'org');
12
- const ALLOWED_AUTOMATIONS = ['Flow', 'Trigger', 'VR'];
13
- const CREDITS_TEXT = 'Generated by sfdx-hardis : https://sfdx-hardis.cloudity.com/hardis/project/generate/bypass/';
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
- 'target-org': requiredOrgFlagWithDeprecations,
17
- 'sObjects': Flags.string({
18
- char: 's',
19
- description: 'Comma-separated list of sObjects to bypass (e.g., Account,Contact,Opportunity). If omitted, you will be prompted to select.',
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
- 'automations': Flags.string({
23
- char: 'a',
24
- description: `Comma-separated list of automations to bypass. Allowed values: ${ALLOWED_AUTOMATIONS.join(', ')}`,
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('websocket'),
41
+ description: messages.getMessage("websocket"),
35
42
  }),
36
43
  skipauth: Flags.boolean({
37
- description: 'Skip authentication check when a default username is required',
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
- 'skipCredits': Flags.boolean({
40
- aliases: ['skip-credits'],
41
- char: 'k',
42
- description: 'If true, omit the "Generated by" line in the XML files.',
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
- '$ sf hardis:project:generate:bypass',
52
- '$ sf hardis:project:generate:bypass --global',
53
- '$ sf hardis:project:generate:bypass --sObjects Account,Contact,Opportunity',
54
- '$ sf hardis:project:generate:bypass --automations Flow,Trigger,VR',
55
- '$ sf hardis:project:generate:bypass --sObjects Account,Opportunity --automations Flow,Trigger',
56
- '$ sf hardis:project:generate:bypass --skip-credits',
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, IsTriggerable, IsCustomizable, IsApexTriggerable, NamespacePrefix, PublisherId FROM EntityDefinition WHERE IsTriggerable = true AND IsCustomizable = true and IsCustomSetting = false ORDER BY DeveloperName`;
61
- const sObjectResults = await soqlQuery(sObjectsQuery, connection);
62
- uxLog(this, `Found ${sObjectResults.records.length} sObjects.`);
63
- return sObjectResults;
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.forEach((record) => {
69
- if (!record.DeveloperName.endsWith('__Share') && !record.DeveloperName.endsWith('__ChangeEvent')) {
70
- sObjectsDict[record.DeveloperName] = record.Label;
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
- generateCustomPermissionXML(sObject, automation, skipCredits = false) {
76
- const creditsText = skipCredits ? '' : ` ${CREDITS_TEXT}`;
77
- return `<?xml version="1.0" encoding="UTF-8"?>
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
- generatePermissionSetXML(sObject, automation, skipCredits = false) {
85
- const creditsText = skipCredits ? '' : ` ${CREDITS_TEXT}`;
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, skipCredits) {
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
- fs.ensureDirSync(path.dirname(customPermissionFile));
101
- fs.ensureDirSync(path.dirname(permissionSetFile));
102
- fs.writeFileSync(customPermissionFile, this.generateCustomPermissionXML(sObject, automation, skipCredits), 'utf-8');
103
- fs.writeFileSync(permissionSetFile, this.generatePermissionSetXML(sObject, automation, skipCredits), 'utf-8');
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, skipCredits) {
108
- Object.entries(targetSObjects).map(([developerName]) => {
109
- targetAutomations.forEach(automation => {
110
- this.generateXMLFiles(developerName, automation, skipCredits);
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
- async run() {
115
- const { flags } = await this.parse(HardisProjectGenerateBypass);
116
- const connection = flags['target-org'].getConnection();
117
- const generateGlobalBypasses = flags['global'];
118
- const sObjectsFromFlag = flags['sObjects'] ? flags['sObjects'].split(',').map(sObject => sObject.trim().replace(/__c$/, '')).map(s => s.trim()) : undefined;
119
- const automationsFromFlag = flags['automations'] ? flags['automations'].split(',').map(s => s.trim()).filter(s => ALLOWED_AUTOMATIONS.includes(s)) : undefined;
120
- const availableSObjects = await this.getFilteredSObjects(connection);
121
- const skipCredits = flags['skipCredits'];
122
- // Generate global bypasses if they are requested.
123
- if (generateGlobalBypasses) {
124
- this.generateFiles({ 'All': 'All' }, ALLOWED_AUTOMATIONS, skipCredits);
125
- }
126
- let targetSObjects = sObjectsFromFlag ? Object.fromEntries(Object.entries(availableSObjects).filter(([key]) => sObjectsFromFlag.includes(key))) : {};
127
- let targetAutomations = automationsFromFlag || [];
128
- const possiblePrompts = [];
129
- if (!sObjectsFromFlag || Object.keys(targetSObjects).length === 0) {
130
- uxLog(this, c.yellow('[sfdx-hardis] WARNING : No matching sObjects found. Please check your input or select from the prompt.'));
131
- possiblePrompts.push({
132
- type: 'multiselect',
133
- name: 'sobjects',
134
- message: 'Please select the sObjects for which you want to generate the bypass(es)',
135
- choices: Object.entries(availableSObjects).map(([developerName, label]) => {
136
- return { title: label, value: developerName };
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
- if (!automationsFromFlag) {
141
- possiblePrompts.push({
142
- type: 'multiselect',
143
- name: 'automations',
144
- message: 'Please which automations you wish to bypass.',
145
- choices: [
146
- { title: "Flows", value: "Flow" },
147
- { title: "Triggers", value: "Trigger" },
148
- { title: "Validation Rules", value: "VR" }
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
- if (possiblePrompts.length > 0) {
153
- const promptSelection = await prompts(possiblePrompts);
154
- if (!sObjectsFromFlag) {
155
- if (promptSelection?.sobjects.length === 0) {
156
- throw new SfError(c.red(`[sfdx-hardis] ERROR: You must select or provide (--sObjects) at least one sObject available on your org.`));
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
- if (!automationsFromFlag) {
161
- if (promptSelection?.automations.length === 0) {
162
- throw new SfError(c.red(`[sfdx-hardis] ERROR: You must select or provide (--automations) at least one automation to bypass.`));
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
- if (Object.keys(targetSObjects).length > 0 && targetAutomations.length > 0) {
168
- this.generateFiles(targetSObjects, targetAutomations, skipCredits);
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
- return {
171
- outputString: 'Generated bypass custom permissions and permission sets',
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