mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +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
|
||||
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 = {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"@n8n/typescript-config/tsconfig.backend.json"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["es2022"],
|
||||
"rootDir": ".",
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
|
||||
Reference in New Issue
Block a user