Skip to content

Commit 8dd8e10

Browse files
committed
chore(core): introduces getStartProperty on RuleCommon and deprecates legacy start node implementation
1 parent 62dd14a commit 8dd8e10

13 files changed

+220
-164
lines changed

packages/core/src/main/models/Flow.ts

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,50 @@ export class Flow {
4545
public static readonly RESOURCE_TAGS = ["textTemplates", "stages"] as const;
4646
public static readonly VARIABLE_TAGS = ["choices", "constants", "dynamicChoiceSets", "formulas", "variables"] as const;
4747

48+
// Flow elements (excludes legacy start nodes)
4849
public elements: FlowElement[] = [];
49-
public fsPath?: string;
50-
public uri?: string;
51-
public interviewLabel?: string;
50+
51+
// Path properties
52+
public fsPath?: string; // Resolved absolute path (Node.js only)
53+
public uri?: string; // Input path (could be relative, absolute, or virtual)
54+
55+
// Flow metadata
5256
public label: string = "";
57+
public interviewLabel?: string;
5358
public name: string = "unnamed";
5459
public processMetadataValues?: any;
5560
public processType: string = "AutoLaunchedFlow";
56-
public type: string = "";
57-
public root?: any;
61+
public type: string = ""; // Alias for processType (backward compatibility)
62+
public status: string = "";
63+
public triggerOrder?: number;
64+
65+
// Start-related properties
66+
/**
67+
* @deprecated Use startNode.element instead. Kept for backward compatibility.
68+
*/
5869
public start?: any;
70+
71+
/**
72+
* Direct reference to first element (from XML attribute).
73+
* Used in newer flows as an alternative to the start element.
74+
*/
5975
public startElementReference?: string;
76+
77+
/**
78+
* Computed reference to the first element to execute.
79+
* This is what rules should use for traversal.
80+
*/
6081
public startReference?: string;
82+
83+
/**
84+
* Parsed FlowNode object of the start element.
85+
* Contains trigger information and connectors.
86+
* Access start element data via startNode.element
87+
*/
6188
public startNode?: FlowNode;
62-
public status: string = "";
63-
public triggerOrder?: number;
89+
90+
// Legacy/internal
91+
public root?: any;
6492
public xmldata: any;
6593

6694
constructor(path?: string, data?: unknown) {
@@ -113,7 +141,6 @@ export class Flow {
113141
this.type = this.processType;
114142
this.processMetadataValues = this.xmldata.processMetadataValues;
115143
this.startElementReference = this.xmldata.startElementReference;
116-
this.start = this.xmldata.start;
117144
this.status = this.xmldata.status || "Draft";
118145
this.triggerOrder = this.xmldata.triggerOrder;
119146

@@ -127,14 +154,14 @@ export class Flow {
127154

128155
const data = this.xmldata[nodeType];
129156

130-
// Handle start node separately - store in startNode, don't add to elements
157+
// Handle start nodes separately - store in startNode, don't add to elements
131158
if (nodeType === "start") {
132159
if (Array.isArray(data) && data.length > 0) {
133160
this.startNode = new FlowNode(data[0].name || "start", "start", data[0]);
134161
} else if (!Array.isArray(data)) {
135162
this.startNode = new FlowNode(data.name || "start", "start", data);
136163
}
137-
continue;
164+
continue; // Don't add to elements array
138165
}
139166

140167
// Process other node types
@@ -168,39 +195,44 @@ export class Flow {
168195
}
169196
}
170197

198+
/**
199+
* Find the name of the first element to execute.
200+
* Priority order:
201+
* 1. startElementReference (newer flows, direct XML attribute)
202+
* 2. Start node connector (older flows, points to first element)
203+
* 3. Start node scheduledPaths (async flows)
204+
*/
171205
private findStart(): string {
172-
// Priority 1: Explicit startElementReference
173-
if (this.startElementReference) {
174-
return this.startElementReference;
175-
}
206+
// Priority 1: Explicit startElementReference
207+
if (this.startElementReference) {
208+
return this.startElementReference;
209+
}
176210

177-
// Priority 2: Start node with regular connector
178-
if (this.startNode && this.startNode.connectors && this.startNode.connectors.length > 0) {
179-
const connector = this.startNode.connectors[0];
180-
// FlowElementConnector stores targetReference in the 'reference' property
181-
if (connector.reference) {
182-
return connector.reference;
211+
// Priority 2: Start node with regular connector
212+
if (this.startNode && this.startNode.connectors && this.startNode.connectors.length > 0) {
213+
const connector = this.startNode.connectors[0];
214+
if (connector.reference) {
215+
return connector.reference;
216+
}
183217
}
184-
}
185218

186-
// Priority 3: Start node with scheduledPaths (async flows)
187-
// scheduledPaths can be an array or a single object
188-
if (this.startNode?.element) {
189-
const scheduledPaths = this.startNode.element['scheduledPaths'];
190-
if (scheduledPaths) {
191-
const paths = Array.isArray(scheduledPaths) ? scheduledPaths : [scheduledPaths];
192-
if (paths.length > 0 && paths[0]?.connector) {
193-
const targetRef = paths[0].connector.targetReference;
194-
if (targetRef) {
195-
return targetRef;
219+
// Priority 3: Start node with scheduledPaths (async flows)
220+
if (this.startNode?.element) {
221+
const scheduledPaths = this.startNode.element['scheduledPaths'];
222+
if (scheduledPaths) {
223+
const paths = Array.isArray(scheduledPaths) ? scheduledPaths : [scheduledPaths];
224+
if (paths.length > 0 && paths[0]?.connector) {
225+
const targetRef = paths[0].connector.targetReference;
226+
if (targetRef) {
227+
return targetRef;
228+
}
196229
}
197230
}
198231
}
199-
}
200232

201-
// No valid start found
202-
return "";
203-
}
233+
// No valid start found
234+
return "";
235+
}
204236

205237
public toXMLString(): string {
206238
try {

packages/core/src/main/models/RuleCommon.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export abstract class RuleCommon {
1111
public supportedTypes: string[];
1212
public suppressionElement?: string;
1313
public uri?: string;
14-
14+
1515
constructor(info: RuleInfo, optional?: { severity?: string }) {
1616
this.name = info.name;
1717
this.supportedTypes = info.supportedTypes;
@@ -37,6 +37,7 @@ export abstract class RuleCommon {
3737
options?: object,
3838
suppressions: string[] = []
3939
): core.RuleResult {
40+
// Wildcard suppression disables entire rule
4041
if (suppressions.includes("*")) {
4142
return new core.RuleResult(this as any, []);
4243
}
@@ -93,4 +94,18 @@ export abstract class RuleCommon {
9394
}
9495
return flowElements.findIndex(n => n.name === startRef);
9596
}
97+
98+
/**
99+
* Safely get a property from the start element.
100+
*
101+
* @param flow - The Flow instance
102+
* @param propertyName - The property to retrieve (e.g., 'triggerType', 'object')
103+
* @returns The property value or undefined
104+
*/
105+
protected getStartProperty(flow: core.Flow, propertyName: string): any {
106+
if (flow.startNode?.element) {
107+
return (flow.startNode.element as Record<string, unknown>)?.[propertyName];
108+
}
109+
return undefined;
110+
}
96111
}

packages/core/src/main/rules/MissingFaultPath.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import * as core from "../internals/internals";
32
import { RuleCommon } from "../models/RuleCommon";
43
import { IRuleDefinition } from "../interfaces/IRuleDefinition";
@@ -35,6 +34,7 @@ export class MissingFaultPath extends RuleCommon implements IRuleDefinition {
3534
return false;
3635
}
3736

37+
// Exclude specific wait element subtypes that don't need fault paths
3838
if (proxyNode.subtype === "waits") {
3939
const elementSubtype: string = (proxyNode.element as Record<string, unknown>)?.["elementSubtype"] as string;
4040
const excludedSubtypes: string[] = ["WaitDuration", "WaitDate"];
@@ -59,14 +59,15 @@ export class MissingFaultPath extends RuleCommon implements IRuleDefinition {
5959
}) as core.FlowNode[]
6060
).map((e) => e.name);
6161

62-
// Safely check if this is a RecordBeforeSave flow
62+
// Check if this is a RecordBeforeSave flow
6363
const isRecordBeforeSave = this.isRecordBeforeSaveFlow(flow);
6464

6565
const visitCallback = (element: core.FlowNode) => {
6666
if (
6767
!element?.connectors?.find((connector) => connector.type === "faultConnector") &&
6868
elementsWhereFaultPathIsApplicable.includes(element.name)
6969
) {
70+
// Skip record updates in before-save flows (they're safe by design)
7071
if (isRecordBeforeSave && element.subtype === "recordUpdates") {
7172
return;
7273
}
@@ -79,32 +80,23 @@ export class MissingFaultPath extends RuleCommon implements IRuleDefinition {
7980
}
8081
};
8182

82-
const startRef = this.getStartReference(flow);
83-
if (startRef) {
84-
compiler.traverseFlow(flow, startRef, visitCallback);
83+
if (flow.startReference) {
84+
compiler.traverseFlow(flow, flow.startReference, visitCallback);
8585
}
8686

8787
return results;
8888
}
8989

9090
/**
91-
* Safely determine if this is a RecordBeforeSave flow.
92-
* Checks the startNode property for trigger type.
91+
* Determine if this is a RecordBeforeSave flow.
9392
*/
9493
private isRecordBeforeSaveFlow(flow: core.Flow): boolean {
95-
const startNode = this.getStartNode(flow);
96-
97-
if (startNode?.element) {
98-
const triggerType = (startNode.element as Record<string, unknown>)?.["triggerType"];
99-
return triggerType === "RecordBeforeSave";
100-
}
101-
102-
// Fallback: check raw start data if startNode is not available
103-
if (flow.start && typeof flow.start === "object") {
104-
const triggerType = (flow.start as Record<string, unknown>)?.["triggerType"];
105-
return triggerType === "RecordBeforeSave";
94+
if (flow.startNode?.element) {
95+
const triggerType = (flow.startNode.element as Record<string, unknown>)?.["triggerType"];
96+
if (triggerType === "RecordBeforeSave") {
97+
return true;
98+
}
10699
}
107-
108100
return false;
109101
}
110102

packages/core/src/main/rules/MissingFilterRecordTrigger.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ export class MissingFilterRecordTrigger extends RuleCommon implements IRuleDefin
2121
): core.Violation[] {
2222
const violations: core.Violation[] = [];
2323
// Check if this is a record-triggered flow
24-
const triggerType = flow.xmldata?.start?.triggerType;
24+
const triggerType = this.getStartProperty(flow, 'triggerType');
2525
// Only check flows with record trigger types
2626
if (!triggerType || !["RecordAfterSave", "RecordBeforeSave"].includes(triggerType)) {
2727
return violations;
2828
}
2929
// Check if the flow has filters or entry conditions at the flow level
30-
const filters = flow.xmldata?.start?.filters;
30+
const filters = this.getStartProperty(flow, 'filters');
31+
3132
const hasFilters = !!filters;
3233
const scheduledPaths = flow.xmldata?.start?.scheduledPaths;
3334
const hasScheduledPaths = !!scheduledPaths;

packages/core/src/main/rules/RecordIdAsString.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ export class RecordIdAsString extends RuleCommon implements IRuleDefinition {
3030
const violations: core.Violation[] = [];
3131

3232
// Skip record-triggered flows - they don't support this pattern
33+
const triggerType = this.getStartProperty(flow, 'triggerType');
3334
const isRecordTriggered =
34-
flow.start?.triggerType === "RecordAfterSave" ||
35-
flow.start?.triggerType === "RecordBeforeSave";
35+
triggerType === "RecordAfterSave" ||
36+
triggerType === "RecordBeforeDelete" ||
37+
triggerType === "RecordBeforeSave";
3638

3739
if (isRecordTriggered) {
3840
return violations;

packages/core/src/main/rules/RecursiveAfterUpdate.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ export class RecursiveAfterUpdate extends RuleCommon implements IRuleDefinition
3131
suppressions: Set<string>
3232
): core.Violation[] {
3333
const results: core.Violation[] = [];
34-
const isAfterSave = flow.start?.triggerType === "RecordAfterSave";
35-
const isQualifiedTriggerTypes = this.qualifiedRecordTriggerTypes.has(
36-
flow.start?.recordTriggerType
37-
);
34+
35+
const triggerType = this.getStartProperty(flow, 'triggerType');
36+
const recordTriggerType = this.getStartProperty(flow, 'recordTriggerType');
37+
38+
const isAfterSave = triggerType === "RecordAfterSave";
39+
const isQualifiedTriggerTypes = this.qualifiedRecordTriggerTypes.has(recordTriggerType);
40+
3841
if (!isAfterSave || !isQualifiedTriggerTypes) {
3942
return results;
4043
}
@@ -57,13 +60,14 @@ export class RecursiveAfterUpdate extends RuleCommon implements IRuleDefinition
5760
}
5861
}
5962
// === Lookup → same object type updates ===
63+
const flowObject = this.getStartProperty(flow, 'object');
6064
const lookupElementsWithTheSameObjectType = flow.elements
6165
?.filter(
6266
(node) =>
6367
node.subtype === "recordLookups" &&
6468
typeof node.element === "object" &&
6569
"object" in node.element &&
66-
flow.start.object === node.element["object"]
70+
flowObject === node.element["object"]
6771
)
6872
?.map((node) => node.name);
6973
if (

packages/core/src/main/rules/SameRecordFieldUpdates.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ export class SameRecordFieldUpdates extends RuleCommon implements IRuleDefinitio
3434
_suppressions: Set<string>
3535
): core.Violation[] {
3636
const results: core.Violation[] = [];
37-
const isBeforeSaveType = flow.start?.triggerType === "RecordBeforeSave";
38-
const isQualifiedTriggerTypes = this.qualifiedRecordTriggerTypes.has(
39-
flow.start?.recordTriggerType
40-
);
37+
const triggerType = this.getStartProperty(flow, 'triggerType');
38+
const recordTriggerType = this.getStartProperty(flow, 'recordTriggerType');
4139

40+
const isBeforeSaveType = triggerType === "RecordBeforeSave";
41+
const isQualifiedTriggerTypes = this.qualifiedRecordTriggerTypes.has(recordTriggerType);
4242
if (!isBeforeSaveType || !isQualifiedTriggerTypes) {
4343
return results;
4444
}

packages/core/src/main/rules/TriggerOrder.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ import { RuleCommon } from "../models/RuleCommon";
33
import { IRuleDefinition } from "../interfaces/IRuleDefinition";
44

55
export class TriggerOrder extends RuleCommon implements IRuleDefinition {
6-
protected qualifiedRecordTriggerTypes: Set<string> = new Set<string>(["Create", "Update"]);
76

87
constructor() {
98
super(
109
{
1110
name: "TriggerOrder",
1211
label: "Trigger Order",
1312
description:
14-
"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.",
13+
"With flow trigger ordering, introduced in Spring '22, admins can now assign a priority " +
14+
"value to their flows and guarantee their execution order. This priority value is not an " +
15+
"absolute value, so the values need not be sequentially numbered as 1, 2, 3, and so on.",
1516
supportedTypes: [core.FlowType.autolaunchedType],
1617
docRefs: [
1718
{
@@ -29,8 +30,14 @@ export class TriggerOrder extends RuleCommon implements IRuleDefinition {
2930
_options: object | undefined,
3031
_suppressions: Set<string>
3132
): core.Violation[] {
32-
if (!("object" in flow.start)) return [];
3333

34+
const startObject = this.getStartProperty(flow, "object");
35+
// If there's no `object` on the start node, this is NOT a record-triggered flow
36+
if (!startObject) {
37+
return [];
38+
}
39+
40+
// This *is* a record-triggered flow → should have triggerOrder
3441
if (!flow.triggerOrder) {
3542
return [
3643
new core.Violation(

packages/core/tests/HardcodedUrl.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe("HardcodedUrl", () => {
2828
{
2929
flow: {
3030
type: "AutoLaunchedFlow",
31+
status: "Active",
3132
elements: [
3233
{
3334
name: "hardcodedUrl",

0 commit comments

Comments
 (0)