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

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.
Files changed (38) hide show
  1. package/README.md +19 -16
  2. package/index.d.ts +5 -2
  3. package/index.js +19 -0
  4. package/main/config/NodeIcons.d.ts +24 -0
  5. package/main/config/NodeIcons.js +122 -0
  6. package/main/config/VariableIcons.d.ts +25 -0
  7. package/main/config/VariableIcons.js +53 -0
  8. package/main/libs/Compiler.d.ts +1 -2
  9. package/main/libs/Compiler.js +10 -16
  10. package/main/libs/ExportDiagram.d.ts +41 -0
  11. package/main/libs/ExportDiagram.js +40 -0
  12. package/main/libs/ExportSarif.js +1 -1
  13. package/main/models/Flow.d.ts +42 -11
  14. package/main/models/Flow.js +164 -76
  15. package/main/models/FlowGraph.d.ts +85 -0
  16. package/main/models/FlowGraph.js +532 -0
  17. package/main/models/FlowNode.d.ts +58 -2
  18. package/main/models/FlowNode.js +161 -3
  19. package/main/models/FlowVariable.d.ts +59 -1
  20. package/main/models/FlowVariable.js +118 -1
  21. package/main/models/LoopRuleCommon.js +11 -12
  22. package/main/models/ParsedFlow.d.ts +1 -1
  23. package/main/models/RuleCommon.d.ts +30 -7
  24. package/main/models/RuleCommon.js +49 -11
  25. package/main/rules/APIVersion.js +31 -1
  26. package/main/rules/DuplicateDMLOperation.d.ts +1 -2
  27. package/main/rules/DuplicateDMLOperation.js +35 -73
  28. package/main/rules/MissingFaultPath.d.ts +4 -0
  29. package/main/rules/MissingFaultPath.js +19 -15
  30. package/main/rules/MissingFilterRecordTrigger.js +4 -4
  31. package/main/rules/RecordIdAsString.js +3 -2
  32. package/main/rules/RecursiveAfterUpdate.js +7 -4
  33. package/main/rules/SameRecordFieldUpdates.js +5 -3
  34. package/main/rules/TriggerOrder.d.ts +0 -1
  35. package/main/rules/TriggerOrder.js +8 -19
  36. package/main/rules/UnconnectedElement.d.ts +0 -1
  37. package/main/rules/UnconnectedElement.js +2 -13
  38. package/package.json +2 -2
@@ -14,6 +14,7 @@ const _FlowMetadata = require("./FlowMetadata");
14
14
  const _FlowNode = require("./FlowNode");
15
15
  const _FlowResource = require("./FlowResource");
16
16
  const _FlowVariable = require("./FlowVariable");
17
+ const _FlowGraph = require("./FlowGraph");
17
18
  function _define_property(obj, key, value) {
18
19
  if (key in obj) {
19
20
  Object.defineProperty(obj, key, {
@@ -83,7 +84,39 @@ function _object_spread(target) {
83
84
  }
84
85
  return target;
85
86
  }
87
+ function ownKeys(object, enumerableOnly) {
88
+ var keys = Object.keys(object);
89
+ if (Object.getOwnPropertySymbols) {
90
+ var symbols = Object.getOwnPropertySymbols(object);
91
+ if (enumerableOnly) {
92
+ symbols = symbols.filter(function(sym) {
93
+ return Object.getOwnPropertyDescriptor(object, sym).enumerable;
94
+ });
95
+ }
96
+ keys.push.apply(keys, symbols);
97
+ }
98
+ return keys;
99
+ }
100
+ function _object_spread_props(target, source) {
101
+ source = source != null ? source : {};
102
+ if (Object.getOwnPropertyDescriptors) {
103
+ Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
104
+ } else {
105
+ ownKeys(Object(source)).forEach(function(key) {
106
+ Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
107
+ });
108
+ }
109
+ return target;
110
+ }
86
111
  let Flow = class Flow {
112
+ get graph() {
113
+ if (!this._graph) {
114
+ const flowNodes = this.elements.filter((e)=>e instanceof _FlowNode.FlowNode);
115
+ this.startReference || (this.startReference = this.findStart());
116
+ this._graph = new _FlowGraph.FlowGraph(flowNodes, this.startReference, this.startNode);
117
+ }
118
+ return this._graph;
119
+ }
87
120
  static from(obj) {
88
121
  if (obj instanceof Flow) {
89
122
  return obj;
@@ -96,14 +129,17 @@ let Flow = class Flow {
96
129
  return flow;
97
130
  }
98
131
  preProcessNodes() {
99
- this.label = this.xmldata.label;
132
+ if (!this.xmldata) {
133
+ return;
134
+ }
135
+ // Extract top-level attributes
136
+ this.label = this.xmldata.label || "";
100
137
  this.interviewLabel = this.xmldata.interviewLabel;
101
- this.processType = this.xmldata.processType;
138
+ this.processType = this.xmldata.processType || "AutoLaunchedFlow";
139
+ this.type = this.processType;
102
140
  this.processMetadataValues = this.xmldata.processMetadataValues;
103
141
  this.startElementReference = this.xmldata.startElementReference;
104
- this.start = this.xmldata.start;
105
- this.status = this.xmldata.status;
106
- this.type = this.xmldata.processType;
142
+ this.status = this.xmldata.status || "Draft";
107
143
  this.triggerOrder = this.xmldata.triggerOrder;
108
144
  const allNodes = [];
109
145
  for(const nodeType in this.xmldata){
@@ -112,129 +148,182 @@ let Flow = class Flow {
112
148
  continue;
113
149
  }
114
150
  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));
151
+ // Handle start nodes separately - store in startNode property
152
+ if (nodeType === "start") {
153
+ if (Array.isArray(data) && data.length > 0) {
154
+ this.startNode = new _FlowNode.FlowNode(data[0].name || "start", "start", data[0]);
155
+ } else if (!Array.isArray(data)) {
156
+ this.startNode = new _FlowNode.FlowNode(data.name || "start", "start", data);
122
157
  }
158
+ continue;
159
+ }
160
+ // Process other node types
161
+ if (Flow.ATTRIBUTE_TAGS.includes(nodeType)) {
162
+ this.processNodeType(data, nodeType, allNodes, _FlowMetadata.FlowMetadata);
123
163
  } 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
- }
164
+ this.processNodeType(data, nodeType, allNodes, _FlowVariable.FlowVariable);
131
165
  } 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
- }
166
+ this.processNodeType(data, nodeType, allNodes, _FlowNode.FlowNode);
139
167
  } 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
- }
168
+ this.processNodeType(data, nodeType, allNodes, _FlowResource.FlowResource);
147
169
  }
148
170
  }
149
171
  this.elements = allNodes;
150
172
  this.startReference = this.findStart();
173
+ // Build the connectivity graph
174
+ const flowNodes = allNodes.filter((e)=>e instanceof _FlowNode.FlowNode);
175
+ this._graph = new _FlowGraph.FlowGraph(flowNodes, this.startReference, this.startNode);
176
+ }
177
+ visualize(format = 'mermaid', options = {}) {
178
+ if (format === 'mermaid') {
179
+ var _this_xmldata, _this_startNode_element, _this_startNode, _this_startNode_element1, _this_startNode1;
180
+ return this.graph.toMermaid(_object_spread_props(_object_spread({}, options), {
181
+ flowMetadata: {
182
+ label: this.label,
183
+ processType: this.processType,
184
+ status: this.status,
185
+ description: (_this_xmldata = this.xmldata) === null || _this_xmldata === void 0 ? void 0 : _this_xmldata.description,
186
+ triggerType: (_this_startNode = this.startNode) === null || _this_startNode === void 0 ? void 0 : (_this_startNode_element = _this_startNode.element) === null || _this_startNode_element === void 0 ? void 0 : _this_startNode_element['triggerType'],
187
+ object: (_this_startNode1 = this.startNode) === null || _this_startNode1 === void 0 ? void 0 : (_this_startNode_element1 = _this_startNode1.element) === null || _this_startNode_element1 === void 0 ? void 0 : _this_startNode_element1['object']
188
+ }
189
+ }));
190
+ } else if (format === 'plantuml') {
191
+ return this.graph.toPlantUML();
192
+ }
193
+ throw new Error('Unsupported format');
194
+ }
195
+ processNodeType(data, nodeType, allNodes, NodeClass) {
196
+ if (Array.isArray(data)) {
197
+ for (const node of data){
198
+ allNodes.push(new NodeClass(node.name, nodeType, node));
199
+ }
200
+ } else {
201
+ allNodes.push(new NodeClass(data.name, nodeType, data));
202
+ }
203
+ }
204
+ /**
205
+ * Find the name of the first element to execute.
206
+ * Priority order:
207
+ * 1. startElementReference (newer flows, direct XML attribute)
208
+ * 2. Start node connector (older flows, points to first element)
209
+ * 3. Start node scheduledPaths (async flows)
210
+ */ findStart() {
211
+ var _this_startNode;
212
+ // Priority 1: Explicit startElementReference
213
+ if (this.startElementReference) {
214
+ return this.startElementReference;
215
+ }
216
+ // Priority 2: Start node with regular connector
217
+ if (this.startNode && this.startNode.connectors && this.startNode.connectors.length > 0) {
218
+ const connector = this.startNode.connectors[0];
219
+ if (connector.reference) {
220
+ return connector.reference;
221
+ }
222
+ }
223
+ // Priority 3: Start node with scheduledPaths (async flows)
224
+ if ((_this_startNode = this.startNode) === null || _this_startNode === void 0 ? void 0 : _this_startNode.element) {
225
+ const scheduledPaths = this.startNode.element['scheduledPaths'];
226
+ if (scheduledPaths) {
227
+ var _paths_;
228
+ const paths = Array.isArray(scheduledPaths) ? scheduledPaths : [
229
+ scheduledPaths
230
+ ];
231
+ if (paths.length > 0 && ((_paths_ = paths[0]) === null || _paths_ === void 0 ? void 0 : _paths_.connector)) {
232
+ const targetRef = paths[0].connector.targetReference;
233
+ if (targetRef) {
234
+ return targetRef;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ // No valid start found
240
+ return "";
151
241
  }
152
242
  toXMLString() {
153
243
  try {
154
244
  return this.generateDoc();
155
245
  } catch (exception) {
156
- console.warn(`Unable to write xml, caught an error ${exception.toString()}`);
246
+ const errorMsg = exception instanceof Error ? exception.message : String(exception);
247
+ console.warn(`Unable to write xml, caught an error: ${errorMsg}`);
157
248
  return "";
158
249
  }
159
250
  }
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
251
  generateDoc() {
176
- // eslint-disable-next-line sonarjs/no-clear-text-protocols
177
252
  const flowXmlNamespace = "http://soap.sforce.com/2006/04/metadata";
178
253
  const builderOptions = {
179
254
  attributeNamePrefix: "@_",
180
255
  format: true,
181
256
  ignoreAttributes: false,
182
257
  suppressBooleanAttributes: false,
183
- suppressEmptyNode: false // Keep empty tags (but doesn't force self-closing in pretty)
258
+ suppressEmptyNode: false
184
259
  };
185
260
  const builder = new _fastxmlparser.XMLBuilder(builderOptions);
186
- // Fallback: Inject xmlns as attribute if missing
187
261
  const xmldataWithNs = _object_spread({}, this.xmldata);
188
262
  if (!xmldataWithNs["@_xmlns"]) {
189
263
  xmldataWithNs["@_xmlns"] = flowXmlNamespace;
190
264
  }
191
- // Optional: Add xsi if needed (often in parsed data; test has it in root)
192
265
  if (!xmldataWithNs["@_xmlns:xsi"]) {
193
266
  xmldataWithNs["@_xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance";
194
267
  }
195
- // Build: Wrap in { Flow: ... }
196
268
  const rootObj = {
197
269
  Flow: xmldataWithNs
198
270
  };
199
271
  return builder.build(rootObj);
200
272
  }
201
273
  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)
274
+ // Flow elements (excludes legacy start nodes)
275
+ _define_property(this, "elements", []);
276
+ // Path properties
277
+ _define_property(this, "fsPath", void 0); // Resolved absolute path (Node.js only)
278
+ _define_property(this, "uri", void 0); // Input path (could be relative, absolute, or virtual)
279
+ // Flow metadata
280
+ _define_property(this, "label", "");
205
281
  _define_property(this, "interviewLabel", void 0);
206
- _define_property(this, "label", void 0);
207
- _define_property(this, "name", void 0);
282
+ _define_property(this, "name", "unnamed");
208
283
  _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);
284
+ _define_property(this, "processType", "AutoLaunchedFlow");
285
+ _define_property(this, "type", ""); // Alias for processType (backward compatibility)
286
+ _define_property(this, "status", "");
215
287
  _define_property(this, "triggerOrder", void 0);
216
- _define_property(this, "type", void 0);
288
+ // Start-related properties
217
289
  /**
218
- * XML to JSON conversion in raw format
219
- */ _define_property(this, "xmldata", void 0);
290
+ * @deprecated Use startNode.element instead. Kept for backward compatibility.
291
+ */ _define_property(this, "start", void 0);
292
+ /**
293
+ * Direct reference to first element (from XML attribute).
294
+ * Used in newer flows as an alternative to the start element.
295
+ */ _define_property(this, "startElementReference", void 0);
296
+ /**
297
+ * Computed reference to the first element to execute.
298
+ * This is what rules should use for traversal.
299
+ */ _define_property(this, "startReference", void 0);
300
+ /**
301
+ * Parsed FlowNode object of the start element.
302
+ * Contains trigger information and connectors.
303
+ * Access start element data via startNode.element
304
+ */ _define_property(this, "startNode", void 0);
305
+ _define_property(this, "_graph", void 0);
306
+ // Legacy/internal
307
+ _define_property(this, "root", void 0);
308
+ _define_property(this, "xmldata", void 0);
220
309
  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) {
310
+ this.uri = path;
311
+ if (typeof process !== 'undefined' && typeof process.cwd === 'function') {
225
312
  this.fsPath = _path.resolve(path);
226
313
  }
227
314
  let flowName = _path.basename(_path.basename(path), _path.extname(path));
228
315
  if (flowName.includes(".")) {
229
316
  flowName = flowName.split(".")[0];
230
317
  }
231
- this.name = flowName;
318
+ this.name = flowName || "unnamed";
232
319
  }
233
320
  if (data) {
234
- const hasFlowElement = typeof data === "object" && "Flow" in data;
321
+ const hasFlowElement = typeof data === "object" && data !== null && "Flow" in data;
235
322
  if (hasFlowElement) {
236
323
  this.xmldata = data.Flow;
237
- } else this.xmldata = data;
324
+ } else {
325
+ this.xmldata = data;
326
+ }
238
327
  this.preProcessNodes();
239
328
  }
240
329
  }
@@ -276,7 +365,6 @@ let Flow = class Flow {
276
365
  "recordUpdates",
277
366
  "recordRollbacks",
278
367
  "screens",
279
- "start",
280
368
  "steps",
281
369
  "subflows",
282
370
  "waits",
@@ -0,0 +1,85 @@
1
+ import { FlowNode } from "./FlowNode";
2
+ /**
3
+ * FlowGraph: Pre-computed connectivity cache built using Compiler.
4
+ * Built once during Flow.preProcessNodes() to avoid repeated traversals.
5
+ *
6
+ * Uses the existing Compiler to build the graph - no duplicate logic!
7
+ */
8
+ export declare class FlowGraph {
9
+ private nodeMap;
10
+ private reachableFromStart;
11
+ private elementsInLoop;
12
+ private faultConnectors;
13
+ private normalConnectors;
14
+ private allConnectors;
15
+ private reverseConnectors;
16
+ constructor(nodes: FlowNode[], startReference?: string, startNode?: FlowNode);
17
+ /**
18
+ * Add START node connectors to the connector maps (for flows with explicit <start> element)
19
+ */
20
+ private addStartNodeConnectors;
21
+ /**
22
+ * Add START edge for newer flows that use startElementReference (no explicit <start> node)
23
+ */
24
+ private addStartEdgeFromReference;
25
+ /**
26
+ * Build node map for O(1) lookups
27
+ */
28
+ private buildNodeMaps;
29
+ /**
30
+ * Build connector maps by inspecting node connectors
31
+ */
32
+ private buildConnectorMaps;
33
+ /**
34
+ * Use Compiler to compute which elements are reachable from start.
35
+ * This reuses the existing IDDFS traversal logic!
36
+ */
37
+ private computeReachability;
38
+ /**
39
+ * Use Compiler to compute which elements are inside loops.
40
+ * Calls Compiler.traverseFlow() for each loop with endElementName.
41
+ */
42
+ private computeLoopBoundaries;
43
+ isReachable(elementName: string): boolean;
44
+ getReachableElements(): Set<string>;
45
+ isInLoop(elementName: string): boolean;
46
+ getContainingLoop(elementName: string): string | undefined;
47
+ getLoopElements(loopName: string): Set<string>;
48
+ hasFaultConnector(elementName: string): boolean;
49
+ getFaultTargets(elementName: string): string[];
50
+ getNextElements(elementName: string): string[];
51
+ getAllNextElements(elementName: string): string[];
52
+ getPreviousElements(elementName: string): string[];
53
+ getNode(elementName: string): FlowNode | undefined;
54
+ isPartOfFaultHandling(elementName: string): boolean;
55
+ getLoopNodes(): FlowNode[];
56
+ forEachReachable(callback: (node: FlowNode) => void): void;
57
+ /**
58
+ * Export the graph to Mermaid flowchart syntax with rich documentation.
59
+ */
60
+ toMermaid(options?: {
61
+ includeDetails?: boolean;
62
+ includeMarkdownDocs?: boolean;
63
+ collapsedDetails?: boolean;
64
+ flowMetadata?: any;
65
+ }): string;
66
+ private generateMermaidDiagram;
67
+ private generateStartNode;
68
+ private getNodeShape;
69
+ private generateEdges;
70
+ private findEndNodes;
71
+ private generateLoopSubgraphs;
72
+ private generateMermaidStyles;
73
+ private generateNodeDetailsMarkdown;
74
+ private nodeToMarkdownTable;
75
+ private prettifyValue;
76
+ /**
77
+ * Generate full markdown documentation with diagram and node details
78
+ */
79
+ private generateFullMarkdownDoc;
80
+ /**
81
+ * Export the graph to PlantUML syntax for UML-style diagrams.
82
+ * @returns PlantUML string.
83
+ */
84
+ toPlantUML(): string;
85
+ }