mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-21 09:53:08 +00:00
- Normalize name→nodeName and id→nodeId for node-targeting operations in the Zod schema transform, so LLMs using natural field names no longer get "Node not found" errors - Replace hardcoded ALL_CONNECTION_TYPES with dynamic iteration so AI sub-nodes (ai_outputParser, ai_document, ai_textSplitter, etc.) are not flagged as disconnected during save - Add .catchall() to workflowConnectionSchema and extend connection reference validation to cover all connection types, not just main - Fix filterOperationsByFixes ID-vs-name mismatch: typeversion-upgrade operations now include nodeName alongside nodeId, and the filter checks both fields Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
65ab94deb2
commit
f7a1cfe8bf
@@ -31,6 +31,11 @@ function getValidator(repository: NodeRepository): WorkflowValidator {
|
||||
return cachedValidator;
|
||||
}
|
||||
|
||||
// Operation types that identify nodes by nodeId/nodeName
|
||||
const NODE_TARGETING_OPERATIONS = new Set([
|
||||
'updateNode', 'removeNode', 'moveNode', 'enableNode', 'disableNode'
|
||||
]);
|
||||
|
||||
// Zod schema for the diff request
|
||||
const workflowDiffSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -63,6 +68,23 @@ const workflowDiffSchema = z.object({
|
||||
settings: z.any().optional(),
|
||||
name: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
// Aliases: LLMs often use "id" instead of "nodeId" — accept both
|
||||
id: z.string().optional(),
|
||||
}).transform((op) => {
|
||||
// Normalize common field aliases for node-targeting operations:
|
||||
// - "name" → "nodeName" (LLMs confuse the updateName "name" field with node identification)
|
||||
// - "id" → "nodeId" (natural alias)
|
||||
if (NODE_TARGETING_OPERATIONS.has(op.type)) {
|
||||
if (!op.nodeName && !op.nodeId && op.name) {
|
||||
op.nodeName = op.name;
|
||||
op.name = undefined;
|
||||
}
|
||||
if (!op.nodeId && op.id) {
|
||||
op.nodeId = op.id;
|
||||
op.id = undefined;
|
||||
}
|
||||
}
|
||||
return op;
|
||||
})),
|
||||
validateOnly: z.boolean().optional(),
|
||||
continueOnError: z.boolean().optional(),
|
||||
|
||||
@@ -49,7 +49,7 @@ export const workflowConnectionSchema = z.record(
|
||||
ai_memory: connectionArraySchema.optional(),
|
||||
ai_embedding: connectionArraySchema.optional(),
|
||||
ai_vectorStore: connectionArraySchema.optional(),
|
||||
})
|
||||
}).catchall(connectionArraySchema) // Allow additional AI connection types (ai_outputParser, ai_document, ai_textSplitter, etc.)
|
||||
);
|
||||
|
||||
export const workflowSettingsSchema = z.object({
|
||||
@@ -248,15 +248,15 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
||||
const connectedNodes = new Set<string>();
|
||||
|
||||
// Collect all nodes that appear in connections (as source or target)
|
||||
// Check ALL connection types, not just 'main' - AI workflows use ai_tool, ai_languageModel, etc.
|
||||
const ALL_CONNECTION_TYPES = ['main', 'error', 'ai_tool', 'ai_languageModel', 'ai_memory', 'ai_embedding', 'ai_vectorStore'] as const;
|
||||
|
||||
// Iterate over ALL connection types present in the data — not a hardcoded list —
|
||||
// so that every AI connection type (ai_outputParser, ai_document, ai_textSplitter,
|
||||
// ai_agent, ai_chain, ai_retriever, etc.) is covered automatically.
|
||||
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
|
||||
connectedNodes.add(sourceName); // Node has outgoing connection
|
||||
|
||||
// Check all connection types for target nodes
|
||||
ALL_CONNECTION_TYPES.forEach(connType => {
|
||||
const connData = (connection as Record<string, unknown>)[connType];
|
||||
// Check every connection type key present on this source node
|
||||
const connectionRecord = connection as Record<string, unknown>;
|
||||
Object.values(connectionRecord).forEach((connData) => {
|
||||
if (connData && Array.isArray(connData)) {
|
||||
connData.forEach((outputs) => {
|
||||
if (Array.isArray(outputs)) {
|
||||
@@ -429,24 +429,29 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
||||
}
|
||||
}
|
||||
|
||||
if (connection.main && Array.isArray(connection.main)) {
|
||||
connection.main.forEach((outputs, outputIndex) => {
|
||||
if (Array.isArray(outputs)) {
|
||||
outputs.forEach((target, targetIndex) => {
|
||||
// Check if target exists by name (correct)
|
||||
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}])`);
|
||||
// Check all connection types (main, error, ai_tool, ai_languageModel, etc.)
|
||||
const connectionRecord = connection as Record<string, unknown>;
|
||||
Object.values(connectionRecord).forEach((connData) => {
|
||||
if (connData && Array.isArray(connData)) {
|
||||
connData.forEach((outputs: any, outputIndex: number) => {
|
||||
if (Array.isArray(outputs)) {
|
||||
outputs.forEach((target: any, targetIndex: number) => {
|
||||
if (!target?.node) return;
|
||||
// Check if target exists by name (correct)
|
||||
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}])`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -671,11 +671,13 @@ export class WorkflowAutoFixer {
|
||||
filteredFixes: FixOperation[],
|
||||
allFixes: FixOperation[]
|
||||
): WorkflowDiffOperation[] {
|
||||
// fixedNodes contains node names (FixOperation.node = node.name)
|
||||
const fixedNodes = new Set(filteredFixes.map(f => f.node));
|
||||
const hasConnectionFixes = filteredFixes.some(f => CONNECTION_FIX_TYPES.includes(f.type));
|
||||
return operations.filter(op => {
|
||||
if (op.type === 'updateNode') {
|
||||
return fixedNodes.has(op.nodeId || '');
|
||||
// Check both nodeName and nodeId — operations may use either
|
||||
return fixedNodes.has(op.nodeName || '') || fixedNodes.has(op.nodeId || '');
|
||||
}
|
||||
if (op.type === 'replaceConnections') {
|
||||
return hasConnectionFixes;
|
||||
@@ -1186,10 +1188,11 @@ export class WorkflowAutoFixer {
|
||||
description: `Upgrade ${node.name} from v${currentVersion} to v${latestVersion}. ${analysis.reason}`
|
||||
});
|
||||
|
||||
// Create update operation
|
||||
// Create update operation — both nodeId and nodeName needed for fix filtering
|
||||
const operation: UpdateNodeOperation = {
|
||||
type: 'updateNode',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
updates: {
|
||||
typeVersion: parseFloat(latestVersion),
|
||||
parameters: migrationResult.updatedNode.parameters,
|
||||
|
||||
Reference in New Issue
Block a user