Skip to content

Commit

Permalink
feat(RuleEngine): trigger order on record triggered flows
Browse files Browse the repository at this point in the history
  • Loading branch information
junners committed Nov 16, 2024
1 parent ed885b8 commit 5841b0b
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 1 deletion.
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/main/models/Flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/main/models/FlowType.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export class FlowType {
public static autolaunchedType = "AutoLaunchedFlow";

public static backEndTypes = [
"AutoLaunchedFlow",
this.autolaunchedType,
"CustomEvent",
"InvocableProcess",
"Orchestrator",
Expand Down
45 changes: 45 additions & 0 deletions src/main/rules/TriggerOrder.ts
Original file line number Diff line number Diff line change
@@ -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<string> = new Set<string>(["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);
}
}
174 changes: 174 additions & 0 deletions tests/TriggerOrder.test.ts
Original file line number Diff line number Diff line change
@@ -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;
});
});

0 comments on commit 5841b0b

Please sign in to comment.