feat: enhance workflow validation to prevent broken workflows (v2.6.2)
- Add node type existence validation that catches invalid types - Fix critical issue: now catches 'nodes-base.webhook' BEFORE database lookup - Add smart suggestions for common mistakes (webhook → n8n-nodes-base.webhook) - Add minimum viable workflow validation (prevents single-node workflows) - Add empty connection detection for multi-node workflows - Add helper functions for workflow structure examples and fix suggestions - Prevent AI agents from creating workflows with question mark nodes This fixes the exact issue where workflows created with 'nodes-base.webhook' would show as broken (question marks) in the n8n UI. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
15
CLAUDE.md
15
CLAUDE.md
@@ -6,7 +6,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
n8n-mcp is a comprehensive documentation and knowledge server that provides AI assistants with complete access to n8n node information through the Model Context Protocol (MCP). It serves as a bridge between n8n's workflow automation platform and AI models, enabling them to understand and work with n8n nodes effectively.
|
n8n-mcp is a comprehensive documentation and knowledge server that provides AI assistants with complete access to n8n node information through the Model Context Protocol (MCP). It serves as a bridge between n8n's workflow automation platform and AI models, enabling them to understand and work with n8n nodes effectively.
|
||||||
|
|
||||||
## ✅ Latest Updates (v2.6.1)
|
## ✅ Latest Updates (v2.6.2)
|
||||||
|
|
||||||
|
### Update (v2.6.2) - Enhanced Workflow Creation Validation:
|
||||||
|
- ✅ **NEW: Node type validation** - Verifies node types actually exist in n8n
|
||||||
|
- ✅ **FIXED: nodes-base prefix detection** - Now catches `nodes-base.webhook` BEFORE database lookup
|
||||||
|
- ✅ **NEW: Smart suggestions** - Detects `nodes-base.webhook` and suggests `n8n-nodes-base.webhook`
|
||||||
|
- ✅ **NEW: Common mistake detection** - Catches missing package prefixes (e.g., `webhook` → `n8n-nodes-base.webhook`)
|
||||||
|
- ✅ **NEW: Minimum viable workflow validation** - Prevents single-node workflows (except webhooks)
|
||||||
|
- ✅ **NEW: Empty connection detection** - Catches multi-node workflows with no connections
|
||||||
|
- ✅ **Enhanced error messages** - Clear guidance on proper workflow structure
|
||||||
|
- ✅ **Connection examples** - Shows correct format: `connections: { "Node Name": { "main": [[{ "node": "Target", "type": "main", "index": 0 }]] } }`
|
||||||
|
- ✅ **Helper functions** - `getWorkflowStructureExample()` and `getWorkflowFixSuggestions()`
|
||||||
|
- ✅ **Prevents broken workflows** - Like single webhook nodes with empty connections that show as question marks
|
||||||
|
- ✅ **Reinforces best practices** - Use node NAMES (not IDs) in connections
|
||||||
|
|
||||||
### Update (v2.6.1) - Enhanced typeVersion Validation:
|
### Update (v2.6.1) - Enhanced typeVersion Validation:
|
||||||
- ✅ **NEW: typeVersion validation** - Workflow validator now enforces typeVersion on all versioned nodes
|
- ✅ **NEW: typeVersion validation** - Workflow validator now enforces typeVersion on all versioned nodes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.6.1",
|
"version": "2.6.2",
|
||||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
157
scripts/test-empty-connection-validation.ts
Normal file
157
scripts/test-empty-connection-validation.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test script for empty connection validation
|
||||||
|
* Tests the improvements to prevent broken workflows like the one in the logs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WorkflowValidator } from '../src/services/workflow-validator';
|
||||||
|
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator';
|
||||||
|
import { NodeRepository } from '../src/database/node-repository';
|
||||||
|
import { createDatabaseAdapter } from '../src/database/database-adapter';
|
||||||
|
import { validateWorkflowStructure, getWorkflowFixSuggestions, getWorkflowStructureExample } from '../src/services/n8n-validation';
|
||||||
|
import { Logger } from '../src/utils/logger';
|
||||||
|
|
||||||
|
const logger = new Logger({ prefix: '[TestEmptyConnectionValidation]' });
|
||||||
|
|
||||||
|
async function testValidation() {
|
||||||
|
const adapter = await createDatabaseAdapter('./data/nodes.db');
|
||||||
|
const repository = new NodeRepository(adapter);
|
||||||
|
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||||
|
|
||||||
|
logger.info('Testing empty connection validation...\n');
|
||||||
|
|
||||||
|
// Test 1: The broken workflow from the logs
|
||||||
|
const brokenWorkflow = {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "webhook_node",
|
||||||
|
"name": "Webhook",
|
||||||
|
"type": "nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [260, 300] as [number, number]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {},
|
||||||
|
"pinData": {},
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "74e11c77e266f2c77f6408eb6c88e3fec63c9a5d8c4a3a2ea4c135c542012d6b"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('Test 1: Broken single-node workflow with empty connections');
|
||||||
|
const result1 = await validator.validateWorkflow(brokenWorkflow as any);
|
||||||
|
|
||||||
|
logger.info('Validation result:');
|
||||||
|
logger.info(`Valid: ${result1.valid}`);
|
||||||
|
logger.info(`Errors: ${result1.errors.length}`);
|
||||||
|
result1.errors.forEach(err => {
|
||||||
|
if (typeof err === 'string') {
|
||||||
|
logger.error(` - ${err}`);
|
||||||
|
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||||
|
logger.error(` - ${err.message}`);
|
||||||
|
} else {
|
||||||
|
logger.error(` - ${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
logger.info(`Warnings: ${result1.warnings.length}`);
|
||||||
|
result1.warnings.forEach(warn => logger.warn(` - ${warn.message || JSON.stringify(warn)}`));
|
||||||
|
logger.info(`Suggestions: ${result1.suggestions.length}`);
|
||||||
|
result1.suggestions.forEach(sug => logger.info(` - ${sug}`));
|
||||||
|
|
||||||
|
// Test 2: Multi-node workflow with no connections
|
||||||
|
const multiNodeNoConnections = {
|
||||||
|
"name": "Test Workflow",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "manual-1",
|
||||||
|
"name": "Manual Trigger",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [250, 300] as [number, number],
|
||||||
|
"parameters": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "set-1",
|
||||||
|
"name": "Set",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [450, 300] as [number, number],
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('\nTest 2: Multi-node workflow with empty connections');
|
||||||
|
const result2 = await validator.validateWorkflow(multiNodeNoConnections as any);
|
||||||
|
|
||||||
|
logger.info('Validation result:');
|
||||||
|
logger.info(`Valid: ${result2.valid}`);
|
||||||
|
logger.info(`Errors: ${result2.errors.length}`);
|
||||||
|
result2.errors.forEach(err => logger.error(` - ${err.message || JSON.stringify(err)}`));
|
||||||
|
logger.info(`Suggestions: ${result2.suggestions.length}`);
|
||||||
|
result2.suggestions.forEach(sug => logger.info(` - ${sug}`));
|
||||||
|
|
||||||
|
// Test 3: Using n8n-validation functions
|
||||||
|
logger.info('\nTest 3: Testing n8n-validation.ts functions');
|
||||||
|
|
||||||
|
const errors = validateWorkflowStructure(brokenWorkflow as any);
|
||||||
|
logger.info('Validation errors:');
|
||||||
|
errors.forEach(err => logger.error(` - ${err}`));
|
||||||
|
|
||||||
|
const suggestions = getWorkflowFixSuggestions(errors);
|
||||||
|
logger.info('Fix suggestions:');
|
||||||
|
suggestions.forEach(sug => logger.info(` - ${sug}`));
|
||||||
|
|
||||||
|
logger.info('\nExample of proper workflow structure:');
|
||||||
|
logger.info(getWorkflowStructureExample());
|
||||||
|
|
||||||
|
// Test 4: Workflow using IDs instead of names in connections
|
||||||
|
const workflowWithIdConnections = {
|
||||||
|
"name": "Test Workflow",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "manual-1",
|
||||||
|
"name": "Manual Trigger",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [250, 300] as [number, number],
|
||||||
|
"parameters": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "set-1",
|
||||||
|
"name": "Set Data",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [450, 300] as [number, number],
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"manual-1": { // Using ID instead of name!
|
||||||
|
"main": [[{
|
||||||
|
"node": "set-1", // Using ID instead of name!
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('\nTest 4: Workflow using IDs instead of names in connections');
|
||||||
|
const result4 = await validator.validateWorkflow(workflowWithIdConnections as any);
|
||||||
|
|
||||||
|
logger.info('Validation result:');
|
||||||
|
logger.info(`Valid: ${result4.valid}`);
|
||||||
|
logger.info(`Errors: ${result4.errors.length}`);
|
||||||
|
result4.errors.forEach(err => logger.error(` - ${err.message || JSON.stringify(err)}`));
|
||||||
|
|
||||||
|
adapter.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
testValidation().catch(err => {
|
||||||
|
logger.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
187
scripts/test-node-type-validation.ts
Normal file
187
scripts/test-node-type-validation.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test script for node type validation
|
||||||
|
* Tests the improvements to catch invalid node types like "nodes-base.webhook"
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WorkflowValidator } from '../src/services/workflow-validator';
|
||||||
|
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator';
|
||||||
|
import { NodeRepository } from '../src/database/node-repository';
|
||||||
|
import { createDatabaseAdapter } from '../src/database/database-adapter';
|
||||||
|
import { validateWorkflowStructure } from '../src/services/n8n-validation';
|
||||||
|
import { Logger } from '../src/utils/logger';
|
||||||
|
|
||||||
|
const logger = new Logger({ prefix: '[TestNodeTypeValidation]' });
|
||||||
|
|
||||||
|
async function testValidation() {
|
||||||
|
const adapter = await createDatabaseAdapter('./data/nodes.db');
|
||||||
|
const repository = new NodeRepository(adapter);
|
||||||
|
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||||
|
|
||||||
|
logger.info('Testing node type validation...\n');
|
||||||
|
|
||||||
|
// Test 1: The exact broken workflow from Claude Desktop
|
||||||
|
const brokenWorkflowFromLogs = {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "webhook_node",
|
||||||
|
"name": "Webhook",
|
||||||
|
"type": "nodes-base.webhook", // WRONG! Missing n8n- prefix
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [260, 300] as [number, number]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {},
|
||||||
|
"pinData": {},
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "74e11c77e266f2c77f6408eb6c88e3fec63c9a5d8c4a3a2ea4c135c542012d6b"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('Test 1: Invalid node type "nodes-base.webhook" (missing n8n- prefix)');
|
||||||
|
const result1 = await validator.validateWorkflow(brokenWorkflowFromLogs as any);
|
||||||
|
|
||||||
|
logger.info('Validation result:');
|
||||||
|
logger.info(`Valid: ${result1.valid}`);
|
||||||
|
logger.info(`Errors: ${result1.errors.length}`);
|
||||||
|
result1.errors.forEach(err => {
|
||||||
|
if (typeof err === 'string') {
|
||||||
|
logger.error(` - ${err}`);
|
||||||
|
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||||
|
logger.error(` - ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the specific error about nodes-base.webhook was caught
|
||||||
|
const hasNodeBaseError = result1.errors.some(err =>
|
||||||
|
err && typeof err === 'object' && 'message' in err &&
|
||||||
|
err.message.includes('nodes-base.webhook') &&
|
||||||
|
err.message.includes('n8n-nodes-base.webhook')
|
||||||
|
);
|
||||||
|
logger.info(`Caught nodes-base.webhook error: ${hasNodeBaseError ? 'YES ✅' : 'NO ❌'}`);
|
||||||
|
|
||||||
|
// Test 2: Node type without any prefix
|
||||||
|
const noPrefixWorkflow = {
|
||||||
|
"name": "Test Workflow",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "webhook-1",
|
||||||
|
"name": "My Webhook",
|
||||||
|
"type": "webhook", // WRONG! No package prefix
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [250, 300] as [number, number],
|
||||||
|
"parameters": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "set-1",
|
||||||
|
"name": "Set Data",
|
||||||
|
"type": "set", // WRONG! No package prefix
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [450, 300] as [number, number],
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"My Webhook": {
|
||||||
|
"main": [[{
|
||||||
|
"node": "Set Data",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('\nTest 2: Node types without package prefix ("webhook", "set")');
|
||||||
|
const result2 = await validator.validateWorkflow(noPrefixWorkflow as any);
|
||||||
|
|
||||||
|
logger.info('Validation result:');
|
||||||
|
logger.info(`Valid: ${result2.valid}`);
|
||||||
|
logger.info(`Errors: ${result2.errors.length}`);
|
||||||
|
result2.errors.forEach(err => {
|
||||||
|
if (typeof err === 'string') {
|
||||||
|
logger.error(` - ${err}`);
|
||||||
|
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||||
|
logger.error(` - ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Completely invalid node type
|
||||||
|
const invalidNodeWorkflow = {
|
||||||
|
"name": "Test Workflow",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "fake-1",
|
||||||
|
"name": "Fake Node",
|
||||||
|
"type": "n8n-nodes-base.fakeNodeThatDoesNotExist",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [250, 300] as [number, number],
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('\nTest 3: Completely invalid node type');
|
||||||
|
const result3 = await validator.validateWorkflow(invalidNodeWorkflow as any);
|
||||||
|
|
||||||
|
logger.info('Validation result:');
|
||||||
|
logger.info(`Valid: ${result3.valid}`);
|
||||||
|
logger.info(`Errors: ${result3.errors.length}`);
|
||||||
|
result3.errors.forEach(err => {
|
||||||
|
if (typeof err === 'string') {
|
||||||
|
logger.error(` - ${err}`);
|
||||||
|
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||||
|
logger.error(` - ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Using n8n-validation.ts function
|
||||||
|
logger.info('\nTest 4: Testing n8n-validation.ts with invalid node types');
|
||||||
|
|
||||||
|
const errors = validateWorkflowStructure(brokenWorkflowFromLogs as any);
|
||||||
|
logger.info('Validation errors:');
|
||||||
|
errors.forEach(err => logger.error(` - ${err}`));
|
||||||
|
|
||||||
|
// Test 5: Valid workflow (should pass)
|
||||||
|
const validWorkflow = {
|
||||||
|
"name": "Valid Webhook Workflow",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "webhook-1",
|
||||||
|
"name": "Webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook", // CORRECT!
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [250, 300] as [number, number],
|
||||||
|
"parameters": {
|
||||||
|
"path": "my-webhook",
|
||||||
|
"responseMode": "onReceived",
|
||||||
|
"responseData": "allEntries"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('\nTest 5: Valid workflow with correct node type');
|
||||||
|
const result5 = await validator.validateWorkflow(validWorkflow as any);
|
||||||
|
|
||||||
|
logger.info('Validation result:');
|
||||||
|
logger.info(`Valid: ${result5.valid}`);
|
||||||
|
logger.info(`Errors: ${result5.errors.length}`);
|
||||||
|
logger.info(`Warnings: ${result5.warnings.length}`);
|
||||||
|
result5.warnings.forEach(warn => {
|
||||||
|
if (warn && typeof warn === 'object' && 'message' in warn) {
|
||||||
|
logger.warn(` - ${warn.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
adapter.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
testValidation().catch(err => {
|
||||||
|
logger.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
100
scripts/test-nodes-base-prefix.ts
Normal file
100
scripts/test-nodes-base-prefix.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific test for nodes-base. prefix validation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WorkflowValidator } from '../src/services/workflow-validator';
|
||||||
|
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator';
|
||||||
|
import { NodeRepository } from '../src/database/node-repository';
|
||||||
|
import { createDatabaseAdapter } from '../src/database/database-adapter';
|
||||||
|
import { Logger } from '../src/utils/logger';
|
||||||
|
|
||||||
|
const logger = new Logger({ prefix: '[TestNodesBasePrefix]' });
|
||||||
|
|
||||||
|
async function testValidation() {
|
||||||
|
const adapter = await createDatabaseAdapter('./data/nodes.db');
|
||||||
|
const repository = new NodeRepository(adapter);
|
||||||
|
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||||
|
|
||||||
|
logger.info('Testing nodes-base. prefix validation...\n');
|
||||||
|
|
||||||
|
// Test various nodes-base. prefixed types
|
||||||
|
const testCases = [
|
||||||
|
{ type: 'nodes-base.webhook', expected: 'n8n-nodes-base.webhook' },
|
||||||
|
{ type: 'nodes-base.httpRequest', expected: 'n8n-nodes-base.httpRequest' },
|
||||||
|
{ type: 'nodes-base.set', expected: 'n8n-nodes-base.set' },
|
||||||
|
{ type: 'nodes-base.code', expected: 'n8n-nodes-base.code' },
|
||||||
|
{ type: 'nodes-base.slack', expected: 'n8n-nodes-base.slack' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
const workflow = {
|
||||||
|
name: `Test ${testCase.type}`,
|
||||||
|
nodes: [{
|
||||||
|
id: 'test-node',
|
||||||
|
name: 'Test Node',
|
||||||
|
type: testCase.type,
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [100, 100] as [number, number],
|
||||||
|
parameters: {}
|
||||||
|
}],
|
||||||
|
connections: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(`Testing: "${testCase.type}"`);
|
||||||
|
const result = await validator.validateWorkflow(workflow as any);
|
||||||
|
|
||||||
|
const nodeTypeError = result.errors.find(err =>
|
||||||
|
err && typeof err === 'object' && 'message' in err &&
|
||||||
|
err.message.includes(testCase.type) &&
|
||||||
|
err.message.includes(testCase.expected)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nodeTypeError) {
|
||||||
|
logger.info(`✅ Caught and suggested: "${testCase.expected}"`);
|
||||||
|
} else {
|
||||||
|
logger.error(`❌ Failed to catch invalid type: "${testCase.type}"`);
|
||||||
|
result.errors.forEach(err => {
|
||||||
|
if (err && typeof err === 'object' && 'message' in err) {
|
||||||
|
logger.error(` Error: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that n8n-nodes-base. prefix still works
|
||||||
|
const validWorkflow = {
|
||||||
|
name: 'Valid Workflow',
|
||||||
|
nodes: [{
|
||||||
|
id: 'webhook',
|
||||||
|
name: 'Webhook',
|
||||||
|
type: 'n8n-nodes-base.webhook',
|
||||||
|
typeVersion: 2,
|
||||||
|
position: [100, 100] as [number, number],
|
||||||
|
parameters: {}
|
||||||
|
}],
|
||||||
|
connections: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('\nTesting valid n8n-nodes-base.webhook:');
|
||||||
|
const validResult = await validator.validateWorkflow(validWorkflow as any);
|
||||||
|
|
||||||
|
const hasNodeTypeError = validResult.errors.some(err =>
|
||||||
|
err && typeof err === 'object' && 'message' in err &&
|
||||||
|
err.message.includes('node type')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasNodeTypeError) {
|
||||||
|
logger.info('✅ Correctly accepted n8n-nodes-base.webhook');
|
||||||
|
} else {
|
||||||
|
logger.error('❌ Incorrectly rejected valid n8n-nodes-base.webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
testValidation().catch(err => {
|
||||||
|
logger.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1412,6 +1412,16 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
|||||||
"Tags": "Read-only during creation/update"
|
"Tags": "Read-only during creation/update"
|
||||||
},
|
},
|
||||||
workflow_examples: {
|
workflow_examples: {
|
||||||
|
"⚠️ CRITICAL: Connection Rules": {
|
||||||
|
warning: "ALWAYS use node NAMES in connections, NEVER use node IDs!",
|
||||||
|
explanation: "Using IDs will make nodes appear disconnected in n8n UI",
|
||||||
|
wrong: {
|
||||||
|
connections: {"1": {main: [[{node: "2", type: "main", index: 0}]]}} // ❌ WRONG - uses IDs
|
||||||
|
},
|
||||||
|
correct: {
|
||||||
|
connections: {"Start": {main: [[{node: "Set", type: "main", index: 0}]]}} // ✅ CORRECT - uses names
|
||||||
|
}
|
||||||
|
},
|
||||||
"Create Simple Workflow": {
|
"Create Simple Workflow": {
|
||||||
tools: ["n8n_create_workflow"],
|
tools: ["n8n_create_workflow"],
|
||||||
example: {
|
example: {
|
||||||
@@ -1420,7 +1430,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
|||||||
{id: "1", name: "Start", type: "n8n-nodes-base.start", position: [250, 300]},
|
{id: "1", name: "Start", type: "n8n-nodes-base.start", position: [250, 300]},
|
||||||
{id: "2", name: "Set", type: "n8n-nodes-base.set", position: [450, 300]}
|
{id: "2", name: "Set", type: "n8n-nodes-base.set", position: [450, 300]}
|
||||||
],
|
],
|
||||||
connections: {"1": {main: [[{node: "2", type: "main", index: 0}]]}}
|
connections: {"Start": {main: [[{node: "Set", type: "main", index: 0}]]}} // ✅ Uses node names!
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Execute via Webhook": {
|
"Execute via Webhook": {
|
||||||
@@ -1433,6 +1443,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
best_practices: [
|
best_practices: [
|
||||||
|
"⚠️ ALWAYS use node NAMES in connections, NEVER node IDs",
|
||||||
"Always use n8n_health_check first to verify connectivity",
|
"Always use n8n_health_check first to verify connectivity",
|
||||||
"Fetch full workflow before updating (n8n_get_workflow)",
|
"Fetch full workflow before updating (n8n_get_workflow)",
|
||||||
"Validate workflows before creating (validate_workflow)",
|
"Validate workflows before creating (validate_workflow)",
|
||||||
|
|||||||
@@ -134,11 +134,38 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
|||||||
errors.push('Workflow connections are required');
|
errors.push('Workflow connections are required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for minimum viable workflow
|
||||||
|
if (workflow.nodes && workflow.nodes.length === 1) {
|
||||||
|
const singleNode = workflow.nodes[0];
|
||||||
|
const isWebhookOnly = singleNode.type === 'n8n-nodes-base.webhook' ||
|
||||||
|
singleNode.type === 'n8n-nodes-base.webhookTrigger';
|
||||||
|
|
||||||
|
if (!isWebhookOnly) {
|
||||||
|
errors.push('Single-node workflows are only valid for webhooks. Add at least one more node and connect them. Example: Manual Trigger → Set node');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty connections in multi-node workflows
|
||||||
|
if (workflow.nodes && workflow.nodes.length > 1 && workflow.connections) {
|
||||||
|
const connectionCount = Object.keys(workflow.connections).length;
|
||||||
|
|
||||||
|
if (connectionCount === 0) {
|
||||||
|
errors.push('Multi-node workflow has empty connections. Connect nodes like this: connections: { "Node1 Name": { "main": [[{ "node": "Node2 Name", "type": "main", "index": 0 }]] } }');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate nodes
|
// Validate nodes
|
||||||
if (workflow.nodes) {
|
if (workflow.nodes) {
|
||||||
workflow.nodes.forEach((node, index) => {
|
workflow.nodes.forEach((node, index) => {
|
||||||
try {
|
try {
|
||||||
validateWorkflowNode(node);
|
validateWorkflowNode(node);
|
||||||
|
|
||||||
|
// Additional check for common node type mistakes
|
||||||
|
if (node.type.startsWith('nodes-base.')) {
|
||||||
|
errors.push(`Invalid node type "${node.type}" at index ${index}. Use "n8n-nodes-base.${node.type.substring(11)}" instead.`);
|
||||||
|
} else if (!node.type.includes('.')) {
|
||||||
|
errors.push(`Invalid node type "${node.type}" at index ${index}. Node types must include package prefix (e.g., "n8n-nodes-base.webhook").`);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push(`Invalid node at index ${index}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
errors.push(`Invalid node at index ${index}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
@@ -154,19 +181,35 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that all connection references exist
|
// Validate that all connection references exist and use node NAMES (not IDs)
|
||||||
if (workflow.nodes && workflow.connections) {
|
if (workflow.nodes && workflow.connections) {
|
||||||
|
const nodeNames = new Set(workflow.nodes.map(node => node.name));
|
||||||
const nodeIds = new Set(workflow.nodes.map(node => node.id));
|
const nodeIds = new Set(workflow.nodes.map(node => node.id));
|
||||||
|
const nodeIdToName = new Map(workflow.nodes.map(node => [node.id, node.name]));
|
||||||
|
|
||||||
Object.entries(workflow.connections).forEach(([sourceId, connection]) => {
|
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
|
||||||
if (!nodeIds.has(sourceId)) {
|
// Check if source exists by name (correct)
|
||||||
errors.push(`Connection references non-existent source node: ${sourceId}`);
|
if (!nodeNames.has(sourceName)) {
|
||||||
|
// Check if they're using an ID instead of name
|
||||||
|
if (nodeIds.has(sourceName)) {
|
||||||
|
const correctName = nodeIdToName.get(sourceName);
|
||||||
|
errors.push(`Connection uses node ID '${sourceName}' but must use node name '${correctName}'. Change connections.${sourceName} to connections['${correctName}']`);
|
||||||
|
} else {
|
||||||
|
errors.push(`Connection references non-existent node: ${sourceName}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.main.forEach((outputs, outputIndex) => {
|
connection.main.forEach((outputs, outputIndex) => {
|
||||||
outputs.forEach((target, targetIndex) => {
|
outputs.forEach((target, targetIndex) => {
|
||||||
if (!nodeIds.has(target.node)) {
|
// Check if target exists by name (correct)
|
||||||
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceId}[${outputIndex}][${targetIndex}])`);
|
if (!nodeNames.has(target.node)) {
|
||||||
|
// Check if they're using an ID instead of name
|
||||||
|
if (nodeIds.has(target.node)) {
|
||||||
|
const correctName = nodeIdToName.get(target.node);
|
||||||
|
errors.push(`Connection target uses node ID '${target.node}' but must use node name '${correctName}' (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||||
|
} else {
|
||||||
|
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -204,4 +247,74 @@ export function getWebhookUrl(workflow: Workflow): string | null {
|
|||||||
// Note: We can't construct the full URL without knowing the n8n instance URL
|
// Note: We can't construct the full URL without knowing the n8n instance URL
|
||||||
// The caller will need to prepend the base URL
|
// The caller will need to prepend the base URL
|
||||||
return path;
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to generate proper workflow structure examples
|
||||||
|
export function getWorkflowStructureExample(): string {
|
||||||
|
return `
|
||||||
|
Minimal Workflow Example:
|
||||||
|
{
|
||||||
|
"name": "My Workflow",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "manual-trigger-1",
|
||||||
|
"name": "Manual Trigger",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [250, 300],
|
||||||
|
"parameters": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "set-1",
|
||||||
|
"name": "Set Data",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [450, 300],
|
||||||
|
"parameters": {
|
||||||
|
"mode": "manual",
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [{
|
||||||
|
"id": "1",
|
||||||
|
"name": "message",
|
||||||
|
"value": "Hello World",
|
||||||
|
"type": "string"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Manual Trigger": {
|
||||||
|
"main": [[{
|
||||||
|
"node": "Set Data",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPORTANT: In connections, use the node NAME (e.g., "Manual Trigger"), NOT the node ID or type!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to fix common workflow issues
|
||||||
|
export function getWorkflowFixSuggestions(errors: string[]): string[] {
|
||||||
|
const suggestions: string[] = [];
|
||||||
|
|
||||||
|
if (errors.some(e => e.includes('empty connections'))) {
|
||||||
|
suggestions.push('Add connections between your nodes. Each node (except endpoints) should connect to another node.');
|
||||||
|
suggestions.push('Connection format: connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.some(e => e.includes('Single-node workflows'))) {
|
||||||
|
suggestions.push('Add at least one more node to process data. Common patterns: Trigger → Process → Output');
|
||||||
|
suggestions.push('Examples: Manual Trigger → Set, Webhook → HTTP Request, Schedule Trigger → Database Query');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.some(e => e.includes('node ID') && e.includes('instead of node name'))) {
|
||||||
|
suggestions.push('Replace node IDs with node names in connections. The name is what appears in the node header.');
|
||||||
|
suggestions.push('Wrong: connections: { "set-1": {...} }, Right: connections: { "Set Data": {...} }');
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
}
|
}
|
||||||
@@ -173,6 +173,39 @@ export class WorkflowValidator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for minimum viable workflow
|
||||||
|
if (workflow.nodes.length === 1) {
|
||||||
|
const singleNode = workflow.nodes[0];
|
||||||
|
const normalizedType = singleNode.type.replace('n8n-nodes-base.', 'nodes-base.');
|
||||||
|
const isWebhook = normalizedType === 'nodes-base.webhook' ||
|
||||||
|
normalizedType === 'nodes-base.webhookTrigger';
|
||||||
|
|
||||||
|
if (!isWebhook) {
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Single-node workflows are only valid for webhook endpoints. Add at least one more connected node to create a functional workflow.'
|
||||||
|
});
|
||||||
|
} else if (Object.keys(workflow.connections).length === 0) {
|
||||||
|
result.warnings.push({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Webhook node has no connections. Consider adding nodes to process the webhook data.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty connections in multi-node workflows
|
||||||
|
if (workflow.nodes.length > 1) {
|
||||||
|
const hasEnabledNodes = workflow.nodes.some(n => !n.disabled);
|
||||||
|
const hasConnections = Object.keys(workflow.connections).length > 0;
|
||||||
|
|
||||||
|
if (hasEnabledNodes && !hasConnections) {
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Multi-node workflow has no connections. Nodes must be connected to create a workflow. Use connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for duplicate node names
|
// Check for duplicate node names
|
||||||
const nodeNames = new Set<string>();
|
const nodeNames = new Set<string>();
|
||||||
const nodeIds = new Set<string>();
|
const nodeIds = new Set<string>();
|
||||||
@@ -230,6 +263,19 @@ export class WorkflowValidator {
|
|||||||
if (node.disabled) continue;
|
if (node.disabled) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// FIRST: Check for common invalid patterns before database lookup
|
||||||
|
if (node.type.startsWith('nodes-base.')) {
|
||||||
|
// This is ALWAYS invalid in workflows - must use n8n-nodes-base prefix
|
||||||
|
const correctType = node.type.replace('nodes-base.', 'n8n-nodes-base.');
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeName: node.name,
|
||||||
|
message: `Invalid node type: "${node.type}". Use "${correctType}" instead. Node types in workflows must use the full package name.`
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Get node definition - try multiple formats
|
// Get node definition - try multiple formats
|
||||||
let nodeInfo = this.nodeRepository.getNode(node.type);
|
let nodeInfo = this.nodeRepository.getNode(node.type);
|
||||||
|
|
||||||
@@ -250,11 +296,44 @@ export class WorkflowValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!nodeInfo) {
|
if (!nodeInfo) {
|
||||||
|
// Check for common mistakes
|
||||||
|
let suggestion = '';
|
||||||
|
|
||||||
|
// Missing package prefix
|
||||||
|
if (node.type.startsWith('nodes-base.')) {
|
||||||
|
const withPrefix = node.type.replace('nodes-base.', 'n8n-nodes-base.');
|
||||||
|
const exists = this.nodeRepository.getNode(withPrefix) ||
|
||||||
|
this.nodeRepository.getNode(withPrefix.replace('n8n-nodes-base.', 'nodes-base.'));
|
||||||
|
if (exists) {
|
||||||
|
suggestion = ` Did you mean "n8n-nodes-base.${node.type.substring(11)}"?`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if it's just the node name without package
|
||||||
|
else if (!node.type.includes('.')) {
|
||||||
|
// Try common node names
|
||||||
|
const commonNodes = [
|
||||||
|
'webhook', 'httpRequest', 'set', 'code', 'manualTrigger',
|
||||||
|
'scheduleTrigger', 'emailSend', 'slack', 'discord'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (commonNodes.includes(node.type)) {
|
||||||
|
suggestion = ` Did you mean "n8n-nodes-base.${node.type}"?`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no specific suggestion, try to find similar nodes
|
||||||
|
if (!suggestion) {
|
||||||
|
const similarNodes = this.findSimilarNodeTypes(node.type);
|
||||||
|
if (similarNodes.length > 0) {
|
||||||
|
suggestion = ` Did you mean: ${similarNodes.map(n => `"${n}"`).join(', ')}?`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result.errors.push({
|
result.errors.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
nodeName: node.name,
|
nodeName: node.name,
|
||||||
message: `Unknown node type: ${node.type}`
|
message: `Unknown node type: "${node.type}".${suggestion} Node types must include the package prefix (e.g., "n8n-nodes-base.webhook", not "webhook" or "nodes-base.webhook").`
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -346,16 +425,28 @@ export class WorkflowValidator {
|
|||||||
result: WorkflowValidationResult
|
result: WorkflowValidationResult
|
||||||
): void {
|
): void {
|
||||||
const nodeMap = new Map(workflow.nodes.map(n => [n.name, n]));
|
const nodeMap = new Map(workflow.nodes.map(n => [n.name, n]));
|
||||||
|
const nodeIdMap = new Map(workflow.nodes.map(n => [n.id, n]));
|
||||||
|
|
||||||
// Check all connections
|
// Check all connections
|
||||||
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
||||||
const sourceNode = nodeMap.get(sourceName);
|
const sourceNode = nodeMap.get(sourceName);
|
||||||
|
|
||||||
if (!sourceNode) {
|
if (!sourceNode) {
|
||||||
result.errors.push({
|
// Check if this is an ID being used instead of a name
|
||||||
type: 'error',
|
const nodeById = nodeIdMap.get(sourceName);
|
||||||
message: `Connection from non-existent node: "${sourceName}"`
|
if (nodeById) {
|
||||||
});
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeId: nodeById.id,
|
||||||
|
nodeName: nodeById.name,
|
||||||
|
message: `Connection uses node ID '${sourceName}' instead of node name '${nodeById.name}'. In n8n, connections must use node names, not IDs.`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
message: `Connection from non-existent node: "${sourceName}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
result.statistics.invalidConnections++;
|
result.statistics.invalidConnections++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -366,6 +457,7 @@ export class WorkflowValidator {
|
|||||||
sourceName,
|
sourceName,
|
||||||
outputs.main,
|
outputs.main,
|
||||||
nodeMap,
|
nodeMap,
|
||||||
|
nodeIdMap,
|
||||||
result,
|
result,
|
||||||
'main'
|
'main'
|
||||||
);
|
);
|
||||||
@@ -377,6 +469,7 @@ export class WorkflowValidator {
|
|||||||
sourceName,
|
sourceName,
|
||||||
outputs.error,
|
outputs.error,
|
||||||
nodeMap,
|
nodeMap,
|
||||||
|
nodeIdMap,
|
||||||
result,
|
result,
|
||||||
'error'
|
'error'
|
||||||
);
|
);
|
||||||
@@ -388,6 +481,7 @@ export class WorkflowValidator {
|
|||||||
sourceName,
|
sourceName,
|
||||||
outputs.ai_tool,
|
outputs.ai_tool,
|
||||||
nodeMap,
|
nodeMap,
|
||||||
|
nodeIdMap,
|
||||||
result,
|
result,
|
||||||
'ai_tool'
|
'ai_tool'
|
||||||
);
|
);
|
||||||
@@ -456,6 +550,7 @@ export class WorkflowValidator {
|
|||||||
sourceName: string,
|
sourceName: string,
|
||||||
outputs: Array<Array<{ node: string; type: string; index: number }>>,
|
outputs: Array<Array<{ node: string; type: string; index: number }>>,
|
||||||
nodeMap: Map<string, WorkflowNode>,
|
nodeMap: Map<string, WorkflowNode>,
|
||||||
|
nodeIdMap: Map<string, WorkflowNode>,
|
||||||
result: WorkflowValidationResult,
|
result: WorkflowValidationResult,
|
||||||
outputType: 'main' | 'error' | 'ai_tool'
|
outputType: 'main' | 'error' | 'ai_tool'
|
||||||
): void {
|
): void {
|
||||||
@@ -466,10 +561,21 @@ export class WorkflowValidator {
|
|||||||
const targetNode = nodeMap.get(connection.node);
|
const targetNode = nodeMap.get(connection.node);
|
||||||
|
|
||||||
if (!targetNode) {
|
if (!targetNode) {
|
||||||
result.errors.push({
|
// Check if this is an ID being used instead of a name
|
||||||
type: 'error',
|
const nodeById = nodeIdMap.get(connection.node);
|
||||||
message: `Connection to non-existent node: "${connection.node}" from "${sourceName}"`
|
if (nodeById) {
|
||||||
});
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeId: nodeById.id,
|
||||||
|
nodeName: nodeById.name,
|
||||||
|
message: `Connection target uses node ID '${connection.node}' instead of node name '${nodeById.name}' (from ${sourceName}). In n8n, connections must use node names, not IDs.`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
message: `Connection to non-existent node: "${connection.node}" from "${sourceName}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
result.statistics.invalidConnections++;
|
result.statistics.invalidConnections++;
|
||||||
} else if (targetNode.disabled) {
|
} else if (targetNode.disabled) {
|
||||||
result.warnings.push({
|
result.warnings.push({
|
||||||
@@ -772,6 +878,66 @@ export class WorkflowValidator {
|
|||||||
return maxChain;
|
return maxChain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find similar node types for suggestions
|
||||||
|
*/
|
||||||
|
private findSimilarNodeTypes(invalidType: string): string[] {
|
||||||
|
// Since we don't have a method to list all nodes, we'll use a predefined list
|
||||||
|
// of common node types that users might be looking for
|
||||||
|
const suggestions: string[] = [];
|
||||||
|
const nodeName = invalidType.includes('.') ? invalidType.split('.').pop()! : invalidType;
|
||||||
|
|
||||||
|
const commonNodeMappings: Record<string, string[]> = {
|
||||||
|
'webhook': ['nodes-base.webhook'],
|
||||||
|
'httpRequest': ['nodes-base.httpRequest'],
|
||||||
|
'http': ['nodes-base.httpRequest'],
|
||||||
|
'set': ['nodes-base.set'],
|
||||||
|
'code': ['nodes-base.code'],
|
||||||
|
'manualTrigger': ['nodes-base.manualTrigger'],
|
||||||
|
'manual': ['nodes-base.manualTrigger'],
|
||||||
|
'scheduleTrigger': ['nodes-base.scheduleTrigger'],
|
||||||
|
'schedule': ['nodes-base.scheduleTrigger'],
|
||||||
|
'cron': ['nodes-base.scheduleTrigger'],
|
||||||
|
'emailSend': ['nodes-base.emailSend'],
|
||||||
|
'email': ['nodes-base.emailSend'],
|
||||||
|
'slack': ['nodes-base.slack'],
|
||||||
|
'discord': ['nodes-base.discord'],
|
||||||
|
'postgres': ['nodes-base.postgres'],
|
||||||
|
'mysql': ['nodes-base.mySql'],
|
||||||
|
'mongodb': ['nodes-base.mongoDb'],
|
||||||
|
'redis': ['nodes-base.redis'],
|
||||||
|
'if': ['nodes-base.if'],
|
||||||
|
'switch': ['nodes-base.switch'],
|
||||||
|
'merge': ['nodes-base.merge'],
|
||||||
|
'splitInBatches': ['nodes-base.splitInBatches'],
|
||||||
|
'loop': ['nodes-base.splitInBatches'],
|
||||||
|
'googleSheets': ['nodes-base.googleSheets'],
|
||||||
|
'sheets': ['nodes-base.googleSheets'],
|
||||||
|
'airtable': ['nodes-base.airtable'],
|
||||||
|
'github': ['nodes-base.github'],
|
||||||
|
'git': ['nodes-base.github'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for exact match
|
||||||
|
const lowerNodeName = nodeName.toLowerCase();
|
||||||
|
if (commonNodeMappings[lowerNodeName]) {
|
||||||
|
suggestions.push(...commonNodeMappings[lowerNodeName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for partial matches
|
||||||
|
Object.entries(commonNodeMappings).forEach(([key, values]) => {
|
||||||
|
if (key.includes(lowerNodeName) || lowerNodeName.includes(key)) {
|
||||||
|
values.forEach(v => {
|
||||||
|
if (!suggestions.includes(v)) {
|
||||||
|
suggestions.push(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return suggestions.slice(0, 3); // Return top 3 suggestions
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate suggestions based on validation results
|
* Generate suggestions based on validation results
|
||||||
*/
|
*/
|
||||||
@@ -786,6 +952,24 @@ export class WorkflowValidator {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suggest proper connection structure for workflows with connection errors
|
||||||
|
const hasConnectionErrors = result.errors.some(e =>
|
||||||
|
e.message && (
|
||||||
|
e.message.includes('connection') ||
|
||||||
|
e.message.includes('Connection') ||
|
||||||
|
e.message.includes('Multi-node workflow has no connections')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasConnectionErrors) {
|
||||||
|
result.suggestions.push(
|
||||||
|
'Example connection structure: connections: { "Manual Trigger": { "main": [[{ "node": "Set", "type": "main", "index": 0 }]] } }'
|
||||||
|
);
|
||||||
|
result.suggestions.push(
|
||||||
|
'Remember: Use node NAMES (not IDs) in connections. The name is what you see in the UI, not the node type.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Suggest error handling
|
// Suggest error handling
|
||||||
if (!Object.values(workflow.connections).some(o => o.error)) {
|
if (!Object.values(workflow.connections).some(o => o.error)) {
|
||||||
result.suggestions.push(
|
result.suggestions.push(
|
||||||
@@ -812,5 +996,12 @@ export class WorkflowValidator {
|
|||||||
'Consider using a Code node for complex data transformations instead of multiple expressions'
|
'Consider using a Code node for complex data transformations instead of multiple expressions'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suggest minimum workflow structure
|
||||||
|
if (workflow.nodes.length === 1 && Object.keys(workflow.connections).length === 0) {
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user