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.
Files changed (59) hide show
  1. package/README.md +67 -8
  2. package/data/nodes.db +0 -0
  3. package/dist/database/node-repository.d.ts +12 -1
  4. package/dist/database/node-repository.d.ts.map +1 -1
  5. package/dist/database/node-repository.js +122 -2
  6. package/dist/database/node-repository.js.map +1 -1
  7. package/dist/http-server.js +1 -1
  8. package/dist/http-server.js.map +1 -1
  9. package/dist/mcp/server.d.ts +1 -0
  10. package/dist/mcp/server.d.ts.map +1 -1
  11. package/dist/mcp/server.js +31 -9
  12. package/dist/mcp/server.js.map +1 -1
  13. package/dist/mcp-engine.d.ts +1 -1
  14. package/dist/mcp-engine.d.ts.map +1 -1
  15. package/dist/mcp-tools-engine.d.ts +48 -0
  16. package/dist/mcp-tools-engine.d.ts.map +1 -0
  17. package/dist/mcp-tools-engine.js +92 -0
  18. package/dist/mcp-tools-engine.js.map +1 -0
  19. package/dist/parsers/node-parser.d.ts.map +1 -1
  20. package/dist/parsers/node-parser.js +16 -10
  21. package/dist/parsers/node-parser.js.map +1 -1
  22. package/dist/parsers/simple-parser.d.ts.map +1 -1
  23. package/dist/parsers/simple-parser.js +11 -0
  24. package/dist/parsers/simple-parser.js.map +1 -1
  25. package/dist/services/config-validator.d.ts +6 -1
  26. package/dist/services/config-validator.d.ts.map +1 -1
  27. package/dist/services/config-validator.js +99 -25
  28. package/dist/services/config-validator.js.map +1 -1
  29. package/dist/services/enhanced-config-validator.d.ts.map +1 -1
  30. package/dist/services/enhanced-config-validator.js +4 -1
  31. package/dist/services/enhanced-config-validator.js.map +1 -1
  32. package/dist/services/expression-validator.d.ts.map +1 -1
  33. package/dist/services/expression-validator.js +36 -15
  34. package/dist/services/expression-validator.js.map +1 -1
  35. package/dist/services/n8n-validation.d.ts +3 -3
  36. package/dist/services/n8n-validation.d.ts.map +1 -1
  37. package/dist/services/n8n-validation.js +16 -12
  38. package/dist/services/n8n-validation.js.map +1 -1
  39. package/dist/services/property-filter.d.ts.map +1 -1
  40. package/dist/services/property-filter.js +35 -11
  41. package/dist/services/property-filter.js.map +1 -1
  42. package/dist/services/sqlite-storage-service.d.ts +11 -0
  43. package/dist/services/sqlite-storage-service.d.ts.map +1 -0
  44. package/dist/services/sqlite-storage-service.js +74 -0
  45. package/dist/services/sqlite-storage-service.js.map +1 -0
  46. package/dist/services/workflow-validator.d.ts +3 -1
  47. package/dist/services/workflow-validator.d.ts.map +1 -1
  48. package/dist/services/workflow-validator.js +296 -202
  49. package/dist/services/workflow-validator.js.map +1 -1
  50. package/dist/templates/template-repository.js +1 -1
  51. package/dist/templates/template-repository.js.map +1 -1
  52. package/dist/utils/logger.d.ts +1 -0
  53. package/dist/utils/logger.d.ts.map +1 -1
  54. package/dist/utils/logger.js +2 -1
  55. package/dist/utils/logger.js.map +1 -1
  56. package/dist/utils/template-sanitizer.d.ts.map +1 -1
  57. package/dist/utils/template-sanitizer.js +10 -1
  58. package/dist/utils/template-sanitizer.js.map +1 -1
  59. package/package.json +1 -1
@@ -16,8 +16,8 @@ class WorkflowValidator {
16
16
  errors: [],
17
17
  warnings: [],
18
18
  statistics: {
19
- totalNodes: workflow.nodes.length,
20
- enabledNodes: workflow.nodes.filter(n => !n.disabled).length,
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
- this.validateWorkflowStructure(workflow, result);
30
- if (validateNodes) {
31
- await this.validateAllNodes(workflow, result, profile);
32
- }
33
- if (validateConnections) {
34
- this.validateConnections(workflow, result);
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
- if (validateExpressions) {
37
- this.validateExpressions(workflow, result);
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 || !Array.isArray(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 (!workflow.connections || typeof workflow.connections !== 'object') {
74
+ if (!Array.isArray(workflow.nodes)) {
61
75
  result.errors.push({
62
76
  type: 'error',
63
- message: 'Workflow must have a connections object'
77
+ message: 'nodes must be an array'
64
78
  });
65
79
  return;
66
80
  }
67
- if (workflow.nodes.length === 0) {
81
+ if (!workflow.connections) {
68
82
  result.errors.push({
69
83
  type: 'error',
70
- message: 'Workflow has no nodes'
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
- result.statistics.expressionsValidated += exprValidation.usedVariables.size;
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
- this.checkNodeErrorHandling(workflow, result);
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
- 'httpRequest',
753
+ 'httprequest',
658
754
  'webhook',
659
- 'emailSend',
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
- 'googleSheets',
674
- 'googleDrive',
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
- for (const node of workflow.nodes) {
687
- if (node.disabled)
688
- continue;
689
- const normalizedType = node.type.toLowerCase();
690
- const isErrorProne = errorProneNodeTypes.some(type => normalizedType.includes(type));
691
- const nodeLevelProps = [
692
- 'onError', 'continueOnFail', 'retryOnFail', 'maxTries', 'waitBetweenTries', 'alwaysOutputData',
693
- 'executeOnce', 'disabled', 'notes', 'notesInFlow', 'credentials'
694
- ];
695
- const misplacedProps = [];
696
- if (node.parameters) {
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
- if (misplacedProps.length > 0) {
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: `Node-level properties ${misplacedProps.join(', ')} are in the wrong location. They must be at the node level, not inside parameters.`,
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
- if (node.onError !== undefined) {
725
- const validOnErrorValues = ['continueRegularOutput', 'continueErrorOutput', 'stopWorkflow'];
726
- if (!validOnErrorValues.includes(node.onError)) {
727
- result.errors.push({
728
- type: 'error',
729
- nodeId: node.id,
730
- nodeName: node.name,
731
- message: `Invalid onError value: "${node.onError}". Must be one of: ${validOnErrorValues.join(', ')}`
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 !== undefined) {
736
- if (typeof node.continueOnFail !== 'boolean') {
737
- result.errors.push({
738
- type: 'error',
739
- nodeId: node.id,
740
- nodeName: node.name,
741
- message: 'continueOnFail must be a boolean value'
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
- if (node.continueOnFail !== undefined && node.onError !== undefined) {
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: 'Cannot use both "continueOnFail" and "onError" properties. Use only "onError" for modern workflows.'
860
+ message: 'retryOnFail must be a boolean value'
759
861
  });
760
862
  }
761
- if (node.retryOnFail !== undefined) {
762
- if (typeof node.retryOnFail !== 'boolean') {
763
- result.errors.push({
764
- type: 'error',
765
- nodeId: node.id,
766
- nodeName: node.name,
767
- message: 'retryOnFail must be a boolean value'
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: 'retryOnFail is enabled but maxTries is not specified. Default is 3 attempts.'
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: `${nodeTypeSimple} node interacts with external services but has no error handling configured. Consider using "onError" property.`
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
- if (node.continueOnFail && node.retryOnFail) {
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: 'Both continueOnFail and retryOnFail are enabled. The node will retry first, then continue on failure.'
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 (node.disabled !== undefined && typeof node.disabled !== 'boolean') {
878
- result.errors.push({
879
- type: 'error',
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: 'notesInFlow must be a boolean value'
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 (node.notes !== undefined && typeof node.notes !== 'string') {
894
- result.errors.push({
895
- type: 'error',
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: 'notes must be a string value'
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
- if (node.executeOnce === true) {
945
+ else {
902
946
  result.warnings.push({
903
947
  type: 'warning',
904
948
  nodeId: node.id,
905
949
  nodeName: node.name,
906
- message: 'executeOnce is enabled. This node will execute only once regardless of input items.'
950
+ message: `${nodeTypeSimple} node without error handling. Consider using "onError" property for better error management.`
907
951
  });
908
952
  }
909
- if ((node.continueOnFail || node.retryOnFail) && !node.alwaysOutputData) {
910
- if (normalizedType.includes('httprequest') || normalizedType.includes('webhook')) {
911
- result.suggestions.push(`Consider enabling alwaysOutputData on "${node.name}" to capture error responses for debugging`);
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).');