fix: Load workflows with unconnected Switch outputs (#12020)

This commit is contained in:
Mutasem Aldmour
2024-12-04 13:44:25 +01:00
committed by GitHub
parent bd693162b8
commit abc851c0cf
24 changed files with 324 additions and 89 deletions

View File

@@ -76,6 +76,10 @@ export function getCanvasNodes() {
); );
} }
export function getCanvasNodeByName(nodeName: string) {
return getCanvasNodes().filter(`:contains(${nodeName})`);
}
export function getSaveButton() { export function getSaveButton() {
return cy.getByTestId('workflow-save-button'); return cy.getByTestId('workflow-save-button');
} }
@@ -194,3 +198,8 @@ export function pasteWorkflow(workflow: object) {
export function clickZoomToFit() { export function clickZoomToFit() {
getZoomToFitButton().click(); getZoomToFitButton().click();
} }
export function deleteNode(name: string) {
getCanvasNodeByName(name).first().click();
cy.get('body').type('{del}');
}

View File

@@ -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);
});
});

View File

@@ -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": {}
}

View File

@@ -38,7 +38,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements Irreversibl
// It filters out all connections that are connected to a node that cannot receive input // It filters out all connections that are connected to a node that cannot receive input
outputConnection.forEach((outputConnectionItem, outputConnectionItemIdx) => { outputConnection.forEach((outputConnectionItem, outputConnectionItemIdx) => {
outputConnection[outputConnectionItemIdx] = outputConnectionItem.filter( outputConnection[outputConnectionItemIdx] = (outputConnectionItem ?? []).filter(
(outgoingConnections) => (outgoingConnections) =>
!nodesThatCannotReceiveInput.includes(outgoingConnections.node), !nodesThatCannotReceiveInput.includes(outgoingConnections.node),
); );

View File

@@ -119,7 +119,7 @@ export class InstanceRiskReporter implements RiskReporter {
node: WorkflowEntity['nodes'][number]; node: WorkflowEntity['nodes'][number];
workflow: WorkflowEntity; 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; if (!childNodeNames) return false;

View File

@@ -448,7 +448,7 @@ export class DirectedGraph {
for (const [outputType, outputs] of Object.entries(iConnection)) { for (const [outputType, outputs] of Object.entries(iConnection)) {
for (const [outputIndex, conns] of outputs.entries()) { for (const [outputIndex, conns] of outputs.entries()) {
for (const conn of conns) { for (const conn of conns ?? []) {
// TODO: What's with the input type? // TODO: What's with the input type?
const { node: toNodeName, type: _inputType, index: inputIndex } = conn; const { node: toNodeName, type: _inputType, index: inputIndex } = conn;
const to = workflow.getNode(toNodeName); const to = workflow.getNode(toNodeName);

View File

@@ -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', () => { describe('getChildren', () => {
// ┌─────┐ ┌─────┐ ┌─────┐ // ┌─────┐ ┌─────┐ ┌─────┐
// │node1├───►│node2├──►│node3│ // │node1├───►│node2├──►│node3│

View File

@@ -36,6 +36,7 @@ import type {
CloseFunction, CloseFunction,
StartNodeData, StartNodeData,
NodeExecutionHint, NodeExecutionHint,
NodeInputConnections,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
LoggerProxy as Logger, LoggerProxy as Logger,
@@ -208,6 +209,9 @@ export class WorkflowExecute {
// Get the data of the incoming connections // Get the data of the incoming connections
incomingSourceData = { main: [] }; incomingSourceData = { main: [] };
for (const connections of incomingNodeConnections.main) { for (const connections of incomingNodeConnections.main) {
if (!connections) {
continue;
}
for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) { for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
connection = connections[inputIndex]; connection = connections[inputIndex];
@@ -249,6 +253,9 @@ export class WorkflowExecute {
incomingNodeConnections = workflow.connectionsByDestinationNode[destinationNode]; incomingNodeConnections = workflow.connectionsByDestinationNode[destinationNode];
if (incomingNodeConnections !== undefined) { if (incomingNodeConnections !== undefined) {
for (const connections of incomingNodeConnections.main) { for (const connections of incomingNodeConnections.main) {
if (!connections) {
continue;
}
for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) { for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
connection = connections[inputIndex]; connection = connections[inputIndex];
@@ -642,7 +649,7 @@ export class WorkflowExecute {
} }
for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[ for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[
outputIndexParent outputIndexParent
]) { ] ?? []) {
checkOutputNodes.push(connectionDataCheck.node); checkOutputNodes.push(connectionDataCheck.node);
} }
} }
@@ -661,7 +668,7 @@ export class WorkflowExecute {
) { ) {
for (const inputData of workflow.connectionsByDestinationNode[connectionData.node].main[ for (const inputData of workflow.connectionsByDestinationNode[connectionData.node].main[
inputIndex inputIndex
]) { ] ?? []) {
if (inputData.node === parentNodeName) { if (inputData.node === parentNodeName) {
// Is the node we come from so its data will be available for sure // Is the node we come from so its data will be available for sure
continue; continue;
@@ -681,7 +688,7 @@ export class WorkflowExecute {
if ( if (
!this.incomingConnectionIsEmpty( !this.incomingConnectionIsEmpty(
this.runExecutionData.resultData.runData, this.runExecutionData.resultData.runData,
workflow.connectionsByDestinationNode[inputData.node].main[0], workflow.connectionsByDestinationNode[inputData.node].main[0] ?? [],
runIndex, runIndex,
) )
) { ) {
@@ -770,7 +777,7 @@ export class WorkflowExecute {
} else if ( } else if (
this.incomingConnectionIsEmpty( this.incomingConnectionIsEmpty(
this.runExecutionData.resultData.runData, this.runExecutionData.resultData.runData,
workflow.connectionsByDestinationNode[nodeToAdd].main[0], workflow.connectionsByDestinationNode[nodeToAdd].main[0] ?? [],
runIndex, runIndex,
) )
) { ) {
@@ -1066,7 +1073,7 @@ export class WorkflowExecute {
if (workflow.connectionsByDestinationNode.hasOwnProperty(executionNode.name)) { if (workflow.connectionsByDestinationNode.hasOwnProperty(executionNode.name)) {
// Check if the node has incoming connections // Check if the node has incoming connections
if (workflow.connectionsByDestinationNode[executionNode.name].hasOwnProperty('main')) { if (workflow.connectionsByDestinationNode[executionNode.name].hasOwnProperty('main')) {
let inputConnections: IConnection[][]; let inputConnections: NodeInputConnections;
let connectionIndex: number; let connectionIndex: number;
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
@@ -1586,7 +1593,7 @@ export class WorkflowExecute {
// Iterate over all the different connections of this output // Iterate over all the different connections of this output
for (connectionData of workflow.connectionsBySourceNode[executionNode.name].main[ for (connectionData of workflow.connectionsBySourceNode[executionNode.name].main[
outputIndex outputIndex
]) { ] ?? []) {
if (!workflow.nodes.hasOwnProperty(connectionData.node)) { if (!workflow.nodes.hasOwnProperty(connectionData.node)) {
throw new ApplicationError('Destination node not found', { throw new ApplicationError('Destination node not found', {
extra: { extra: {

View File

@@ -7,6 +7,7 @@
"clean": "rimraf dist .turbo", "clean": "rimraf dist .turbo",
"build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=8192\" vite build", "build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"typecheck:watch": "vue-tsc --watch --noEmit",
"dev": "pnpm serve", "dev": "pnpm serve",
"lint": "eslint src --ext .js,.ts,.vue --quiet", "lint": "eslint src --ext .js,.ts,.vue --quiet",
"lintfix": "eslint src --ext .js,.ts,.vue --fix", "lintfix": "eslint src --ext .js,.ts,.vue --fix",

View File

@@ -256,7 +256,7 @@ export function useChatMessaging({
]; ];
if (!connectedMemoryInputs) return []; if (!connectedMemoryInputs) return [];
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0]; const memoryConnection = (connectedMemoryInputs ?? []).find((i) => (i ?? []).length > 0)?.[0];
if (!memoryConnection) return []; if (!memoryConnection) return [];

View File

@@ -176,7 +176,7 @@ function useJsonFieldCompletions() {
if (activeNode) { if (activeNode) {
const workflow = workflowsStore.getCurrentWorkflow(); const workflow = workflowsStore.getCurrentWorkflow();
const input = workflow.connectionsByDestinationNode[activeNode.name]; const input = workflow.connectionsByDestinationNode[activeNode.name];
return input.main[0][0].node; return input.main[0] ? input.main[0][0].node : null;
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@@ -86,7 +86,7 @@ function getMultipleNodesText(nodeName: string): string {
props.workflow.connectionsByDestinationNode[activeNode.value.name].main || []; props.workflow.connectionsByDestinationNode[activeNode.value.name].main || [];
// Collect indexes of connected nodes // Collect indexes of connected nodes
const connectedInputIndexes = activeNodeConnections.reduce((acc: number[], node, index) => { 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; return acc;
}, []); }, []);

View File

@@ -238,7 +238,7 @@ describe('useCanvasMapping', () => {
expect( expect(
mappedNodes.value[0]?.data?.connections[CanvasConnectionMode.Output][ mappedNodes.value[0]?.data?.connections[CanvasConnectionMode.Output][
NodeConnectionType.Main NodeConnectionType.Main
][0][0], ][0]?.[0],
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
node: setNode.name, node: setNode.name,
@@ -253,7 +253,7 @@ describe('useCanvasMapping', () => {
expect( expect(
mappedNodes.value[1]?.data?.connections[CanvasConnectionMode.Input][ mappedNodes.value[1]?.data?.connections[CanvasConnectionMode.Input][
NodeConnectionType.Main NodeConnectionType.Main
][0][0], ][0]?.[0],
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
node: manualTriggerNode.name, node: manualTriggerNode.name,

View File

@@ -802,6 +802,82 @@ describe('useCanvasOperations', () => {
expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(nodes[1].id); expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(nodes[1].id);
expect(workflowsStore.removeNodeById).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', () => { describe('revertDeleteNode', () => {

View File

@@ -1149,11 +1149,9 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
for (const type of Object.keys(connections[nodeName])) { for (const type of Object.keys(connections[nodeName])) {
for (const index of Object.keys(connections[nodeName][type])) { for (const index of Object.keys(connections[nodeName][type])) {
for (const connectionIndex of Object.keys( const connectionsToDelete = connections[nodeName][type][parseInt(index, 10)] ?? [];
connections[nodeName][type][parseInt(index, 10)], for (const connectionIndex of Object.keys(connectionsToDelete)) {
)) { const connectionData = connectionsToDelete[parseInt(connectionIndex, 10)];
const connectionData =
connections[nodeName][type][parseInt(index, 10)][parseInt(connectionIndex, 10)];
if (!connectionData) { if (!connectionData) {
continue; continue;
} }
@@ -1490,13 +1488,14 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
sourceIndex++ sourceIndex++
) { ) {
const nodeSourceConnections = []; const nodeSourceConnections = [];
if (currentConnections[sourceNode][type][sourceIndex]) { const connectionsToCheck = currentConnections[sourceNode][type][sourceIndex];
if (connectionsToCheck) {
for ( for (
connectionIndex = 0; connectionIndex = 0;
connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionIndex < connectionsToCheck.length;
connectionIndex++ connectionIndex++
) { ) {
connectionData = currentConnections[sourceNode][type][sourceIndex][connectionIndex]; connectionData = connectionsToCheck[connectionIndex];
if (!createNodeNames.includes(connectionData.node)) { if (!createNodeNames.includes(connectionData.node)) {
// Node does not get created so skip input connection // Node does not get created so skip input connection
continue; continue;
@@ -1814,7 +1813,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
for (const [type, typeConnections] of Object.entries(connections)) { for (const [type, typeConnections] of Object.entries(connections)) {
const validConnections = typeConnections.map((sourceConnections) => const validConnections = typeConnections.map((sourceConnections) =>
sourceConnections.filter((connection) => includeNodeNames.has(connection.node)), (sourceConnections ?? []).filter((connection) => includeNodeNames.has(connection.node)),
); );
if (validConnections.length) { if (validConnections.length) {

View File

@@ -413,7 +413,7 @@ export function executeData(
mainConnections: for (const mainConnections of workflow.connectionsByDestinationNode[ mainConnections: for (const mainConnections of workflow.connectionsByDestinationNode[
currentNode currentNode
].main) { ].main) {
for (const connection of mainConnections) { for (const connection of mainConnections ?? []) {
if ( if (
connection.type === NodeConnectionType.Main && connection.type === NodeConnectionType.Main &&
connection.node === parentNodeName connection.node === parentNodeName

View File

@@ -156,7 +156,7 @@ export const useNDVStore = defineStore(STORES.NDV, () => {
if (!activeNodeConections || activeNodeConections.length < 2) return returnData; if (!activeNodeConections || activeNodeConections.length < 2) return returnData;
for (const [index, connection] of activeNodeConections.entries()) { for (const [index, connection] of activeNodeConections.entries()) {
for (const node of connection) { for (const node of connection ?? []) {
if (!returnData[node.node]) { if (!returnData[node.node]) {
returnData[node.node] = []; returnData[node.node] = [];
} }

View File

@@ -891,23 +891,28 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
let propertyName: keyof IConnection; let propertyName: keyof IConnection;
let connectionExists = false; let connectionExists = false;
connectionLoop: for (const existingConnection of workflow.value.connections[sourceData.node][ const nodeConnections = workflow.value.connections[sourceData.node][sourceData.type];
sourceData.type const connectionsToCheck = nodeConnections[sourceData.index];
][sourceData.index]) {
for (propertyName of checkProperties) { if (connectionsToCheck) {
if (existingConnection[propertyName] !== destinationData[propertyName]) { connectionLoop: for (const existingConnection of connectionsToCheck) {
continue connectionLoop; 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 // Add the new connection if it does not exist already
if (!connectionExists) { if (!connectionExists) {
workflow.value.connections[sourceData.node][sourceData.type][sourceData.index].push( nodeConnections[sourceData.index] = nodeConnections[sourceData.index] ?? [];
destinationData, const connections = nodeConnections[sourceData.index];
); if (connections) {
connections.push(destinationData);
}
} }
} }
@@ -934,6 +939,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const connections = const connections =
workflow.value.connections[sourceData.node][sourceData.type][sourceData.index]; workflow.value.connections[sourceData.node][sourceData.type][sourceData.index];
if (!connections) {
return;
}
for (const index in connections) { for (const index in connections) {
if ( if (
connections[index].node === destinationData.node && 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 (type of Object.keys(workflow.value.connections[sourceNode])) {
for (sourceIndex of Object.keys(workflow.value.connections[sourceNode][type])) { for (sourceIndex of Object.keys(workflow.value.connections[sourceNode][type])) {
indexesToRemove.length = 0; indexesToRemove.length = 0;
for (connectionIndex of Object.keys( const connectionsToRemove =
workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)], workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)];
)) { if (connectionsToRemove) {
connectionData = for (connectionIndex of Object.keys(connectionsToRemove)) {
workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)][ connectionData = connectionsToRemove[parseInt(connectionIndex, 10)];
parseInt(connectionIndex, 10) if (connectionData.node === node.name) {
]; indexesToRemove.push(connectionIndex);
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,
);
});
} }
} }
} }

View File

@@ -56,7 +56,7 @@ describe('updateDynamicConnections', () => {
const updatedConnections = updateDynamicConnections(node, connections, parameterData); const updatedConnections = updateDynamicConnections(node, connections, parameterData);
expect(updatedConnections?.TestNode.main).toHaveLength(2); 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', () => { it('should handle fallbackOutput === "extra" and all rules removed', () => {
@@ -82,7 +82,7 @@ describe('updateDynamicConnections', () => {
const updatedConnections = updateDynamicConnections(node, connections, parameterData); const updatedConnections = updateDynamicConnections(node, connections, parameterData);
expect(updatedConnections?.TestNode.main).toHaveLength(1); 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', () => { 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).toHaveLength(4);
expect(updatedConnections?.TestNode.main[2]).toEqual([]); 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', () => { it('should return null if no conditions are met', () => {

View File

@@ -77,7 +77,7 @@ export function updateDynamicConnections(
} }
} else if (parameterData.name === 'parameters.rules.values') { } else if (parameterData.name === 'parameters.rules.values') {
const curentRulesvalues = (node.parameters?.rules as { values: IDataObject[] })?.values; const curentRulesvalues = (node.parameters?.rules as { values: IDataObject[] })?.values;
let lastConnection: IConnection[] | undefined = undefined; let lastConnection: IConnection[] | null | undefined = undefined;
if ( if (
fallbackOutput === 'extra' && fallbackOutput === 'extra' &&
connections[node.name].main.length === curentRulesvalues.length + 1 connections[node.name].main.length === curentRulesvalues.length + 1

View File

@@ -1394,7 +1394,11 @@ export default defineComponent({
lastSelectedNode.name, lastSelectedNode.name,
); );
if (connections.main === undefined || connections.main.length === 0) { if (
connections.main === undefined ||
connections.main.length === 0 ||
!connections.main[0]
) {
return; return;
} }
@@ -1428,7 +1432,11 @@ export default defineComponent({
const connections = workflow.connectionsByDestinationNode[lastSelectedNode.name]; 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; return;
} }
@@ -1460,7 +1468,11 @@ export default defineComponent({
return; return;
} }
const parentNode = connections.main[0][0].node; const parentNode = connections.main[0]?.[0].node;
if (!parentNode) {
return;
}
const connectionsParent = this.workflowsStore.outgoingConnectionsByNodeName(parentNode); const connectionsParent = this.workflowsStore.outgoingConnectionsByNodeName(parentNode);
if (!Array.isArray(connectionsParent.main) || !connectionsParent.main.length) { if (!Array.isArray(connectionsParent.main) || !connectionsParent.main.length) {
@@ -1472,7 +1484,7 @@ export default defineComponent({
let lastCheckedNodePosition = e.key === 'ArrowUp' ? -99999999 : 99999999; let lastCheckedNodePosition = e.key === 'ArrowUp' ? -99999999 : 99999999;
let nextSelectNode: string | null = null; let nextSelectNode: string | null = null;
for (const ouputConnections of connectionsParent.main) { for (const ouputConnections of connectionsParent.main) {
for (const ouputConnection of ouputConnections) { for (const ouputConnection of ouputConnections ?? []) {
if (ouputConnection.node === lastSelectedNode.name) { if (ouputConnection.node === lastSelectedNode.name) {
// Ignore current node // Ignore current node
continue; continue;
@@ -3877,13 +3889,10 @@ export default defineComponent({
sourceIndex++ sourceIndex++
) { ) {
const nodeSourceConnections = []; const nodeSourceConnections = [];
if (currentConnections[sourceNode][type][sourceIndex]) { const connections = currentConnections[sourceNode][type][sourceIndex];
for ( if (connections) {
connectionIndex = 0; for (connectionIndex = 0; connectionIndex < connections.length; connectionIndex++) {
connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionData = connections[connectionIndex];
connectionIndex++
) {
connectionData = currentConnections[sourceNode][type][sourceIndex][connectionIndex];
if (!createNodeNames.includes(connectionData.node)) { if (!createNodeNames.includes(connectionData.node)) {
// Node does not get created so skip input connection // Node does not get created so skip input connection
continue; continue;
@@ -4013,14 +4022,17 @@ export default defineComponent({
for (type of Object.keys(connections)) { for (type of Object.keys(connections)) {
for (sourceIndex = 0; sourceIndex < connections[type].length; sourceIndex++) { for (sourceIndex = 0; sourceIndex < connections[type].length; sourceIndex++) {
connectionToKeep = []; connectionToKeep = [];
for ( const connectionsToCheck = connections[type][sourceIndex];
connectionIndex = 0; if (connectionsToCheck) {
connectionIndex < connections[type][sourceIndex].length; for (
connectionIndex++ connectionIndex = 0;
) { connectionIndex < connectionsToCheck.length;
connectionData = connections[type][sourceIndex][connectionIndex]; connectionIndex++
if (exportNodeNames.indexOf(connectionData.node) !== -1) { ) {
connectionToKeep.push(connectionData); connectionData = connectionsToCheck[connectionIndex];
if (exportNodeNames.indexOf(connectionData.node) !== -1) {
connectionToKeep.push(connectionData);
}
} }
} }

View File

@@ -364,7 +364,8 @@ export interface ICredentialDataDecryptedObject {
// First array index: The output/input-index (if node has multiple inputs/outputs of the same type) // 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) // 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<IConnection[] | null>;
export interface INodeConnection { export interface INodeConnection {
sourceIndex: number; sourceIndex: number;

View File

@@ -194,7 +194,7 @@ export class Workflow {
returnConnection[connectionInfo.node][connectionInfo.type].push([]); returnConnection[connectionInfo.node][connectionInfo.type].push([]);
} }
returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index].push({ returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index]?.push({
node: sourceNode, node: sourceNode,
type, type,
index: parseInt(inputIndex, 10), index: parseInt(inputIndex, 10),
@@ -551,18 +551,18 @@ export class Workflow {
let type: string; let type: string;
let sourceIndex: string; let sourceIndex: string;
let connectionIndex: string; let connectionIndex: string;
let connectionData: IConnection; let connectionData: IConnection | undefined;
for (sourceNode of Object.keys(this.connectionsBySourceNode)) { for (sourceNode of Object.keys(this.connectionsBySourceNode)) {
for (type of Object.keys(this.connectionsBySourceNode[sourceNode])) { for (type of Object.keys(this.connectionsBySourceNode[sourceNode])) {
for (sourceIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type])) { for (sourceIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type])) {
for (connectionIndex of Object.keys( for (connectionIndex of Object.keys(
this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)], this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)] || [],
)) { )) {
connectionData = connectionData =
this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)][ this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)]?.[
parseInt(connectionIndex, 10) parseInt(connectionIndex, 10)
]; ];
if (connectionData.node === currentName) { if (connectionData?.node === currentName) {
connectionData.node = newName; connectionData.node = newName;
} }
} }
@@ -615,7 +615,7 @@ export class Workflow {
const returnNodes: string[] = []; const returnNodes: string[] = [];
let addNodes: string[]; let addNodes: string[];
let connectionsByIndex: IConnection[]; let connectionsByIndex: IConnection[] | null;
for ( for (
let connectionIndex = 0; let connectionIndex = 0;
connectionIndex < this.connectionsByDestinationNode[nodeName][type].length; connectionIndex < this.connectionsByDestinationNode[nodeName][type].length;
@@ -627,7 +627,7 @@ export class Workflow {
} }
connectionsByIndex = this.connectionsByDestinationNode[nodeName][type][connectionIndex]; connectionsByIndex = this.connectionsByDestinationNode[nodeName][type][connectionIndex];
// eslint-disable-next-line @typescript-eslint/no-loop-func // eslint-disable-next-line @typescript-eslint/no-loop-func
connectionsByIndex.forEach((connection) => { connectionsByIndex?.forEach((connection) => {
if (checkedNodes.includes(connection.node)) { if (checkedNodes.includes(connection.node)) {
// Node got checked already before // Node got checked already before
return; return;
@@ -742,7 +742,7 @@ export class Workflow {
checkedNodes.push(nodeName); checkedNodes.push(nodeName);
connections[nodeName][type].forEach((connectionsByIndex) => { connections[nodeName][type].forEach((connectionsByIndex) => {
connectionsByIndex.forEach((connection) => { connectionsByIndex?.forEach((connection) => {
if (checkedNodes.includes(connection.node)) { if (checkedNodes.includes(connection.node)) {
// Node got checked already before // Node got checked already before
return; return;
@@ -839,7 +839,7 @@ export class Workflow {
} }
connections[curr.name][type].forEach((connectionsByIndex) => { connections[curr.name][type].forEach((connectionsByIndex) => {
connectionsByIndex.forEach((connection) => { connectionsByIndex?.forEach((connection) => {
queue.push({ queue.push({
name: connection.node, name: connection.node,
indicies: [connection.index], indicies: [connection.index],
@@ -943,6 +943,10 @@ export class Workflow {
let outputIndex: INodeConnection | undefined; let outputIndex: INodeConnection | undefined;
for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) { for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) {
if (!connectionsByIndex) {
continue;
}
for ( for (
let destinationIndex = 0; let destinationIndex = 0;
destinationIndex < connectionsByIndex.length; destinationIndex < connectionsByIndex.length;

View File

@@ -990,11 +990,8 @@ function alphanumericId() {
const chooseRandomly = <T>(array: T[]) => array[randomInt(array.length)]; const chooseRandomly = <T>(array: T[]) => array[randomInt(array.length)];
function generateTestWorkflowAndRunData(): { workflow: IWorkflowBase; runData: IRunData } { function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; runData: IRunData } {
const workflow: IWorkflowBase = { const workflow: Partial<IWorkflowBase> = {
meta: {
instanceId: 'a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0',
},
nodes: [ nodes: [
{ {
parameters: {}, parameters: {},