mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
refactor(core): Fix lints, improve comment and add initial blackbox tests for the engine (#18748)
This commit is contained in:
@@ -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<INodeExecutionData[][]> {
|
||||||
|
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<INodeExecutionData[][]> {
|
||||||
|
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<keyof typeof nodeTypeArguments, string> = {
|
||||||
|
passThrough: 'passThrough',
|
||||||
|
testNodeWithRequiredProperty: 'testNodeWithRequiredProperty',
|
||||||
|
};
|
||||||
@@ -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<IWorkflowExecuteAdditionalData>({
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -768,7 +768,7 @@ export class WorkflowExecute {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||||
for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) {
|
for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName].main) {
|
||||||
if (
|
if (
|
||||||
!workflow.connectionsBySourceNode[parentNodeName].main.hasOwnProperty(outputIndexParent)
|
!Object.hasOwn(workflow.connectionsBySourceNode[parentNodeName].main, outputIndexParent)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1091,7 +1091,7 @@ export class WorkflowExecute {
|
|||||||
* Handles execution of disabled nodes by passing through input data
|
* Handles execution of disabled nodes by passing through input data
|
||||||
*/
|
*/
|
||||||
private handleDisabledNode(inputData: ITaskDataConnections): IRunNodeResponse {
|
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 the node is disabled simply return the data from the first main input
|
||||||
if (inputData.main[0] === null) {
|
if (inputData.main[0] === null) {
|
||||||
return { data: undefined };
|
return { data: undefined };
|
||||||
@@ -1649,7 +1649,7 @@ export class WorkflowExecute {
|
|||||||
|
|
||||||
// Get the index of the current run
|
// Get the index of the current run
|
||||||
runIndex = 0;
|
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;
|
runIndex = this.runExecutionData.resultData.runData[executionNode.name].length;
|
||||||
}
|
}
|
||||||
currentExecutionTry = `${executionNode.name}:${runIndex}`;
|
currentExecutionTry = `${executionNode.name}:${runIndex}`;
|
||||||
@@ -1849,7 +1849,7 @@ export class WorkflowExecute {
|
|||||||
// Add the data to return to the user
|
// 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?!?!)
|
// (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] = [];
|
this.runExecutionData.resultData.runData[executionNode.name] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1885,7 +1885,7 @@ export class WorkflowExecute {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Workflow should continue running even if node errors
|
// 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
|
// Simply get the input data of the node if it has any and pass it through
|
||||||
// to the next node
|
// to the next node
|
||||||
if (executionData.data.main[0] !== null) {
|
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
|
// Add the nodes to which the current node has an output connection to that they can
|
||||||
// be executed next
|
// be executed next
|
||||||
if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) {
|
if (Object.hasOwn(workflow.connectionsBySourceNode, executionNode.name)) {
|
||||||
if (workflow.connectionsBySourceNode[executionNode.name].hasOwnProperty('main')) {
|
if (Object.hasOwn(workflow.connectionsBySourceNode[executionNode.name], 'main')) {
|
||||||
let outputIndex: string;
|
let outputIndex: string;
|
||||||
let connectionData: IConnection;
|
let connectionData: IConnection;
|
||||||
// Iterate over all the outputs
|
// Iterate over all the outputs
|
||||||
@@ -1993,7 +1993,8 @@ export class WorkflowExecute {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||||
for (outputIndex in workflow.connectionsBySourceNode[executionNode.name].main) {
|
for (outputIndex in workflow.connectionsBySourceNode[executionNode.name].main) {
|
||||||
if (
|
if (
|
||||||
!workflow.connectionsBySourceNode[executionNode.name].main.hasOwnProperty(
|
!Object.hasOwn(
|
||||||
|
workflow.connectionsBySourceNode[executionNode.name].main,
|
||||||
outputIndex,
|
outputIndex,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -2004,7 +2005,7 @@ export class WorkflowExecute {
|
|||||||
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 (!Object.hasOwn(workflow.nodes, connectionData.node)) {
|
||||||
throw new ApplicationError('Destination node not found', {
|
throw new ApplicationError('Destination node not found', {
|
||||||
extra: {
|
extra: {
|
||||||
sourceNodeName: executionNode.name,
|
sourceNodeName: executionNode.name,
|
||||||
@@ -2324,7 +2325,7 @@ export class WorkflowExecute {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!executionData.data.hasOwnProperty('main')) {
|
if (!Object.hasOwn(executionData.data, 'main')) {
|
||||||
// ExecutionData does not even have the connection set up so can
|
// ExecutionData does not even have the connection set up so can
|
||||||
// not have that data, so add it again to be executed later
|
// not have that data, so add it again to be executed later
|
||||||
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
|
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
|
||||||
@@ -2595,7 +2596,7 @@ export class WorkflowExecute {
|
|||||||
item: 0,
|
item: 0,
|
||||||
};
|
};
|
||||||
} else if (isSameNumberOfItems) {
|
} 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
|
// make the reasonable assumption that each of the input items
|
||||||
// is the origin of the corresponding output items
|
// is the origin of the corresponding output items
|
||||||
item.pairedItem = {
|
item.pairedItem = {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"@n8n/typescript-config/tsconfig.backend.json"
|
"@n8n/typescript-config/tsconfig.backend.json"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"lib": ["es2022"],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"baseUrl": "src",
|
"baseUrl": "src",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
Reference in New Issue
Block a user