From c30dbc6dd46d3d8edf15d6dd3d3ac0626daae5c5 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Thu, 24 Jul 2025 12:51:32 +0300 Subject: [PATCH] refactor(editor): Remove part of `getCurrentWorkflow` usages (#16148) --- package.json | 1 + .../completions/jsonField.completions.ts | 3 +- .../src/components/InputNodeSelect.vue | 2 +- .../editor-ui/src/components/RunData.vue | 3 +- .../ExperimentalEmbeddedNodeDetails.vue | 1 + .../src/composables/useCanvasMapping.test.ts | 5 + .../src/composables/useCanvasMapping.ts | 103 +++++------ .../src/composables/useRunWorkflow.ts | 1 + .../composables/useWorkflowHelpers.test.ts | 162 ++++++++++-------- .../src/composables/useWorkflowHelpers.ts | 34 ++-- .../editor-ui/src/stores/ndv.store.ts | 3 +- .../editor-ui/src/stores/workflows.store.ts | 14 +- .../editor-ui/src/types/expressions.ts | 3 +- .../editor-ui/src/utils/nodeViewUtils.test.ts | 23 ++- .../editor-ui/src/utils/nodeViewUtils.ts | 20 ++- packages/frontend/editor-ui/tsconfig.json | 1 + packages/workflow/package.json | 5 + .../workflow/src/common/get-child-nodes.ts | 12 ++ .../src/common/get-connected-nodes.ts | 98 +++++++++++ .../workflow/src/common/get-node-by-name.ts | 19 ++ .../workflow/src/common/get-parent-nodes.ts | 18 ++ packages/workflow/src/common/index.ts | 5 + .../common/map-connections-by-destination.ts | 49 ++++++ packages/workflow/src/index.ts | 1 + packages/workflow/src/workflow.ts | 113 ++---------- packages/workflow/test/common.test.ts | 112 ++++++++++++ packages/workflow/test/workflow.test.ts | 121 ------------- patches/@lezer__highlight.patch | 12 ++ pnpm-lock.yaml | 25 +-- 29 files changed, 578 insertions(+), 391 deletions(-) create mode 100644 packages/workflow/src/common/get-child-nodes.ts create mode 100644 packages/workflow/src/common/get-connected-nodes.ts create mode 100644 packages/workflow/src/common/get-node-by-name.ts create mode 100644 packages/workflow/src/common/get-parent-nodes.ts create mode 100644 packages/workflow/src/common/index.ts create mode 100644 packages/workflow/src/common/map-connections-by-destination.ts create mode 100644 packages/workflow/test/common.test.ts create mode 100644 patches/@lezer__highlight.patch diff --git a/package.json b/package.json index a8056ef542..f633b21fa5 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "ics": "patches/ics.patch", "minifaker": "patches/minifaker.patch", "z-vue-scan": "patches/z-vue-scan.patch", + "@lezer/highlight": "patches/@lezer__highlight.patch", "v-code-diff": "patches/v-code-diff.patch" } } diff --git a/packages/frontend/editor-ui/src/components/CodeNodeEditor/completions/jsonField.completions.ts b/packages/frontend/editor-ui/src/components/CodeNodeEditor/completions/jsonField.completions.ts index bcd9df0575..7def9fe42c 100644 --- a/packages/frontend/editor-ui/src/components/CodeNodeEditor/completions/jsonField.completions.ts +++ b/packages/frontend/editor-ui/src/components/CodeNodeEditor/completions/jsonField.completions.ts @@ -174,8 +174,7 @@ function useJsonFieldCompletions() { try { const activeNode = ndvStore.activeNode; if (activeNode) { - const workflow = workflowsStore.getCurrentWorkflow(); - const input = workflow.connectionsByDestinationNode[activeNode.name]; + const input = workflowsStore.connectionsByDestinationNode[activeNode.name]; return input.main[0] ? input.main[0][0].node : null; } } catch (e) { diff --git a/packages/frontend/editor-ui/src/components/InputNodeSelect.vue b/packages/frontend/editor-ui/src/components/InputNodeSelect.vue index e5156d9a69..4925ff6059 100644 --- a/packages/frontend/editor-ui/src/components/InputNodeSelect.vue +++ b/packages/frontend/editor-ui/src/components/InputNodeSelect.vue @@ -84,7 +84,7 @@ function getMultipleNodesText(nodeName: string): string { return ''; const activeNodeConnections = - props.workflow.connectionsByDestinationNode[activeNode.value.name].main || []; + workflowsStore.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]; diff --git a/packages/frontend/editor-ui/src/components/RunData.vue b/packages/frontend/editor-ui/src/components/RunData.vue index 23586185a4..55c13d740a 100644 --- a/packages/frontend/editor-ui/src/components/RunData.vue +++ b/packages/frontend/editor-ui/src/components/RunData.vue @@ -804,7 +804,8 @@ function getNodeHints(): NodeHint[] { node: node.value, nodeType: nodeType.value, nodeOutputData, - workflow: props.workflow, + nodes: workflowsStore.allNodes, + connections: workflowsStore.connectionsBySourceNode, hasNodeRun: hasNodeRun.value, hasMultipleInputItems, }); diff --git a/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue b/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue index 32adad237c..f83c87b8c0 100644 --- a/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue +++ b/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue @@ -114,6 +114,7 @@ const expressionResolveCtx = computed nodeName, additionalKeys: {}, inputNode: findInputNode(), + connections: workflowsStore.connectionsBySourceNode, }; }); diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts index 72a08e8232..e0ad11087c 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts @@ -216,6 +216,8 @@ describe('useCanvasMapping', () => { }); it('should handle input and output connections', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const [manualTriggerNode, setNode] = mockNodes.slice(0, 2); const nodes = [manualTriggerNode, setNode]; const connections = { @@ -225,6 +227,9 @@ describe('useCanvasMapping', () => { ], }, }; + + workflowsStore.workflow.connections = connections; + const workflowObject = createTestWorkflowObject({ nodes, connections, diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts index 24358bf063..28d71b596c 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts @@ -59,6 +59,7 @@ import { useNodeHelpers } from './useNodeHelpers'; import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils'; import { useNodeDirtiness } from '@/composables/useNodeDirtiness'; import { getNodeIconSource } from '../utils/nodeIcon'; +import * as workflowUtils from 'n8n-workflow/common'; export function useCanvasMapping({ nodes, @@ -571,56 +572,62 @@ export function useCanvasMapping({ }, {}); }); - const mappedNodes = computed(() => [ - ...nodes.value.map((node) => { - const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {}; - const outputConnections = workflowObject.value.connectionsBySourceNode[node.name] ?? {}; + const mappedNodes = computed(() => { + const connectionsBySourceNode = connections.value; + const connectionsByDestinationNode = + workflowUtils.mapConnectionsByDestination(connectionsBySourceNode); - const data: CanvasNodeData = { - id: node.id, - name: node.name, - subtitle: nodeSubtitleById.value[node.id] ?? '', - type: node.type, - typeVersion: node.typeVersion, - disabled: node.disabled, - inputs: nodeInputsById.value[node.id] ?? [], - outputs: nodeOutputsById.value[node.id] ?? [], - connections: { - [CanvasConnectionMode.Input]: inputConnections, - [CanvasConnectionMode.Output]: outputConnections, - }, - issues: { - items: nodeIssuesById.value[node.id], - visible: nodeHasIssuesById.value[node.id], - }, - pinnedData: { - count: nodePinnedDataById.value[node.id]?.length ?? 0, - visible: !!nodePinnedDataById.value[node.id], - }, - execution: { - status: nodeExecutionStatusById.value[node.id], - waiting: nodeExecutionWaitingById.value[node.id], - waitingForNext: nodeExecutionWaitingForNextById.value[node.id], - running: nodeExecutionRunningById.value[node.id], - }, - runData: { - outputMap: nodeExecutionRunDataOutputMapById.value[node.id], - iterations: nodeExecutionRunDataById.value[node.id]?.length ?? 0, - visible: !!nodeExecutionRunDataById.value[node.id], - }, - render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} }, - }; + return [ + ...nodes.value.map((node) => { + const outputConnections = connectionsBySourceNode[node.name] ?? {}; + const inputConnections = connectionsByDestinationNode[node.name] ?? {}; - return { - id: node.id, - label: node.name, - type: 'canvas-node', - position: { x: node.position[0], y: node.position[1] }, - data, - ...additionalNodePropertiesById.value[node.id], - }; - }), - ]); + const data: CanvasNodeData = { + id: node.id, + name: node.name, + subtitle: nodeSubtitleById.value[node.id] ?? '', + type: node.type, + typeVersion: node.typeVersion, + disabled: node.disabled, + inputs: nodeInputsById.value[node.id] ?? [], + outputs: nodeOutputsById.value[node.id] ?? [], + connections: { + [CanvasConnectionMode.Input]: inputConnections, + [CanvasConnectionMode.Output]: outputConnections, + }, + issues: { + items: nodeIssuesById.value[node.id], + visible: nodeHasIssuesById.value[node.id], + }, + pinnedData: { + count: nodePinnedDataById.value[node.id]?.length ?? 0, + visible: !!nodePinnedDataById.value[node.id], + }, + execution: { + status: nodeExecutionStatusById.value[node.id], + waiting: nodeExecutionWaitingById.value[node.id], + waitingForNext: nodeExecutionWaitingForNextById.value[node.id], + running: nodeExecutionRunningById.value[node.id], + }, + runData: { + outputMap: nodeExecutionRunDataOutputMapById.value[node.id], + iterations: nodeExecutionRunDataById.value[node.id]?.length ?? 0, + visible: !!nodeExecutionRunDataById.value[node.id], + }, + render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} }, + }; + + return { + id: node.id, + label: node.name, + type: 'canvas-node', + position: { x: node.position[0], y: node.position[1] }, + data, + ...additionalNodePropertiesById.value[node.id], + }; + }), + ]; + }); const mappedConnections = computed(() => { return mapLegacyConnectionsToCanvasConnections(connections.value ?? [], nodes.value ?? []).map( diff --git a/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts b/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts index 88d43db1bc..1c6fd8a695 100644 --- a/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts +++ b/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts @@ -253,6 +253,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType { const inputName = 'main'; const runIndex = 0; - const result = executeData(parentNodes, currentNode, inputName, runIndex); + const result = executeData({}, parentNodes, currentNode, inputName, runIndex); expect(result).toEqual({ node: {}, @@ -538,18 +535,15 @@ describe('useWorkflowHelpers', () => { const jsonData = { name: 'Test', }; - workflowsStore.getCurrentWorkflow.mockReturnValue({ - connectionsByDestinationNode: { - Set: { - main: [ - [ - { node: 'Start', index: 0, type: 'main' }, - { node: 'Set', index: 0, type: 'main' }, - ], - ], - }, + + const connectionsBySourceNode: IConnections = { + Start: { + main: [[{ node: 'Set', index: 0, type: 'main' }]], }, - } as never); + Set: { + main: [[{ node: 'Start', index: 0, type: 'main' }]], + }, + }; workflowsStore.workflowExecutionData = { data: { @@ -575,7 +569,13 @@ describe('useWorkflowHelpers', () => { }, } as unknown as IExecutionResponse; - const result = executeData(parentNodes, currentNode, inputName, runIndex); + const result = executeData( + connectionsBySourceNode, + parentNodes, + currentNode, + inputName, + runIndex, + ); expect(result).toEqual({ node: {}, @@ -609,18 +609,15 @@ describe('useWorkflowHelpers', () => { const jsonData = { name: 'Test', }; - workflowsStore.getCurrentWorkflow.mockReturnValue({ - connectionsByDestinationNode: { - Set: { - main: [ - [ - { node: 'Start', index: 0, type: 'main' }, - { node: 'Set', index: 0, type: 'main' }, - ], - ], - }, + + const connectionsBySourceNode: IConnections = { + Start: { + main: [[{ node: 'Set', index: 0, type: 'main' }]], }, - } as never); + Set: { + main: [[{ node: 'Start', index: 0, type: 'main' }]], + }, + }; workflowsStore.workflowExecutionData = { data: { @@ -646,7 +643,13 @@ describe('useWorkflowHelpers', () => { }, } as unknown as IExecutionResponse; - const result = executeData(parentNodes, currentNode, inputName, runIndex); + const result = executeData( + connectionsBySourceNode, + parentNodes, + currentNode, + inputName, + runIndex, + ); expect(result).toEqual({ node: {}, @@ -686,22 +689,20 @@ describe('useWorkflowHelpers', () => { name: 'Test B', }; - workflowsStore.getCurrentWorkflow.mockReturnValue({ - connectionsByDestinationNode: { - Set: { - main: [ - [ - { node: 'Parent A', index: 0, type: 'main' }, - { node: 'Set', index: 0, type: 'main' }, - ], - [ - { node: 'Parent B', index: 0, type: 'main' }, - { node: 'Set', index: 0, type: 'main' }, - ], - ], - }, + const connectionsBySourceNode: IConnections = { + 'Parent A': { + main: [[{ node: 'Set', type: 'main', index: 0 }]], }, - } as never); + 'Parent B': { + main: [[{ node: 'Set', type: 'main', index: 1 }]], + }, + Set: { + main: [ + [{ node: 'Set', type: 'main', index: 0 }], + [{ node: 'Set', type: 'main', index: 1 }], + ], + }, + }; workflowsStore.workflowExecutionData = { data: { @@ -742,7 +743,13 @@ describe('useWorkflowHelpers', () => { }, } as unknown as IExecutionResponse; - const result = executeData(parentNodes, currentNode, inputName, runIndex); + const result = executeData( + connectionsBySourceNode, + parentNodes, + currentNode, + inputName, + runIndex, + ); expect(result).toEqual({ node: {}, @@ -779,7 +786,7 @@ describe('useWorkflowHelpers', () => { }; workflowsStore.shouldReplaceInputDataWithPinData = true; - const result = executeData(parentNodes, currentNode, inputName, runIndex); + const result = executeData({}, parentNodes, currentNode, inputName, runIndex); expect(result.data).toEqual({ main: [[{ json: { key: 'value' } }]] }); expect(result.source).toEqual({ main: [{ previousNode: 'ParentNode' }] }); @@ -802,20 +809,23 @@ describe('useWorkflowHelpers', () => { } as never, ], }; - workflowsStore.getCurrentWorkflow.mockReturnValue({ - connectionsByDestinationNode: { - CurrentNode: { - main: [ - [ - { node: 'ParentNode', index: 0, type: 'main' }, - { node: 'CurrentNode', index: 0, type: 'main' }, - ], - ], - }, - }, - } as never); - const result = executeData(parentNodes, currentNode, inputName, runIndex); + const connectionsBySourceNode: IConnections = { + CurrentNode: { + main: [[{ node: 'CurrentNode', index: 0, type: 'main' }]], + }, + ParentNode: { + main: [[{ node: 'CurrentNode', index: 0, type: 'main' }]], + }, + }; + + const result = executeData( + connectionsBySourceNode, + parentNodes, + currentNode, + inputName, + runIndex, + ); expect(result.data).toEqual({ main: [[{ json: { key: 'valueFromRunData' } }]] }); expect(result.source).toEqual({ @@ -841,20 +851,24 @@ describe('useWorkflowHelpers', () => { } as never, ], }; - workflowsStore.getCurrentWorkflow.mockReturnValue({ - connectionsByDestinationNode: { - CurrentNode: { - main: [ - [ - { node: 'ParentNode', index: 1, type: 'main' }, - { node: 'CurrentNode', index: 0, type: 'main' }, - ], - ], - }, - }, - } as never); - const result = executeData(parentNodes, currentNode, inputName, runIndex, parentRunIndex); + const connectionsBySourceNode: IConnections = { + CurrentNode: { + main: [[{ node: 'CurrentNode', index: 0, type: 'main' }]], + }, + ParentNode: { + main: [[], [{ node: 'CurrentNode', index: 1, type: 'main' }]], + }, + }; + + const result = executeData( + connectionsBySourceNode, + parentNodes, + currentNode, + inputName, + runIndex, + parentRunIndex, + ); expect(result.data).toEqual({ main: [[{ json: { key: 'valueFromRunData' } }]] }); expect(result.source).toEqual({ @@ -874,7 +888,7 @@ describe('useWorkflowHelpers', () => { workflowsStore.shouldReplaceInputDataWithPinData = false; workflowsStore.getWorkflowRunData = null; - const result = executeData(parentNodes, currentNode, inputName, runIndex); + const result = executeData({}, parentNodes, currentNode, inputName, runIndex); expect(result.data).toEqual({}); expect(result.source).toBeNull(); diff --git a/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts index c7e0983144..9bdbc3daf4 100644 --- a/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts +++ b/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts @@ -29,6 +29,7 @@ import { NodeHelpers, WEBHOOK_NODE_TYPE, } from 'n8n-workflow'; +import * as workflowUtils from 'n8n-workflow/common'; import type { ICredentialsResponse, @@ -71,6 +72,7 @@ export type ResolveParameterOptions = { additionalKeys?: IWorkflowDataProxyAdditionalKeys; isForCredential?: boolean; contextNodeName?: string; + connections?: IConnections; }; export function resolveParameter( @@ -81,6 +83,7 @@ export function resolveParameter( return resolveParameterImpl( parameter, () => opts.workflow, + opts.connections, opts.envVars, opts.workflow.getNode(opts.nodeName), opts.execution, @@ -100,6 +103,7 @@ export function resolveParameter( return resolveParameterImpl( parameter, workflowsStore.getCurrentWorkflow, + workflowsStore.connectionsBySourceNode, useEnvironmentsStore().variablesAsObject, useNDVStore().activeNode, workflowsStore.workflowExecutionData, @@ -113,6 +117,7 @@ export function resolveParameter( function resolveParameterImpl( parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], getContextWorkflow: () => Workflow, + connections: IConnections, envVars: Record, ndvActiveNode: INodeUi | null, executionData: IExecutionResponse | null, @@ -200,11 +205,11 @@ function resolveParameterImpl( } let _connectionInputData = connectionInputData( + connections, parentNode, contextNode!.name, inputName, runIndexParent, - getContextWorkflow, shouldReplaceInputDataWithPinData, pinData, executionData?.data?.resultData.runData ?? null, @@ -215,11 +220,11 @@ function resolveParameterImpl( // For Sub-Nodes connected to Trigger-Nodes use the data of the root-node // (Gets for example used by the Memory connected to the Chat-Trigger-Node) const _executeData = executeDataImpl( + connections, [contextNode.name], contextNode.name, inputName, 0, - getContextWorkflow, shouldReplaceInputDataWithPinData, pinData, executionData?.data?.resultData.runData ?? null, @@ -265,11 +270,11 @@ function resolveParameterImpl( runIndexCurrent = workflowRunData[contextNode!.name].length - 1; } let _executeData = executeDataImpl( + connections, parentNode, contextNode!.name, inputName, runIndexCurrent, - getContextWorkflow, shouldReplaceInputDataWithPinData, pinData, executionData?.data?.resultData.runData ?? null, @@ -279,11 +284,11 @@ function resolveParameterImpl( if (!_executeData.source) { // fallback to parent's run index for multi-output case _executeData = executeDataImpl( + connections, parentNode, contextNode!.name, inputName, runIndexParent, - getContextWorkflow, shouldReplaceInputDataWithPinData, pinData, executionData?.data?.resultData.runData ?? null, @@ -310,6 +315,7 @@ export function resolveRequiredParameters( currentParameter: INodeProperties, parameters: INodeParameters, opts: { + connections?: IConnections; targetItem?: TargetItem; inputNodeName?: string; inputRunIndex?: number; @@ -382,11 +388,11 @@ function getNodeTypes(): INodeTypes { // TODO: move to separate file // Returns connectionInputData to be able to execute an expression. function connectionInputData( + connections: IConnections, parentNode: string[], currentNode: string, inputName: string, runIndex: number, - getContextWorkflow: () => Workflow, shouldReplaceInputDataWithPinData: boolean, pinData: IPinData | undefined, workflowRunData: IRunData | null, @@ -394,11 +400,11 @@ function connectionInputData( ): INodeExecutionData[] | null { let connectionInputData: INodeExecutionData[] | null = null; const _executeData = executeDataImpl( + connections, parentNode, currentNode, inputName, runIndex, - getContextWorkflow, shouldReplaceInputDataWithPinData, pinData, workflowRunData, @@ -431,6 +437,7 @@ function connectionInputData( } export function executeData( + connections: IConnections, parentNodes: string[], currentNode: string, inputName: string, @@ -440,11 +447,11 @@ export function executeData( const workflowsStore = useWorkflowsStore(); return executeDataImpl( + connections, parentNodes, currentNode, inputName, runIndex, - workflowsStore.getCurrentWorkflow, workflowsStore.shouldReplaceInputDataWithPinData, workflowsStore.pinnedWorkflowData, workflowsStore.getWorkflowRunData, @@ -454,16 +461,18 @@ export function executeData( // TODO: move to separate file function executeDataImpl( + connections: IConnections, parentNodes: string[], currentNode: string, inputName: string, runIndex: number, - getContextWorkflow: () => Workflow, shouldReplaceInputDataWithPinData: boolean, pinData: IPinData | undefined, workflowRunData: IRunData | null, parentRunIndex?: number, ): IExecuteData { + const connectionsByDestinationNode = workflowUtils.mapConnectionsByDestination(connections); + const executeData = { node: {}, data: {}, @@ -507,15 +516,12 @@ function executeDataImpl( [inputName]: workflowRunData[currentNode][runIndex].source, }; } else { - const workflow = getContextWorkflow(); - let previousNodeOutput: number | undefined; // As the node can be connected through either of the outputs find the correct one // and set it to make pairedItem work on not executed nodes - if (workflow.connectionsByDestinationNode[currentNode]?.main) { - mainConnections: for (const mainConnections of workflow.connectionsByDestinationNode[ - currentNode - ].main) { + if (connectionsByDestinationNode[currentNode]?.main) { + mainConnections: for (const mainConnections of connectionsByDestinationNode[currentNode] + .main) { for (const connection of mainConnections ?? []) { if ( connection.type === NodeConnectionTypes.Main && diff --git a/packages/frontend/editor-ui/src/stores/ndv.store.ts b/packages/frontend/editor-ui/src/stores/ndv.store.ts index e3609ece80..bb772459da 100644 --- a/packages/frontend/editor-ui/src/stores/ndv.store.ts +++ b/packages/frontend/editor-ui/src/stores/ndv.store.ts @@ -149,9 +149,8 @@ export const useNDVStore = defineStore(STORES.NDV, () => { const ndvNodeInputNumber = computed(() => { const returnData: { [nodeName: string]: number[] } = {}; - const workflow = workflowsStore.getCurrentWorkflow(); const activeNodeConections = ( - workflow.connectionsByDestinationNode[activeNode.value?.name || ''] ?? {} + workflowsStore.connectionsByDestinationNode[activeNode.value?.name || ''] ?? {} ).main; if (!activeNodeConections || activeNodeConections.length < 2) return returnData; diff --git a/packages/frontend/editor-ui/src/stores/workflows.store.ts b/packages/frontend/editor-ui/src/stores/workflows.store.ts index c26e6be453..471966f109 100644 --- a/packages/frontend/editor-ui/src/stores/workflows.store.ts +++ b/packages/frontend/editor-ui/src/stores/workflows.store.ts @@ -66,6 +66,7 @@ import { Workflow, TelemetryHelpers, } from 'n8n-workflow'; +import * as workflowUtils from 'n8n-workflow/common'; import findLast from 'lodash/findLast'; import isEqual from 'lodash/isEqual'; import pick from 'lodash/pick'; @@ -294,10 +295,17 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { const getPastChatMessages = computed(() => Array.from(new Set(chatMessages.value))); + /** + * This section contains functions migrated from the workflow class + */ + + const connectionsBySourceNode = computed(() => workflow.value.connections); const connectionsByDestinationNode = computed(() => - Workflow.getConnectionsByDestination(workflow.value.connections), + workflowUtils.mapConnectionsByDestination(workflow.value.connections), ); + // End section + const selectableTriggerNodes = computed(() => workflowTriggerNodes.value.filter((node) => !node.disabled && !isChatNode(node)), ); @@ -384,7 +392,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { } function getNodeByName(nodeName: string): INodeUi | null { - return nodesByName.value[nodeName] || null; + return workflowUtils.getNodeByName(nodesByName.value, nodeName); } function getNodeById(nodeId: string): INodeUi | undefined { @@ -1955,6 +1963,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { getWorkflowResultDataByNodeName, allConnections, allNodes, + connectionsBySourceNode, + connectionsByDestinationNode, isWaitingExecution, isWorkflowRunning, canvasNames, diff --git a/packages/frontend/editor-ui/src/types/expressions.ts b/packages/frontend/editor-ui/src/types/expressions.ts index 0c21f916cf..81953e19fd 100644 --- a/packages/frontend/editor-ui/src/types/expressions.ts +++ b/packages/frontend/editor-ui/src/types/expressions.ts @@ -1,5 +1,5 @@ import type { Basic, IExecutionResponse } from '@/Interface'; -import type { IWorkflowDataProxyAdditionalKeys, Workflow } from 'n8n-workflow'; +import type { IConnections, IWorkflowDataProxyAdditionalKeys, Workflow } from 'n8n-workflow'; type Range = { from: number; to: number }; @@ -40,6 +40,7 @@ export interface ExpressionLocalResolveContext { envVars: Record; additionalKeys: IWorkflowDataProxyAdditionalKeys; workflow: Workflow; + connections: IConnections; execution: IExecutionResponse | null; nodeName: string; /** diff --git a/packages/frontend/editor-ui/src/utils/nodeViewUtils.test.ts b/packages/frontend/editor-ui/src/utils/nodeViewUtils.test.ts index 7033f22ae9..0084f17289 100644 --- a/packages/frontend/editor-ui/src/utils/nodeViewUtils.test.ts +++ b/packages/frontend/editor-ui/src/utils/nodeViewUtils.test.ts @@ -23,6 +23,8 @@ import { SET_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants'; import { createTestNode } from '@/__tests__/mocks'; import type { GraphNode } from '@vue-flow/core'; import { v4 as uuid } from 'uuid'; +import { createTestingPinia } from '@pinia/testing'; +import { setActivePinia } from 'pinia'; describe('getGenericHints', () => { let mockWorkflowNode: MockProxy; @@ -34,6 +36,9 @@ describe('getGenericHints', () => { let hasNodeRun: boolean; beforeEach(() => { + const pinia = createTestingPinia({}); + setActivePinia(pinia); + mockWorkflowNode = mock(); mockNode = mock({ type: 'test' }); mockNodeType = mock(); @@ -55,8 +60,9 @@ describe('getGenericHints', () => { nodeType: mockNodeType, nodeOutputData: mockNodeOutputData, hasMultipleInputItems, - workflow: mockWorkflow, hasNodeRun, + nodes: [], + connections: {}, }); expect(hints).toEqual([ @@ -80,8 +86,9 @@ describe('getGenericHints', () => { nodeType: mockNodeType, nodeOutputData: mockNodeOutputData, hasMultipleInputItems, - workflow: mockWorkflow, hasNodeRun, + nodes: [], + connections: {}, }); expect(hints).toEqual([ @@ -118,8 +125,9 @@ describe('getGenericHints', () => { nodeType: mockNodeType, nodeOutputData: mockNodeOutputData, hasMultipleInputItems, - workflow: mockWorkflow, hasNodeRun, + nodes: [], + connections: {}, }); expect(hints).toEqual([ @@ -142,8 +150,9 @@ describe('getGenericHints', () => { nodeType: mockNodeType, nodeOutputData: mockNodeOutputData, hasMultipleInputItems, - workflow: mockWorkflow, hasNodeRun, + nodes: [], + connections: {}, }); expect(hints).toEqual([ @@ -166,8 +175,9 @@ describe('getGenericHints', () => { nodeType: mockNodeType, nodeOutputData: mockNodeOutputData, hasMultipleInputItems, - workflow: mockWorkflow, hasNodeRun, + nodes: [], + connections: {}, }); expect(hints).toEqual([ @@ -191,8 +201,9 @@ describe('getGenericHints', () => { nodeType: mockNodeType, nodeOutputData: mockNodeOutputData, hasMultipleInputItems, - workflow: mockWorkflow, hasNodeRun, + nodes: [], + connections: {}, }); expect(hints).toEqual([ diff --git a/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts b/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts index 068988a6c3..4457f480fb 100644 --- a/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts +++ b/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts @@ -11,11 +11,11 @@ import { import type { INodeUi, XYPosition } from '@/Interface'; import type { AssignmentCollectionValue, + IConnections, INode, INodeExecutionData, INodeTypeDescription, NodeHint, - Workflow, } from 'n8n-workflow'; import { NodeHelpers, SEND_AND_WAIT_OPERATION } from 'n8n-workflow'; import type { RouteLocation } from 'vue-router'; @@ -27,6 +27,7 @@ import { type Rect, type ViewportTransform, } from '@vue-flow/core'; +import * as workflowUtils from 'n8n-workflow/common'; /* * Canvas constants and functions @@ -371,7 +372,8 @@ export function getGenericHints({ nodeType, nodeOutputData, hasMultipleInputItems, - workflow, + nodes, + connections, hasNodeRun, }: { workflowNode: INode; @@ -379,7 +381,8 @@ export function getGenericHints({ nodeType: INodeTypeDescription; nodeOutputData: INodeExecutionData[]; hasMultipleInputItems: boolean; - workflow: Workflow; + nodes: INode[]; + connections: IConnections; hasNodeRun: boolean; }) { const nodeHints: NodeHint[] = []; @@ -417,7 +420,7 @@ export function getGenericHints({ hasMultipleInputItems && LIST_LIKE_NODE_OPERATIONS.includes((workflowNode.parameters.operation as string) || '') ) { - const executeOnce = workflow.getNode(node.name)?.executeOnce; + const executeOnce = workflowUtils.getNodeByName(nodes, node.name)?.executeOnce; if (!executeOnce) { nodeHints.push({ message: @@ -429,7 +432,7 @@ export function getGenericHints({ // add sendAndWait hint if (hasMultipleInputItems && workflowNode.parameters.operation === SEND_AND_WAIT_OPERATION) { - const executeOnce = workflow.getNode(node.name)?.executeOnce; + const executeOnce = workflowUtils.getNodeByName(nodes, node.name)?.executeOnce; if (!executeOnce) { nodeHints.push({ message: 'This action will run only once, for the first input item', @@ -470,9 +473,8 @@ export function getGenericHints({ // Split In Batches setup hints if (node.type === SPLIT_IN_BATCHES_NODE_TYPE) { - const { connectionsBySourceNode } = workflow; - - const firstNodesInLoop = connectionsBySourceNode[node.name]?.main[1] || []; + const firstNodesInLoop = + workflowUtils.mapConnectionsByDestination(connections)[node.name]?.main[1] || []; if (!firstNodesInLoop.length) { nodeHints.push({ @@ -482,7 +484,7 @@ export function getGenericHints({ }); } else { for (const nodeInConnection of firstNodesInLoop || []) { - const nodeChilds = workflow.getChildNodes(nodeInConnection.node) || []; + const nodeChilds = workflowUtils.getChildNodes(connections, nodeInConnection.node) || []; if (!nodeChilds.includes(node.name)) { nodeHints.push({ message: diff --git a/packages/frontend/editor-ui/tsconfig.json b/packages/frontend/editor-ui/tsconfig.json index 009ac7eeba..14a4b4aa0c 100644 --- a/packages/frontend/editor-ui/tsconfig.json +++ b/packages/frontend/editor-ui/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@n8n/typescript-config/tsconfig.frontend.json", "compilerOptions": { "baseUrl": ".", + "moduleResolution": "bundler", "rootDirs": [ ".", "../@n8n/rest-api-client/src", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 48fceeb066..c408e4fce1 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -11,6 +11,11 @@ "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" }, + "./common": { + "types": "./dist/esm/common/index.d.ts", + "import": "./dist/esm/common/index.js", + "require": "./dist/cjs/common/index.js" + }, "./*": "./*" }, "scripts": { diff --git a/packages/workflow/src/common/get-child-nodes.ts b/packages/workflow/src/common/get-child-nodes.ts new file mode 100644 index 0000000000..49dd85d66d --- /dev/null +++ b/packages/workflow/src/common/get-child-nodes.ts @@ -0,0 +1,12 @@ +import { getConnectedNodes } from './get-connected-nodes'; +import { NodeConnectionTypes } from '../interfaces'; +import type { IConnections, NodeConnectionType } from '../interfaces'; + +export function getChildNodes( + connectionsBySourceNode: IConnections, + nodeName: string, + type: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main, + depth = -1, +): string[] { + return getConnectedNodes(connectionsBySourceNode, nodeName, type, depth); +} diff --git a/packages/workflow/src/common/get-connected-nodes.ts b/packages/workflow/src/common/get-connected-nodes.ts new file mode 100644 index 0000000000..e58d23996a --- /dev/null +++ b/packages/workflow/src/common/get-connected-nodes.ts @@ -0,0 +1,98 @@ +import { NodeConnectionTypes } from '../interfaces'; +import type { IConnections, NodeConnectionType } from '../interfaces'; + +/** + * Gets all the nodes which are connected nodes starting from + * the given one + * + * @param {NodeConnectionType} [type='main'] + * @param {*} [depth=-1] + */ +export function getConnectedNodes( + connections: IConnections, + nodeName: string, + connectionType: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main, + depth = -1, + checkedNodesIncoming?: string[], +): string[] { + const newDepth = depth === -1 ? depth : depth - 1; + if (depth === 0) { + // Reached max depth + return []; + } + + if (!connections.hasOwnProperty(nodeName)) { + // Node does not have incoming connections + return []; + } + + let types: NodeConnectionType[]; + if (connectionType === 'ALL') { + types = Object.keys(connections[nodeName]) as NodeConnectionType[]; + } else if (connectionType === 'ALL_NON_MAIN') { + types = Object.keys(connections[nodeName]).filter( + (type) => type !== 'main', + ) as NodeConnectionType[]; + } else { + types = [connectionType]; + } + + let addNodes: string[]; + let nodeIndex: number; + let i: number; + let parentNodeName: string; + const returnNodes: string[] = []; + + types.forEach((type) => { + if (!connections[nodeName].hasOwnProperty(type)) { + // Node does not have incoming connections of given type + return; + } + + const checkedNodes = checkedNodesIncoming ? [...checkedNodesIncoming] : []; + + if (checkedNodes.includes(nodeName)) { + // Node got checked already before + return; + } + + checkedNodes.push(nodeName); + + connections[nodeName][type].forEach((connectionsByIndex) => { + connectionsByIndex?.forEach((connection) => { + if (checkedNodes.includes(connection.node)) { + // Node got checked already before + return; + } + + returnNodes.unshift(connection.node); + + addNodes = getConnectedNodes( + connections, + connection.node, + connectionType, + newDepth, + checkedNodes, + ); + + for (i = addNodes.length; i--; i > 0) { + // Because nodes can have multiple parents it is possible that + // parts of the tree is parent of both and to not add nodes + // twice check first if they already got added before. + parentNodeName = addNodes[i]; + nodeIndex = returnNodes.indexOf(parentNodeName); + + if (nodeIndex !== -1) { + // Node got found before so remove it from current location + // that node-order stays correct + returnNodes.splice(nodeIndex, 1); + } + + returnNodes.unshift(parentNodeName); + } + }); + }); + }); + + return returnNodes; +} diff --git a/packages/workflow/src/common/get-node-by-name.ts b/packages/workflow/src/common/get-node-by-name.ts new file mode 100644 index 0000000000..566d2f8fb1 --- /dev/null +++ b/packages/workflow/src/common/get-node-by-name.ts @@ -0,0 +1,19 @@ +import type { INode, INodes } from '../interfaces'; + +/** + * Returns the node with the given name if it exists else null + * + * @param {INodes} nodes Nodes to search in + * @param {string} name Name of the node to return + */ +export function getNodeByName(nodes: INodes | INode[], name: string) { + if (Array.isArray(nodes)) { + return nodes.find((node) => node.name === name) || null; + } + + if (nodes.hasOwnProperty(name)) { + return nodes[name]; + } + + return null; +} diff --git a/packages/workflow/src/common/get-parent-nodes.ts b/packages/workflow/src/common/get-parent-nodes.ts new file mode 100644 index 0000000000..c40d4766bb --- /dev/null +++ b/packages/workflow/src/common/get-parent-nodes.ts @@ -0,0 +1,18 @@ +import { getConnectedNodes } from './get-connected-nodes'; +import { NodeConnectionTypes } from '../interfaces'; +import type { IConnections, NodeConnectionType } from '../interfaces'; + +/** + * Returns all the nodes before the given one + * + * @param {NodeConnectionType} [type='main'] + * @param {*} [depth=-1] + */ +export function getParentNodes( + connectionsByDestinationNode: IConnections, + nodeName: string, + type: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main, + depth = -1, +): string[] { + return getConnectedNodes(connectionsByDestinationNode, nodeName, type, depth); +} diff --git a/packages/workflow/src/common/index.ts b/packages/workflow/src/common/index.ts new file mode 100644 index 0000000000..196ec09a37 --- /dev/null +++ b/packages/workflow/src/common/index.ts @@ -0,0 +1,5 @@ +export * from './get-child-nodes'; +export * from './get-connected-nodes'; +export * from './get-node-by-name'; +export * from './get-parent-nodes'; +export * from './map-connections-by-destination'; diff --git a/packages/workflow/src/common/map-connections-by-destination.ts b/packages/workflow/src/common/map-connections-by-destination.ts new file mode 100644 index 0000000000..c37370bfab --- /dev/null +++ b/packages/workflow/src/common/map-connections-by-destination.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-for-in-array */ + +import type { IConnections, NodeConnectionType } from '../interfaces'; + +export function mapConnectionsByDestination(connections: IConnections) { + const returnConnection: IConnections = {}; + + let connectionInfo; + let maxIndex: number; + for (const sourceNode in connections) { + if (!connections.hasOwnProperty(sourceNode)) { + continue; + } + + for (const type of Object.keys(connections[sourceNode]) as NodeConnectionType[]) { + if (!connections[sourceNode].hasOwnProperty(type)) { + continue; + } + + for (const inputIndex in connections[sourceNode][type]) { + if (!connections[sourceNode][type].hasOwnProperty(inputIndex)) { + continue; + } + + for (connectionInfo of connections[sourceNode][type][inputIndex] ?? []) { + if (!returnConnection.hasOwnProperty(connectionInfo.node)) { + returnConnection[connectionInfo.node] = {}; + } + if (!returnConnection[connectionInfo.node].hasOwnProperty(connectionInfo.type)) { + returnConnection[connectionInfo.node][connectionInfo.type] = []; + } + + maxIndex = returnConnection[connectionInfo.node][connectionInfo.type].length - 1; + for (let j = maxIndex; j < connectionInfo.index; j++) { + returnConnection[connectionInfo.node][connectionInfo.type].push([]); + } + + returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index]?.push({ + node: sourceNode, + type, + index: parseInt(inputIndex, 10), + }); + } + } + } + } + + return returnConnection; +} diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 2c6d1ae931..4be0cbb82a 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -5,6 +5,7 @@ import * as TelemetryHelpers from './telemetry-helpers'; export * from './errors'; export * from './constants'; +export * from './common'; export * from './cron'; export * from './deferred-promise'; export * from './global-state'; diff --git a/packages/workflow/src/workflow.ts b/packages/workflow/src/workflow.ts index a7e625e3cf..2b00afe648 100644 --- a/packages/workflow/src/workflow.ts +++ b/packages/workflow/src/workflow.ts @@ -1,6 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-for-in-array */ +import { + getNodeByName, + getConnectedNodes, + getChildNodes, + getParentNodes, + mapConnectionsByDestination, +} from './common'; + import { MANUAL_CHAT_TRIGGER_LANGCHAIN_NODE_TYPE, NODES_WITH_RENAMABLE_CONTENT, @@ -123,9 +131,7 @@ export class Workflow { this.connectionsBySourceNode = parameters.connections; // Save also the connections by the destination nodes - this.connectionsByDestinationNode = Workflow.getConnectionsByDestination( - parameters.connections, - ); + this.connectionsByDestinationNode = mapConnectionsByDestination(parameters.connections); this.active = parameters.active || false; @@ -146,11 +152,6 @@ export class Workflow { this.staticData.__dataChanged = true; } - /** - * The default connections are by source node. This function rewrites them by destination nodes - * to easily find parent nodes. - * - */ static getConnectionsByDestination(connections: IConnections): IConnections { const returnConnection: IConnections = {}; @@ -288,11 +289,7 @@ export class Workflow { * @param {string} nodeName Name of the node to return */ getNode(nodeName: string): INode | null { - if (this.nodes.hasOwnProperty(nodeName)) { - return this.nodes[nodeName]; - } - - return null; + return getNodeByName(this.nodes, nodeName); } /** @@ -477,9 +474,7 @@ export class Workflow { } // Use the updated connections to create updated connections by destination nodes - this.connectionsByDestinationNode = Workflow.getConnectionsByDestination( - this.connectionsBySourceNode, - ); + this.connectionsByDestinationNode = mapConnectionsByDestination(this.connectionsBySourceNode); } /** @@ -576,7 +571,7 @@ export class Workflow { type: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main, depth = -1, ): string[] { - return this.getConnectedNodes(this.connectionsBySourceNode, nodeName, type, depth); + return getChildNodes(this.connectionsBySourceNode, nodeName, type, depth); } /** @@ -590,7 +585,7 @@ export class Workflow { type: NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionTypes.Main, depth = -1, ): string[] { - return this.getConnectedNodes(this.connectionsByDestinationNode, nodeName, type, depth); + return getParentNodes(this.connectionsByDestinationNode, nodeName, type, depth); } /** @@ -607,87 +602,7 @@ export class Workflow { depth = -1, checkedNodesIncoming?: string[], ): string[] { - depth = depth === -1 ? -1 : depth; - const newDepth = depth === -1 ? depth : depth - 1; - if (depth === 0) { - // Reached max depth - return []; - } - - if (!connections.hasOwnProperty(nodeName)) { - // Node does not have incoming connections - return []; - } - - let types: NodeConnectionType[]; - if (connectionType === 'ALL') { - types = Object.keys(connections[nodeName]) as NodeConnectionType[]; - } else if (connectionType === 'ALL_NON_MAIN') { - types = Object.keys(connections[nodeName]).filter( - (type) => type !== 'main', - ) as NodeConnectionType[]; - } else { - types = [connectionType]; - } - - let addNodes: string[]; - let nodeIndex: number; - let i: number; - let parentNodeName: string; - const returnNodes: string[] = []; - - types.forEach((type) => { - if (!connections[nodeName].hasOwnProperty(type)) { - // Node does not have incoming connections of given type - return; - } - - const checkedNodes = checkedNodesIncoming ? [...checkedNodesIncoming] : []; - - if (checkedNodes.includes(nodeName)) { - // Node got checked already before - return; - } - - checkedNodes.push(nodeName); - - connections[nodeName][type].forEach((connectionsByIndex) => { - connectionsByIndex?.forEach((connection) => { - if (checkedNodes.includes(connection.node)) { - // Node got checked already before - return; - } - - returnNodes.unshift(connection.node); - - addNodes = this.getConnectedNodes( - connections, - connection.node, - connectionType, - newDepth, - checkedNodes, - ); - - for (i = addNodes.length; i--; i > 0) { - // Because nodes can have multiple parents it is possible that - // parts of the tree is parent of both and to not add nodes - // twice check first if they already got added before. - parentNodeName = addNodes[i]; - nodeIndex = returnNodes.indexOf(parentNodeName); - - if (nodeIndex !== -1) { - // Node got found before so remove it from current location - // that node-order stays correct - returnNodes.splice(nodeIndex, 1); - } - - returnNodes.unshift(parentNodeName); - } - }); - }); - }); - - return returnNodes; + return getConnectedNodes(connections, nodeName, connectionType, depth, checkedNodesIncoming); } /** diff --git a/packages/workflow/test/common.test.ts b/packages/workflow/test/common.test.ts new file mode 100644 index 0000000000..eb1cb33010 --- /dev/null +++ b/packages/workflow/test/common.test.ts @@ -0,0 +1,112 @@ +import type { IConnections, IConnection } from '../src/interfaces'; +import { NodeConnectionTypes } from '../src/interfaces'; +import { mapConnectionsByDestination } from '../src/common'; + +describe('getConnectionsByDestination', () => { + it('should return empty object when there are no connections', () => { + const result = mapConnectionsByDestination({}); + + expect(result).toEqual({}); + }); + + it('should return connections by destination node', () => { + const connections: IConnections = { + Node1: { + [NodeConnectionTypes.Main]: [ + [ + { node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }, + { node: 'Node3', type: NodeConnectionTypes.Main, index: 1 }, + ], + ], + }, + }; + const result = mapConnectionsByDestination(connections); + expect(result).toEqual({ + Node2: { + [NodeConnectionTypes.Main]: [[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }]], + }, + Node3: { + [NodeConnectionTypes.Main]: [ + [], + [{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }], + ], + }, + }); + }); + + it('should handle multiple connection types', () => { + const connections: IConnections = { + Node1: { + [NodeConnectionTypes.Main]: [[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }]], + [NodeConnectionTypes.AiAgent]: [ + [{ node: 'Node3', type: NodeConnectionTypes.AiAgent, index: 0 }], + ], + }, + }; + + const result = mapConnectionsByDestination(connections); + expect(result).toEqual({ + Node2: { + [NodeConnectionTypes.Main]: [[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }]], + }, + Node3: { + [NodeConnectionTypes.AiAgent]: [ + [{ node: 'Node1', type: NodeConnectionTypes.AiAgent, index: 0 }], + ], + }, + }); + }); + + it('should handle nodes with no connections', () => { + const connections: IConnections = { + Node1: { + [NodeConnectionTypes.Main]: [[]], + }, + }; + + const result = mapConnectionsByDestination(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: { + [NodeConnectionTypes.Main]: [ + null as unknown as IConnection[], + [{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }], + ], + }, + }; + + const result = mapConnectionsByDestination(connections); + expect(result).toEqual({ + Node2: { + [NodeConnectionTypes.Main]: [[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 1 }]], + }, + }); + }); + + it('should handle nodes with multiple input connections', () => { + const connections: IConnections = { + Node1: { + [NodeConnectionTypes.Main]: [[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }]], + }, + Node3: { + [NodeConnectionTypes.Main]: [[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }]], + }, + }; + + const result = mapConnectionsByDestination(connections); + expect(result).toEqual({ + Node2: { + [NodeConnectionTypes.Main]: [ + [ + { node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }, + { node: 'Node3', type: NodeConnectionTypes.Main, index: 0 }, + ], + ], + }, + }); + }); +}); diff --git a/packages/workflow/test/workflow.test.ts b/packages/workflow/test/workflow.test.ts index 50de376e37..fd0c8e8c1a 100644 --- a/packages/workflow/test/workflow.test.ts +++ b/packages/workflow/test/workflow.test.ts @@ -2106,127 +2106,6 @@ describe('Workflow', () => { }); }); - describe('getConnectionsByDestination', () => { - it('should return empty object when there are no connections', () => { - const result = Workflow.getConnectionsByDestination({}); - - expect(result).toEqual({}); - }); - - it('should return connections by destination node', () => { - const connections: IConnections = { - Node1: { - [NodeConnectionTypes.Main]: [ - [ - { node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }, - { node: 'Node3', type: NodeConnectionTypes.Main, index: 1 }, - ], - ], - }, - }; - const result = Workflow.getConnectionsByDestination(connections); - expect(result).toEqual({ - Node2: { - [NodeConnectionTypes.Main]: [ - [{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }], - ], - }, - Node3: { - [NodeConnectionTypes.Main]: [ - [], - [{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }], - ], - }, - }); - }); - - it('should handle multiple connection types', () => { - const connections: IConnections = { - Node1: { - [NodeConnectionTypes.Main]: [ - [{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }], - ], - [NodeConnectionTypes.AiAgent]: [ - [{ node: 'Node3', type: NodeConnectionTypes.AiAgent, index: 0 }], - ], - }, - }; - - const result = Workflow.getConnectionsByDestination(connections); - expect(result).toEqual({ - Node2: { - [NodeConnectionTypes.Main]: [ - [{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }], - ], - }, - Node3: { - [NodeConnectionTypes.AiAgent]: [ - [{ node: 'Node1', type: NodeConnectionTypes.AiAgent, index: 0 }], - ], - }, - }); - }); - - it('should handle nodes with no connections', () => { - const connections: IConnections = { - Node1: { - [NodeConnectionTypes.Main]: [[]], - }, - }; - - 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: { - [NodeConnectionTypes.Main]: [ - null as unknown as IConnection[], - [{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }], - ], - }, - }; - - const result = Workflow.getConnectionsByDestination(connections); - expect(result).toEqual({ - Node2: { - [NodeConnectionTypes.Main]: [ - [{ node: 'Node1', type: NodeConnectionTypes.Main, index: 1 }], - ], - }, - }); - }); - - it('should handle nodes with multiple input connections', () => { - const connections: IConnections = { - Node1: { - [NodeConnectionTypes.Main]: [ - [{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }], - ], - }, - Node3: { - [NodeConnectionTypes.Main]: [ - [{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }], - ], - }, - }; - - const result = Workflow.getConnectionsByDestination(connections); - expect(result).toEqual({ - Node2: { - [NodeConnectionTypes.Main]: [ - [ - { node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }, - { node: 'Node3', type: NodeConnectionTypes.Main, index: 0 }, - ], - ], - }, - }); - }); - }); - describe('getHighestNode', () => { const createNode = (name: string, disabled = false) => ({ diff --git a/patches/@lezer__highlight.patch b/patches/@lezer__highlight.patch new file mode 100644 index 0000000000..f7b5f5a4f8 --- /dev/null +++ b/patches/@lezer__highlight.patch @@ -0,0 +1,12 @@ +diff --git a/package.json b/package.json +index 2d52edb23f0c7defdfbf5f95ad1ee1fa75672b41..8d9dcbdc07cb5c73e629e22696235b19db98940d 100644 +--- a/package.json ++++ b/package.json +@@ -5,6 +5,7 @@ + "main": "dist/index.cjs", + "type": "module", + "exports": { ++ "types": "./dist/highlight.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0305da6edc..986f6f08aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ overrides: date-fns-tz: 2.0.0 patchedDependencies: + '@lezer/highlight': + hash: 97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c + path: patches/@lezer__highlight.patch '@types/express-serve-static-core@5.0.6': hash: d602248fcd302cf5a794d1e85a411633ba9635ea5d566d6f2e0429c7ae0fa3eb path: patches/@types__express-serve-static-core@5.0.6.patch @@ -590,7 +593,7 @@ importers: version: 6.9.3 '@lezer/highlight': specifier: '*' - version: 1.1.1 + version: 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/lr': specifier: ^1.4.0 version: 1.4.0 @@ -18284,7 +18287,7 @@ snapshots: '@codemirror/state': 6.4.1 '@codemirror/view': 6.26.3 '@lezer/common': 1.1.0 - '@lezer/highlight': 1.1.1 + '@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/lr': 1.4.0 style-mod: 4.1.0 @@ -18293,7 +18296,7 @@ snapshots: '@codemirror/state': 6.3.3 '@codemirror/view': 6.22.3 '@lezer/common': 1.1.0 - '@lezer/highlight': 1.1.1 + '@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/lr': 1.4.0 style-mod: 4.1.0 @@ -19467,7 +19470,7 @@ snapshots: '@lezer/css@1.1.1': dependencies: - '@lezer/highlight': 1.1.1 + '@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/lr': 1.4.0 '@lezer/generator@1.7.0': @@ -19475,24 +19478,24 @@ snapshots: '@lezer/common': 1.1.0 '@lezer/lr': 1.4.0 - '@lezer/highlight@1.1.1': + '@lezer/highlight@1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c)': dependencies: '@lezer/common': 1.1.0 '@lezer/html@1.3.0': dependencies: '@lezer/common': 1.1.0 - '@lezer/highlight': 1.1.1 + '@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/lr': 1.4.0 '@lezer/javascript@1.0.2': dependencies: - '@lezer/highlight': 1.1.1 + '@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/lr': 1.4.0 '@lezer/json@1.0.0': dependencies: - '@lezer/highlight': 1.1.1 + '@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/lr': 1.4.0 '@lezer/lr@1.4.0': @@ -19501,7 +19504,7 @@ snapshots: '@lezer/python@1.1.5': dependencies: - '@lezer/highlight': 1.1.1 + '@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/lr': 1.4.0 '@mdx-js/react@3.0.1(@types/react@18.0.27)(react@18.2.0)': @@ -19605,7 +19608,7 @@ snapshots: '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.1.0) '@codemirror/language': 6.10.1 '@codemirror/state': 6.4.1 - '@lezer/highlight': 1.1.1 + '@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/lr': 1.4.0 transitivePeerDependencies: - '@codemirror/view' @@ -23724,7 +23727,7 @@ snapshots: '@codemirror/view': 6.26.3 '@lezer/common': 1.1.0 '@lezer/css': 1.1.1 - '@lezer/highlight': 1.1.1 + '@lezer/highlight': 1.1.1(patch_hash=97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c) '@lezer/html': 1.3.0 '@lezer/lr': 1.4.0