diff --git a/readme.md b/readme.md index 87735cb..f05e2cc 100644 --- a/readme.md +++ b/readme.md @@ -31,6 +31,7 @@ _An Extensible Rule Engine capable of conducting static analysis on the metadata | **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. | +| **Trigger Order** ([`TriggerOrder`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/TriggerOrder.ts)) | Guarantee your flow execution order with the Trigger Order property introduced in Spring '22 | ## Core Functions diff --git a/src/main/models/Flow.ts b/src/main/models/Flow.ts index 58c6c47..3f25bf5 100644 --- a/src/main/models/Flow.ts +++ b/src/main/models/Flow.ts @@ -21,6 +21,7 @@ export class Flow { public root?; public elements?: FlowElement[]; public startReference; + public triggerOrder?: number; private flowVariables = ["choices", "constants", "dynamicChoiceSets", "formulas", "variables"]; private flowResources = ["textTemplates", "stages"]; @@ -88,6 +89,7 @@ export class Flow { this.start = this.xmldata.start; this.status = this.xmldata.status; this.type = this.xmldata.processType; + this.triggerOrder = this.xmldata.triggerOrder; const allNodes: (FlowVariable | FlowNode | FlowMetadata)[] = []; for (const nodeType in this.xmldata) { // skip xmlns url diff --git a/src/main/models/FlowType.ts b/src/main/models/FlowType.ts index 6d08829..7b2b08d 100644 --- a/src/main/models/FlowType.ts +++ b/src/main/models/FlowType.ts @@ -1,6 +1,8 @@ export class FlowType { + public static autolaunchedType = "AutoLaunchedFlow"; + public static backEndTypes = [ - "AutoLaunchedFlow", + this.autolaunchedType, "CustomEvent", "InvocableProcess", "Orchestrator", diff --git a/src/main/rules/TriggerOrder.ts b/src/main/rules/TriggerOrder.ts new file mode 100644 index 0000000..71f17e5 --- /dev/null +++ b/src/main/rules/TriggerOrder.ts @@ -0,0 +1,45 @@ +import * as core from "../internals/internals"; +import { RuleCommon } from "../models/RuleCommon"; + +export class TriggerOrder extends RuleCommon implements core.IRuleDefinition { + protected qualifiedRecordTriggerTypes: Set = new Set(["Create", "Update"]); + + constructor() { + super( + { + name: "TriggerOrder", + label: "Trigger Order", + description: + "With flow trigger ordering, introduced in Spring '22, admins can now assign a priority value to their flows and guarantee their execution order. This priority value is not an absolute value, so the values need not be sequentially numbered as 1, 2, 3, and so on.", + supportedTypes: [core.FlowType.autolaunchedType], + docRefs: [ + { + label: "Learn more about flow ordering orchestration", + path: "https://architect.salesforce.com/decision-guides/trigger-automation#Ordering___Orchestration", + }, + ], + isConfigurable: false, + autoFixable: false, + }, + { severity: "warning" } + ); + } + + public execute(flow: core.Flow): core.RuleResult { + const results: core.ResultDetails[] = []; + + const qualifiedFlowType = "object" in flow.start; + + if (!qualifiedFlowType) { + return new core.RuleResult(this, results); + } + + results.push( + new core.ResultDetails( + new core.FlowAttribute("TriggerOrder", "TriggerOrder", "10, 20, 30 ...") + ) + ); + + return new core.RuleResult(this, results); + } +} diff --git a/tests/TriggerOrder.test.ts b/tests/TriggerOrder.test.ts new file mode 100644 index 0000000..12c94a3 --- /dev/null +++ b/tests/TriggerOrder.test.ts @@ -0,0 +1,174 @@ +import "mocha"; + +import { ParsedFlow } from "../src/main/models/ParsedFlow"; +import { TriggerOrder } from "../src/main/rules/TriggerOrder"; +import { RuleResult, Flow, parse, scan, ScanResult } from "../src"; +import * as path from "path-browserify"; + +describe("TriggerOrder", () => { + let expect; + let rule; + before(async () => { + expect = (await import("chai")).expect; + rule = new TriggerOrder(); + }); + + it("should not trigger from default configuration on store", async () => { + let example_uri1 = path.join(__dirname, "./xmlfiles/Same_Record_Field_Updates.flow-meta.xml"); + let flows = await parse([example_uri1]); + const ruleConfig = { + rules: {}, + exceptions: {}, + }; + const results: ScanResult[] = scan(flows, ruleConfig); + const scanResults = results.pop(); + + scanResults?.ruleResults.forEach((rule) => { + expect(rule.occurs).to.be.false; + }); + }); + + it("should trigger when opt-in configuration", async () => { + let example_uri1 = path.join(__dirname, "./xmlfiles/Same_Record_Field_Updates.flow-meta.xml"); + let flows = await parse([example_uri1]); + const ruleConfig = { + rules: { + TriggerOrder: { + severity: "error", + }, + }, + exceptions: {}, + }; + const results: ScanResult[] = scan(flows, ruleConfig); + const scanResults = results.pop(); + + const expectedRule = scanResults?.ruleResults.find((rule) => rule.ruleName === "TriggerOrder"); + expect(expectedRule).to.be.ok; + expect(expectedRule?.occurs).to.be.true; + expect(expectedRule?.details[0].details).to.deep.eq({ expression: "10, 20, 30 ..." }); + }); + + it("should flag trigger order when not present", async () => { + const testData: ParsedFlow = { + flow: { + start: { + locationX: "50", + locationY: "0", + connector: { targetReference: "Update_triggering_records" }, + object: "Account", + recordTriggerType: "Create", + triggerType: "RecordBeforeSave", + }, + elements: [ + { + 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 trigger order when present", async () => { + const testData: ParsedFlow = { + flow: { + triggerOrder: 10, + start: { + locationX: "50", + locationY: "0", + connector: { targetReference: "Update_triggering_records" }, + object: "Account", + recordTriggerType: "Create", + triggerType: "RecordBeforeSave", + }, + elements: [ + { + 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 trigger order when autolaunched flow", async () => { + const testData: ParsedFlow = { + flow: { + start: { + locationX: "50", + locationY: "0", + connector: { targetReference: "Update_triggering_records" }, + }, + 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", + }, + ], + }, + } as {} as ParsedFlow; + + const ruleResult: RuleResult = rule.execute(testData.flow as Flow); + + expect(ruleResult.occurs).to.be.false; + }); +});