diff --git a/readme.md b/readme.md index 80ea8ab..87735cb 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,7 @@ _An Extensible Rule Engine capable of conducting static analysis on the metadata | **Unconnected Element** ([`UnconnectedElement`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/UnconnectedElement.ts)) | Unconnected elements which are not being used by the Flow should be avoided to keep Flows efficient and maintainable. | | **Unused Variable** ([`UnusedVariable`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/UnusedVariable.ts)) | To maintain the efficiency and manageability of your Flow, it's advisable to avoid including unconnected variables that are not in use. | | **Unsafe Running Context** ([`UnsafeRunningContext`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/UnsafeRunningContext.ts)) | This flow is configured to run in System Mode without Sharing. This system context grants all running users the permission to view and edit all data in your org. Running a flow in System Mode without Sharing can lead to unsafe data access. | +| **Same Record Field Updates** ([`SameRecordFieldUpdates`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/SameRecordFieldUpdates.ts)) | Much like triggers, before contexts can update the same record by accessing the trigger variables `$Record` without needing to invoke a DML. | ## Core Functions diff --git a/src/main/rules/SameRecordFieldUpdates.ts b/src/main/rules/SameRecordFieldUpdates.ts new file mode 100644 index 0000000..6f726f2 --- /dev/null +++ b/src/main/rules/SameRecordFieldUpdates.ts @@ -0,0 +1,58 @@ +import * as core from "../internals/internals"; +import { RuleCommon } from "../models/RuleCommon"; + +export class SameRecordFieldUpdates extends RuleCommon implements core.IRuleDefinition { + protected qualifiedRecordTriggerTypes: Set = new Set(["Create", "Update"]); + + constructor() { + super( + { + name: "SameRecordFieldUpdates", + label: "Same Record Field Updates", + description: + "Before-save same-record field updates allows you to update the record using variable assignments to `$Record`. This is significantly faster than doing another DML on the same-record that triggered the flow", + supportedTypes: [...core.FlowType.backEndTypes], + docRefs: [ + { + label: "Learn about same record field updates", + path: "https://architect.salesforce.com/decision-guides/trigger-automation#Same_Record_Field_Updates", + }, + ], + isConfigurable: false, + autoFixable: false, + }, + { severity: "warning" } + ); + } + + public execute(flow: core.Flow): core.RuleResult { + const results: core.ResultDetails[] = []; + + const isBeforeSaveType = flow.start?.triggerType === "RecordBeforeSave"; + const isQualifiedTriggerTypes = this.qualifiedRecordTriggerTypes.has( + flow.start?.recordTriggerType + ); + + if (!isBeforeSaveType || !isQualifiedTriggerTypes) { + return new core.RuleResult(this, []); + } + + const resultDetails: core.ResultDetails[] = []; + + const potentialElements = flow.elements?.filter( + (node) => node.subtype === "recordUpdates" + ) as core.FlowNode[]; + + for (const node of potentialElements) { + if ( + typeof node.element === "object" && + "inputReference" in node.element && + node.element.inputReference === "$Record" + ) { + resultDetails.push(new core.ResultDetails(node)); + } + } + + return new core.RuleResult(this, resultDetails); + } +} diff --git a/tests/SameRecordFieldUpdates.test.ts b/tests/SameRecordFieldUpdates.test.ts new file mode 100644 index 0000000..a12e2a9 --- /dev/null +++ b/tests/SameRecordFieldUpdates.test.ts @@ -0,0 +1,136 @@ +import "mocha"; + +import { ParsedFlow } from "../src/main/models/ParsedFlow"; +import { SameRecordFieldUpdates } from "../src/main/rules/SameRecordFieldUpdates"; +import { RuleResult, Flow } from "../src"; + +describe("SameRecordFieldUpdates", () => { + let expect; + let rule; + before(async () => { + expect = (await import("chai")).expect; + rule = new SameRecordFieldUpdates(); + }); + + it("should flag same record updates on before context flows", async () => { + const testData: ParsedFlow = { + flow: { + start: { + locationX: "50", + locationY: "0", + connector: { targetReference: "Update_triggering_records" }, + object: "Account", + recordTriggerType: "Create", + triggerType: "RecordBeforeSave", + }, + elements: [ + { + element: { + description: "test", + name: "Update_triggering_records", + label: "Update triggering records", + locationX: "176", + locationY: "287", + inputAssignments: { field: "Active__c", value: { stringValue: "Yes" } }, + inputReference: "$Record", + }, + subtype: "recordUpdates", + metaType: "node", + connectors: [], + name: "Update_triggering_records", + locationX: "176", + locationY: "287", + }, + { + element: { + locationX: "50", + locationY: "0", + connector: { targetReference: "Update_triggering_records" }, + object: "Account", + recordTriggerType: "Create", + triggerType: "RecordBeforeSave", + }, + subtype: "start", + metaType: "node", + connectors: [ + { + element: { targetReference: "Update_triggering_records" }, + processed: false, + type: "connector", + reference: "Update_triggering_records", + }, + ], + name: "flowstart", + locationX: "50", + locationY: "0", + }, + ], + }, + } as {} as ParsedFlow; + + const ruleResult: RuleResult = rule.execute(testData.flow as Flow); + + expect(ruleResult.occurs).to.be.true; + }); + + it("should not flag same record updates on after context flows", async () => { + const testData: ParsedFlow = { + flow: { + start: { + locationX: "50", + locationY: "0", + connector: { targetReference: "Update_triggering_records" }, + object: "Account", + recordTriggerType: "Create", + triggerType: "RecordAfterSave", + }, + elements: [ + { + element: { + description: "test", + name: "Update_triggering_records", + label: "Update triggering records", + locationX: "176", + locationY: "287", + inputAssignments: { field: "Active__c", value: { stringValue: "Yes" } }, + inputReference: "$Record", + }, + subtype: "recordUpdates", + metaType: "node", + connectors: [], + name: "Update_triggering_records", + locationX: "176", + locationY: "287", + }, + { + element: { + locationX: "50", + locationY: "0", + connector: { targetReference: "Update_triggering_records" }, + object: "Account", + recordTriggerType: "Create", + triggerType: "RecordAfterSave", + }, + subtype: "start", + metaType: "node", + connectors: [ + { + element: { targetReference: "Update_triggering_records" }, + processed: false, + type: "connector", + reference: "Update_triggering_records", + }, + ], + name: "flowstart", + locationX: "50", + locationY: "0", + }, + ], + }, + } as {} as ParsedFlow; + + const ruleResult: RuleResult = rule.execute(testData.flow as Flow); + + expect(ruleResult.occurs).to.be.false; + }); +});