diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index 251db6e75d498..bc270482192dd 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -76,6 +76,10 @@ export function getCanvasNodes() { ); } +export function getCanvasNodeByName(nodeName: string) { + return getCanvasNodes().filter(`:contains(${nodeName})`); +} + export function getSaveButton() { return cy.getByTestId('workflow-save-button'); } @@ -194,3 +198,8 @@ export function pasteWorkflow(workflow: object) { export function clickZoomToFit() { getZoomToFitButton().click(); } + +export function deleteNode(name: string) { + getCanvasNodeByName(name).first().click(); + cy.get('body').type('{del}'); +} diff --git a/cypress/e2e/2929-ado-can-load-old-switch-node-workflows.cy.ts b/cypress/e2e/2929-ado-can-load-old-switch-node-workflows.cy.ts new file mode 100644 index 0000000000000..39edc54163cde --- /dev/null +++ b/cypress/e2e/2929-ado-can-load-old-switch-node-workflows.cy.ts @@ -0,0 +1,17 @@ +import { + deleteNode, + getCanvasNodes, + navigateToNewWorkflowPage, + pasteWorkflow, +} from '../composables/workflow'; +import Workflow from '../fixtures/Switch_node_with_null_connection.json'; + +describe('ADO-2929 can load Switch nodes', () => { + it('can load workflows with Switch nodes with null at connection index', () => { + navigateToNewWorkflowPage(); + pasteWorkflow(Workflow); + getCanvasNodes().should('have.length', 3); + deleteNode('Switch'); + getCanvasNodes().should('have.length', 2); + }); +}); diff --git a/cypress/fixtures/Switch_node_with_null_connection.json b/cypress/fixtures/Switch_node_with_null_connection.json new file mode 100644 index 0000000000000..325e097bd0756 --- /dev/null +++ b/cypress/fixtures/Switch_node_with_null_connection.json @@ -0,0 +1,85 @@ +{ + "nodes": [ + { + "parameters": {}, + "id": "418350b8-b402-4d3b-93ba-3794d36c1ad5", + "name": "When clicking \"Test workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [440, 380] + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "leftValue": "", + "rightValue": "", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + } + }, + {}, + {} + ] + }, + "options": {} + }, + "id": "b67ad46f-6b0d-4ff4-b2d2-dfbde44e287c", + "name": "Switch", + "type": "n8n-nodes-base.switch", + "typeVersion": 3, + "position": [660, 380] + }, + { + "parameters": { + "options": {} + }, + "id": "24731c11-e2a4-4854-81a6-277ce72e8a93", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [840, 480] + } + ], + "connections": { + "When clicking \"Test workflow\"": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + null, + null, + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/packages/cli/src/databases/migrations/common/1675940580449-PurgeInvalidWorkflowConnections.ts b/packages/cli/src/databases/migrations/common/1675940580449-PurgeInvalidWorkflowConnections.ts index 894e741344528..7ee46ca0d0936 100644 --- a/packages/cli/src/databases/migrations/common/1675940580449-PurgeInvalidWorkflowConnections.ts +++ b/packages/cli/src/databases/migrations/common/1675940580449-PurgeInvalidWorkflowConnections.ts @@ -38,7 +38,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements Irreversibl // It filters out all connections that are connected to a node that cannot receive input outputConnection.forEach((outputConnectionItem, outputConnectionItemIdx) => { - outputConnection[outputConnectionItemIdx] = outputConnectionItem.filter( + outputConnection[outputConnectionItemIdx] = (outputConnectionItem ?? []).filter( (outgoingConnections) => !nodesThatCannotReceiveInput.includes(outgoingConnections.node), ); diff --git a/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts b/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts index fafad308ad662..37113e4c40489 100644 --- a/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts +++ b/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts @@ -119,7 +119,7 @@ export class InstanceRiskReporter implements RiskReporter { node: WorkflowEntity['nodes'][number]; workflow: WorkflowEntity; }) { - const childNodeNames = workflow.connections[node.name]?.main[0].map((i) => i.node); + const childNodeNames = workflow.connections[node.name]?.main[0]?.map((i) => i.node); if (!childNodeNames) return false; diff --git a/packages/core/src/PartialExecutionUtils/DirectedGraph.ts b/packages/core/src/PartialExecutionUtils/DirectedGraph.ts index 481a73392d136..e8695743a1399 100644 --- a/packages/core/src/PartialExecutionUtils/DirectedGraph.ts +++ b/packages/core/src/PartialExecutionUtils/DirectedGraph.ts @@ -448,7 +448,7 @@ export class DirectedGraph { for (const [outputType, outputs] of Object.entries(iConnection)) { for (const [outputIndex, conns] of outputs.entries()) { - for (const conn of conns) { + for (const conn of conns ?? []) { // TODO: What's with the input type? const { node: toNodeName, type: _inputType, index: inputIndex } = conn; const to = workflow.getNode(toNodeName); diff --git a/packages/core/src/PartialExecutionUtils/__tests__/DirectedGraph.test.ts b/packages/core/src/PartialExecutionUtils/__tests__/DirectedGraph.test.ts index 9530ed2217c29..c8feb20e113ec 100644 --- a/packages/core/src/PartialExecutionUtils/__tests__/DirectedGraph.test.ts +++ b/packages/core/src/PartialExecutionUtils/__tests__/DirectedGraph.test.ts @@ -42,6 +42,28 @@ describe('DirectedGraph', () => { ); }); + // ┌─────┐ ┌─────┐──► null + // │node1├───►│node2| ┌─────┐ + // └─────┘ └─────┘──►│node3| + // └─────┘ + // + test('linear workflow with null connections', () => { + // ARRANGE + const node1 = createNodeData({ name: 'Node1' }); + const node2 = createNodeData({ name: 'Node2' }); + const node3 = createNodeData({ name: 'Node3' }); + + // ACT + const graph = new DirectedGraph() + .addNodes(node1, node2, node3) + .addConnections({ from: node1, to: node2 }, { from: node2, to: node3, outputIndex: 1 }); + + // ASSERT + expect(DirectedGraph.fromWorkflow(graph.toWorkflow({ ...defaultWorkflowParameter }))).toEqual( + graph, + ); + }); + describe('getChildren', () => { // ┌─────┐ ┌─────┐ ┌─────┐ // │node1├───►│node2├──►│node3│ diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 808bc17c9b521..515f58a657b9c 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -36,6 +36,7 @@ import type { CloseFunction, StartNodeData, NodeExecutionHint, + NodeInputConnections, } from 'n8n-workflow'; import { LoggerProxy as Logger, @@ -208,6 +209,9 @@ export class WorkflowExecute { // Get the data of the incoming connections incomingSourceData = { main: [] }; for (const connections of incomingNodeConnections.main) { + if (!connections) { + continue; + } for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) { connection = connections[inputIndex]; @@ -249,6 +253,9 @@ export class WorkflowExecute { incomingNodeConnections = workflow.connectionsByDestinationNode[destinationNode]; if (incomingNodeConnections !== undefined) { for (const connections of incomingNodeConnections.main) { + if (!connections) { + continue; + } for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) { connection = connections[inputIndex]; @@ -642,7 +649,7 @@ export class WorkflowExecute { } for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[ outputIndexParent - ]) { + ] ?? []) { checkOutputNodes.push(connectionDataCheck.node); } } @@ -661,7 +668,7 @@ export class WorkflowExecute { ) { for (const inputData of workflow.connectionsByDestinationNode[connectionData.node].main[ inputIndex - ]) { + ] ?? []) { if (inputData.node === parentNodeName) { // Is the node we come from so its data will be available for sure continue; @@ -681,7 +688,7 @@ export class WorkflowExecute { if ( !this.incomingConnectionIsEmpty( this.runExecutionData.resultData.runData, - workflow.connectionsByDestinationNode[inputData.node].main[0], + workflow.connectionsByDestinationNode[inputData.node].main[0] ?? [], runIndex, ) ) { @@ -770,7 +777,7 @@ export class WorkflowExecute { } else if ( this.incomingConnectionIsEmpty( this.runExecutionData.resultData.runData, - workflow.connectionsByDestinationNode[nodeToAdd].main[0], + workflow.connectionsByDestinationNode[nodeToAdd].main[0] ?? [], runIndex, ) ) { @@ -1066,7 +1073,7 @@ export class WorkflowExecute { if (workflow.connectionsByDestinationNode.hasOwnProperty(executionNode.name)) { // Check if the node has incoming connections if (workflow.connectionsByDestinationNode[executionNode.name].hasOwnProperty('main')) { - let inputConnections: IConnection[][]; + let inputConnections: NodeInputConnections; let connectionIndex: number; // eslint-disable-next-line prefer-const @@ -1586,7 +1593,7 @@ export class WorkflowExecute { // Iterate over all the different connections of this output for (connectionData of workflow.connectionsBySourceNode[executionNode.name].main[ outputIndex - ]) { + ] ?? []) { if (!workflow.nodes.hasOwnProperty(connectionData.node)) { throw new ApplicationError('Destination node not found', { extra: { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 073440d07c13c..dc358b416676b 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -7,6 +7,7 @@ "clean": "rimraf dist .turbo", "build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=8192\" vite build", "typecheck": "vue-tsc --noEmit", + "typecheck:watch": "vue-tsc --watch --noEmit", "dev": "pnpm serve", "lint": "eslint src --ext .js,.ts,.vue --quiet", "lintfix": "eslint src --ext .js,.ts,.vue --fix", diff --git a/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts b/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts index 4f09135b20aba..e30d4e4adbec6 100644 --- a/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts +++ b/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts @@ -256,7 +256,7 @@ export function useChatMessaging({ ]; if (!connectedMemoryInputs) return []; - const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0]; + const memoryConnection = (connectedMemoryInputs ?? []).find((i) => (i ?? []).length > 0)?.[0]; if (!memoryConnection) return []; diff --git a/packages/editor-ui/src/components/CodeNodeEditor/completions/jsonField.completions.ts b/packages/editor-ui/src/components/CodeNodeEditor/completions/jsonField.completions.ts index 7f2c890ca3540..ce587796fda39 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/completions/jsonField.completions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/completions/jsonField.completions.ts @@ -176,7 +176,7 @@ function useJsonFieldCompletions() { if (activeNode) { const workflow = workflowsStore.getCurrentWorkflow(); const input = workflow.connectionsByDestinationNode[activeNode.name]; - return input.main[0][0].node; + return input.main[0] ? input.main[0][0].node : null; } } catch (e) { console.error(e); diff --git a/packages/editor-ui/src/components/InputNodeSelect.vue b/packages/editor-ui/src/components/InputNodeSelect.vue index d0ec011e0778c..c219043ab9a47 100644 --- a/packages/editor-ui/src/components/InputNodeSelect.vue +++ b/packages/editor-ui/src/components/InputNodeSelect.vue @@ -86,7 +86,7 @@ function getMultipleNodesText(nodeName: string): string { props.workflow.connectionsByDestinationNode[activeNode.value.name].main || []; // Collect indexes of connected nodes const connectedInputIndexes = activeNodeConnections.reduce((acc: number[], node, index) => { - if (node[0] && node[0].node === nodeName) return [...acc, index]; + if (node?.[0] && node[0].node === nodeName) return [...acc, index]; return acc; }, []); diff --git a/packages/editor-ui/src/composables/useCanvasMapping.test.ts b/packages/editor-ui/src/composables/useCanvasMapping.test.ts index d573c205de92f..a37f836cca899 100644 --- a/packages/editor-ui/src/composables/useCanvasMapping.test.ts +++ b/packages/editor-ui/src/composables/useCanvasMapping.test.ts @@ -238,7 +238,7 @@ describe('useCanvasMapping', () => { expect( mappedNodes.value[0]?.data?.connections[CanvasConnectionMode.Output][ NodeConnectionType.Main - ][0][0], + ][0]?.[0], ).toEqual( expect.objectContaining({ node: setNode.name, @@ -253,7 +253,7 @@ describe('useCanvasMapping', () => { expect( mappedNodes.value[1]?.data?.connections[CanvasConnectionMode.Input][ NodeConnectionType.Main - ][0][0], + ][0]?.[0], ).toEqual( expect.objectContaining({ node: manualTriggerNode.name, diff --git a/packages/editor-ui/src/composables/useCanvasOperations.test.ts b/packages/editor-ui/src/composables/useCanvasOperations.test.ts index 6776dda3cdd29..5858bb6ca5622 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.test.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.test.ts @@ -802,6 +802,82 @@ describe('useCanvasOperations', () => { expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(nodes[1].id); expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id); }); + + it('should handle nodes with null connections for unconnected indexes', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + + nodeTypesStore.nodeTypes = { + [SET_NODE_TYPE]: { 1: mockNodeTypeDescription({ name: SET_NODE_TYPE }) }, + }; + + const nodes = [ + createTestNode({ + id: 'input', + type: SET_NODE_TYPE, + position: [10, 20], + name: 'Input Node', + }), + createTestNode({ + id: 'middle', + type: SET_NODE_TYPE, + position: [10, 20], + name: 'Middle Node', + }), + createTestNode({ + id: 'output', + type: SET_NODE_TYPE, + position: [10, 20], + name: 'Output Node', + }), + ]; + + workflowsStore.getNodeByName = vi + .fn() + .mockImplementation((name: string) => nodes.find((node) => node.name === name)); + + workflowsStore.workflow.nodes = nodes; + workflowsStore.workflow.connections = { + [nodes[0].name]: { + main: [ + null, + [ + { + node: nodes[1].name, + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + [nodes[1].name]: { + main: [ + // null here to simulate no connection at index + null, + [ + { + node: nodes[2].name, + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }; + + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); + workflowsStore.incomingConnectionsByNodeName.mockReturnValue({}); + + workflowsStore.getNodeById.mockReturnValue(nodes[1]); + + const { deleteNode } = useCanvasOperations({ router }); + deleteNode(nodes[1].id); + + expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id); + expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(nodes[1].id); + expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id); + }); }); describe('revertDeleteNode', () => { diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts index ec633493831b5..939d2132b8697 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.ts @@ -1149,11 +1149,9 @@ export function useCanvasOperations({ router }: { router: ReturnType - sourceConnections.filter((connection) => includeNodeNames.has(connection.node)), + (sourceConnections ?? []).filter((connection) => includeNodeNames.has(connection.node)), ); if (validConnections.length) { diff --git a/packages/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/editor-ui/src/composables/useWorkflowHelpers.ts index 23200cdc769d0..93089c12491a1 100644 --- a/packages/editor-ui/src/composables/useWorkflowHelpers.ts +++ b/packages/editor-ui/src/composables/useWorkflowHelpers.ts @@ -413,7 +413,7 @@ export function executeData( mainConnections: for (const mainConnections of workflow.connectionsByDestinationNode[ currentNode ].main) { - for (const connection of mainConnections) { + for (const connection of mainConnections ?? []) { if ( connection.type === NodeConnectionType.Main && connection.node === parentNodeName diff --git a/packages/editor-ui/src/stores/ndv.store.ts b/packages/editor-ui/src/stores/ndv.store.ts index 74cbd40d55dca..a22964f35a6a8 100644 --- a/packages/editor-ui/src/stores/ndv.store.ts +++ b/packages/editor-ui/src/stores/ndv.store.ts @@ -156,7 +156,7 @@ export const useNDVStore = defineStore(STORES.NDV, () => { if (!activeNodeConections || activeNodeConections.length < 2) return returnData; for (const [index, connection] of activeNodeConections.entries()) { - for (const node of connection) { + for (const node of connection ?? []) { if (!returnData[node.node]) { returnData[node.node] = []; } diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 0fead90bedb84..4b695d066aa5e 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -891,23 +891,28 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { let propertyName: keyof IConnection; let connectionExists = false; - connectionLoop: for (const existingConnection of workflow.value.connections[sourceData.node][ - sourceData.type - ][sourceData.index]) { - for (propertyName of checkProperties) { - if (existingConnection[propertyName] !== destinationData[propertyName]) { - continue connectionLoop; + const nodeConnections = workflow.value.connections[sourceData.node][sourceData.type]; + const connectionsToCheck = nodeConnections[sourceData.index]; + + if (connectionsToCheck) { + connectionLoop: for (const existingConnection of connectionsToCheck) { + for (propertyName of checkProperties) { + if (existingConnection[propertyName] !== destinationData[propertyName]) { + continue connectionLoop; + } } + connectionExists = true; + break; } - connectionExists = true; - break; } // Add the new connection if it does not exist already if (!connectionExists) { - workflow.value.connections[sourceData.node][sourceData.type][sourceData.index].push( - destinationData, - ); + nodeConnections[sourceData.index] = nodeConnections[sourceData.index] ?? []; + const connections = nodeConnections[sourceData.index]; + if (connections) { + connections.push(destinationData); + } } } @@ -934,6 +939,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { const connections = workflow.value.connections[sourceData.node][sourceData.type][sourceData.index]; + if (!connections) { + return; + } + for (const index in connections) { if ( connections[index].node === destinationData.node && @@ -979,23 +988,19 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { for (type of Object.keys(workflow.value.connections[sourceNode])) { for (sourceIndex of Object.keys(workflow.value.connections[sourceNode][type])) { indexesToRemove.length = 0; - for (connectionIndex of Object.keys( - workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)], - )) { - connectionData = - workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)][ - parseInt(connectionIndex, 10) - ]; - if (connectionData.node === node.name) { - indexesToRemove.push(connectionIndex); + const connectionsToRemove = + workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)]; + if (connectionsToRemove) { + for (connectionIndex of Object.keys(connectionsToRemove)) { + connectionData = connectionsToRemove[parseInt(connectionIndex, 10)]; + if (connectionData.node === node.name) { + indexesToRemove.push(connectionIndex); + } } + indexesToRemove.forEach((index) => { + connectionsToRemove.splice(parseInt(index, 10), 1); + }); } - indexesToRemove.forEach((index) => { - workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)].splice( - parseInt(index, 10), - 1, - ); - }); } } } diff --git a/packages/editor-ui/src/utils/nodeSettingsUtils.test.ts b/packages/editor-ui/src/utils/nodeSettingsUtils.test.ts index a5b122b89335e..9968bc6984539 100644 --- a/packages/editor-ui/src/utils/nodeSettingsUtils.test.ts +++ b/packages/editor-ui/src/utils/nodeSettingsUtils.test.ts @@ -56,7 +56,7 @@ describe('updateDynamicConnections', () => { const updatedConnections = updateDynamicConnections(node, connections, parameterData); expect(updatedConnections?.TestNode.main).toHaveLength(2); - expect(updatedConnections?.TestNode.main[1][0].node).toEqual('Node3'); + expect(updatedConnections?.TestNode.main[1]?.[0].node).toEqual('Node3'); }); it('should handle fallbackOutput === "extra" and all rules removed', () => { @@ -82,7 +82,7 @@ describe('updateDynamicConnections', () => { const updatedConnections = updateDynamicConnections(node, connections, parameterData); expect(updatedConnections?.TestNode.main).toHaveLength(1); - expect(updatedConnections?.TestNode.main[0][0].node).toEqual('Node3'); + expect(updatedConnections?.TestNode.main[0]?.[0].node).toEqual('Node3'); }); it('should add a new connection when a rule is added', () => { @@ -137,7 +137,7 @@ describe('updateDynamicConnections', () => { expect(updatedConnections?.TestNode.main).toHaveLength(4); expect(updatedConnections?.TestNode.main[2]).toEqual([]); - expect(updatedConnections?.TestNode.main[3][0].node).toEqual('Node3'); + expect(updatedConnections?.TestNode.main[3]?.[0].node).toEqual('Node3'); }); it('should return null if no conditions are met', () => { diff --git a/packages/editor-ui/src/utils/nodeSettingsUtils.ts b/packages/editor-ui/src/utils/nodeSettingsUtils.ts index f4966dfb4f7ca..09f1c4f2db691 100644 --- a/packages/editor-ui/src/utils/nodeSettingsUtils.ts +++ b/packages/editor-ui/src/utils/nodeSettingsUtils.ts @@ -77,7 +77,7 @@ export function updateDynamicConnections( } } else if (parameterData.name === 'parameters.rules.values') { const curentRulesvalues = (node.parameters?.rules as { values: IDataObject[] })?.values; - let lastConnection: IConnection[] | undefined = undefined; + let lastConnection: IConnection[] | null | undefined = undefined; if ( fallbackOutput === 'extra' && connections[node.name].main.length === curentRulesvalues.length + 1 diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index abc895039242c..f802cc3cd1cb8 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -1394,7 +1394,11 @@ export default defineComponent({ lastSelectedNode.name, ); - if (connections.main === undefined || connections.main.length === 0) { + if ( + connections.main === undefined || + connections.main.length === 0 || + !connections.main[0] + ) { return; } @@ -1428,7 +1432,11 @@ export default defineComponent({ const connections = workflow.connectionsByDestinationNode[lastSelectedNode.name]; - if (connections.main === undefined || connections.main.length === 0) { + if ( + connections.main === undefined || + connections.main.length === 0 || + !connections.main[0] + ) { return; } @@ -1460,7 +1468,11 @@ export default defineComponent({ return; } - const parentNode = connections.main[0][0].node; + const parentNode = connections.main[0]?.[0].node; + if (!parentNode) { + return; + } + const connectionsParent = this.workflowsStore.outgoingConnectionsByNodeName(parentNode); if (!Array.isArray(connectionsParent.main) || !connectionsParent.main.length) { @@ -1472,7 +1484,7 @@ export default defineComponent({ let lastCheckedNodePosition = e.key === 'ArrowUp' ? -99999999 : 99999999; let nextSelectNode: string | null = null; for (const ouputConnections of connectionsParent.main) { - for (const ouputConnection of ouputConnections) { + for (const ouputConnection of ouputConnections ?? []) { if (ouputConnection.node === lastSelectedNode.name) { // Ignore current node continue; @@ -3877,13 +3889,10 @@ export default defineComponent({ sourceIndex++ ) { const nodeSourceConnections = []; - if (currentConnections[sourceNode][type][sourceIndex]) { - for ( - connectionIndex = 0; - connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; - connectionIndex++ - ) { - connectionData = currentConnections[sourceNode][type][sourceIndex][connectionIndex]; + const connections = currentConnections[sourceNode][type][sourceIndex]; + if (connections) { + for (connectionIndex = 0; connectionIndex < connections.length; connectionIndex++) { + connectionData = connections[connectionIndex]; if (!createNodeNames.includes(connectionData.node)) { // Node does not get created so skip input connection continue; @@ -4013,14 +4022,17 @@ export default defineComponent({ for (type of Object.keys(connections)) { for (sourceIndex = 0; sourceIndex < connections[type].length; sourceIndex++) { connectionToKeep = []; - for ( - connectionIndex = 0; - connectionIndex < connections[type][sourceIndex].length; - connectionIndex++ - ) { - connectionData = connections[type][sourceIndex][connectionIndex]; - if (exportNodeNames.indexOf(connectionData.node) !== -1) { - connectionToKeep.push(connectionData); + const connectionsToCheck = connections[type][sourceIndex]; + if (connectionsToCheck) { + for ( + connectionIndex = 0; + connectionIndex < connectionsToCheck.length; + connectionIndex++ + ) { + connectionData = connectionsToCheck[connectionIndex]; + if (exportNodeNames.indexOf(connectionData.node) !== -1) { + connectionToKeep.push(connectionData); + } } } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index fbaea8f631436..e193e96eb94f5 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -364,7 +364,8 @@ export interface ICredentialDataDecryptedObject { // First array index: The output/input-index (if node has multiple inputs/outputs of the same type) // Second array index: The different connections (if one node is connected to multiple nodes) -export type NodeInputConnections = IConnection[][]; +// Any index can be null, for example in a switch node with multiple indexes some of which are not connected +export type NodeInputConnections = Array; export interface INodeConnection { sourceIndex: number; diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 9fb5ef8beb50f..b2eb597e2ed27 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -194,7 +194,7 @@ export class Workflow { returnConnection[connectionInfo.node][connectionInfo.type].push([]); } - returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index].push({ + returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index]?.push({ node: sourceNode, type, index: parseInt(inputIndex, 10), @@ -551,18 +551,18 @@ export class Workflow { let type: string; let sourceIndex: string; let connectionIndex: string; - let connectionData: IConnection; + let connectionData: IConnection | undefined; for (sourceNode of Object.keys(this.connectionsBySourceNode)) { for (type of Object.keys(this.connectionsBySourceNode[sourceNode])) { for (sourceIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type])) { for (connectionIndex of Object.keys( - this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)], + this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)] || [], )) { connectionData = - this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)][ + this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)]?.[ parseInt(connectionIndex, 10) ]; - if (connectionData.node === currentName) { + if (connectionData?.node === currentName) { connectionData.node = newName; } } @@ -615,7 +615,7 @@ export class Workflow { const returnNodes: string[] = []; let addNodes: string[]; - let connectionsByIndex: IConnection[]; + let connectionsByIndex: IConnection[] | null; for ( let connectionIndex = 0; connectionIndex < this.connectionsByDestinationNode[nodeName][type].length; @@ -627,7 +627,7 @@ export class Workflow { } connectionsByIndex = this.connectionsByDestinationNode[nodeName][type][connectionIndex]; // eslint-disable-next-line @typescript-eslint/no-loop-func - connectionsByIndex.forEach((connection) => { + connectionsByIndex?.forEach((connection) => { if (checkedNodes.includes(connection.node)) { // Node got checked already before return; @@ -742,7 +742,7 @@ export class Workflow { checkedNodes.push(nodeName); connections[nodeName][type].forEach((connectionsByIndex) => { - connectionsByIndex.forEach((connection) => { + connectionsByIndex?.forEach((connection) => { if (checkedNodes.includes(connection.node)) { // Node got checked already before return; @@ -839,7 +839,7 @@ export class Workflow { } connections[curr.name][type].forEach((connectionsByIndex) => { - connectionsByIndex.forEach((connection) => { + connectionsByIndex?.forEach((connection) => { queue.push({ name: connection.node, indicies: [connection.index], @@ -943,6 +943,10 @@ export class Workflow { let outputIndex: INodeConnection | undefined; for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) { + if (!connectionsByIndex) { + continue; + } + for ( let destinationIndex = 0; destinationIndex < connectionsByIndex.length; diff --git a/packages/workflow/test/TelemetryHelpers.test.ts b/packages/workflow/test/TelemetryHelpers.test.ts index bedf12f23127c..526b4797f6c7e 100644 --- a/packages/workflow/test/TelemetryHelpers.test.ts +++ b/packages/workflow/test/TelemetryHelpers.test.ts @@ -990,11 +990,8 @@ function alphanumericId() { const chooseRandomly = (array: T[]) => array[randomInt(array.length)]; -function generateTestWorkflowAndRunData(): { workflow: IWorkflowBase; runData: IRunData } { - const workflow: IWorkflowBase = { - meta: { - instanceId: 'a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0', - }, +function generateTestWorkflowAndRunData(): { workflow: Partial; runData: IRunData } { + const workflow: Partial = { nodes: [ { parameters: {},