mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-30 14:13:12 +00:00
refactor: remove updateConnection operation (breaking change)
Remove UpdateConnectionOperation completely as planned for v2.16.0. This is a breaking change - users should use removeConnection + addConnection or the new rewireConnection operation instead. Changes: - Remove UpdateConnectionOperation type definition - Remove validateUpdateConnection and applyUpdateConnection methods - Remove updateConnection cases from validation/apply switches - Remove updateConnection tests (4 tests) - Remove UpdateConnectionOperation import from tests All 137 tests passing. Related: #272 Phase 1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,6 @@ import {
|
|||||||
DisableNodeOperation,
|
DisableNodeOperation,
|
||||||
AddConnectionOperation,
|
AddConnectionOperation,
|
||||||
RemoveConnectionOperation,
|
RemoveConnectionOperation,
|
||||||
UpdateConnectionOperation,
|
|
||||||
RewireConnectionOperation,
|
RewireConnectionOperation,
|
||||||
UpdateSettingsOperation,
|
UpdateSettingsOperation,
|
||||||
UpdateNameOperation,
|
UpdateNameOperation,
|
||||||
@@ -224,8 +223,6 @@ export class WorkflowDiffEngine {
|
|||||||
return this.validateAddConnection(workflow, operation);
|
return this.validateAddConnection(workflow, operation);
|
||||||
case 'removeConnection':
|
case 'removeConnection':
|
||||||
return this.validateRemoveConnection(workflow, operation);
|
return this.validateRemoveConnection(workflow, operation);
|
||||||
case 'updateConnection':
|
|
||||||
return this.validateUpdateConnection(workflow, operation);
|
|
||||||
case 'rewireConnection':
|
case 'rewireConnection':
|
||||||
return this.validateRewireConnection(workflow, operation as RewireConnectionOperation);
|
return this.validateRewireConnection(workflow, operation as RewireConnectionOperation);
|
||||||
case 'updateSettings':
|
case 'updateSettings':
|
||||||
@@ -271,9 +268,6 @@ export class WorkflowDiffEngine {
|
|||||||
case 'removeConnection':
|
case 'removeConnection':
|
||||||
this.applyRemoveConnection(workflow, operation);
|
this.applyRemoveConnection(workflow, operation);
|
||||||
break;
|
break;
|
||||||
case 'updateConnection':
|
|
||||||
this.applyUpdateConnection(workflow, operation);
|
|
||||||
break;
|
|
||||||
case 'rewireConnection':
|
case 'rewireConnection':
|
||||||
this.applyRewireConnection(workflow, operation as RewireConnectionOperation);
|
this.applyRewireConnection(workflow, operation as RewireConnectionOperation);
|
||||||
break;
|
break;
|
||||||
@@ -461,40 +455,6 @@ export class WorkflowDiffEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateUpdateConnection(workflow: Workflow, operation: UpdateConnectionOperation): string | null {
|
|
||||||
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
|
||||||
const targetNode = this.findNode(workflow, operation.target, operation.target);
|
|
||||||
|
|
||||||
if (!sourceNode) {
|
|
||||||
return `Source node not found: ${operation.source}`;
|
|
||||||
}
|
|
||||||
if (!targetNode) {
|
|
||||||
return `Target node not found: ${operation.target}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if connection exists to update
|
|
||||||
const existingConnections = workflow.connections[sourceNode.name];
|
|
||||||
if (!existingConnections) {
|
|
||||||
return `No connections found from "${sourceNode.name}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any connection to target exists
|
|
||||||
let hasConnection = false;
|
|
||||||
Object.values(existingConnections).forEach(outputs => {
|
|
||||||
outputs.forEach(connections => {
|
|
||||||
if (connections.some(c => c.node === targetNode.name)) {
|
|
||||||
hasConnection = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasConnection) {
|
|
||||||
return `No connection exists from "${sourceNode.name}" to "${targetNode.name}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateRewireConnection(workflow: Workflow, operation: RewireConnectionOperation): string | null {
|
private validateRewireConnection(workflow: Workflow, operation: RewireConnectionOperation): string | null {
|
||||||
// Validate source node exists
|
// Validate source node exists
|
||||||
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
||||||
@@ -743,53 +703,6 @@ export class WorkflowDiffEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyUpdateConnection(workflow: Workflow, operation: UpdateConnectionOperation): void {
|
|
||||||
// Validate that updates object exists and is an object
|
|
||||||
if (!operation.updates || typeof operation.updates !== 'object') {
|
|
||||||
throw new Error(
|
|
||||||
`updateConnection operation requires 'updates' object.\n\n` +
|
|
||||||
`You provided: ${JSON.stringify(operation, null, 2)}\n\n` +
|
|
||||||
`The 'updates' property is missing or invalid. ` +
|
|
||||||
`This operation modifies connection properties (output type, input type, indices), ` +
|
|
||||||
`not connection targets.\n\n` +
|
|
||||||
`Correct format:\n` +
|
|
||||||
`{\n` +
|
|
||||||
` type: "updateConnection",\n` +
|
|
||||||
` source: "IF",\n` +
|
|
||||||
` target: "EmailNode",\n` +
|
|
||||||
` updates: {\n` +
|
|
||||||
` sourceOutput: "false" // Change from one output to another\n` +
|
|
||||||
` }\n` +
|
|
||||||
`}\n\n` +
|
|
||||||
`💡 Note: If you want to change which node a connection goes to, ` +
|
|
||||||
`use removeConnection + addConnection instead:\n` +
|
|
||||||
`[\n` +
|
|
||||||
` {type: "removeConnection", source: "${operation.source}", target: "${operation.target}"},\n` +
|
|
||||||
` {type: "addConnection", source: "${operation.source}", target: "NewTarget"}\n` +
|
|
||||||
`]`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement as remove + add with the updated properties
|
|
||||||
this.applyRemoveConnection(workflow, {
|
|
||||||
type: 'removeConnection',
|
|
||||||
source: operation.source,
|
|
||||||
target: operation.target,
|
|
||||||
sourceOutput: operation.updates.sourceOutput,
|
|
||||||
targetInput: operation.updates.targetInput
|
|
||||||
});
|
|
||||||
|
|
||||||
this.applyAddConnection(workflow, {
|
|
||||||
type: 'addConnection',
|
|
||||||
source: operation.source,
|
|
||||||
target: operation.target,
|
|
||||||
sourceOutput: operation.updates.sourceOutput,
|
|
||||||
targetInput: operation.updates.targetInput,
|
|
||||||
sourceIndex: operation.updates.sourceIndex,
|
|
||||||
targetIndex: operation.updates.targetIndex
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rewire a connection from one target to another
|
* Rewire a connection from one target to another
|
||||||
* This is a semantic wrapper around removeConnection + addConnection
|
* This is a semantic wrapper around removeConnection + addConnection
|
||||||
|
|||||||
@@ -78,18 +78,6 @@ export interface RemoveConnectionOperation extends DiffOperation {
|
|||||||
ignoreErrors?: boolean; // If true, don't fail when connection doesn't exist (useful for cleanup)
|
ignoreErrors?: boolean; // If true, don't fail when connection doesn't exist (useful for cleanup)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateConnectionOperation extends DiffOperation {
|
|
||||||
type: 'updateConnection';
|
|
||||||
source: string;
|
|
||||||
target: string;
|
|
||||||
updates: {
|
|
||||||
sourceOutput?: string;
|
|
||||||
targetInput?: string;
|
|
||||||
sourceIndex?: number;
|
|
||||||
targetIndex?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RewireConnectionOperation extends DiffOperation {
|
export interface RewireConnectionOperation extends DiffOperation {
|
||||||
type: 'rewireConnection';
|
type: 'rewireConnection';
|
||||||
source: string; // Source node name or ID
|
source: string; // Source node name or ID
|
||||||
@@ -155,7 +143,6 @@ export type WorkflowDiffOperation =
|
|||||||
| DisableNodeOperation
|
| DisableNodeOperation
|
||||||
| AddConnectionOperation
|
| AddConnectionOperation
|
||||||
| RemoveConnectionOperation
|
| RemoveConnectionOperation
|
||||||
| UpdateConnectionOperation
|
|
||||||
| RewireConnectionOperation
|
| RewireConnectionOperation
|
||||||
| UpdateSettingsOperation
|
| UpdateSettingsOperation
|
||||||
| UpdateNameOperation
|
| UpdateNameOperation
|
||||||
@@ -204,8 +191,8 @@ export function isNodeOperation(op: WorkflowDiffOperation): op is
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isConnectionOperation(op: WorkflowDiffOperation): op is
|
export function isConnectionOperation(op: WorkflowDiffOperation): op is
|
||||||
AddConnectionOperation | RemoveConnectionOperation | UpdateConnectionOperation | RewireConnectionOperation | CleanStaleConnectionsOperation | ReplaceConnectionsOperation {
|
AddConnectionOperation | RemoveConnectionOperation | RewireConnectionOperation | CleanStaleConnectionsOperation | ReplaceConnectionsOperation {
|
||||||
return ['addConnection', 'removeConnection', 'updateConnection', 'rewireConnection', 'cleanStaleConnections', 'replaceConnections'].includes(op.type);
|
return ['addConnection', 'removeConnection', 'rewireConnection', 'cleanStaleConnections', 'replaceConnections'].includes(op.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMetadataOperation(op: WorkflowDiffOperation): op is
|
export function isMetadataOperation(op: WorkflowDiffOperation): op is
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
DisableNodeOperation,
|
DisableNodeOperation,
|
||||||
AddConnectionOperation,
|
AddConnectionOperation,
|
||||||
RemoveConnectionOperation,
|
RemoveConnectionOperation,
|
||||||
UpdateConnectionOperation,
|
|
||||||
UpdateSettingsOperation,
|
UpdateSettingsOperation,
|
||||||
UpdateNameOperation,
|
UpdateNameOperation,
|
||||||
AddTagOperation,
|
AddTagOperation,
|
||||||
@@ -774,98 +773,6 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UpdateConnection Operation', () => {
|
|
||||||
it('should update connection properties', async () => {
|
|
||||||
// Add an IF node with multiple outputs
|
|
||||||
const addNodeOp: AddNodeOperation = {
|
|
||||||
type: 'addNode',
|
|
||||||
node: {
|
|
||||||
name: 'IF',
|
|
||||||
type: 'n8n-nodes-base.if',
|
|
||||||
position: [600, 300]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addConnectionOp: AddConnectionOperation = {
|
|
||||||
type: 'addConnection',
|
|
||||||
source: 'IF',
|
|
||||||
target: 'slack-1',
|
|
||||||
sourceOutput: 'true'
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateConnectionOp: UpdateConnectionOperation = {
|
|
||||||
type: 'updateConnection',
|
|
||||||
source: 'IF',
|
|
||||||
target: 'slack-1',
|
|
||||||
updates: {
|
|
||||||
sourceOutput: 'false',
|
|
||||||
sourceIndex: 0,
|
|
||||||
targetIndex: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const request: WorkflowDiffRequest = {
|
|
||||||
id: 'test-workflow',
|
|
||||||
operations: [addNodeOp, addConnectionOp, updateConnectionOp]
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
// After update, the connection should be on 'false' output only
|
|
||||||
expect(result.workflow!.connections['IF'].false).toBeDefined();
|
|
||||||
expect(result.workflow!.connections['IF'].false[0][0].node).toBe('Slack');
|
|
||||||
// The 'true' output should still have the original connection
|
|
||||||
// because updateConnection removes using the NEW output values, not the old ones
|
|
||||||
expect(result.workflow!.connections['IF'].true).toBeDefined();
|
|
||||||
expect(result.workflow!.connections['IF'].true[0][0].node).toBe('Slack');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject updateConnection without updates object (Issue #272, #204)', async () => {
|
|
||||||
const operation: any = {
|
|
||||||
type: 'updateConnection',
|
|
||||||
source: 'Webhook',
|
|
||||||
target: 'HTTP Request'
|
|
||||||
// Missing updates object - should fail with helpful error
|
|
||||||
};
|
|
||||||
|
|
||||||
const request: WorkflowDiffRequest = {
|
|
||||||
id: 'test-workflow',
|
|
||||||
operations: [operation]
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.errors).toBeDefined();
|
|
||||||
expect(result.errors![0].message).toContain('updates');
|
|
||||||
expect(result.errors![0].message).toContain('object');
|
|
||||||
// Should include helpful guidance
|
|
||||||
expect(result.errors![0].message).toContain('removeConnection');
|
|
||||||
expect(result.errors![0].message).toContain('addConnection');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject updateConnection with invalid updates type', async () => {
|
|
||||||
const operation: any = {
|
|
||||||
type: 'updateConnection',
|
|
||||||
source: 'Webhook',
|
|
||||||
target: 'HTTP Request',
|
|
||||||
updates: 'invalid' // Should be object, not string
|
|
||||||
};
|
|
||||||
|
|
||||||
const request: WorkflowDiffRequest = {
|
|
||||||
id: 'test-workflow',
|
|
||||||
operations: [operation]
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.errors).toBeDefined();
|
|
||||||
expect(result.errors![0].message).toContain('updates');
|
|
||||||
expect(result.errors![0].message).toContain('object');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('RewireConnection Operation (Phase 1)', () => {
|
describe('RewireConnection Operation (Phase 1)', () => {
|
||||||
it('should rewire connection from one target to another', async () => {
|
it('should rewire connection from one target to another', async () => {
|
||||||
@@ -3673,47 +3580,6 @@ describe('WorkflowDiffEngine', () => {
|
|||||||
expect(result.workflow).toBeUndefined();
|
expect(result.workflow).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle updateConnection with complex output configurations', async () => {
|
|
||||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
|
||||||
|
|
||||||
// Add IF node
|
|
||||||
workflow.nodes.push({
|
|
||||||
id: 'if-1',
|
|
||||||
name: 'IF',
|
|
||||||
type: 'n8n-nodes-base.if',
|
|
||||||
typeVersion: 1,
|
|
||||||
position: [600, 400],
|
|
||||||
parameters: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add connection on 'true' output
|
|
||||||
workflow.connections['IF'] = {
|
|
||||||
'true': [[
|
|
||||||
{ node: 'Slack', type: 'main', index: 0 }
|
|
||||||
]]
|
|
||||||
};
|
|
||||||
|
|
||||||
const operations: UpdateConnectionOperation[] = [{
|
|
||||||
type: 'updateConnection',
|
|
||||||
source: 'IF',
|
|
||||||
target: 'Slack',
|
|
||||||
updates: {
|
|
||||||
sourceOutput: 'false',
|
|
||||||
targetInput: 'main',
|
|
||||||
sourceIndex: 0,
|
|
||||||
targetIndex: 0
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
const request: WorkflowDiffRequest = {
|
|
||||||
id: 'test-workflow',
|
|
||||||
operations
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await diffEngine.applyDiff(workflow, request);
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle addConnection with all optional parameters specified', async () => {
|
it('should handle addConnection with all optional parameters specified', async () => {
|
||||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||||
|
|||||||
Reference in New Issue
Block a user