@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.
- package/main/libs/ExportSarif.js +1 -1
- package/main/models/Flow.d.ts +34 -11
- package/main/models/Flow.js +109 -76
- 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 +0 -1
- package/main/rules/DuplicateDMLOperation.js +5 -12
- package/main/rules/MissingFaultPath.d.ts +4 -0
- package/main/rules/MissingFaultPath.js +22 -8
- 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 +4 -8
- package/package.json +1 -1
package/main/libs/ExportSarif.js
CHANGED
|
@@ -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
|
|
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: {
|
package/main/models/Flow.d.ts
CHANGED
|
@@ -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", "
|
|
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
|
|
14
|
+
elements: FlowElement[];
|
|
14
15
|
fsPath?: string;
|
|
15
16
|
uri?: string;
|
|
16
|
-
interviewLabel?: string;
|
|
17
17
|
label: string;
|
|
18
|
-
|
|
18
|
+
interviewLabel?: string;
|
|
19
|
+
name: string;
|
|
19
20
|
processMetadataValues?: any;
|
|
20
|
-
processType
|
|
21
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/main/models/Flow.js
CHANGED
|
@@ -96,14 +96,17 @@ let Flow = class Flow {
|
|
|
96
96
|
return flow;
|
|
97
97
|
}
|
|
98
98
|
preProcessNodes() {
|
|
99
|
-
|
|
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.
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
203
|
-
_define_property(this, "
|
|
204
|
-
|
|
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, "
|
|
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",
|
|
210
|
-
_define_property(this, "
|
|
211
|
-
_define_property(this, "
|
|
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
|
-
|
|
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
|
-
*
|
|
219
|
-
|
|
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;
|
|
222
|
-
|
|
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
|
|
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",
|
|
@@ -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
|
-
*
|
|
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
|
-
|
|
29
|
+
protected getStartNode(flow: core.Flow): core.FlowNode | undefined;
|
|
23
30
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
37
|
+
protected getStartReference(flow: core.Flow): string | undefined;
|
|
28
38
|
/**
|
|
29
|
-
*
|
|
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
|
|
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);
|
|
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);
|
package/main/rules/APIVersion.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
58
|
-
const
|
|
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 =
|
|
87
|
+
const elementsWhereFaultPathIsApplicable = flow.elements.filter((node)=>{
|
|
89
88
|
const proxyNode = node;
|
|
90
89
|
return this.isValidSubtype(proxyNode);
|
|
91
|
-
})
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
56
|
+
var _flow_xmldata_start, _flow_xmldata;
|
|
57
57
|
const violations = [];
|
|
58
58
|
// Check if this is a record-triggered flow
|
|
59
|
-
const 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 = (
|
|
68
|
+
const filters = this.getStartProperty(flow, 'filters');
|
|
69
69
|
const hasFilters = !!filters;
|
|
70
|
-
const 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
|
|
56
|
+
var _flow_elements;
|
|
57
57
|
const violations = [];
|
|
58
58
|
// Skip record-triggered flows - they don't support this pattern
|
|
59
|
-
const
|
|
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
|
|
69
|
+
var _flow_elements, _flow_elements_filter, _flow_elements1;
|
|
70
70
|
const results = [];
|
|
71
|
-
const
|
|
72
|
-
const
|
|
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
|
|
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
|
|
69
|
+
var _flow_elements;
|
|
70
70
|
const results = [];
|
|
71
|
-
const
|
|
72
|
-
const
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
4
|
+
"version": "6.10.6",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|