diff --git a/packages/core/src/execution-engine/__tests__/mock-node-types.ts b/packages/core/src/execution-engine/__tests__/mock-node-types.ts new file mode 100644 index 0000000000..bd29e3904d --- /dev/null +++ b/packages/core/src/execution-engine/__tests__/mock-node-types.ts @@ -0,0 +1,71 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeType } from 'n8n-workflow'; + +import { NodeTypes } from '@test/helpers'; + +export const passThroughNode: INodeType = { + description: { + displayName: 'Test Node', + name: 'testNode', + group: ['transform'], + version: 1, + description: 'A minimal node for testing', + defaults: { + name: 'Test Node', + }, + inputs: ['main'], + outputs: ['main'], + properties: [], + }, + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + return await Promise.resolve([items]); + }, +}; + +export const testNodeWithRequiredProperty: INodeType = { + description: { + displayName: 'Test Node with Required Property', + name: 'testNodeWithRequiredProperty', + group: ['transform'], + version: 1, + description: 'A node for testing with required property', + defaults: { + name: 'Test Node with Required Property', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Required Text', + name: 'requiredText', + type: 'string', + default: '', + placeholder: 'Enter some text', + description: 'A required text input', + required: true, + }, + ], + }, + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + return await Promise.resolve([items]); + }, +}; + +const nodeTypeArguments = { + passThrough: { + type: passThroughNode, + sourcePath: '', + }, + testNodeWithRequiredProperty: { + type: testNodeWithRequiredProperty, + sourcePath: '', + }, +}; + +export const nodeTypes = NodeTypes(nodeTypeArguments); + +export const types: Record = { + passThrough: 'passThrough', + testNodeWithRequiredProperty: 'testNodeWithRequiredProperty', +}; diff --git a/packages/core/src/execution-engine/__tests__/workflow-execute-process-process-run-execution-data.test.ts b/packages/core/src/execution-engine/__tests__/workflow-execute-process-process-run-execution-data.test.ts new file mode 100644 index 0000000000..9c2b1cb46d --- /dev/null +++ b/packages/core/src/execution-engine/__tests__/workflow-execute-process-process-run-execution-data.test.ts @@ -0,0 +1,145 @@ +import { mock } from 'jest-mock-extended'; +import type { + IRunExecutionData, + IWorkflowExecuteAdditionalData, + WorkflowExecuteMode, +} from 'n8n-workflow'; +import { ApplicationError } from 'n8n-workflow'; + +import { DirectedGraph } from '../partial-execution-utils'; +import { createNodeData } from '../partial-execution-utils/__tests__/helpers'; +import { WorkflowExecute } from '../workflow-execute'; +import { types, nodeTypes } from './mock-node-types'; + +describe('processRunExecutionData', () => { + const runHook = jest.fn().mockResolvedValue(undefined); + const additionalData = mock({ + hooks: { runHook }, + restartExecutionId: undefined, + }); + const executionMode: WorkflowExecuteMode = 'trigger'; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('throws if execution-data is missing', () => { + // ARRANGE + const node = createNodeData({ name: 'passThrough', type: types.passThrough }); + const workflow = new DirectedGraph() + .addNodes(node) + .toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder: 'v1' } }); + + const executionData: IRunExecutionData = { + startData: { startNodes: [{ name: node.name, sourceData: null }] }, + resultData: { runData: {} }, + }; + + const workflowExecute = new WorkflowExecute(additionalData, executionMode, executionData); + + // ACT & ASSERT + // The function returns a Promise, but throws synchronously, so we can't await it. + // eslint-disable-next-line @typescript-eslint/promise-function-async + expect(() => workflowExecute.processRunExecutionData(workflow)).toThrowError( + new ApplicationError('Failed to run workflow due to missing execution data'), + ); + }); + + test('throws if workflow contains nodes with missing required properties', () => { + // ARRANGE + const node = createNodeData({ name: 'node', type: types.testNodeWithRequiredProperty }); + const workflow = new DirectedGraph() + .addNodes(node) + .toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder: 'v1' } }); + + const taskDataConnection = { main: [[{ json: { foo: 1 } }]] }; + const executionData: IRunExecutionData = { + startData: { startNodes: [{ name: node.name, sourceData: null }] }, + resultData: { runData: {} }, + executionData: { + contextData: {}, + nodeExecutionStack: [{ data: taskDataConnection, node, source: null }], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + const workflowExecute = new WorkflowExecute(additionalData, executionMode, executionData); + + // ACT & ASSERT + // The function returns a Promise, but throws synchronously, so we can't await it. + // eslint-disable-next-line @typescript-eslint/promise-function-async + expect(() => workflowExecute.processRunExecutionData(workflow)).toThrowError( + new ApplicationError( + 'The workflow has issues and cannot be executed for that reason. Please fix them first.', + ), + ); + }); + + test('returns input data verbatim', async () => { + // ARRANGE + const node = createNodeData({ name: 'node', type: types.passThrough }); + const workflow = new DirectedGraph() + .addNodes(node) + .toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder: 'v1' } }); + + const taskDataConnection = { main: [[{ json: { foo: 1 } }]] }; + const executionData: IRunExecutionData = { + startData: { startNodes: [{ name: node.name, sourceData: null }] }, + resultData: { runData: {} }, + executionData: { + contextData: {}, + nodeExecutionStack: [{ data: taskDataConnection, node, source: null }], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + const workflowExecute = new WorkflowExecute(additionalData, executionMode, executionData); + + // ACT + const result = await workflowExecute.processRunExecutionData(workflow); + + // ASSERT + expect(result.data.resultData.runData).toMatchObject({ node: [{ data: taskDataConnection }] }); + }); + + test('calls the right hooks in the right order', async () => { + // ARRANGE + const node1 = createNodeData({ name: 'node1', type: types.passThrough }); + const node2 = createNodeData({ name: 'node2', type: types.passThrough }); + const workflow = new DirectedGraph() + .addNodes(node1, node2) + .addConnections({ from: node1, to: node2 }) + .toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder: 'v1' } }); + + const taskDataConnection = { main: [[{ json: { foo: 1 } }]] }; + const executionData: IRunExecutionData = { + startData: { startNodes: [{ name: node1.name, sourceData: null }] }, + resultData: { runData: {} }, + executionData: { + contextData: {}, + nodeExecutionStack: [{ data: taskDataConnection, node: node1, source: null }], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + const workflowExecute = new WorkflowExecute(additionalData, executionMode, executionData); + + // ACT + await workflowExecute.processRunExecutionData(workflow); + + // ASSERT + expect(runHook).toHaveBeenCalledTimes(6); + expect(runHook).toHaveBeenNthCalledWith(1, 'workflowExecuteBefore', expect.any(Array)); + expect(runHook).toHaveBeenNthCalledWith(2, 'nodeExecuteBefore', expect.any(Array)); + expect(runHook).toHaveBeenNthCalledWith(3, 'nodeExecuteAfter', expect.any(Array)); + expect(runHook).toHaveBeenNthCalledWith(4, 'nodeExecuteBefore', expect.any(Array)); + expect(runHook).toHaveBeenNthCalledWith(5, 'nodeExecuteAfter', expect.any(Array)); + expect(runHook).toHaveBeenNthCalledWith(6, 'workflowExecuteAfter', expect.any(Array)); + }); +}); diff --git a/packages/core/src/execution-engine/workflow-execute.ts b/packages/core/src/execution-engine/workflow-execute.ts index 9dc7b34805..de4682b68c 100644 --- a/packages/core/src/execution-engine/workflow-execute.ts +++ b/packages/core/src/execution-engine/workflow-execute.ts @@ -768,7 +768,7 @@ export class WorkflowExecute { // eslint-disable-next-line @typescript-eslint/no-for-in-array for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) { if ( - !workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent) + !Object.hasOwn(workflow.connectionsBySourceNode[parentNodeName].main, outputIndexParent) ) { continue; } @@ -1091,7 +1091,7 @@ export class WorkflowExecute { * Handles execution of disabled nodes by passing through input data */ private handleDisabledNode(inputData: ITaskDataConnections): IRunNodeResponse { - if (inputData.hasOwnProperty('main') && inputData.main.length > 0) { + if (Object.hasOwn(inputData, 'main') && inputData.main.length > 0) { // If the node is disabled simply return the data from the first main input if (inputData.main[0] === null) { return { data: undefined }; @@ -1649,7 +1649,7 @@ export class WorkflowExecute { // Get the index of the current run runIndex = 0; - if (this.runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) { + if (Object.hasOwn(this.runExecutionData.resultData.runData, executionNode.name)) { runIndex = this.runExecutionData.resultData.runData[executionNode.name].length; } currentExecutionTry = `${executionNode.name}:${runIndex}`; @@ -1849,7 +1849,7 @@ export class WorkflowExecute { // Add the data to return to the user // (currently does not get cloned as data does not get changed, maybe later we should do that?!?!) - if (!this.runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) { + if (!Object.hasOwn(this.runExecutionData.resultData.runData, executionNode.name)) { this.runExecutionData.resultData.runData[executionNode.name] = []; } @@ -1885,7 +1885,7 @@ export class WorkflowExecute { ) ) { // Workflow should continue running even if node errors - if (executionData.data.hasOwnProperty('main') && executionData.data.main.length > 0) { + if (Object.hasOwn(executionData.data, 'main') && executionData.data.main.length > 0) { // Simply get the input data of the node if it has any and pass it through // to the next node if (executionData.data.main[0] !== null) { @@ -1977,8 +1977,8 @@ export class WorkflowExecute { // Add the nodes to which the current node has an output connection to that they can // be executed next - if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) { - if (workflow.connectionsBySourceNode[executionNode.name].hasOwnProperty('main')) { + if (Object.hasOwn(workflow.connectionsBySourceNode, executionNode.name)) { + if (Object.hasOwn(workflow.connectionsBySourceNode[executionNode.name], 'main')) { let outputIndex: string; let connectionData: IConnection; // Iterate over all the outputs @@ -1993,7 +1993,8 @@ export class WorkflowExecute { // eslint-disable-next-line @typescript-eslint/no-for-in-array for (outputIndex in workflow.connectionsBySourceNode[executionNode.name].main) { if ( - !workflow.connectionsBySourceNode[executionNode.name].main.hasOwnProperty( + !Object.hasOwn( + workflow.connectionsBySourceNode[executionNode.name].main, outputIndex, ) ) { @@ -2004,7 +2005,7 @@ export class WorkflowExecute { for (connectionData of workflow.connectionsBySourceNode[executionNode.name].main[ outputIndex ] ?? []) { - if (!workflow.nodes.hasOwnProperty(connectionData.node)) { + if (!Object.hasOwn(workflow.nodes, connectionData.node)) { throw new ApplicationError('Destination node not found', { extra: { sourceNodeName: executionNode.name, @@ -2324,7 +2325,7 @@ export class WorkflowExecute { return true; } - if (!executionData.data.hasOwnProperty('main')) { + if (!Object.hasOwn(executionData.data, 'main')) { // ExecutionData does not even have the connection set up so can // not have that data, so add it again to be executed later this.runExecutionData.executionData!.nodeExecutionStack.push(executionData); @@ -2595,7 +2596,7 @@ export class WorkflowExecute { item: 0, }; } else if (isSameNumberOfItems) { - // The number of oncoming and outcoming items is identical so we can + // The number of incoming and outgoing items is identical so we can // make the reasonable assumption that each of the input items // is the origin of the corresponding output items item.pairedItem = { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index a0e437a043..8fa825a1de 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -4,6 +4,7 @@ "@n8n/typescript-config/tsconfig.backend.json" ], "compilerOptions": { + "lib": ["es2022"], "rootDir": ".", "baseUrl": "src", "paths": {