mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor(core): Move execution engine code out of n8n-workflow (no-changelog) (#12147)
This commit is contained in:
committed by
GitHub
parent
73f0c4cca9
commit
5a055ed526
@@ -675,7 +675,6 @@ describe('NodeExecuteFunctions', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll();
|
||||
nock.disableNetConnect();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
||||
2149
packages/core/test/RoutingNode.test.ts
Normal file
2149
packages/core/test/RoutingNode.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
157
packages/core/test/TriggersAndPollers.test.ts
Normal file
157
packages/core/test/TriggersAndPollers.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import type {
|
||||
Workflow,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
IPollFunctions,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
INodeType,
|
||||
INodeTypes,
|
||||
ITriggerFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { TriggersAndPollers } from '@/TriggersAndPollers';
|
||||
|
||||
describe('TriggersAndPollers', () => {
|
||||
const node = mock<INode>();
|
||||
const nodeType = mock<INodeType>({
|
||||
trigger: undefined,
|
||||
poll: undefined,
|
||||
});
|
||||
const nodeTypes = mock<INodeTypes>();
|
||||
const workflow = mock<Workflow>({ nodeTypes });
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({
|
||||
hooks: {
|
||||
hookFunctions: {
|
||||
sendResponse: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
const triggersAndPollers = new TriggersAndPollers();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
|
||||
});
|
||||
|
||||
describe('runTrigger()', () => {
|
||||
const triggerFunctions = mock<ITriggerFunctions>();
|
||||
const getTriggerFunctions = jest.fn().mockReturnValue(triggerFunctions);
|
||||
const triggerFn = jest.fn();
|
||||
|
||||
it('should throw error if node type does not have trigger function', async () => {
|
||||
await expect(
|
||||
triggersAndPollers.runTrigger(
|
||||
workflow,
|
||||
node,
|
||||
getTriggerFunctions,
|
||||
additionalData,
|
||||
'trigger',
|
||||
'init',
|
||||
),
|
||||
).rejects.toThrow(ApplicationError);
|
||||
});
|
||||
|
||||
it('should call trigger function in regular mode', async () => {
|
||||
nodeType.trigger = triggerFn;
|
||||
triggerFn.mockResolvedValue({ test: true });
|
||||
|
||||
const result = await triggersAndPollers.runTrigger(
|
||||
workflow,
|
||||
node,
|
||||
getTriggerFunctions,
|
||||
additionalData,
|
||||
'trigger',
|
||||
'init',
|
||||
);
|
||||
|
||||
expect(triggerFn).toHaveBeenCalled();
|
||||
expect(result).toEqual({ test: true });
|
||||
});
|
||||
|
||||
it('should handle manual mode with promise resolution', async () => {
|
||||
const mockEmitData: INodeExecutionData[][] = [[{ json: { data: 'test' } }]];
|
||||
const mockTriggerResponse = { workflowId: '123' };
|
||||
|
||||
nodeType.trigger = triggerFn;
|
||||
triggerFn.mockResolvedValue(mockTriggerResponse);
|
||||
|
||||
const result = await triggersAndPollers.runTrigger(
|
||||
workflow,
|
||||
node,
|
||||
getTriggerFunctions,
|
||||
additionalData,
|
||||
'manual',
|
||||
'init',
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.manualTriggerResponse).toBeInstanceOf(Promise);
|
||||
|
||||
// Simulate emit
|
||||
const mockTriggerFunctions = getTriggerFunctions.mock.results[0]?.value;
|
||||
if (mockTriggerFunctions?.emit) {
|
||||
mockTriggerFunctions.emit(mockEmitData);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle error emission in manual mode', async () => {
|
||||
const testError = new Error('Test error');
|
||||
|
||||
nodeType.trigger = triggerFn;
|
||||
triggerFn.mockResolvedValue({});
|
||||
|
||||
const result = await triggersAndPollers.runTrigger(
|
||||
workflow,
|
||||
node,
|
||||
getTriggerFunctions,
|
||||
additionalData,
|
||||
'manual',
|
||||
'init',
|
||||
);
|
||||
|
||||
expect(result?.manualTriggerResponse).toBeInstanceOf(Promise);
|
||||
|
||||
// Simulate error
|
||||
const mockTriggerFunctions = getTriggerFunctions.mock.results[0]?.value;
|
||||
if (mockTriggerFunctions?.emitError) {
|
||||
mockTriggerFunctions.emitError(testError);
|
||||
}
|
||||
|
||||
await expect(result?.manualTriggerResponse).rejects.toThrow(testError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runPoll()', () => {
|
||||
const pollFunctions = mock<IPollFunctions>();
|
||||
const pollFn = jest.fn();
|
||||
|
||||
it('should throw error if node type does not have poll function', async () => {
|
||||
await expect(triggersAndPollers.runPoll(workflow, node, pollFunctions)).rejects.toThrow(
|
||||
ApplicationError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call poll function and return result', async () => {
|
||||
const mockPollResult: INodeExecutionData[][] = [[{ json: { data: 'test' } }]];
|
||||
nodeType.poll = pollFn;
|
||||
pollFn.mockResolvedValue(mockPollResult);
|
||||
|
||||
const result = await triggersAndPollers.runPoll(workflow, node, pollFunctions);
|
||||
|
||||
expect(pollFn).toHaveBeenCalled();
|
||||
expect(result).toBe(mockPollResult);
|
||||
});
|
||||
|
||||
it('should return null if poll function returns no data', async () => {
|
||||
nodeType.poll = pollFn;
|
||||
pollFn.mockResolvedValue(null);
|
||||
|
||||
const result = await triggersAndPollers.runPoll(workflow, node, pollFunctions);
|
||||
|
||||
expect(pollFn).toHaveBeenCalled();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,12 +9,26 @@
|
||||
// XX denotes that the node is disabled
|
||||
// PD denotes that the node has pinned data
|
||||
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { pick } from 'lodash';
|
||||
import type { IPinData, IRun, IRunData, WorkflowTestData } from 'n8n-workflow';
|
||||
import type {
|
||||
IExecuteData,
|
||||
INode,
|
||||
INodeType,
|
||||
INodeTypes,
|
||||
IPinData,
|
||||
IRun,
|
||||
IRunData,
|
||||
IRunExecutionData,
|
||||
ITriggerResponse,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
WorkflowTestData,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
ApplicationError,
|
||||
createDeferredPromise,
|
||||
NodeExecutionOutput,
|
||||
NodeHelpers,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
@@ -444,4 +458,150 @@ describe('WorkflowExecute', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkReadyForExecution', () => {
|
||||
const disabledNode = mock<INode>({ name: 'Disabled Node', disabled: true });
|
||||
const startNode = mock<INode>({ name: 'Start Node' });
|
||||
const unknownNode = mock<INode>({ name: 'Unknown Node', type: 'unknownNode' });
|
||||
|
||||
const nodeParamIssuesSpy = jest.spyOn(NodeHelpers, 'getNodeParametersIssues');
|
||||
|
||||
const nodeTypes = mock<INodeTypes>();
|
||||
nodeTypes.getByNameAndVersion.mockImplementation((type) => {
|
||||
// TODO: getByNameAndVersion signature needs to be updated to allow returning undefined
|
||||
if (type === 'unknownNode') return undefined as unknown as INodeType;
|
||||
return mock<INodeType>({
|
||||
description: {
|
||||
properties: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
const workflowExecute = new WorkflowExecute(mock(), 'manual');
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('should return null if there are no nodes', () => {
|
||||
const workflow = new Workflow({
|
||||
nodes: [],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const issues = workflowExecute.checkReadyForExecution(workflow);
|
||||
expect(issues).toBe(null);
|
||||
expect(nodeTypes.getByNameAndVersion).not.toHaveBeenCalled();
|
||||
expect(nodeParamIssuesSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null if there are no enabled nodes', () => {
|
||||
const workflow = new Workflow({
|
||||
nodes: [disabledNode],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const issues = workflowExecute.checkReadyForExecution(workflow, {
|
||||
startNode: disabledNode.name,
|
||||
});
|
||||
expect(issues).toBe(null);
|
||||
expect(nodeTypes.getByNameAndVersion).toHaveBeenCalledTimes(1);
|
||||
expect(nodeParamIssuesSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return typeUnknown for unknown nodes', () => {
|
||||
const workflow = new Workflow({
|
||||
nodes: [unknownNode],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const issues = workflowExecute.checkReadyForExecution(workflow, {
|
||||
startNode: unknownNode.name,
|
||||
});
|
||||
expect(issues).toEqual({ [unknownNode.name]: { typeUnknown: true } });
|
||||
expect(nodeTypes.getByNameAndVersion).toHaveBeenCalledTimes(2);
|
||||
expect(nodeParamIssuesSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return issues for regular nodes', () => {
|
||||
const workflow = new Workflow({
|
||||
nodes: [startNode],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
nodeParamIssuesSpy.mockReturnValue({ execution: false });
|
||||
|
||||
const issues = workflowExecute.checkReadyForExecution(workflow, {
|
||||
startNode: startNode.name,
|
||||
});
|
||||
expect(issues).toEqual({ [startNode.name]: { execution: false } });
|
||||
expect(nodeTypes.getByNameAndVersion).toHaveBeenCalledTimes(2);
|
||||
expect(nodeParamIssuesSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('runNode', () => {
|
||||
const nodeTypes = mock<INodeTypes>();
|
||||
const triggerNode = mock<INode>();
|
||||
const triggerResponse = mock<ITriggerResponse>({
|
||||
closeFunction: jest.fn(),
|
||||
// This node should never trigger, or return
|
||||
manualTriggerFunction: async () => await new Promise(() => {}),
|
||||
});
|
||||
const triggerNodeType = mock<INodeType>({
|
||||
description: {
|
||||
properties: [],
|
||||
},
|
||||
execute: undefined,
|
||||
poll: undefined,
|
||||
webhook: undefined,
|
||||
async trigger() {
|
||||
return triggerResponse;
|
||||
},
|
||||
});
|
||||
|
||||
nodeTypes.getByNameAndVersion.mockReturnValue(triggerNodeType);
|
||||
|
||||
const workflow = new Workflow({
|
||||
nodeTypes,
|
||||
nodes: [triggerNode],
|
||||
connections: {},
|
||||
active: false,
|
||||
});
|
||||
|
||||
const executionData = mock<IExecuteData>();
|
||||
const runExecutionData = mock<IRunExecutionData>();
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>();
|
||||
const abortController = new AbortController();
|
||||
const workflowExecute = new WorkflowExecute(additionalData, 'manual');
|
||||
|
||||
test('should call closeFunction when manual trigger is aborted', async () => {
|
||||
const runPromise = workflowExecute.runNode(
|
||||
workflow,
|
||||
executionData,
|
||||
runExecutionData,
|
||||
0,
|
||||
additionalData,
|
||||
'manual',
|
||||
abortController.signal,
|
||||
);
|
||||
// Yield back to the event-loop to let async parts of `runNode` execute
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
let isSettled = false;
|
||||
void runPromise.then(() => {
|
||||
isSettled = true;
|
||||
});
|
||||
expect(isSettled).toBe(false);
|
||||
expect(abortController.signal.aborted).toBe(false);
|
||||
expect(triggerResponse.closeFunction).not.toHaveBeenCalled();
|
||||
|
||||
abortController.abort();
|
||||
expect(triggerResponse.closeFunction).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,6 +102,89 @@ export const predefinedNodesTypes: INodeTypeData = {
|
||||
},
|
||||
},
|
||||
},
|
||||
'test.set': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets a value',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: [NodeConnectionType.Main],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'test.setMulti': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set Multi',
|
||||
name: 'setMulti',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets multiple values',
|
||||
defaults: {
|
||||
name: 'Set Multi',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: [NodeConnectionType.Main],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
placeholder: 'Name of the property to write data to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'The string value to write in the property.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const legacyWorkflowExecuteTests: WorkflowTestData[] = [
|
||||
|
||||
@@ -24,7 +24,7 @@ import { predefinedNodesTypes } from './constants';
|
||||
const BASE_DIR = path.resolve(__dirname, '../../..');
|
||||
|
||||
class NodeTypesClass implements INodeTypes {
|
||||
constructor(private nodeTypes: INodeTypeData = predefinedNodesTypes) {}
|
||||
constructor(private nodeTypes: INodeTypeData) {}
|
||||
|
||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||
return this.nodeTypes[nodeType].type;
|
||||
@@ -41,7 +41,7 @@ class NodeTypesClass implements INodeTypes {
|
||||
|
||||
let nodeTypesInstance: NodeTypesClass | undefined;
|
||||
|
||||
export function NodeTypes(nodeTypes?: INodeTypeData): INodeTypes {
|
||||
export function NodeTypes(nodeTypes: INodeTypeData = predefinedNodesTypes): INodeTypes {
|
||||
if (nodeTypesInstance === undefined || nodeTypes !== undefined) {
|
||||
nodeTypesInstance = new NodeTypesClass(nodeTypes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user