diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.test.ts b/packages/editor-ui/src/utils/canvasUtilsV2.test.ts index 25ede479f2..088a219738 100644 --- a/packages/editor-ui/src/utils/canvasUtilsV2.test.ts +++ b/packages/editor-ui/src/utils/canvasUtilsV2.test.ts @@ -7,7 +7,8 @@ import { parseCanvasConnectionHandleString, checkOverlap, } from '@/utils/canvasUtilsV2'; -import { type IConnections, type INodeTypeDescription, NodeConnectionType } from 'n8n-workflow'; +import { NodeConnectionType } from 'n8n-workflow'; +import type { IConnections, INodeTypeDescription, IConnection } from 'n8n-workflow'; import type { CanvasConnection } from '@/types'; import { CanvasConnectionMode } from '@/types'; import type { INodeUi } from '@/Interface'; @@ -22,7 +23,7 @@ describe('mapLegacyConnectionsToCanvasConnections', () => { it('should map legacy connections to canvas connections', () => { const legacyConnections: IConnections = { 'Node A': { - main: [[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }]], + [NodeConnectionType.Main]: [[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }]], }, }; const nodes: INodeUi[] = [ @@ -93,7 +94,7 @@ describe('mapLegacyConnectionsToCanvasConnections', () => { it('should return empty array when no matching nodes found', () => { const legacyConnections: IConnections = { 'Node A': { - main: [[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }]], + [NodeConnectionType.Main]: [[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }]], }, }; const nodes: INodeUi[] = []; @@ -138,7 +139,7 @@ describe('mapLegacyConnectionsToCanvasConnections', () => { it('should map multiple connections between the same nodes', () => { const legacyConnections: IConnections = { 'Node A': { - main: [ + [NodeConnectionType.Main]: [ [{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }], [{ node: 'Node B', type: NodeConnectionType.Main, index: 1 }], ], @@ -249,7 +250,7 @@ describe('mapLegacyConnectionsToCanvasConnections', () => { it('should map multiple connections from one node to different nodes', () => { const legacyConnections: IConnections = { 'Node A': { - main: [ + [NodeConnectionType.Main]: [ [{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }], [{ node: 'Node C', type: NodeConnectionType.Main, index: 0 }], ], @@ -368,13 +369,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => { it('should map complex node setup with mixed inputs and outputs', () => { const legacyConnections: IConnections = { 'Node A': { - main: [[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }]], + [NodeConnectionType.Main]: [[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }]], [NodeConnectionType.AiMemory]: [ [{ node: 'Node C', type: NodeConnectionType.AiMemory, index: 1 }], ], }, 'Node B': { - main: [[{ node: 'Node C', type: NodeConnectionType.Main, index: 0 }]], + [NodeConnectionType.Main]: [[{ node: 'Node C', type: NodeConnectionType.Main, index: 0 }]], }, }; const nodes: INodeUi[] = [ @@ -527,7 +528,7 @@ describe('mapLegacyConnectionsToCanvasConnections', () => { it('should handle edge cases with invalid data gracefully', () => { const legacyConnections: IConnections = { 'Node A': { - main: [ + [NodeConnectionType.Main]: [ [{ node: 'Nonexistent Node', type: NodeConnectionType.Main, index: 0 }], [{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }], ], @@ -599,6 +600,81 @@ describe('mapLegacyConnectionsToCanvasConnections', () => { }, ]); }); + + // @issue https://linear.app/n8n/issue/N8N-7880/cannot-load-some-templates + it('should handle null connections gracefully', () => { + const legacyConnections: IConnections = { + 'Node A': { + [NodeConnectionType.Main]: [ + null as unknown as IConnection[], + [{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }], + ], + }, + }; + const nodes: INodeUi[] = [ + { + id: '1', + name: 'Node A', + type: 'n8n-nodes-base.node', + typeVersion: 1, + position: [100, 100], + parameters: {}, + }, + { + id: '2', + name: 'Node B', + type: 'n8n-nodes-base.node', + typeVersion: 1, + position: [200, 200], + parameters: {}, + }, + ]; + + const result: CanvasConnection[] = mapLegacyConnectionsToCanvasConnections( + legacyConnections, + nodes, + ); + + const source = nodes[0].id; + const sourceHandle = createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Output, + type: NodeConnectionType.Main, + index: 1, + }); + const target = nodes[1].id; + const targetHandle = createCanvasConnectionHandleString({ + mode: CanvasConnectionMode.Input, + type: NodeConnectionType.Main, + index: 0, + }); + const id = createCanvasConnectionId({ + source, + target, + sourceHandle, + targetHandle, + }); + + expect(result).toEqual([ + { + id, + source, + target, + sourceHandle, + targetHandle, + data: { + fromNodeName: nodes[0].name, + source: { + index: 1, + type: NodeConnectionType.Main, + }, + target: { + index: 0, + type: NodeConnectionType.Main, + }, + }, + }, + ]); + }); }); describe('parseCanvasConnectionHandleString', () => { diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.ts b/packages/editor-ui/src/utils/canvasUtilsV2.ts index c4a49d8c94..c86e4a7f2c 100644 --- a/packages/editor-ui/src/utils/canvasUtilsV2.ts +++ b/packages/editor-ui/src/utils/canvasUtilsV2.ts @@ -20,8 +20,8 @@ export function mapLegacyConnectionsToCanvasConnections( fromConnectionTypes.forEach((fromConnectionType) => { const fromPorts = legacyConnections[fromNodeName][fromConnectionType]; - fromPorts.forEach((toPorts, fromIndex) => { - toPorts.forEach((toPort) => { + fromPorts?.forEach((toPorts, fromIndex) => { + toPorts?.forEach((toPort) => { const toId = nodes.find((node) => node.name === toPort.node)?.id ?? ''; const toConnectionType = toPort.type as NodeConnectionType; const toIndex = toPort.index; diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index c60b949449..ea569e5886 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -180,7 +180,8 @@ export class Workflow { if (!connections[sourceNode][type].hasOwnProperty(inputIndex)) { continue; } - for (connectionInfo of connections[sourceNode][type][inputIndex]) { + + for (connectionInfo of connections[sourceNode][type][inputIndex] ?? []) { if (!returnConnection.hasOwnProperty(connectionInfo.node)) { returnConnection[connectionInfo.node] = {}; } diff --git a/packages/workflow/test/Workflow.test.ts b/packages/workflow/test/Workflow.test.ts index bdd5fec8b9..ded592c9a6 100644 --- a/packages/workflow/test/Workflow.test.ts +++ b/packages/workflow/test/Workflow.test.ts @@ -1,6 +1,7 @@ import { mock } from 'jest-mock-extended'; import { NodeConnectionType } from '@/Interfaces'; +import type { IConnection } from '@/Interfaces'; import type { IBinaryKeyData, IConnections, @@ -2084,4 +2085,154 @@ describe('Workflow', () => { expect(triggerResponse.closeFunction).toHaveBeenCalled(); }); }); + + describe('__getConnectionsByDestination', () => { + it('should return empty object when there are no connections', () => { + const workflow = new Workflow({ + nodes: [], + connections: {}, + active: false, + nodeTypes: mock(), + }); + + const result = workflow.__getConnectionsByDestination({}); + + expect(result).toEqual({}); + }); + + it('should return connections by destination node', () => { + const connections: IConnections = { + Node1: { + [NodeConnectionType.Main]: [ + [ + { node: 'Node2', type: NodeConnectionType.Main, index: 0 }, + { node: 'Node3', type: NodeConnectionType.Main, index: 1 }, + ], + ], + }, + }; + const workflow = new Workflow({ + nodes: [], + connections, + active: false, + nodeTypes: mock(), + }); + const result = workflow.__getConnectionsByDestination(connections); + expect(result).toEqual({ + Node2: { + [NodeConnectionType.Main]: [[{ node: 'Node1', type: NodeConnectionType.Main, index: 0 }]], + }, + Node3: { + [NodeConnectionType.Main]: [ + [], + [{ node: 'Node1', type: NodeConnectionType.Main, index: 0 }], + ], + }, + }); + }); + + it('should handle multiple connection types', () => { + const connections: IConnections = { + Node1: { + [NodeConnectionType.Main]: [[{ node: 'Node2', type: NodeConnectionType.Main, index: 0 }]], + [NodeConnectionType.AiAgent]: [ + [{ node: 'Node3', type: NodeConnectionType.AiAgent, index: 0 }], + ], + }, + }; + + const workflow = new Workflow({ + nodes: [], + connections, + active: false, + nodeTypes: mock(), + }); + + const result = workflow.__getConnectionsByDestination(connections); + expect(result).toEqual({ + Node2: { + [NodeConnectionType.Main]: [[{ node: 'Node1', type: NodeConnectionType.Main, index: 0 }]], + }, + Node3: { + [NodeConnectionType.AiAgent]: [ + [{ node: 'Node1', type: NodeConnectionType.AiAgent, index: 0 }], + ], + }, + }); + }); + + it('should handle nodes with no connections', () => { + const connections: IConnections = { + Node1: { + [NodeConnectionType.Main]: [[]], + }, + }; + + const workflow = new Workflow({ + nodes: [], + connections, + active: false, + nodeTypes: mock(), + }); + + const result = workflow.__getConnectionsByDestination(connections); + expect(result).toEqual({}); + }); + + // @issue https://linear.app/n8n/issue/N8N-7880/cannot-load-some-templates + it('should handle nodes with null connections', () => { + const connections: IConnections = { + Node1: { + [NodeConnectionType.Main]: [ + null as unknown as IConnection[], + [{ node: 'Node2', type: NodeConnectionType.Main, index: 0 }], + ], + }, + }; + + const workflow = new Workflow({ + nodes: [], + connections, + active: false, + nodeTypes: mock(), + }); + + const result = workflow.__getConnectionsByDestination(connections); + expect(result).toEqual({ + Node2: { + [NodeConnectionType.Main]: [[{ node: 'Node1', type: NodeConnectionType.Main, index: 1 }]], + }, + }); + }); + + it('should handle nodes with multiple input connections', () => { + const connections: IConnections = { + Node1: { + [NodeConnectionType.Main]: [[{ node: 'Node2', type: NodeConnectionType.Main, index: 0 }]], + }, + Node3: { + [NodeConnectionType.Main]: [[{ node: 'Node2', type: NodeConnectionType.Main, index: 0 }]], + }, + }; + + const workflow = new Workflow({ + nodes: [], + connections, + active: false, + nodeTypes: mock(), + }); + + const result = workflow.__getConnectionsByDestination(connections); + expect(result).toEqual({ + Node2: { + [NodeConnectionType.Main]: [ + [ + { node: 'Node1', type: NodeConnectionType.Main, index: 0 }, + { node: 'Node3', type: NodeConnectionType.Main, index: 0 }, + ], + ], + }, + }); + }); + }); });