@flow-scanner/lightning-flow-scanner-core 6.10.5 → 6.10.6

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.
@@ -74,7 +74,7 @@ function exportSarif(results) {
74
74
  }))),
75
75
  tool: {
76
76
  driver: {
77
- informationUri: "https://github.com/Flow-Scanner/lightning-flow-scanner-core",
77
+ informationUri: "https://github.com/Flow-Scanner/lightning-flow-scanner",
78
78
  name: "Lightning Flow Scanner",
79
79
  rules: result.ruleResults.filter((r)=>r.occurs).map((r)=>({
80
80
  defaultConfiguration: {
@@ -1,4 +1,5 @@
1
1
  import { FlowElement } from "./FlowElement";
2
+ import { FlowNode } from "./FlowNode";
2
3
  export declare class Flow {
3
4
  /**
4
5
  * Metadata Tags of Salesforce Flow Attributes
@@ -7,32 +8,54 @@ export declare class Flow {
7
8
  /**
8
9
  * Metadata Tags of Salesforce Flow Nodes
9
10
  */
10
- static readonly NODE_TAGS: readonly ["actionCalls", "apexPluginCalls", "assignments", "collectionProcessors", "decisions", "loops", "orchestratedStages", "recordCreates", "recordDeletes", "recordLookups", "recordUpdates", "recordRollbacks", "screens", "start", "steps", "subflows", "waits", "transforms", "customErrors"];
11
+ static readonly NODE_TAGS: readonly ["actionCalls", "apexPluginCalls", "assignments", "collectionProcessors", "decisions", "loops", "orchestratedStages", "recordCreates", "recordDeletes", "recordLookups", "recordUpdates", "recordRollbacks", "screens", "steps", "subflows", "waits", "transforms", "customErrors"];
11
12
  static readonly RESOURCE_TAGS: readonly ["textTemplates", "stages"];
12
13
  static readonly VARIABLE_TAGS: readonly ["choices", "constants", "dynamicChoiceSets", "formulas", "variables"];
13
- elements?: FlowElement[];
14
+ elements: FlowElement[];
14
15
  fsPath?: string;
15
16
  uri?: string;
16
- interviewLabel?: string;
17
17
  label: string;
18
- name?: string;
18
+ interviewLabel?: string;
19
+ name: string;
19
20
  processMetadataValues?: any;
20
- processType?: string;
21
- root?: any;
21
+ processType: string;
22
+ type: string;
23
+ status: string;
24
+ triggerOrder?: number;
25
+ /**
26
+ * @deprecated Use startNode.element instead. Kept for backward compatibility.
27
+ */
22
28
  start?: any;
29
+ /**
30
+ * Direct reference to first element (from XML attribute).
31
+ * Used in newer flows as an alternative to the start element.
32
+ */
23
33
  startElementReference?: string;
34
+ /**
35
+ * Computed reference to the first element to execute.
36
+ * This is what rules should use for traversal.
37
+ */
24
38
  startReference?: string;
25
- status?: string;
26
- triggerOrder?: number;
27
- type?: string;
28
39
  /**
29
- * XML to JSON conversion in raw format
40
+ * Parsed FlowNode object of the start element.
41
+ * Contains trigger information and connectors.
42
+ * Access start element data via startNode.element
30
43
  */
44
+ startNode?: FlowNode;
45
+ root?: any;
31
46
  xmldata: any;
32
47
  constructor(path?: string, data?: unknown);
33
48
  static from(obj: Partial<Flow>): Flow;
34
49
  preProcessNodes(): void;
35
- toXMLString(): string;
50
+ private processNodeType;
51
+ /**
52
+ * Find the name of the first element to execute.
53
+ * Priority order:
54
+ * 1. startElementReference (newer flows, direct XML attribute)
55
+ * 2. Start node connector (older flows, points to first element)
56
+ * 3. Start node scheduledPaths (async flows)
57
+ */
36
58
  private findStart;
59
+ toXMLString(): string;
37
60
  private generateDoc;
38
61
  }
@@ -96,14 +96,17 @@ let Flow = class Flow {
96
96
  return flow;
97
97
  }
98
98
  preProcessNodes() {
99
- this.label = this.xmldata.label;
99
+ if (!this.xmldata) {
100
+ return;
101
+ }
102
+ // Extract top-level attributes
103
+ this.label = this.xmldata.label || "";
100
104
  this.interviewLabel = this.xmldata.interviewLabel;
101
- this.processType = this.xmldata.processType;
105
+ this.processType = this.xmldata.processType || "AutoLaunchedFlow";
106
+ this.type = this.processType;
102
107
  this.processMetadataValues = this.xmldata.processMetadataValues;
103
108
  this.startElementReference = this.xmldata.startElementReference;
104
- this.start = this.xmldata.start;
105
- this.status = this.xmldata.status;
106
- this.type = this.xmldata.processType;
109
+ this.status = this.xmldata.status || "Draft";
107
110
  this.triggerOrder = this.xmldata.triggerOrder;
108
111
  const allNodes = [];
109
112
  for(const nodeType in this.xmldata){
@@ -112,129 +115,160 @@ let Flow = class Flow {
112
115
  continue;
113
116
  }
114
117
  const data = this.xmldata[nodeType];
115
- if (Flow.ATTRIBUTE_TAGS.includes(nodeType)) {
116
- if (Array.isArray(data)) {
117
- for (const node of data){
118
- allNodes.push(new _FlowMetadata.FlowMetadata(node.name, nodeType, node));
119
- }
120
- } else {
121
- allNodes.push(new _FlowMetadata.FlowMetadata(data.name, nodeType, data));
118
+ // Handle start nodes separately - store in startNode, don't add to elements
119
+ if (nodeType === "start") {
120
+ if (Array.isArray(data) && data.length > 0) {
121
+ this.startNode = new _FlowNode.FlowNode(data[0].name || "start", "start", data[0]);
122
+ } else if (!Array.isArray(data)) {
123
+ this.startNode = new _FlowNode.FlowNode(data.name || "start", "start", data);
122
124
  }
125
+ continue; // Don't add to elements array
126
+ }
127
+ // Process other node types
128
+ if (Flow.ATTRIBUTE_TAGS.includes(nodeType)) {
129
+ this.processNodeType(data, nodeType, allNodes, _FlowMetadata.FlowMetadata);
123
130
  } else if (Flow.VARIABLE_TAGS.includes(nodeType)) {
124
- if (Array.isArray(data)) {
125
- for (const node of data){
126
- allNodes.push(new _FlowVariable.FlowVariable(node.name, nodeType, node));
127
- }
128
- } else {
129
- allNodes.push(new _FlowVariable.FlowVariable(data.name, nodeType, data));
130
- }
131
+ this.processNodeType(data, nodeType, allNodes, _FlowVariable.FlowVariable);
131
132
  } else if (Flow.NODE_TAGS.includes(nodeType)) {
132
- if (Array.isArray(data)) {
133
- for (const node of data){
134
- allNodes.push(new _FlowNode.FlowNode(node.name, nodeType, node));
135
- }
136
- } else {
137
- allNodes.push(new _FlowNode.FlowNode(data.name, nodeType, data));
138
- }
133
+ this.processNodeType(data, nodeType, allNodes, _FlowNode.FlowNode);
139
134
  } else if (Flow.RESOURCE_TAGS.includes(nodeType)) {
140
- if (Array.isArray(data)) {
141
- for (const node of data){
142
- allNodes.push(new _FlowResource.FlowResource(node.name, nodeType, node));
143
- }
144
- } else {
145
- allNodes.push(new _FlowResource.FlowResource(data.name, nodeType, data));
146
- }
135
+ this.processNodeType(data, nodeType, allNodes, _FlowResource.FlowResource);
147
136
  }
148
137
  }
149
138
  this.elements = allNodes;
150
139
  this.startReference = this.findStart();
151
140
  }
141
+ processNodeType(data, nodeType, allNodes, NodeClass) {
142
+ if (Array.isArray(data)) {
143
+ for (const node of data){
144
+ allNodes.push(new NodeClass(node.name, nodeType, node));
145
+ }
146
+ } else {
147
+ allNodes.push(new NodeClass(data.name, nodeType, data));
148
+ }
149
+ }
150
+ /**
151
+ * Find the name of the first element to execute.
152
+ * Priority order:
153
+ * 1. startElementReference (newer flows, direct XML attribute)
154
+ * 2. Start node connector (older flows, points to first element)
155
+ * 3. Start node scheduledPaths (async flows)
156
+ */ findStart() {
157
+ var _this_startNode;
158
+ // Priority 1: Explicit startElementReference
159
+ if (this.startElementReference) {
160
+ return this.startElementReference;
161
+ }
162
+ // Priority 2: Start node with regular connector
163
+ if (this.startNode && this.startNode.connectors && this.startNode.connectors.length > 0) {
164
+ const connector = this.startNode.connectors[0];
165
+ if (connector.reference) {
166
+ return connector.reference;
167
+ }
168
+ }
169
+ // Priority 3: Start node with scheduledPaths (async flows)
170
+ if ((_this_startNode = this.startNode) === null || _this_startNode === void 0 ? void 0 : _this_startNode.element) {
171
+ const scheduledPaths = this.startNode.element['scheduledPaths'];
172
+ if (scheduledPaths) {
173
+ var _paths_;
174
+ const paths = Array.isArray(scheduledPaths) ? scheduledPaths : [
175
+ scheduledPaths
176
+ ];
177
+ if (paths.length > 0 && ((_paths_ = paths[0]) === null || _paths_ === void 0 ? void 0 : _paths_.connector)) {
178
+ const targetRef = paths[0].connector.targetReference;
179
+ if (targetRef) {
180
+ return targetRef;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ // No valid start found
186
+ return "";
187
+ }
152
188
  toXMLString() {
153
189
  try {
154
190
  return this.generateDoc();
155
191
  } catch (exception) {
156
- console.warn(`Unable to write xml, caught an error ${exception.toString()}`);
192
+ const errorMsg = exception instanceof Error ? exception.message : String(exception);
193
+ console.warn(`Unable to write xml, caught an error: ${errorMsg}`);
157
194
  return "";
158
195
  }
159
196
  }
160
- findStart() {
161
- let start = "";
162
- const flowElements = this.elements.filter((node)=>node instanceof _FlowNode.FlowNode);
163
- if (this.startElementReference) {
164
- start = this.startElementReference;
165
- } else if (flowElements.find((n)=>{
166
- return n.subtype === "start";
167
- })) {
168
- const startElement = flowElements.find((n)=>{
169
- return n.subtype === "start";
170
- });
171
- start = startElement.connectors[0]["reference"];
172
- }
173
- return start;
174
- }
175
197
  generateDoc() {
176
- // eslint-disable-next-line sonarjs/no-clear-text-protocols
177
198
  const flowXmlNamespace = "http://soap.sforce.com/2006/04/metadata";
178
199
  const builderOptions = {
179
200
  attributeNamePrefix: "@_",
180
201
  format: true,
181
202
  ignoreAttributes: false,
182
203
  suppressBooleanAttributes: false,
183
- suppressEmptyNode: false // Keep empty tags (but doesn't force self-closing in pretty)
204
+ suppressEmptyNode: false
184
205
  };
185
206
  const builder = new _fastxmlparser.XMLBuilder(builderOptions);
186
- // Fallback: Inject xmlns as attribute if missing
187
207
  const xmldataWithNs = _object_spread({}, this.xmldata);
188
208
  if (!xmldataWithNs["@_xmlns"]) {
189
209
  xmldataWithNs["@_xmlns"] = flowXmlNamespace;
190
210
  }
191
- // Optional: Add xsi if needed (often in parsed data; test has it in root)
192
211
  if (!xmldataWithNs["@_xmlns:xsi"]) {
193
212
  xmldataWithNs["@_xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance";
194
213
  }
195
- // Build: Wrap in { Flow: ... }
196
214
  const rootObj = {
197
215
  Flow: xmldataWithNs
198
216
  };
199
217
  return builder.build(rootObj);
200
218
  }
201
219
  constructor(path, data){
202
- _define_property(this, "elements", void 0);
203
- _define_property(this, "fsPath", void 0); // This is only set in Node.js environments and is the resolved absolute path. In browser environments, it stays undefined.
204
- _define_property(this, "uri", void 0); // This is always set from the constructor's path parameter and represents the input path (could be relative, absolute, or virtual)
220
+ // Flow elements (excludes legacy start nodes)
221
+ _define_property(this, "elements", []);
222
+ // Path properties
223
+ _define_property(this, "fsPath", void 0); // Resolved absolute path (Node.js only)
224
+ _define_property(this, "uri", void 0); // Input path (could be relative, absolute, or virtual)
225
+ // Flow metadata
226
+ _define_property(this, "label", "");
205
227
  _define_property(this, "interviewLabel", void 0);
206
- _define_property(this, "label", void 0);
207
- _define_property(this, "name", void 0);
228
+ _define_property(this, "name", "unnamed");
208
229
  _define_property(this, "processMetadataValues", void 0);
209
- _define_property(this, "processType", void 0);
210
- _define_property(this, "root", void 0);
211
- _define_property(this, "start", void 0);
212
- _define_property(this, "startElementReference", void 0);
213
- _define_property(this, "startReference", void 0);
214
- _define_property(this, "status", void 0);
230
+ _define_property(this, "processType", "AutoLaunchedFlow");
231
+ _define_property(this, "type", ""); // Alias for processType (backward compatibility)
232
+ _define_property(this, "status", "");
215
233
  _define_property(this, "triggerOrder", void 0);
216
- _define_property(this, "type", void 0);
234
+ // Start-related properties
235
+ /**
236
+ * @deprecated Use startNode.element instead. Kept for backward compatibility.
237
+ */ _define_property(this, "start", void 0);
238
+ /**
239
+ * Direct reference to first element (from XML attribute).
240
+ * Used in newer flows as an alternative to the start element.
241
+ */ _define_property(this, "startElementReference", void 0);
217
242
  /**
218
- * XML to JSON conversion in raw format
219
- */ _define_property(this, "xmldata", void 0);
243
+ * Computed reference to the first element to execute.
244
+ * This is what rules should use for traversal.
245
+ */ _define_property(this, "startReference", void 0);
246
+ /**
247
+ * Parsed FlowNode object of the start element.
248
+ * Contains trigger information and connectors.
249
+ * Access start element data via startNode.element
250
+ */ _define_property(this, "startNode", void 0);
251
+ // Legacy/internal
252
+ _define_property(this, "root", void 0);
253
+ _define_property(this, "xmldata", void 0);
220
254
  if (path) {
221
- this.uri = path; // Always set general URI from input (file path or virtual)
222
- // Only resolve fsPath in Node.js environments
223
- // In browser with polyfills, fsPath stays undefined
224
- if (typeof process !== 'undefined' && process.cwd) {
255
+ this.uri = path;
256
+ if (typeof process !== 'undefined' && typeof process.cwd === 'function') {
225
257
  this.fsPath = _path.resolve(path);
226
258
  }
227
259
  let flowName = _path.basename(_path.basename(path), _path.extname(path));
228
260
  if (flowName.includes(".")) {
229
261
  flowName = flowName.split(".")[0];
230
262
  }
231
- this.name = flowName;
263
+ this.name = flowName || "unnamed";
232
264
  }
233
265
  if (data) {
234
- const hasFlowElement = typeof data === "object" && "Flow" in data;
266
+ const hasFlowElement = typeof data === "object" && data !== null && "Flow" in data;
235
267
  if (hasFlowElement) {
236
268
  this.xmldata = data.Flow;
237
- } else this.xmldata = data;
269
+ } else {
270
+ this.xmldata = data;
271
+ }
238
272
  this.preProcessNodes();
239
273
  }
240
274
  }
@@ -276,7 +310,6 @@ let Flow = class Flow {
276
310
  "recordUpdates",
277
311
  "recordRollbacks",
278
312
  "screens",
279
- "start",
280
313
  "steps",
281
314
  "subflows",
282
315
  "waits",
@@ -3,5 +3,5 @@ export declare class ParsedFlow {
3
3
  uri: string;
4
4
  flow: Flow | undefined;
5
5
  errorMessage?: string;
6
- constructor(uri: string, flow: Flow, errorMessage?: string);
6
+ constructor(uri: string, flow?: Flow, errorMessage?: string);
7
7
  }
@@ -16,17 +16,40 @@ export declare abstract class RuleCommon {
16
16
  constructor(info: RuleInfo, optional?: {
17
17
  severity?: string;
18
18
  });
19
+ execute(flow: core.Flow, options?: object, suppressions?: string[]): core.RuleResult;
20
+ protected abstract check(flow: core.Flow, options: object | undefined, suppressions: Set<string>): core.Violation[];
21
+ protected isSuppressed(name: string, suppressions: Set<string>): boolean;
19
22
  /**
20
- * execute() automatic suppression
23
+ * Get the start node (the special <start> element).
24
+ * This is now stored separately in flow.startNode, not in flow.elements.
25
+ *
26
+ * @param flow - The Flow instance
27
+ * @returns The start FlowNode or undefined if not found
21
28
  */
22
- execute(flow: core.Flow, options?: object, suppressions?: string[]): core.RuleResult;
29
+ protected getStartNode(flow: core.Flow): core.FlowNode | undefined;
23
30
  /**
24
- * Rules implement this. They should return *all* violations,
25
- * NOT pre-filter suppressed ones (unless they need early-exit performance).
31
+ * Get the reference name of the first actual element (what the flow starts at).
32
+ * This is the element that comes AFTER the start node.
33
+ *
34
+ * @param flow - The Flow instance
35
+ * @returns The start reference name or undefined
26
36
  */
27
- protected abstract check(flow: core.Flow, options: object | undefined, suppressions: Set<string>): core.Violation[];
37
+ protected getStartReference(flow: core.Flow): string | undefined;
28
38
  /**
29
- * Legacy/manual suppression helper (still available for early exits)
39
+ * Find the INDEX of the first actual element in a FlowNode array.
40
+ * Useful for rules that need to iterate by index.
41
+ *
42
+ * @param flow - The Flow instance
43
+ * @param flowElements - Array of FlowNodes (typically from flow.elements)
44
+ * @returns The index of the starting element, or -1 if not found
30
45
  */
31
- protected isSuppressed(name: string, suppressions: Set<string>): boolean;
46
+ protected findStartIndex(flow: core.Flow, flowElements: core.FlowNode[]): number;
47
+ /**
48
+ * Safely get a property from the start element.
49
+ *
50
+ * @param flow - The Flow instance
51
+ * @param propertyName - The property to retrieve (e.g., 'triggerType', 'object')
52
+ * @returns The property value or undefined
53
+ */
54
+ protected getStartProperty(flow: core.Flow, propertyName: string): any;
32
55
  }
@@ -64,31 +64,69 @@ function _interop_require_wildcard(obj, nodeInterop) {
64
64
  return newObj;
65
65
  }
66
66
  let RuleCommon = class RuleCommon {
67
- /**
68
- * execute() – automatic suppression
69
- */ execute(flow, options, suppressions = []) {
67
+ execute(flow, options, suppressions = []) {
70
68
  // Wildcard suppression disables entire rule
71
69
  if (suppressions.includes("*")) {
72
70
  return new _internals.RuleResult(this, []);
73
71
  }
74
- // Convert to Set for fast lookup
75
72
  const suppSet = new Set(suppressions);
76
- // Raw violations from rule
77
73
  let violations = this.check(flow, options, suppSet);
78
- // Automatically filter suppressed violations by their .name
79
74
  violations = violations.filter((v)=>!suppSet.has(v.name));
80
- // Wrap into RuleResult
81
75
  return new _internals.RuleResult(this, violations);
82
76
  }
83
- /**
84
- * Legacy/manual suppression helper (still available for early exits)
85
- */ isSuppressed(name, suppressions) {
77
+ isSuppressed(name, suppressions) {
86
78
  return suppressions.has(name);
87
79
  }
80
+ /**
81
+ * Get the start node (the special <start> element).
82
+ * This is now stored separately in flow.startNode, not in flow.elements.
83
+ *
84
+ * @param flow - The Flow instance
85
+ * @returns The start FlowNode or undefined if not found
86
+ */ getStartNode(flow) {
87
+ return flow.startNode;
88
+ }
89
+ /**
90
+ * Get the reference name of the first actual element (what the flow starts at).
91
+ * This is the element that comes AFTER the start node.
92
+ *
93
+ * @param flow - The Flow instance
94
+ * @returns The start reference name or undefined
95
+ */ getStartReference(flow) {
96
+ return flow.startReference || undefined;
97
+ }
98
+ /**
99
+ * Find the INDEX of the first actual element in a FlowNode array.
100
+ * Useful for rules that need to iterate by index.
101
+ *
102
+ * @param flow - The Flow instance
103
+ * @param flowElements - Array of FlowNodes (typically from flow.elements)
104
+ * @returns The index of the starting element, or -1 if not found
105
+ */ findStartIndex(flow, flowElements) {
106
+ const startRef = this.getStartReference(flow);
107
+ if (!startRef) {
108
+ return -1;
109
+ }
110
+ return flowElements.findIndex((n)=>n.name === startRef);
111
+ }
112
+ /**
113
+ * Safely get a property from the start element.
114
+ *
115
+ * @param flow - The Flow instance
116
+ * @param propertyName - The property to retrieve (e.g., 'triggerType', 'object')
117
+ * @returns The property value or undefined
118
+ */ getStartProperty(flow, propertyName) {
119
+ var _flow_startNode;
120
+ if ((_flow_startNode = flow.startNode) === null || _flow_startNode === void 0 ? void 0 : _flow_startNode.element) {
121
+ var _flow_startNode_element;
122
+ return (_flow_startNode_element = flow.startNode.element) === null || _flow_startNode_element === void 0 ? void 0 : _flow_startNode_element[propertyName];
123
+ }
124
+ return undefined;
125
+ }
88
126
  constructor(info, optional){
89
127
  _define_property(this, "description", void 0);
90
128
  _define_property(this, "docRefs", []);
91
- _define_property(this, "isConfigurable", void 0); // Auto-detected by checking if the implemented check() method actually uses "options."
129
+ _define_property(this, "isConfigurable", void 0);
92
130
  _define_property(this, "label", void 0);
93
131
  _define_property(this, "name", void 0);
94
132
  _define_property(this, "severity", void 0);
@@ -65,7 +65,37 @@ let APIVersion = class APIVersion extends _RuleCommon.RuleCommon {
65
65
  }
66
66
  // Custom logic
67
67
  if (options === null || options === void 0 ? void 0 : options.expression) {
68
- const isValid = new Function(`return ${flowAPIVersionNumber}${options.expression};`)();
68
+ // Match something like: >= 58
69
+ const match = options.expression.match(/^\s*(>=|<=|>|<|===|!==)\s*(\d+)\s*$/);
70
+ if (!match) {
71
+ // Invalid expression format
72
+ return [
73
+ new _internals.Violation(new _internals.FlowAttribute("Invalid API rule expression", "apiVersion", options.expression))
74
+ ];
75
+ }
76
+ const [, operator, versionStr] = match;
77
+ const target = parseFloat(versionStr);
78
+ let isValid = true;
79
+ switch(operator){
80
+ case ">":
81
+ isValid = flowAPIVersionNumber > target;
82
+ break;
83
+ case "<":
84
+ isValid = flowAPIVersionNumber < target;
85
+ break;
86
+ case ">=":
87
+ isValid = flowAPIVersionNumber >= target;
88
+ break;
89
+ case "<=":
90
+ isValid = flowAPIVersionNumber <= target;
91
+ break;
92
+ case "===":
93
+ isValid = flowAPIVersionNumber === target;
94
+ break;
95
+ case "!==":
96
+ isValid = flowAPIVersionNumber !== target;
97
+ break;
98
+ }
69
99
  if (!isValid) {
70
100
  return [
71
101
  new _internals.Violation(new _internals.FlowAttribute(`${flowAPIVersionNumber}`, "apiVersion", options.expression))
@@ -4,6 +4,5 @@ import { IRuleDefinition } from "../interfaces/IRuleDefinition";
4
4
  export declare class DuplicateDMLOperation extends RuleCommon implements IRuleDefinition {
5
5
  constructor();
6
6
  protected check(flow: core.Flow, _options: object | undefined, suppressions: Set<string>): core.Violation[];
7
- private findStart;
8
7
  private flagDML;
9
8
  }
@@ -54,13 +54,14 @@ function _interop_require_wildcard(obj, nodeInterop) {
54
54
  let DuplicateDMLOperation = class DuplicateDMLOperation extends _RuleCommon.RuleCommon {
55
55
  check(flow, _options, suppressions) {
56
56
  const flowElements = flow.elements.filter((node)=>node instanceof _internals.FlowNode);
57
- const processedElementIndexes = [];
58
- const unconnectedElementIndexes = [];
59
- const DuplicateDMLOperations = [];
60
- const startingNode = this.findStart(flow);
57
+ // Use the helper to find the starting element index
58
+ const startingNode = this.findStartIndex(flow, flowElements);
61
59
  if (startingNode === -1) {
62
60
  return [];
63
61
  }
62
+ const processedElementIndexes = [];
63
+ const unconnectedElementIndexes = [];
64
+ const DuplicateDMLOperations = [];
64
65
  let dmlFlag = false;
65
66
  let indexesToProcess = [
66
67
  startingNode
@@ -108,14 +109,6 @@ let DuplicateDMLOperation = class DuplicateDMLOperation extends _RuleCommon.Rule
108
109
  }while (processedElementIndexes.length + unconnectedElementIndexes.length < flowElements.length)
109
110
  return DuplicateDMLOperations.map((det)=>new _internals.Violation(det));
110
111
  }
111
- findStart(flow) {
112
- const flowElements = flow.elements.filter((node)=>node instanceof _internals.FlowNode);
113
- if (flow.startElementReference) {
114
- return flowElements.findIndex((n)=>n.name === flow.startElementReference);
115
- } else {
116
- return flowElements.findIndex((n)=>n.subtype === "start");
117
- }
118
- }
119
112
  flagDML(element, dmlFlag) {
120
113
  const dmlStatementTypes = [
121
114
  "recordDeletes",
@@ -6,5 +6,9 @@ export declare class MissingFaultPath extends RuleCommon implements IRuleDefinit
6
6
  constructor();
7
7
  private isValidSubtype;
8
8
  protected check(flow: core.Flow, _options: object | undefined, suppressions: Set<string>): core.Violation[];
9
+ /**
10
+ * Determine if this is a RecordBeforeSave flow.
11
+ */
12
+ private isRecordBeforeSaveFlow;
9
13
  private isPartOfFaultHandlingFlow;
10
14
  }
@@ -82,14 +82,14 @@ let MissingFaultPath = class MissingFaultPath extends _RuleCommon.RuleCommon {
82
82
  return true;
83
83
  }
84
84
  check(flow, _options, suppressions) {
85
- var _flow_elements;
86
85
  const compiler = new _internals.Compiler();
87
86
  const results = [];
88
- const elementsWhereFaultPathIsApplicable = ((_flow_elements = flow.elements) === null || _flow_elements === void 0 ? void 0 : _flow_elements.filter((node)=>{
87
+ const elementsWhereFaultPathIsApplicable = flow.elements.filter((node)=>{
89
88
  const proxyNode = node;
90
89
  return this.isValidSubtype(proxyNode);
91
- })).map((e)=>e.name);
92
- const isRecordBeforeSave = flow.start.triggerType === "RecordBeforeSave";
90
+ }).map((e)=>e.name);
91
+ // Check if this is a RecordBeforeSave flow
92
+ const isRecordBeforeSave = this.isRecordBeforeSaveFlow(flow);
93
93
  const visitCallback = (element)=>{
94
94
  var _element_connectors;
95
95
  if (!(element === null || element === void 0 ? void 0 : (_element_connectors = element.connectors) === null || _element_connectors === void 0 ? void 0 : _element_connectors.find((connector)=>connector.type === "faultConnector")) && elementsWhereFaultPathIsApplicable.includes(element.name)) {
@@ -104,16 +104,30 @@ let MissingFaultPath = class MissingFaultPath extends _RuleCommon.RuleCommon {
104
104
  }
105
105
  }
106
106
  };
107
- compiler.traverseFlow(flow, flow.startReference, visitCallback);
107
+ if (flow.startReference) {
108
+ compiler.traverseFlow(flow, flow.startReference, visitCallback);
109
+ }
108
110
  return results;
109
111
  }
112
+ /**
113
+ * Determine if this is a RecordBeforeSave flow.
114
+ */ isRecordBeforeSaveFlow(flow) {
115
+ var _flow_startNode;
116
+ if ((_flow_startNode = flow.startNode) === null || _flow_startNode === void 0 ? void 0 : _flow_startNode.element) {
117
+ var _flow_startNode_element;
118
+ const triggerType = (_flow_startNode_element = flow.startNode.element) === null || _flow_startNode_element === void 0 ? void 0 : _flow_startNode_element["triggerType"];
119
+ if (triggerType === "RecordBeforeSave") {
120
+ return true;
121
+ }
122
+ }
123
+ return false;
124
+ }
110
125
  isPartOfFaultHandlingFlow(element, flow) {
111
- var _flow_elements;
112
- const flowelements = (_flow_elements = flow.elements) === null || _flow_elements === void 0 ? void 0 : _flow_elements.filter((el)=>el instanceof _internals.FlowNode);
126
+ const flowelements = flow.elements.filter((el)=>el instanceof _internals.FlowNode);
113
127
  for (const otherElement of flowelements){
114
128
  if (otherElement !== element) {
115
129
  var _otherElement_connectors;
116
- if ((_otherElement_connectors = otherElement.connectors) === null || _otherElement_connectors === void 0 ? void 0 : _otherElement_connectors.find((connector)=>connector.type === "faultConnector" && connector.reference === element.name)) {
130
+ if ((_otherElement_connectors = otherElement.connectors) === null || _otherElement_connectors === void 0 ? void 0 : _otherElement_connectors.find((connector)=>connector.type === "faultConnector" && connector.reference && connector.reference === element.name)) {
117
131
  return true;
118
132
  }
119
133
  }
@@ -53,10 +53,10 @@ function _interop_require_wildcard(obj, nodeInterop) {
53
53
  }
54
54
  let MissingFilterRecordTrigger = class MissingFilterRecordTrigger extends _RuleCommon.RuleCommon {
55
55
  check(flow, _options, _suppressions) {
56
- var _flow_xmldata_start, _flow_xmldata, _flow_xmldata_start1, _flow_xmldata1, _flow_xmldata_start2, _flow_xmldata2;
56
+ var _flow_xmldata_start, _flow_xmldata;
57
57
  const violations = [];
58
58
  // Check if this is a record-triggered flow
59
- const triggerType = (_flow_xmldata = flow.xmldata) === null || _flow_xmldata === void 0 ? void 0 : (_flow_xmldata_start = _flow_xmldata.start) === null || _flow_xmldata_start === void 0 ? void 0 : _flow_xmldata_start.triggerType;
59
+ const triggerType = this.getStartProperty(flow, 'triggerType');
60
60
  // Only check flows with record trigger types
61
61
  if (!triggerType || ![
62
62
  "RecordAfterSave",
@@ -65,9 +65,9 @@ let MissingFilterRecordTrigger = class MissingFilterRecordTrigger extends _RuleC
65
65
  return violations;
66
66
  }
67
67
  // Check if the flow has filters or entry conditions at the flow level
68
- const filters = (_flow_xmldata1 = flow.xmldata) === null || _flow_xmldata1 === void 0 ? void 0 : (_flow_xmldata_start1 = _flow_xmldata1.start) === null || _flow_xmldata_start1 === void 0 ? void 0 : _flow_xmldata_start1.filters;
68
+ const filters = this.getStartProperty(flow, 'filters');
69
69
  const hasFilters = !!filters;
70
- const scheduledPaths = (_flow_xmldata2 = flow.xmldata) === null || _flow_xmldata2 === void 0 ? void 0 : (_flow_xmldata_start2 = _flow_xmldata2.start) === null || _flow_xmldata_start2 === void 0 ? void 0 : _flow_xmldata_start2.scheduledPaths;
70
+ const scheduledPaths = (_flow_xmldata = flow.xmldata) === null || _flow_xmldata === void 0 ? void 0 : (_flow_xmldata_start = _flow_xmldata.start) === null || _flow_xmldata_start === void 0 ? void 0 : _flow_xmldata_start.scheduledPaths;
71
71
  const hasScheduledPaths = !!scheduledPaths;
72
72
  // If no filters or scheduled paths (which have their own conditions), flag as violation
73
73
  if (!hasFilters && !hasScheduledPaths) {
@@ -53,10 +53,11 @@ function _interop_require_wildcard(obj, nodeInterop) {
53
53
  }
54
54
  let RecordIdAsString = class RecordIdAsString extends _RuleCommon.RuleCommon {
55
55
  check(flow, _options, _suppressions) {
56
- var _flow_start, _flow_start1, _flow_elements;
56
+ var _flow_elements;
57
57
  const violations = [];
58
58
  // Skip record-triggered flows - they don't support this pattern
59
- const isRecordTriggered = ((_flow_start = flow.start) === null || _flow_start === void 0 ? void 0 : _flow_start.triggerType) === "RecordAfterSave" || ((_flow_start1 = flow.start) === null || _flow_start1 === void 0 ? void 0 : _flow_start1.triggerType) === "RecordBeforeSave";
59
+ const triggerType = this.getStartProperty(flow, 'triggerType');
60
+ const isRecordTriggered = triggerType === "RecordAfterSave" || triggerType === "RecordBeforeDelete" || triggerType === "RecordBeforeSave";
60
61
  if (isRecordTriggered) {
61
62
  return violations;
62
63
  }
@@ -66,10 +66,12 @@ function _interop_require_wildcard(obj, nodeInterop) {
66
66
  }
67
67
  let RecursiveAfterUpdate = class RecursiveAfterUpdate extends _RuleCommon.RuleCommon {
68
68
  check(flow, _options, suppressions) {
69
- var _flow_start, _flow_start1, _flow_elements, _flow_elements_filter, _flow_elements1;
69
+ var _flow_elements, _flow_elements_filter, _flow_elements1;
70
70
  const results = [];
71
- const isAfterSave = ((_flow_start = flow.start) === null || _flow_start === void 0 ? void 0 : _flow_start.triggerType) === "RecordAfterSave";
72
- const isQualifiedTriggerTypes = this.qualifiedRecordTriggerTypes.has((_flow_start1 = flow.start) === null || _flow_start1 === void 0 ? void 0 : _flow_start1.recordTriggerType);
71
+ const triggerType = this.getStartProperty(flow, 'triggerType');
72
+ const recordTriggerType = this.getStartProperty(flow, 'recordTriggerType');
73
+ const isAfterSave = triggerType === "RecordAfterSave";
74
+ const isQualifiedTriggerTypes = this.qualifiedRecordTriggerTypes.has(recordTriggerType);
73
75
  if (!isAfterSave || !isQualifiedTriggerTypes) {
74
76
  return results;
75
77
  }
@@ -86,7 +88,8 @@ let RecursiveAfterUpdate = class RecursiveAfterUpdate extends _RuleCommon.RuleCo
86
88
  }
87
89
  }
88
90
  // === Lookup → same object type updates ===
89
- const lookupElementsWithTheSameObjectType = (_flow_elements1 = flow.elements) === null || _flow_elements1 === void 0 ? void 0 : (_flow_elements_filter = _flow_elements1.filter((node)=>node.subtype === "recordLookups" && typeof node.element === "object" && "object" in node.element && flow.start.object === node.element["object"])) === null || _flow_elements_filter === void 0 ? void 0 : _flow_elements_filter.map((node)=>node.name);
91
+ const flowObject = this.getStartProperty(flow, 'object');
92
+ const lookupElementsWithTheSameObjectType = (_flow_elements1 = flow.elements) === null || _flow_elements1 === void 0 ? void 0 : (_flow_elements_filter = _flow_elements1.filter((node)=>node.subtype === "recordLookups" && typeof node.element === "object" && "object" in node.element && flowObject === node.element["object"])) === null || _flow_elements_filter === void 0 ? void 0 : _flow_elements_filter.map((node)=>node.name);
90
93
  if (lookupElementsWithTheSameObjectType == null || typeof lookupElementsWithTheSameObjectType[Symbol.iterator] !== "function") {
91
94
  return results;
92
95
  }
@@ -66,10 +66,12 @@ function _interop_require_wildcard(obj, nodeInterop) {
66
66
  }
67
67
  let SameRecordFieldUpdates = class SameRecordFieldUpdates extends _RuleCommon.RuleCommon {
68
68
  check(flow, _options, _suppressions) {
69
- var _flow_start, _flow_start1, _flow_elements;
69
+ var _flow_elements;
70
70
  const results = [];
71
- const isBeforeSaveType = ((_flow_start = flow.start) === null || _flow_start === void 0 ? void 0 : _flow_start.triggerType) === "RecordBeforeSave";
72
- const isQualifiedTriggerTypes = this.qualifiedRecordTriggerTypes.has((_flow_start1 = flow.start) === null || _flow_start1 === void 0 ? void 0 : _flow_start1.recordTriggerType);
71
+ const triggerType = this.getStartProperty(flow, 'triggerType');
72
+ const recordTriggerType = this.getStartProperty(flow, 'recordTriggerType');
73
+ const isBeforeSaveType = triggerType === "RecordBeforeSave";
74
+ const isQualifiedTriggerTypes = this.qualifiedRecordTriggerTypes.has(recordTriggerType);
73
75
  if (!isBeforeSaveType || !isQualifiedTriggerTypes) {
74
76
  return results;
75
77
  }
@@ -2,7 +2,6 @@ import * as core from "../internals/internals";
2
2
  import { RuleCommon } from "../models/RuleCommon";
3
3
  import { IRuleDefinition } from "../interfaces/IRuleDefinition";
4
4
  export declare class TriggerOrder extends RuleCommon implements IRuleDefinition {
5
- protected qualifiedRecordTriggerTypes: Set<string>;
6
5
  constructor();
7
6
  protected check(flow: core.Flow, _options: object | undefined, _suppressions: Set<string>): core.Violation[];
8
7
  }
@@ -10,19 +10,6 @@ Object.defineProperty(exports, "TriggerOrder", {
10
10
  });
11
11
  const _internals = /*#__PURE__*/ _interop_require_wildcard(require("../internals/internals"));
12
12
  const _RuleCommon = require("../models/RuleCommon");
13
- function _define_property(obj, key, value) {
14
- if (key in obj) {
15
- Object.defineProperty(obj, key, {
16
- value: value,
17
- enumerable: true,
18
- configurable: true,
19
- writable: true
20
- });
21
- } else {
22
- obj[key] = value;
23
- }
24
- return obj;
25
- }
26
13
  function _getRequireWildcardCache(nodeInterop) {
27
14
  if (typeof WeakMap !== "function") return null;
28
15
  var cacheBabelInterop = new WeakMap();
@@ -66,7 +53,12 @@ function _interop_require_wildcard(obj, nodeInterop) {
66
53
  }
67
54
  let TriggerOrder = class TriggerOrder extends _RuleCommon.RuleCommon {
68
55
  check(flow, _options, _suppressions) {
69
- if (!("object" in flow.start)) return [];
56
+ const startObject = this.getStartProperty(flow, "object");
57
+ // If there's no `object` on the start node, this is NOT a record-triggered flow
58
+ if (!startObject) {
59
+ return [];
60
+ }
61
+ // This *is* a record-triggered flow → should have triggerOrder
70
62
  if (!flow.triggerOrder) {
71
63
  return [
72
64
  new _internals.Violation(new _internals.FlowAttribute("TriggerOrder", "TriggerOrder", "10, 20, 30 ..."))
@@ -78,7 +70,7 @@ let TriggerOrder = class TriggerOrder extends _RuleCommon.RuleCommon {
78
70
  super({
79
71
  name: "TriggerOrder",
80
72
  label: "Trigger Order",
81
- 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.",
73
+ 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.",
82
74
  supportedTypes: [
83
75
  _internals.FlowType.autolaunchedType
84
76
  ],
@@ -90,9 +82,6 @@ let TriggerOrder = class TriggerOrder extends _RuleCommon.RuleCommon {
90
82
  ]
91
83
  }, {
92
84
  severity: "note"
93
- }), _define_property(this, "qualifiedRecordTriggerTypes", new Set([
94
- "Create",
95
- "Update"
96
- ]));
85
+ });
97
86
  }
98
87
  };
@@ -4,5 +4,4 @@ import { IRuleDefinition } from "../interfaces/IRuleDefinition";
4
4
  export declare class UnconnectedElement extends RuleCommon implements IRuleDefinition {
5
5
  constructor();
6
6
  protected check(flow: core.Flow, _options: object | undefined, suppressions: Set<string>): core.Violation[];
7
- private findStart;
8
7
  }
@@ -58,18 +58,14 @@ let UnconnectedElement = class UnconnectedElement extends _RuleCommon.RuleCommon
58
58
  connectedElements.add(element.name);
59
59
  };
60
60
  const flowElements = flow.elements.filter((node)=>node instanceof _internals.FlowNode);
61
- const startIndex = this.findStart(flowElements);
62
- if (startIndex !== -1) {
63
- new _internals.Compiler().traverseFlow(flow, flowElements[startIndex].name, logConnected);
61
+ // Use the helper to get the start reference
62
+ const startRef = this.getStartReference(flow);
63
+ if (startRef) {
64
+ new _internals.Compiler().traverseFlow(flow, startRef, logConnected);
64
65
  }
65
66
  const unconnectedElements = flowElements.filter((element)=>!connectedElements.has(element.name) && !suppressions.has(element.name));
66
67
  return unconnectedElements.map((det)=>new _internals.Violation(det));
67
68
  }
68
- findStart(nodes) {
69
- return nodes.findIndex((n)=>{
70
- return n.subtype === "start";
71
- });
72
- }
73
69
  constructor(){
74
70
  super({
75
71
  description: "To maintain the efficiency and manageability of your Flow, it's best to avoid including unconnected elements that are not in use.",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flow-scanner/lightning-flow-scanner-core",
3
3
  "description": "A lightweight engine for Flow metadata in Node.js, and browser environments. Assess and enhance Salesforce Flow automations for best practices, security, governor limits, and performance issues.",
4
- "version": "6.10.5",
4
+ "version": "6.10.6",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": {