@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.
- package/README.md +19 -16
- package/index.d.ts +5 -2
- package/index.js +19 -0
- package/main/config/NodeIcons.d.ts +24 -0
- package/main/config/NodeIcons.js +122 -0
- package/main/config/VariableIcons.d.ts +25 -0
- package/main/config/VariableIcons.js +53 -0
- package/main/libs/Compiler.d.ts +1 -2
- package/main/libs/Compiler.js +10 -16
- package/main/libs/ExportDiagram.d.ts +41 -0
- package/main/libs/ExportDiagram.js +40 -0
- package/main/libs/ExportSarif.js +1 -1
- package/main/models/Flow.d.ts +42 -11
- package/main/models/Flow.js +164 -76
- package/main/models/FlowGraph.d.ts +85 -0
- package/main/models/FlowGraph.js +532 -0
- package/main/models/FlowNode.d.ts +58 -2
- package/main/models/FlowNode.js +161 -3
- package/main/models/FlowVariable.d.ts +59 -1
- package/main/models/FlowVariable.js +118 -1
- package/main/models/LoopRuleCommon.js +11 -12
- package/main/models/ParsedFlow.d.ts +1 -1
- package/main/models/RuleCommon.d.ts +30 -7
- package/main/models/RuleCommon.js +49 -11
- package/main/rules/APIVersion.js +31 -1
- package/main/rules/DuplicateDMLOperation.d.ts +1 -2
- package/main/rules/DuplicateDMLOperation.js +35 -73
- package/main/rules/MissingFaultPath.d.ts +4 -0
- package/main/rules/MissingFaultPath.js +19 -15
- package/main/rules/MissingFilterRecordTrigger.js +4 -4
- package/main/rules/RecordIdAsString.js +3 -2
- package/main/rules/RecursiveAfterUpdate.js +7 -4
- package/main/rules/SameRecordFieldUpdates.js +5 -3
- package/main/rules/TriggerOrder.d.ts +0 -1
- package/main/rules/TriggerOrder.js +8 -19
- package/main/rules/UnconnectedElement.d.ts +0 -1
- package/main/rules/UnconnectedElement.js +2 -13
- package/package.json +2 -2
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "FlowGraph", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return FlowGraph;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _Compiler = require("../libs/Compiler");
|
|
12
|
+
function _define_property(obj, key, value) {
|
|
13
|
+
if (key in obj) {
|
|
14
|
+
Object.defineProperty(obj, key, {
|
|
15
|
+
value: value,
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true
|
|
19
|
+
});
|
|
20
|
+
} else {
|
|
21
|
+
obj[key] = value;
|
|
22
|
+
}
|
|
23
|
+
return obj;
|
|
24
|
+
}
|
|
25
|
+
let FlowGraph = class FlowGraph {
|
|
26
|
+
/**
|
|
27
|
+
* Add START node connectors to the connector maps (for flows with explicit <start> element)
|
|
28
|
+
*/ addStartNodeConnectors(startNode) {
|
|
29
|
+
const startName = 'START';
|
|
30
|
+
this.faultConnectors.set(startName, new Set());
|
|
31
|
+
this.normalConnectors.set(startName, new Set());
|
|
32
|
+
this.allConnectors.set(startName, new Set());
|
|
33
|
+
if (!startNode.connectors || startNode.connectors.length === 0) return;
|
|
34
|
+
for (const connector of startNode.connectors){
|
|
35
|
+
var _connector_connectorTargetReference, // START node typically has normal connectors, not fault connectors
|
|
36
|
+
_this_normalConnectors_get, _this_allConnectors_get, _this_reverseConnectors_get;
|
|
37
|
+
var _connector_connectorTargetReference_targetReference;
|
|
38
|
+
const targetRef = (_connector_connectorTargetReference_targetReference = (_connector_connectorTargetReference = connector.connectorTargetReference) === null || _connector_connectorTargetReference === void 0 ? void 0 : _connector_connectorTargetReference.targetReference) !== null && _connector_connectorTargetReference_targetReference !== void 0 ? _connector_connectorTargetReference_targetReference : connector.reference;
|
|
39
|
+
if (!targetRef) continue;
|
|
40
|
+
(_this_normalConnectors_get = this.normalConnectors.get(startName)) === null || _this_normalConnectors_get === void 0 ? void 0 : _this_normalConnectors_get.add(targetRef);
|
|
41
|
+
(_this_allConnectors_get = this.allConnectors.get(startName)) === null || _this_allConnectors_get === void 0 ? void 0 : _this_allConnectors_get.add(targetRef);
|
|
42
|
+
// Build reverse map
|
|
43
|
+
if (!this.reverseConnectors.has(targetRef)) {
|
|
44
|
+
this.reverseConnectors.set(targetRef, new Set());
|
|
45
|
+
}
|
|
46
|
+
(_this_reverseConnectors_get = this.reverseConnectors.get(targetRef)) === null || _this_reverseConnectors_get === void 0 ? void 0 : _this_reverseConnectors_get.add(startName);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Add START edge for newer flows that use startElementReference (no explicit <start> node)
|
|
51
|
+
*/ addStartEdgeFromReference(startReference) {
|
|
52
|
+
var // Direct edge: START --> first element
|
|
53
|
+
_this_normalConnectors_get, _this_allConnectors_get, _this_reverseConnectors_get;
|
|
54
|
+
const startName = 'START';
|
|
55
|
+
this.faultConnectors.set(startName, new Set());
|
|
56
|
+
this.normalConnectors.set(startName, new Set());
|
|
57
|
+
this.allConnectors.set(startName, new Set());
|
|
58
|
+
(_this_normalConnectors_get = this.normalConnectors.get(startName)) === null || _this_normalConnectors_get === void 0 ? void 0 : _this_normalConnectors_get.add(startReference);
|
|
59
|
+
(_this_allConnectors_get = this.allConnectors.get(startName)) === null || _this_allConnectors_get === void 0 ? void 0 : _this_allConnectors_get.add(startReference);
|
|
60
|
+
// Build reverse map
|
|
61
|
+
if (!this.reverseConnectors.has(startReference)) {
|
|
62
|
+
this.reverseConnectors.set(startReference, new Set());
|
|
63
|
+
}
|
|
64
|
+
(_this_reverseConnectors_get = this.reverseConnectors.get(startReference)) === null || _this_reverseConnectors_get === void 0 ? void 0 : _this_reverseConnectors_get.add(startName);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build node map for O(1) lookups
|
|
68
|
+
*/ buildNodeMaps(nodes) {
|
|
69
|
+
for (const node of nodes){
|
|
70
|
+
this.nodeMap.set(node.name, node);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build connector maps by inspecting node connectors
|
|
75
|
+
*/ buildConnectorMaps(nodes) {
|
|
76
|
+
for (const node of nodes){
|
|
77
|
+
this.faultConnectors.set(node.name, new Set());
|
|
78
|
+
this.normalConnectors.set(node.name, new Set());
|
|
79
|
+
this.allConnectors.set(node.name, new Set());
|
|
80
|
+
if (!node.connectors || node.connectors.length === 0) continue;
|
|
81
|
+
for (const connector of node.connectors){
|
|
82
|
+
var _connector_connectorTargetReference, _this_allConnectors_get, _this_reverseConnectors_get;
|
|
83
|
+
var _connector_connectorTargetReference_targetReference;
|
|
84
|
+
const targetRef = (_connector_connectorTargetReference_targetReference = (_connector_connectorTargetReference = connector.connectorTargetReference) === null || _connector_connectorTargetReference === void 0 ? void 0 : _connector_connectorTargetReference.targetReference) !== null && _connector_connectorTargetReference_targetReference !== void 0 ? _connector_connectorTargetReference_targetReference : connector.reference;
|
|
85
|
+
if (!targetRef) continue;
|
|
86
|
+
// Categorize by connector type
|
|
87
|
+
if (connector.type === "faultConnector") {
|
|
88
|
+
var _this_faultConnectors_get;
|
|
89
|
+
(_this_faultConnectors_get = this.faultConnectors.get(node.name)) === null || _this_faultConnectors_get === void 0 ? void 0 : _this_faultConnectors_get.add(targetRef);
|
|
90
|
+
} else {
|
|
91
|
+
var _this_normalConnectors_get;
|
|
92
|
+
(_this_normalConnectors_get = this.normalConnectors.get(node.name)) === null || _this_normalConnectors_get === void 0 ? void 0 : _this_normalConnectors_get.add(targetRef);
|
|
93
|
+
}
|
|
94
|
+
(_this_allConnectors_get = this.allConnectors.get(node.name)) === null || _this_allConnectors_get === void 0 ? void 0 : _this_allConnectors_get.add(targetRef);
|
|
95
|
+
// Build reverse map for "previous elements" queries
|
|
96
|
+
if (!this.reverseConnectors.has(targetRef)) {
|
|
97
|
+
this.reverseConnectors.set(targetRef, new Set());
|
|
98
|
+
}
|
|
99
|
+
(_this_reverseConnectors_get = this.reverseConnectors.get(targetRef)) === null || _this_reverseConnectors_get === void 0 ? void 0 : _this_reverseConnectors_get.add(node.name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Use Compiler to compute which elements are reachable from start.
|
|
105
|
+
* This reuses the existing IDDFS traversal logic!
|
|
106
|
+
*/ computeReachability(startReference) {
|
|
107
|
+
const compiler = new _Compiler.Compiler();
|
|
108
|
+
compiler.traverseFlow(startReference, (element)=>{
|
|
109
|
+
this.reachableFromStart.add(element.name);
|
|
110
|
+
}, this.nodeMap, this.allConnectors);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Use Compiler to compute which elements are inside loops.
|
|
114
|
+
* Calls Compiler.traverseFlow() for each loop with endElementName.
|
|
115
|
+
*/ computeLoopBoundaries() {
|
|
116
|
+
const loopNodes = Array.from(this.nodeMap.values()).filter((n)=>n.subtype === "loops");
|
|
117
|
+
for (const loopNode of loopNodes){
|
|
118
|
+
var _loopNode_element_noMoreValuesConnector, _loopNode_element;
|
|
119
|
+
var _loopNode_element_noMoreValuesConnector_targetReference;
|
|
120
|
+
// Find loop end (noMoreValuesConnector)
|
|
121
|
+
const loopEnd = (_loopNode_element_noMoreValuesConnector_targetReference = (_loopNode_element = loopNode.element) === null || _loopNode_element === void 0 ? void 0 : (_loopNode_element_noMoreValuesConnector = _loopNode_element.noMoreValuesConnector) === null || _loopNode_element_noMoreValuesConnector === void 0 ? void 0 : _loopNode_element_noMoreValuesConnector.targetReference) !== null && _loopNode_element_noMoreValuesConnector_targetReference !== void 0 ? _loopNode_element_noMoreValuesConnector_targetReference : loopNode.name;
|
|
122
|
+
// Use Compiler to find all elements between loop start and end
|
|
123
|
+
const compiler = new _Compiler.Compiler();
|
|
124
|
+
compiler.traverseFlow(loopNode.name, (element)=>{
|
|
125
|
+
this.elementsInLoop.set(element.name, loopNode.name);
|
|
126
|
+
}, this.nodeMap, this.allConnectors, loopEnd); // Pass endElementName to stop at loop boundary
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ========== PUBLIC QUERY API ==========
|
|
130
|
+
isReachable(elementName) {
|
|
131
|
+
return this.reachableFromStart.has(elementName);
|
|
132
|
+
}
|
|
133
|
+
getReachableElements() {
|
|
134
|
+
return new Set(this.reachableFromStart);
|
|
135
|
+
}
|
|
136
|
+
isInLoop(elementName) {
|
|
137
|
+
return this.elementsInLoop.has(elementName);
|
|
138
|
+
}
|
|
139
|
+
getContainingLoop(elementName) {
|
|
140
|
+
return this.elementsInLoop.get(elementName);
|
|
141
|
+
}
|
|
142
|
+
getLoopElements(loopName) {
|
|
143
|
+
const result = new Set();
|
|
144
|
+
for (const [element, loop] of this.elementsInLoop){
|
|
145
|
+
if (loop === loopName) {
|
|
146
|
+
result.add(element);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
hasFaultConnector(elementName) {
|
|
152
|
+
const faults = this.faultConnectors.get(elementName);
|
|
153
|
+
return faults ? faults.size > 0 : false;
|
|
154
|
+
}
|
|
155
|
+
getFaultTargets(elementName) {
|
|
156
|
+
return Array.from(this.faultConnectors.get(elementName) || []);
|
|
157
|
+
}
|
|
158
|
+
getNextElements(elementName) {
|
|
159
|
+
return Array.from(this.normalConnectors.get(elementName) || []);
|
|
160
|
+
}
|
|
161
|
+
getAllNextElements(elementName) {
|
|
162
|
+
return Array.from(this.allConnectors.get(elementName) || []);
|
|
163
|
+
}
|
|
164
|
+
getPreviousElements(elementName) {
|
|
165
|
+
return Array.from(this.reverseConnectors.get(elementName) || []);
|
|
166
|
+
}
|
|
167
|
+
getNode(elementName) {
|
|
168
|
+
return this.nodeMap.get(elementName);
|
|
169
|
+
}
|
|
170
|
+
isPartOfFaultHandling(elementName) {
|
|
171
|
+
const previous = this.getPreviousElements(elementName);
|
|
172
|
+
return previous.some((prev)=>{
|
|
173
|
+
const faultTargets = this.faultConnectors.get(prev);
|
|
174
|
+
var _faultTargets_has;
|
|
175
|
+
return (_faultTargets_has = faultTargets === null || faultTargets === void 0 ? void 0 : faultTargets.has(elementName)) !== null && _faultTargets_has !== void 0 ? _faultTargets_has : false;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
getLoopNodes() {
|
|
179
|
+
return Array.from(this.nodeMap.values()).filter((n)=>n.subtype === "loops");
|
|
180
|
+
}
|
|
181
|
+
forEachReachable(callback) {
|
|
182
|
+
for (const elementName of this.reachableFromStart){
|
|
183
|
+
const node = this.nodeMap.get(elementName);
|
|
184
|
+
if (node) {
|
|
185
|
+
callback(node);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Export the graph to Mermaid flowchart syntax with rich documentation.
|
|
191
|
+
*/ toMermaid(options = {}) {
|
|
192
|
+
let output = "";
|
|
193
|
+
const diagram = this.generateMermaidDiagram(options);
|
|
194
|
+
if (options.includeMarkdownDocs) {
|
|
195
|
+
output = this.generateFullMarkdownDoc(diagram, options);
|
|
196
|
+
} else {
|
|
197
|
+
output = `\`\`\`mermaid\n${diagram}\n\`\`\``;
|
|
198
|
+
}
|
|
199
|
+
return output;
|
|
200
|
+
}
|
|
201
|
+
generateMermaidDiagram(options) {
|
|
202
|
+
let mermaid = 'flowchart TB\n';
|
|
203
|
+
// START node with flow metadata
|
|
204
|
+
mermaid += this.generateStartNode(options.flowMetadata) + '\n\n';
|
|
205
|
+
// Define nodes using FlowNode helper methods
|
|
206
|
+
for (const [name, node] of this.nodeMap){
|
|
207
|
+
const icon = node.getIcon();
|
|
208
|
+
const typeLabel = node.getTypeLabel();
|
|
209
|
+
const summary = options.includeDetails ? node.getSummary() : '';
|
|
210
|
+
let label = `${icon} <em>${typeLabel}</em><br/>${node.label || node.name}`;
|
|
211
|
+
if (summary) {
|
|
212
|
+
label += `<br/><small>${summary}</small>`;
|
|
213
|
+
}
|
|
214
|
+
const shape = this.getNodeShape(node.subtype);
|
|
215
|
+
mermaid += ` ${name}${shape[0]}"${label}"${shape[1]}:::${node.subtype}\n`;
|
|
216
|
+
}
|
|
217
|
+
mermaid += '\n';
|
|
218
|
+
mermaid += this.generateEdges() + '\n';
|
|
219
|
+
mermaid += this.generateLoopSubgraphs() + '\n';
|
|
220
|
+
mermaid += this.generateMermaidStyles();
|
|
221
|
+
return mermaid;
|
|
222
|
+
}
|
|
223
|
+
generateStartNode(flowMetadata) {
|
|
224
|
+
if (!flowMetadata) {
|
|
225
|
+
return 'START(["\uD83D\uDE80 <b>START</b>"]):::startClass';
|
|
226
|
+
}
|
|
227
|
+
let label = '\uD83D\uDE80 <b>START</b>'; // ROCKET
|
|
228
|
+
if (flowMetadata.processType === 'Flow') {
|
|
229
|
+
label += '<br/><b>Screen Flow</b>';
|
|
230
|
+
} else if (flowMetadata.processType === 'AutoLaunchedFlow') {
|
|
231
|
+
label += '<br/><b>AutoLaunched Flow</b>';
|
|
232
|
+
if (flowMetadata.triggerType) {
|
|
233
|
+
label += `<br/>Type: <b>${this.prettifyValue(flowMetadata.triggerType)}</b>`;
|
|
234
|
+
}
|
|
235
|
+
} else if (flowMetadata.object) {
|
|
236
|
+
label += `<br/><b>${flowMetadata.object}</b>`;
|
|
237
|
+
if (flowMetadata.triggerType) {
|
|
238
|
+
label += `<br/>Type: <b>${this.prettifyValue(flowMetadata.triggerType)}</b>`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (flowMetadata.status) {
|
|
242
|
+
const statusIcon = flowMetadata.status === 'Active' ? '✅' : '⚠️';
|
|
243
|
+
label += `<br/>${statusIcon} ${flowMetadata.status}`;
|
|
244
|
+
}
|
|
245
|
+
return `START(["${label}"]):::startClass`;
|
|
246
|
+
}
|
|
247
|
+
getNodeShape(subtype) {
|
|
248
|
+
const shapeMap = {
|
|
249
|
+
decisions: [
|
|
250
|
+
'{',
|
|
251
|
+
'}'
|
|
252
|
+
],
|
|
253
|
+
loops: [
|
|
254
|
+
'{{',
|
|
255
|
+
'}}'
|
|
256
|
+
],
|
|
257
|
+
collectionProcessors: [
|
|
258
|
+
'{{',
|
|
259
|
+
'}}'
|
|
260
|
+
],
|
|
261
|
+
transforms: [
|
|
262
|
+
'{{',
|
|
263
|
+
'}}'
|
|
264
|
+
],
|
|
265
|
+
screens: [
|
|
266
|
+
'([',
|
|
267
|
+
'])'
|
|
268
|
+
],
|
|
269
|
+
recordCreates: [
|
|
270
|
+
'[(',
|
|
271
|
+
')]'
|
|
272
|
+
],
|
|
273
|
+
recordDeletes: [
|
|
274
|
+
'[(',
|
|
275
|
+
')]'
|
|
276
|
+
],
|
|
277
|
+
recordLookups: [
|
|
278
|
+
'[(',
|
|
279
|
+
')]'
|
|
280
|
+
],
|
|
281
|
+
recordUpdates: [
|
|
282
|
+
'[(',
|
|
283
|
+
')]'
|
|
284
|
+
],
|
|
285
|
+
subflows: [
|
|
286
|
+
'[[',
|
|
287
|
+
']]'
|
|
288
|
+
],
|
|
289
|
+
assignments: [
|
|
290
|
+
'[\\',
|
|
291
|
+
'/]'
|
|
292
|
+
],
|
|
293
|
+
default: [
|
|
294
|
+
'(',
|
|
295
|
+
')'
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
return shapeMap[subtype] || shapeMap.default;
|
|
299
|
+
}
|
|
300
|
+
generateEdges() {
|
|
301
|
+
let edges = '';
|
|
302
|
+
// Normal connectors
|
|
303
|
+
for (const [from, targets] of this.allConnectors){
|
|
304
|
+
for (const to of targets){
|
|
305
|
+
edges += ` ${from} --> ${to}\n`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Fault connectors (dashed)
|
|
309
|
+
for (const [from, faults] of this.faultConnectors){
|
|
310
|
+
for (const to of faults){
|
|
311
|
+
edges += ` ${from} -. Fault .-> ${to}\n`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Add END nodes
|
|
315
|
+
const endNodes = this.findEndNodes();
|
|
316
|
+
for (const endNode of endNodes){
|
|
317
|
+
edges += ` ${endNode}(( END )):::endClass\n`;
|
|
318
|
+
}
|
|
319
|
+
return edges;
|
|
320
|
+
}
|
|
321
|
+
findEndNodes() {
|
|
322
|
+
const endNodes = new Set();
|
|
323
|
+
for (const [from, targets] of this.allConnectors){
|
|
324
|
+
for (const to of targets){
|
|
325
|
+
// If target doesn't exist in nodeMap, it's an END
|
|
326
|
+
if (!this.nodeMap.has(to)) {
|
|
327
|
+
endNodes.add(to);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return endNodes;
|
|
332
|
+
}
|
|
333
|
+
generateLoopSubgraphs() {
|
|
334
|
+
let subgraphs = '';
|
|
335
|
+
for (const loopNode of this.getLoopNodes()){
|
|
336
|
+
const loopElems = this.getLoopElements(loopNode.name);
|
|
337
|
+
if (loopElems.size > 0) {
|
|
338
|
+
subgraphs += ` subgraph "${loopNode.label || loopNode.name} Loop"\n`;
|
|
339
|
+
for (const elem of loopElems){
|
|
340
|
+
subgraphs += ` ${elem}\n`;
|
|
341
|
+
}
|
|
342
|
+
subgraphs += ' end\n';
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return subgraphs;
|
|
346
|
+
}
|
|
347
|
+
generateMermaidStyles() {
|
|
348
|
+
const styles = {
|
|
349
|
+
actionCalls: {
|
|
350
|
+
fill: '#D4E4FC',
|
|
351
|
+
color: 'black'
|
|
352
|
+
},
|
|
353
|
+
assignments: {
|
|
354
|
+
fill: '#FBEED7',
|
|
355
|
+
color: 'black'
|
|
356
|
+
},
|
|
357
|
+
collectionProcessors: {
|
|
358
|
+
fill: '#F0E3FA',
|
|
359
|
+
color: 'black'
|
|
360
|
+
},
|
|
361
|
+
customErrors: {
|
|
362
|
+
fill: '#FFE9E9',
|
|
363
|
+
color: 'black'
|
|
364
|
+
},
|
|
365
|
+
decisions: {
|
|
366
|
+
fill: '#FDEAF6',
|
|
367
|
+
color: 'black'
|
|
368
|
+
},
|
|
369
|
+
loops: {
|
|
370
|
+
fill: '#FDEAF6',
|
|
371
|
+
color: 'black'
|
|
372
|
+
},
|
|
373
|
+
recordCreates: {
|
|
374
|
+
fill: '#FFF8C9',
|
|
375
|
+
color: 'black'
|
|
376
|
+
},
|
|
377
|
+
recordDeletes: {
|
|
378
|
+
fill: '#FFF8C9',
|
|
379
|
+
color: 'black'
|
|
380
|
+
},
|
|
381
|
+
recordLookups: {
|
|
382
|
+
fill: '#EDEAFF',
|
|
383
|
+
color: 'black'
|
|
384
|
+
},
|
|
385
|
+
recordUpdates: {
|
|
386
|
+
fill: '#FFF8C9',
|
|
387
|
+
color: 'black'
|
|
388
|
+
},
|
|
389
|
+
screens: {
|
|
390
|
+
fill: '#DFF6FF',
|
|
391
|
+
color: 'black'
|
|
392
|
+
},
|
|
393
|
+
subflows: {
|
|
394
|
+
fill: '#D4E4FC',
|
|
395
|
+
color: 'black'
|
|
396
|
+
},
|
|
397
|
+
transforms: {
|
|
398
|
+
fill: '#FDEAF6',
|
|
399
|
+
color: 'black'
|
|
400
|
+
},
|
|
401
|
+
startClass: {
|
|
402
|
+
fill: '#D9F2E6',
|
|
403
|
+
color: 'black'
|
|
404
|
+
},
|
|
405
|
+
endClass: {
|
|
406
|
+
fill: '#F9BABA',
|
|
407
|
+
color: 'black'
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
let styleStr = '';
|
|
411
|
+
for (const [className, style] of Object.entries(styles)){
|
|
412
|
+
styleStr += ` classDef ${className} fill:${style.fill},color:${style.color},stroke:#333,stroke-width:2px\n`;
|
|
413
|
+
}
|
|
414
|
+
return styleStr;
|
|
415
|
+
}
|
|
416
|
+
generateNodeDetailsMarkdown(collapsed) {
|
|
417
|
+
let md = '## Flow Nodes Details\n\n';
|
|
418
|
+
if (collapsed) {
|
|
419
|
+
md += '<details><summary>NODE DETAILS (expand to view)</summary>\n\n';
|
|
420
|
+
}
|
|
421
|
+
for (const [name, node] of this.nodeMap){
|
|
422
|
+
md += `### ${name}\n\n`;
|
|
423
|
+
md += this.nodeToMarkdownTable(node);
|
|
424
|
+
md += '\n';
|
|
425
|
+
}
|
|
426
|
+
if (collapsed) {
|
|
427
|
+
md += '</details>\n\n';
|
|
428
|
+
}
|
|
429
|
+
return md;
|
|
430
|
+
}
|
|
431
|
+
nodeToMarkdownTable(node) {
|
|
432
|
+
let table = '| Property | Value |\n|:---|:---|\n';
|
|
433
|
+
// Use typed properties from FlowNode
|
|
434
|
+
if (node.label) table += `| Label | ${node.label} |\n`;
|
|
435
|
+
table += `| Type | ${node.getTypeLabel()} |\n`;
|
|
436
|
+
// Type-specific properties (now type-safe!)
|
|
437
|
+
if (node.actionType) table += `| Action Type | ${this.prettifyValue(node.actionType)} |\n`;
|
|
438
|
+
if (node.actionName) table += `| Action Name | ${node.actionName} |\n`;
|
|
439
|
+
if (node.object) table += `| Object | ${node.object} |\n`;
|
|
440
|
+
if (node.flowName) table += `| Subflow | ${node.flowName} |\n`;
|
|
441
|
+
if (node.collectionReference) table += `| Collection | ${node.collectionReference} |\n`;
|
|
442
|
+
if (node.elementSubtype) table += `| Subtype | ${this.prettifyValue(node.elementSubtype)} |\n`;
|
|
443
|
+
// Decision rules
|
|
444
|
+
if (node.rules && node.rules.length > 0) {
|
|
445
|
+
table += `| Rules | ${node.rules.length} |\n`;
|
|
446
|
+
for (const rule of node.rules){
|
|
447
|
+
const conditions = Array.isArray(rule.conditions) ? rule.conditions : rule.conditions ? [
|
|
448
|
+
rule.conditions
|
|
449
|
+
] : [];
|
|
450
|
+
table += `| ↳ ${rule.label || rule.name} | ${conditions.length} condition(s) |\n`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Screen fields
|
|
454
|
+
if (node.fields && node.fields.length > 0) {
|
|
455
|
+
table += `| Fields | ${node.fields.length} |\n`;
|
|
456
|
+
}
|
|
457
|
+
if (node.description) table += `| Description | ${node.description} |\n`;
|
|
458
|
+
if (node.faultConnector) table += `| Has Fault Handler | ✅ |\n`;
|
|
459
|
+
return table;
|
|
460
|
+
}
|
|
461
|
+
prettifyValue(value) {
|
|
462
|
+
return value.replace(/([A-Z])/g, ' $1').replace(/^./, (str)=>str.toUpperCase()).trim();
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Generate full markdown documentation with diagram and node details
|
|
466
|
+
*/ generateFullMarkdownDoc(diagram, options) {
|
|
467
|
+
let md = '';
|
|
468
|
+
// Header with flow metadata would come from Flow class
|
|
469
|
+
md += '## Flow Diagram\n\n';
|
|
470
|
+
md += '```mermaid\n';
|
|
471
|
+
md += diagram;
|
|
472
|
+
md += '\n```\n\n';
|
|
473
|
+
// Node details section
|
|
474
|
+
if (options.includeDetails) {
|
|
475
|
+
md += this.generateNodeDetailsMarkdown(options.collapsedDetails);
|
|
476
|
+
}
|
|
477
|
+
return md;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Export the graph to PlantUML syntax for UML-style diagrams.
|
|
481
|
+
* @returns PlantUML string.
|
|
482
|
+
*/ toPlantUML() {
|
|
483
|
+
let plantuml = '@startuml\nskinparam activityBackgroundColor #D4E4FC\n'; // Basic styling
|
|
484
|
+
// Nodes
|
|
485
|
+
for (const [name, node] of this.nodeMap){
|
|
486
|
+
plantuml += `activity "${node.subtype}: ${name}" as ${name}\n`;
|
|
487
|
+
}
|
|
488
|
+
// Edges
|
|
489
|
+
for (const [from, targets] of this.allConnectors){
|
|
490
|
+
for (const to of targets){
|
|
491
|
+
plantuml += `${from} --> ${to}\n`;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Loops as groups
|
|
495
|
+
for (const loopNode of this.getLoopNodes()){
|
|
496
|
+
plantuml += `partition "${loopNode.name} Loop" {\n`;
|
|
497
|
+
const loopElems = this.getLoopElements(loopNode.name);
|
|
498
|
+
for (const elem of loopElems){
|
|
499
|
+
plantuml += ` ${elem}\n`;
|
|
500
|
+
}
|
|
501
|
+
plantuml += '}\n';
|
|
502
|
+
}
|
|
503
|
+
plantuml += '@enduml';
|
|
504
|
+
return plantuml;
|
|
505
|
+
}
|
|
506
|
+
constructor(nodes, startReference, startNode){
|
|
507
|
+
// Fast lookups by element name
|
|
508
|
+
_define_property(this, "nodeMap", new Map());
|
|
509
|
+
// Pre-computed sets for common queries (built using Compiler)
|
|
510
|
+
_define_property(this, "reachableFromStart", new Set());
|
|
511
|
+
_define_property(this, "elementsInLoop", new Map()); // element -> loop name
|
|
512
|
+
// Connector metadata (extracted during node processing)
|
|
513
|
+
_define_property(this, "faultConnectors", new Map());
|
|
514
|
+
_define_property(this, "normalConnectors", new Map());
|
|
515
|
+
_define_property(this, "allConnectors", new Map());
|
|
516
|
+
_define_property(this, "reverseConnectors", new Map());
|
|
517
|
+
this.buildNodeMaps(nodes);
|
|
518
|
+
this.buildConnectorMaps(nodes);
|
|
519
|
+
// ALWAYS ensure START node edges exist
|
|
520
|
+
if (startNode) {
|
|
521
|
+
// Old flows: use explicit <start> element connectors
|
|
522
|
+
this.addStartNodeConnectors(startNode);
|
|
523
|
+
} else if (startReference) {
|
|
524
|
+
// New flows: direct edge from START to startElementReference
|
|
525
|
+
this.addStartEdgeFromReference(startReference);
|
|
526
|
+
}
|
|
527
|
+
this.computeLoopBoundaries();
|
|
528
|
+
if (startReference) {
|
|
529
|
+
this.computeReachability(startReference);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
@@ -1,9 +1,65 @@
|
|
|
1
1
|
import { FlowElement } from "./FlowElement";
|
|
2
2
|
import { FlowElementConnector } from "./FlowElementConnector";
|
|
3
|
+
import { type NodeIconConfig } from "../config/NodeIcons";
|
|
3
4
|
export declare class FlowNode extends FlowElement {
|
|
4
5
|
connectors: FlowElementConnector[];
|
|
5
|
-
locationX
|
|
6
|
-
locationY
|
|
6
|
+
locationX?: string;
|
|
7
|
+
locationY?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
actionType?: string;
|
|
11
|
+
actionName?: string;
|
|
12
|
+
object?: string;
|
|
13
|
+
inputReference?: string;
|
|
14
|
+
outputReference?: string;
|
|
15
|
+
elementSubtype?: string;
|
|
16
|
+
collectionReference?: string;
|
|
17
|
+
flowName?: string;
|
|
18
|
+
rules?: any[];
|
|
19
|
+
defaultConnectorLabel?: string;
|
|
20
|
+
iterationOrder?: string;
|
|
21
|
+
fields?: any[];
|
|
22
|
+
allowPause?: boolean;
|
|
23
|
+
showFooter?: boolean;
|
|
24
|
+
faultConnector?: FlowElementConnector;
|
|
25
|
+
private static iconConfig;
|
|
26
|
+
/**
|
|
27
|
+
* Set custom icon configuration for all FlowNodes
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* // Use ASCII icons for old terminals
|
|
31
|
+
* FlowNode.setIconConfig(ASCII_ICONS);
|
|
32
|
+
*
|
|
33
|
+
* // Or provide custom icons
|
|
34
|
+
* FlowNode.setIconConfig({
|
|
35
|
+
* actionCalls: { default: '[ACTION]' },
|
|
36
|
+
* decisions: { default: '[IF]' }
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
static setIconConfig(config: NodeIconConfig): void;
|
|
41
|
+
/**
|
|
42
|
+
* Use ASCII icons instead of emoji (for older browsers/terminals)
|
|
43
|
+
*/
|
|
44
|
+
static useAsciiIcons(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Reset to default emoji icons
|
|
47
|
+
*/
|
|
48
|
+
static useDefaultIcons(): void;
|
|
7
49
|
constructor(provName: string, subtype: string, element: object);
|
|
50
|
+
private extractTypeSpecificProperties;
|
|
51
|
+
/**
|
|
52
|
+
* Get a human-readable summary of this node
|
|
53
|
+
*/
|
|
54
|
+
getSummary(): string;
|
|
55
|
+
/**
|
|
56
|
+
* Get the icon for this node type
|
|
57
|
+
*/
|
|
58
|
+
getIcon(): string;
|
|
59
|
+
/**
|
|
60
|
+
* Get the display name for this node type
|
|
61
|
+
*/
|
|
62
|
+
getTypeLabel(): string;
|
|
63
|
+
private prettifyValue;
|
|
8
64
|
private getConnectors;
|
|
9
65
|
}
|