n8n-mcp 2.7.20 → 2.8.1
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 +67 -8
- package/data/nodes.db +0 -0
- package/dist/database/node-repository.d.ts +12 -1
- package/dist/database/node-repository.d.ts.map +1 -1
- package/dist/database/node-repository.js +122 -2
- package/dist/database/node-repository.js.map +1 -1
- package/dist/http-server.js +1 -1
- package/dist/http-server.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +31 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp-engine.d.ts +1 -1
- package/dist/mcp-engine.d.ts.map +1 -1
- package/dist/mcp-tools-engine.d.ts +48 -0
- package/dist/mcp-tools-engine.d.ts.map +1 -0
- package/dist/mcp-tools-engine.js +92 -0
- package/dist/mcp-tools-engine.js.map +1 -0
- package/dist/parsers/node-parser.d.ts.map +1 -1
- package/dist/parsers/node-parser.js +16 -10
- package/dist/parsers/node-parser.js.map +1 -1
- package/dist/parsers/simple-parser.d.ts.map +1 -1
- package/dist/parsers/simple-parser.js +11 -0
- package/dist/parsers/simple-parser.js.map +1 -1
- package/dist/services/config-validator.d.ts +6 -1
- package/dist/services/config-validator.d.ts.map +1 -1
- package/dist/services/config-validator.js +99 -25
- package/dist/services/config-validator.js.map +1 -1
- package/dist/services/enhanced-config-validator.d.ts.map +1 -1
- package/dist/services/enhanced-config-validator.js +4 -1
- package/dist/services/enhanced-config-validator.js.map +1 -1
- package/dist/services/expression-validator.d.ts.map +1 -1
- package/dist/services/expression-validator.js +36 -15
- package/dist/services/expression-validator.js.map +1 -1
- package/dist/services/n8n-validation.d.ts +3 -3
- package/dist/services/n8n-validation.d.ts.map +1 -1
- package/dist/services/n8n-validation.js +16 -12
- package/dist/services/n8n-validation.js.map +1 -1
- package/dist/services/property-filter.d.ts.map +1 -1
- package/dist/services/property-filter.js +35 -11
- package/dist/services/property-filter.js.map +1 -1
- package/dist/services/sqlite-storage-service.d.ts +11 -0
- package/dist/services/sqlite-storage-service.d.ts.map +1 -0
- package/dist/services/sqlite-storage-service.js +74 -0
- package/dist/services/sqlite-storage-service.js.map +1 -0
- package/dist/services/workflow-validator.d.ts +3 -1
- package/dist/services/workflow-validator.d.ts.map +1 -1
- package/dist/services/workflow-validator.js +296 -202
- package/dist/services/workflow-validator.js.map +1 -1
- package/dist/templates/template-repository.js +1 -1
- package/dist/templates/template-repository.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +2 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/template-sanitizer.d.ts.map +1 -1
- package/dist/utils/template-sanitizer.js +10 -1
- package/dist/utils/template-sanitizer.js.map +1 -1
- package/package.json +1 -1
|
@@ -16,8 +16,8 @@ class WorkflowValidator {
|
|
|
16
16
|
errors: [],
|
|
17
17
|
warnings: [],
|
|
18
18
|
statistics: {
|
|
19
|
-
totalNodes:
|
|
20
|
-
enabledNodes:
|
|
19
|
+
totalNodes: 0,
|
|
20
|
+
enabledNodes: 0,
|
|
21
21
|
triggerNodes: 0,
|
|
22
22
|
validConnections: 0,
|
|
23
23
|
invalidConnections: 0,
|
|
@@ -26,18 +26,32 @@ class WorkflowValidator {
|
|
|
26
26
|
suggestions: []
|
|
27
27
|
};
|
|
28
28
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
if (!workflow) {
|
|
30
|
+
result.errors.push({
|
|
31
|
+
type: 'error',
|
|
32
|
+
message: 'Invalid workflow structure: workflow is null or undefined'
|
|
33
|
+
});
|
|
34
|
+
result.valid = false;
|
|
35
|
+
return result;
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
result.statistics.totalNodes = Array.isArray(workflow.nodes) ? workflow.nodes.length : 0;
|
|
38
|
+
result.statistics.enabledNodes = Array.isArray(workflow.nodes) ? workflow.nodes.filter(n => !n.disabled).length : 0;
|
|
39
|
+
this.validateWorkflowStructure(workflow, result);
|
|
40
|
+
if (workflow.nodes && Array.isArray(workflow.nodes) && workflow.connections && typeof workflow.connections === 'object') {
|
|
41
|
+
if (validateNodes && workflow.nodes.length > 0) {
|
|
42
|
+
await this.validateAllNodes(workflow, result, profile);
|
|
43
|
+
}
|
|
44
|
+
if (validateConnections) {
|
|
45
|
+
this.validateConnections(workflow, result);
|
|
46
|
+
}
|
|
47
|
+
if (validateExpressions && workflow.nodes.length > 0) {
|
|
48
|
+
this.validateExpressions(workflow, result);
|
|
49
|
+
}
|
|
50
|
+
if (workflow.nodes.length > 0) {
|
|
51
|
+
this.checkWorkflowPatterns(workflow, result);
|
|
52
|
+
}
|
|
53
|
+
this.generateSuggestions(workflow, result);
|
|
38
54
|
}
|
|
39
|
-
this.checkWorkflowPatterns(workflow, result);
|
|
40
|
-
this.generateSuggestions(workflow, result);
|
|
41
55
|
}
|
|
42
56
|
catch (error) {
|
|
43
57
|
logger.error('Error validating workflow:', error);
|
|
@@ -50,24 +64,38 @@ class WorkflowValidator {
|
|
|
50
64
|
return result;
|
|
51
65
|
}
|
|
52
66
|
validateWorkflowStructure(workflow, result) {
|
|
53
|
-
if (!workflow.nodes
|
|
67
|
+
if (!workflow.nodes) {
|
|
54
68
|
result.errors.push({
|
|
55
69
|
type: 'error',
|
|
56
|
-
message: 'Workflow must have a nodes array'
|
|
70
|
+
message: workflow.nodes === null ? 'nodes must be an array' : 'Workflow must have a nodes array'
|
|
57
71
|
});
|
|
58
72
|
return;
|
|
59
73
|
}
|
|
60
|
-
if (!
|
|
74
|
+
if (!Array.isArray(workflow.nodes)) {
|
|
61
75
|
result.errors.push({
|
|
62
76
|
type: 'error',
|
|
63
|
-
message: '
|
|
77
|
+
message: 'nodes must be an array'
|
|
64
78
|
});
|
|
65
79
|
return;
|
|
66
80
|
}
|
|
67
|
-
if (workflow.
|
|
81
|
+
if (!workflow.connections) {
|
|
68
82
|
result.errors.push({
|
|
69
83
|
type: 'error',
|
|
70
|
-
message: 'Workflow
|
|
84
|
+
message: workflow.connections === null ? 'connections must be an object' : 'Workflow must have a connections object'
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (typeof workflow.connections !== 'object' || Array.isArray(workflow.connections)) {
|
|
89
|
+
result.errors.push({
|
|
90
|
+
type: 'error',
|
|
91
|
+
message: 'connections must be an object'
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (workflow.nodes.length === 0) {
|
|
96
|
+
result.warnings.push({
|
|
97
|
+
type: 'warning',
|
|
98
|
+
message: 'Workflow is empty - no nodes defined'
|
|
71
99
|
});
|
|
72
100
|
return;
|
|
73
101
|
}
|
|
@@ -141,6 +169,34 @@ class WorkflowValidator {
|
|
|
141
169
|
if (node.disabled)
|
|
142
170
|
continue;
|
|
143
171
|
try {
|
|
172
|
+
if (node.name && node.name.length > 255) {
|
|
173
|
+
result.warnings.push({
|
|
174
|
+
type: 'warning',
|
|
175
|
+
nodeId: node.id,
|
|
176
|
+
nodeName: node.name,
|
|
177
|
+
message: `Node name is very long (${node.name.length} characters). Consider using a shorter name for better readability.`
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (!Array.isArray(node.position) || node.position.length !== 2) {
|
|
181
|
+
result.errors.push({
|
|
182
|
+
type: 'error',
|
|
183
|
+
nodeId: node.id,
|
|
184
|
+
nodeName: node.name,
|
|
185
|
+
message: 'Node position must be an array with exactly 2 numbers [x, y]'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const [x, y] = node.position;
|
|
190
|
+
if (typeof x !== 'number' || typeof y !== 'number' ||
|
|
191
|
+
!isFinite(x) || !isFinite(y)) {
|
|
192
|
+
result.errors.push({
|
|
193
|
+
type: 'error',
|
|
194
|
+
nodeId: node.id,
|
|
195
|
+
nodeName: node.name,
|
|
196
|
+
message: 'Node position values must be finite numbers'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
144
200
|
if (node.type.startsWith('nodes-base.')) {
|
|
145
201
|
const correctType = node.type.replace('nodes-base.', 'n8n-nodes-base.');
|
|
146
202
|
result.errors.push({
|
|
@@ -236,7 +292,7 @@ class WorkflowValidator {
|
|
|
236
292
|
type: 'error',
|
|
237
293
|
nodeId: node.id,
|
|
238
294
|
nodeName: node.name,
|
|
239
|
-
message: error
|
|
295
|
+
message: typeof error === 'string' ? error : error.message || String(error)
|
|
240
296
|
});
|
|
241
297
|
});
|
|
242
298
|
nodeValidation.warnings.forEach((warning) => {
|
|
@@ -244,7 +300,7 @@ class WorkflowValidator {
|
|
|
244
300
|
type: 'warning',
|
|
245
301
|
nodeId: node.id,
|
|
246
302
|
nodeName: node.name,
|
|
247
|
-
message: warning
|
|
303
|
+
message: typeof warning === 'string' ? warning : warning.message || String(warning)
|
|
248
304
|
});
|
|
249
305
|
});
|
|
250
306
|
}
|
|
@@ -344,6 +400,20 @@ class WorkflowValidator {
|
|
|
344
400
|
if (!outputConnections)
|
|
345
401
|
return;
|
|
346
402
|
outputConnections.forEach(connection => {
|
|
403
|
+
if (connection.index < 0) {
|
|
404
|
+
result.errors.push({
|
|
405
|
+
type: 'error',
|
|
406
|
+
message: `Invalid connection index ${connection.index} from "${sourceName}". Connection indices must be non-negative.`
|
|
407
|
+
});
|
|
408
|
+
result.statistics.invalidConnections++;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (connection.node === sourceName) {
|
|
412
|
+
result.warnings.push({
|
|
413
|
+
type: 'warning',
|
|
414
|
+
message: `Node "${sourceName}" has a self-referencing connection. This can cause infinite loops.`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
347
417
|
const targetNode = nodeMap.get(connection.node);
|
|
348
418
|
if (!targetNode) {
|
|
349
419
|
const nodeById = nodeIdMap.get(connection.node);
|
|
@@ -460,7 +530,8 @@ class WorkflowValidator {
|
|
|
460
530
|
isInLoop: false
|
|
461
531
|
};
|
|
462
532
|
const exprValidation = expression_validator_1.ExpressionValidator.validateNodeExpressions(node.parameters, context);
|
|
463
|
-
|
|
533
|
+
const expressionCount = this.countExpressionsInObject(node.parameters);
|
|
534
|
+
result.statistics.expressionsValidated += expressionCount;
|
|
464
535
|
exprValidation.errors.forEach(error => {
|
|
465
536
|
result.errors.push({
|
|
466
537
|
type: 'error',
|
|
@@ -479,6 +550,26 @@ class WorkflowValidator {
|
|
|
479
550
|
});
|
|
480
551
|
}
|
|
481
552
|
}
|
|
553
|
+
countExpressionsInObject(obj) {
|
|
554
|
+
let count = 0;
|
|
555
|
+
if (typeof obj === 'string') {
|
|
556
|
+
const matches = obj.match(/\{\{[\s\S]+?\}\}/g);
|
|
557
|
+
if (matches) {
|
|
558
|
+
count += matches.length;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
else if (Array.isArray(obj)) {
|
|
562
|
+
for (const item of obj) {
|
|
563
|
+
count += this.countExpressionsInObject(item);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else if (obj && typeof obj === 'object') {
|
|
567
|
+
for (const value of Object.values(obj)) {
|
|
568
|
+
count += this.countExpressionsInObject(value);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return count;
|
|
572
|
+
}
|
|
482
573
|
nodeHasInput(nodeName, workflow) {
|
|
483
574
|
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
|
484
575
|
if (outputs.main) {
|
|
@@ -499,7 +590,9 @@ class WorkflowValidator {
|
|
|
499
590
|
message: 'Consider adding error handling to your workflow'
|
|
500
591
|
});
|
|
501
592
|
}
|
|
502
|
-
|
|
593
|
+
for (const node of workflow.nodes) {
|
|
594
|
+
this.checkNodeErrorHandling(node, workflow, result);
|
|
595
|
+
}
|
|
503
596
|
const linearChainLength = this.getLongestLinearChain(workflow);
|
|
504
597
|
if (linearChainLength > 10) {
|
|
505
598
|
result.warnings.push({
|
|
@@ -507,6 +600,7 @@ class WorkflowValidator {
|
|
|
507
600
|
message: `Long linear chain detected (${linearChainLength} nodes). Consider breaking into sub-workflows.`
|
|
508
601
|
});
|
|
509
602
|
}
|
|
603
|
+
this.generateErrorHandlingSuggestions(workflow, result);
|
|
510
604
|
for (const node of workflow.nodes) {
|
|
511
605
|
if (node.credentials && Object.keys(node.credentials).length > 0) {
|
|
512
606
|
for (const [credType, credConfig] of Object.entries(node.credentials)) {
|
|
@@ -652,11 +746,13 @@ class WorkflowValidator {
|
|
|
652
746
|
result.suggestions.push('A minimal workflow needs: 1) A trigger node (e.g., Manual Trigger), 2) An action node (e.g., Set, HTTP Request), 3) A connection between them');
|
|
653
747
|
}
|
|
654
748
|
}
|
|
655
|
-
checkNodeErrorHandling(workflow, result) {
|
|
749
|
+
checkNodeErrorHandling(node, workflow, result) {
|
|
750
|
+
if (node.disabled === true)
|
|
751
|
+
return;
|
|
656
752
|
const errorProneNodeTypes = [
|
|
657
|
-
'
|
|
753
|
+
'httprequest',
|
|
658
754
|
'webhook',
|
|
659
|
-
'
|
|
755
|
+
'emailsend',
|
|
660
756
|
'slack',
|
|
661
757
|
'discord',
|
|
662
758
|
'telegram',
|
|
@@ -670,8 +766,8 @@ class WorkflowValidator {
|
|
|
670
766
|
'salesforce',
|
|
671
767
|
'hubspot',
|
|
672
768
|
'airtable',
|
|
673
|
-
'
|
|
674
|
-
'
|
|
769
|
+
'googlesheets',
|
|
770
|
+
'googledrive',
|
|
675
771
|
'dropbox',
|
|
676
772
|
's3',
|
|
677
773
|
'ftp',
|
|
@@ -683,235 +779,233 @@ class WorkflowValidator {
|
|
|
683
779
|
'openai',
|
|
684
780
|
'anthropic'
|
|
685
781
|
];
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
for (const prop of nodeLevelProps) {
|
|
698
|
-
if (node.parameters[prop] !== undefined) {
|
|
699
|
-
misplacedProps.push(prop);
|
|
700
|
-
}
|
|
782
|
+
const normalizedType = node.type.toLowerCase();
|
|
783
|
+
const isErrorProne = errorProneNodeTypes.some(type => normalizedType.includes(type));
|
|
784
|
+
const nodeLevelProps = [
|
|
785
|
+
'onError', 'continueOnFail', 'retryOnFail', 'maxTries', 'waitBetweenTries', 'alwaysOutputData',
|
|
786
|
+
'executeOnce', 'disabled', 'notes', 'notesInFlow', 'credentials'
|
|
787
|
+
];
|
|
788
|
+
const misplacedProps = [];
|
|
789
|
+
if (node.parameters) {
|
|
790
|
+
for (const prop of nodeLevelProps) {
|
|
791
|
+
if (node.parameters[prop] !== undefined) {
|
|
792
|
+
misplacedProps.push(prop);
|
|
701
793
|
}
|
|
702
794
|
}
|
|
703
|
-
|
|
795
|
+
}
|
|
796
|
+
if (misplacedProps.length > 0) {
|
|
797
|
+
result.errors.push({
|
|
798
|
+
type: 'error',
|
|
799
|
+
nodeId: node.id,
|
|
800
|
+
nodeName: node.name,
|
|
801
|
+
message: `Node-level properties ${misplacedProps.join(', ')} are in the wrong location. They must be at the node level, not inside parameters.`,
|
|
802
|
+
details: {
|
|
803
|
+
fix: `Move these properties from node.parameters to the node level. Example:\n` +
|
|
804
|
+
`{\n` +
|
|
805
|
+
` "name": "${node.name}",\n` +
|
|
806
|
+
` "type": "${node.type}",\n` +
|
|
807
|
+
` "parameters": { /* operation-specific params */ },\n` +
|
|
808
|
+
` "onError": "continueErrorOutput", // ✅ Correct location\n` +
|
|
809
|
+
` "retryOnFail": true, // ✅ Correct location\n` +
|
|
810
|
+
` "executeOnce": true, // ✅ Correct location\n` +
|
|
811
|
+
` "disabled": false, // ✅ Correct location\n` +
|
|
812
|
+
` "credentials": { /* ... */ } // ✅ Correct location\n` +
|
|
813
|
+
`}`
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (node.onError !== undefined) {
|
|
818
|
+
const validOnErrorValues = ['continueRegularOutput', 'continueErrorOutput', 'stopWorkflow'];
|
|
819
|
+
if (!validOnErrorValues.includes(node.onError)) {
|
|
704
820
|
result.errors.push({
|
|
705
821
|
type: 'error',
|
|
706
822
|
nodeId: node.id,
|
|
707
823
|
nodeName: node.name,
|
|
708
|
-
message: `
|
|
709
|
-
details: {
|
|
710
|
-
fix: `Move these properties from node.parameters to the node level. Example:\n` +
|
|
711
|
-
`{\n` +
|
|
712
|
-
` "name": "${node.name}",\n` +
|
|
713
|
-
` "type": "${node.type}",\n` +
|
|
714
|
-
` "parameters": { /* operation-specific params */ },\n` +
|
|
715
|
-
` "onError": "continueErrorOutput", // ✅ Correct location\n` +
|
|
716
|
-
` "retryOnFail": true, // ✅ Correct location\n` +
|
|
717
|
-
` "executeOnce": true, // ✅ Correct location\n` +
|
|
718
|
-
` "disabled": false, // ✅ Correct location\n` +
|
|
719
|
-
` "credentials": { /* ... */ } // ✅ Correct location\n` +
|
|
720
|
-
`}`
|
|
721
|
-
}
|
|
824
|
+
message: `Invalid onError value: "${node.onError}". Must be one of: ${validOnErrorValues.join(', ')}`
|
|
722
825
|
});
|
|
723
826
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
827
|
+
}
|
|
828
|
+
if (node.continueOnFail !== undefined) {
|
|
829
|
+
if (typeof node.continueOnFail !== 'boolean') {
|
|
830
|
+
result.errors.push({
|
|
831
|
+
type: 'error',
|
|
832
|
+
nodeId: node.id,
|
|
833
|
+
nodeName: node.name,
|
|
834
|
+
message: 'continueOnFail must be a boolean value'
|
|
835
|
+
});
|
|
734
836
|
}
|
|
735
|
-
if (node.continueOnFail
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
else if (node.continueOnFail === true) {
|
|
745
|
-
result.warnings.push({
|
|
746
|
-
type: 'warning',
|
|
747
|
-
nodeId: node.id,
|
|
748
|
-
nodeName: node.name,
|
|
749
|
-
message: 'Using deprecated "continueOnFail: true". Use "onError: \'continueRegularOutput\'" instead for better control and UI compatibility.'
|
|
750
|
-
});
|
|
751
|
-
}
|
|
837
|
+
else if (node.continueOnFail === true) {
|
|
838
|
+
result.warnings.push({
|
|
839
|
+
type: 'warning',
|
|
840
|
+
nodeId: node.id,
|
|
841
|
+
nodeName: node.name,
|
|
842
|
+
message: 'Using deprecated "continueOnFail: true". Use "onError: \'continueRegularOutput\'" instead for better control and UI compatibility.'
|
|
843
|
+
});
|
|
752
844
|
}
|
|
753
|
-
|
|
845
|
+
}
|
|
846
|
+
if (node.continueOnFail !== undefined && node.onError !== undefined) {
|
|
847
|
+
result.errors.push({
|
|
848
|
+
type: 'error',
|
|
849
|
+
nodeId: node.id,
|
|
850
|
+
nodeName: node.name,
|
|
851
|
+
message: 'Cannot use both "continueOnFail" and "onError" properties. Use only "onError" for modern workflows.'
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
if (node.retryOnFail !== undefined) {
|
|
855
|
+
if (typeof node.retryOnFail !== 'boolean') {
|
|
754
856
|
result.errors.push({
|
|
755
857
|
type: 'error',
|
|
756
858
|
nodeId: node.id,
|
|
757
859
|
nodeName: node.name,
|
|
758
|
-
message: '
|
|
860
|
+
message: 'retryOnFail must be a boolean value'
|
|
759
861
|
});
|
|
760
862
|
}
|
|
761
|
-
if (node.retryOnFail
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if (node.retryOnFail === true) {
|
|
771
|
-
if (node.maxTries !== undefined) {
|
|
772
|
-
if (typeof node.maxTries !== 'number' || node.maxTries < 1) {
|
|
773
|
-
result.errors.push({
|
|
774
|
-
type: 'error',
|
|
775
|
-
nodeId: node.id,
|
|
776
|
-
nodeName: node.name,
|
|
777
|
-
message: 'maxTries must be a positive number when retryOnFail is enabled'
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
else if (node.maxTries > 10) {
|
|
781
|
-
result.warnings.push({
|
|
782
|
-
type: 'warning',
|
|
783
|
-
nodeId: node.id,
|
|
784
|
-
nodeName: node.name,
|
|
785
|
-
message: `maxTries is set to ${node.maxTries}. Consider if this many retries is necessary.`
|
|
786
|
-
});
|
|
787
|
-
}
|
|
863
|
+
if (node.retryOnFail === true) {
|
|
864
|
+
if (node.maxTries !== undefined) {
|
|
865
|
+
if (typeof node.maxTries !== 'number' || node.maxTries < 1) {
|
|
866
|
+
result.errors.push({
|
|
867
|
+
type: 'error',
|
|
868
|
+
nodeId: node.id,
|
|
869
|
+
nodeName: node.name,
|
|
870
|
+
message: 'maxTries must be a positive number when retryOnFail is enabled'
|
|
871
|
+
});
|
|
788
872
|
}
|
|
789
|
-
else {
|
|
873
|
+
else if (node.maxTries > 10) {
|
|
790
874
|
result.warnings.push({
|
|
791
875
|
type: 'warning',
|
|
792
876
|
nodeId: node.id,
|
|
793
877
|
nodeName: node.name,
|
|
794
|
-
message:
|
|
878
|
+
message: `maxTries is set to ${node.maxTries}. Consider if this many retries is necessary.`
|
|
795
879
|
});
|
|
796
880
|
}
|
|
797
|
-
if (node.waitBetweenTries !== undefined) {
|
|
798
|
-
if (typeof node.waitBetweenTries !== 'number' || node.waitBetweenTries < 0) {
|
|
799
|
-
result.errors.push({
|
|
800
|
-
type: 'error',
|
|
801
|
-
nodeId: node.id,
|
|
802
|
-
nodeName: node.name,
|
|
803
|
-
message: 'waitBetweenTries must be a non-negative number (milliseconds)'
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
else if (node.waitBetweenTries > 300000) {
|
|
807
|
-
result.warnings.push({
|
|
808
|
-
type: 'warning',
|
|
809
|
-
nodeId: node.id,
|
|
810
|
-
nodeName: node.name,
|
|
811
|
-
message: `waitBetweenTries is set to ${node.waitBetweenTries}ms (${(node.waitBetweenTries / 1000).toFixed(1)}s). This seems excessive.`
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
if (node.alwaysOutputData !== undefined && typeof node.alwaysOutputData !== 'boolean') {
|
|
818
|
-
result.errors.push({
|
|
819
|
-
type: 'error',
|
|
820
|
-
nodeId: node.id,
|
|
821
|
-
nodeName: node.name,
|
|
822
|
-
message: 'alwaysOutputData must be a boolean value'
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
const hasErrorHandling = node.onError || node.continueOnFail || node.retryOnFail;
|
|
826
|
-
if (isErrorProne && !hasErrorHandling) {
|
|
827
|
-
const nodeTypeSimple = normalizedType.split('.').pop() || normalizedType;
|
|
828
|
-
if (normalizedType.includes('httprequest')) {
|
|
829
|
-
result.warnings.push({
|
|
830
|
-
type: 'warning',
|
|
831
|
-
nodeId: node.id,
|
|
832
|
-
nodeName: node.name,
|
|
833
|
-
message: 'HTTP Request node without error handling. Consider adding "onError: \'continueRegularOutput\'" for non-critical requests or "retryOnFail: true" for transient failures.'
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
else if (normalizedType.includes('webhook')) {
|
|
837
|
-
result.warnings.push({
|
|
838
|
-
type: 'warning',
|
|
839
|
-
nodeId: node.id,
|
|
840
|
-
nodeName: node.name,
|
|
841
|
-
message: 'Webhook node without error handling. Consider adding "onError: \'continueRegularOutput\'" to prevent workflow failures from blocking webhook responses.'
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
else if (errorProneNodeTypes.some(db => normalizedType.includes(db) && ['postgres', 'mysql', 'mongodb'].includes(db))) {
|
|
845
|
-
result.warnings.push({
|
|
846
|
-
type: 'warning',
|
|
847
|
-
nodeId: node.id,
|
|
848
|
-
nodeName: node.name,
|
|
849
|
-
message: `Database operation without error handling. Consider adding "retryOnFail: true" for connection issues or "onError: \'continueRegularOutput\'" for non-critical queries.`
|
|
850
|
-
});
|
|
851
881
|
}
|
|
852
882
|
else {
|
|
853
883
|
result.warnings.push({
|
|
854
884
|
type: 'warning',
|
|
855
885
|
nodeId: node.id,
|
|
856
886
|
nodeName: node.name,
|
|
857
|
-
message:
|
|
887
|
+
message: 'retryOnFail is enabled but maxTries is not specified. Default is 3 attempts.'
|
|
858
888
|
});
|
|
859
889
|
}
|
|
890
|
+
if (node.waitBetweenTries !== undefined) {
|
|
891
|
+
if (typeof node.waitBetweenTries !== 'number' || node.waitBetweenTries < 0) {
|
|
892
|
+
result.errors.push({
|
|
893
|
+
type: 'error',
|
|
894
|
+
nodeId: node.id,
|
|
895
|
+
nodeName: node.name,
|
|
896
|
+
message: 'waitBetweenTries must be a non-negative number (milliseconds)'
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
else if (node.waitBetweenTries > 300000) {
|
|
900
|
+
result.warnings.push({
|
|
901
|
+
type: 'warning',
|
|
902
|
+
nodeId: node.id,
|
|
903
|
+
nodeName: node.name,
|
|
904
|
+
message: `waitBetweenTries is set to ${node.waitBetweenTries}ms (${(node.waitBetweenTries / 1000).toFixed(1)}s). This seems excessive.`
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
860
908
|
}
|
|
861
|
-
|
|
909
|
+
}
|
|
910
|
+
if (node.alwaysOutputData !== undefined && typeof node.alwaysOutputData !== 'boolean') {
|
|
911
|
+
result.errors.push({
|
|
912
|
+
type: 'error',
|
|
913
|
+
nodeId: node.id,
|
|
914
|
+
nodeName: node.name,
|
|
915
|
+
message: 'alwaysOutputData must be a boolean value'
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
const hasErrorHandling = node.onError || node.continueOnFail || node.retryOnFail;
|
|
919
|
+
if (isErrorProne && !hasErrorHandling) {
|
|
920
|
+
const nodeTypeSimple = normalizedType.split('.').pop() || normalizedType;
|
|
921
|
+
if (normalizedType.includes('httprequest')) {
|
|
862
922
|
result.warnings.push({
|
|
863
923
|
type: 'warning',
|
|
864
924
|
nodeId: node.id,
|
|
865
925
|
nodeName: node.name,
|
|
866
|
-
message: '
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
if (node.executeOnce !== undefined && typeof node.executeOnce !== 'boolean') {
|
|
870
|
-
result.errors.push({
|
|
871
|
-
type: 'error',
|
|
872
|
-
nodeId: node.id,
|
|
873
|
-
nodeName: node.name,
|
|
874
|
-
message: 'executeOnce must be a boolean value'
|
|
926
|
+
message: 'HTTP Request node without error handling. Consider adding "onError: \'continueRegularOutput\'" for non-critical requests or "retryOnFail: true" for transient failures.'
|
|
875
927
|
});
|
|
876
928
|
}
|
|
877
|
-
if (
|
|
878
|
-
result.
|
|
879
|
-
type: '
|
|
880
|
-
nodeId: node.id,
|
|
881
|
-
nodeName: node.name,
|
|
882
|
-
message: 'disabled must be a boolean value'
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
if (node.notesInFlow !== undefined && typeof node.notesInFlow !== 'boolean') {
|
|
886
|
-
result.errors.push({
|
|
887
|
-
type: 'error',
|
|
929
|
+
else if (normalizedType.includes('webhook')) {
|
|
930
|
+
result.warnings.push({
|
|
931
|
+
type: 'warning',
|
|
888
932
|
nodeId: node.id,
|
|
889
933
|
nodeName: node.name,
|
|
890
|
-
message: '
|
|
934
|
+
message: 'Webhook node without error handling. Consider adding "onError: \'continueRegularOutput\'" to prevent workflow failures from blocking webhook responses.'
|
|
891
935
|
});
|
|
892
936
|
}
|
|
893
|
-
if (
|
|
894
|
-
result.
|
|
895
|
-
type: '
|
|
937
|
+
else if (errorProneNodeTypes.some(db => normalizedType.includes(db) && ['postgres', 'mysql', 'mongodb'].includes(db))) {
|
|
938
|
+
result.warnings.push({
|
|
939
|
+
type: 'warning',
|
|
896
940
|
nodeId: node.id,
|
|
897
941
|
nodeName: node.name,
|
|
898
|
-
message:
|
|
942
|
+
message: `Database operation without error handling. Consider adding "retryOnFail: true" for connection issues or "onError: \'continueRegularOutput\'" for non-critical queries.`
|
|
899
943
|
});
|
|
900
944
|
}
|
|
901
|
-
|
|
945
|
+
else {
|
|
902
946
|
result.warnings.push({
|
|
903
947
|
type: 'warning',
|
|
904
948
|
nodeId: node.id,
|
|
905
949
|
nodeName: node.name,
|
|
906
|
-
message:
|
|
950
|
+
message: `${nodeTypeSimple} node without error handling. Consider using "onError" property for better error management.`
|
|
907
951
|
});
|
|
908
952
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
953
|
+
}
|
|
954
|
+
if (node.continueOnFail && node.retryOnFail) {
|
|
955
|
+
result.warnings.push({
|
|
956
|
+
type: 'warning',
|
|
957
|
+
nodeId: node.id,
|
|
958
|
+
nodeName: node.name,
|
|
959
|
+
message: 'Both continueOnFail and retryOnFail are enabled. The node will retry first, then continue on failure.'
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
if (node.executeOnce !== undefined && typeof node.executeOnce !== 'boolean') {
|
|
963
|
+
result.errors.push({
|
|
964
|
+
type: 'error',
|
|
965
|
+
nodeId: node.id,
|
|
966
|
+
nodeName: node.name,
|
|
967
|
+
message: 'executeOnce must be a boolean value'
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
if (node.disabled !== undefined && typeof node.disabled !== 'boolean') {
|
|
971
|
+
result.errors.push({
|
|
972
|
+
type: 'error',
|
|
973
|
+
nodeId: node.id,
|
|
974
|
+
nodeName: node.name,
|
|
975
|
+
message: 'disabled must be a boolean value'
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
if (node.notesInFlow !== undefined && typeof node.notesInFlow !== 'boolean') {
|
|
979
|
+
result.errors.push({
|
|
980
|
+
type: 'error',
|
|
981
|
+
nodeId: node.id,
|
|
982
|
+
nodeName: node.name,
|
|
983
|
+
message: 'notesInFlow must be a boolean value'
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
if (node.notes !== undefined && typeof node.notes !== 'string') {
|
|
987
|
+
result.errors.push({
|
|
988
|
+
type: 'error',
|
|
989
|
+
nodeId: node.id,
|
|
990
|
+
nodeName: node.name,
|
|
991
|
+
message: 'notes must be a string value'
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
if (node.executeOnce === true) {
|
|
995
|
+
result.warnings.push({
|
|
996
|
+
type: 'warning',
|
|
997
|
+
nodeId: node.id,
|
|
998
|
+
nodeName: node.name,
|
|
999
|
+
message: 'executeOnce is enabled. This node will execute only once regardless of input items.'
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
if ((node.continueOnFail || node.retryOnFail) && !node.alwaysOutputData) {
|
|
1003
|
+
if (normalizedType.includes('httprequest') || normalizedType.includes('webhook')) {
|
|
1004
|
+
result.suggestions.push(`Consider enabling alwaysOutputData on "${node.name}" to capture error responses for debugging`);
|
|
913
1005
|
}
|
|
914
1006
|
}
|
|
1007
|
+
}
|
|
1008
|
+
generateErrorHandlingSuggestions(workflow, result) {
|
|
915
1009
|
const nodesWithoutErrorHandling = workflow.nodes.filter(n => !n.disabled && !n.onError && !n.continueOnFail && !n.retryOnFail).length;
|
|
916
1010
|
if (nodesWithoutErrorHandling > 5 && workflow.nodes.length > 5) {
|
|
917
1011
|
result.suggestions.push('Most nodes lack error handling. Use "onError" property for modern error handling: "continueRegularOutput" (continue on error), "continueErrorOutput" (use error output), or "stopWorkflow" (stop execution).');
|