mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-29 01:39:24 +08:00
feat(core): Add workflowExecuteResume lifecycle hook (#25240)
This commit is contained in:
+7
-1
@@ -43,9 +43,12 @@ describe('OnLifecycleEvent', () => {
|
||||
|
||||
@OnLifecycleEvent('workflowExecuteAfter')
|
||||
async handleWorkflowExecuteAfter() {}
|
||||
|
||||
@OnLifecycleEvent('workflowExecuteResume')
|
||||
async handleWorkflowExecuteResume() {}
|
||||
}
|
||||
|
||||
expect(lifecycleMetadata.register).toHaveBeenCalledTimes(4);
|
||||
expect(lifecycleMetadata.register).toHaveBeenCalledTimes(5);
|
||||
expect(lifecycleMetadata.register).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ eventName: 'nodeExecuteBefore' }),
|
||||
);
|
||||
@@ -58,6 +61,9 @@ describe('OnLifecycleEvent', () => {
|
||||
expect(lifecycleMetadata.register).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ eventName: 'workflowExecuteAfter' }),
|
||||
);
|
||||
expect(lifecycleMetadata.register).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ eventName: 'workflowExecuteResume' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should register multiple handlers in the same class', () => {
|
||||
|
||||
@@ -5,5 +5,6 @@ export type {
|
||||
NodeExecuteAfterContext,
|
||||
WorkflowExecuteBeforeContext,
|
||||
WorkflowExecuteAfterContext,
|
||||
WorkflowExecuteResumeContext,
|
||||
} from './lifecycle-metadata';
|
||||
export { LifecycleMetadata } from './lifecycle-metadata';
|
||||
|
||||
@@ -35,6 +35,7 @@ export type WorkflowExecuteBeforeContext = {
|
||||
workflow: IWorkflowBase;
|
||||
workflowInstance: Workflow;
|
||||
executionData?: IRunExecutionData;
|
||||
executionId: string;
|
||||
};
|
||||
|
||||
export type WorkflowExecuteAfterContext = {
|
||||
@@ -42,6 +43,15 @@ export type WorkflowExecuteAfterContext = {
|
||||
workflow: IWorkflowBase;
|
||||
runData: IRun;
|
||||
newStaticData: IDataObject;
|
||||
executionId: string;
|
||||
};
|
||||
|
||||
export type WorkflowExecuteResumeContext = {
|
||||
type: 'workflowExecuteResume';
|
||||
workflow: IWorkflowBase;
|
||||
workflowInstance: Workflow;
|
||||
executionData: IRunExecutionData;
|
||||
executionId: string;
|
||||
};
|
||||
|
||||
/** Context arg passed to a lifecycle event handler method. */
|
||||
@@ -49,7 +59,8 @@ export type LifecycleContext =
|
||||
| NodeExecuteBeforeContext
|
||||
| NodeExecuteAfterContext
|
||||
| WorkflowExecuteBeforeContext
|
||||
| WorkflowExecuteAfterContext;
|
||||
| WorkflowExecuteAfterContext
|
||||
| WorkflowExecuteResumeContext;
|
||||
|
||||
type LifecycleHandler = {
|
||||
/** Class holding the method to call on a lifecycle event. */
|
||||
|
||||
@@ -326,6 +326,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
expect(handlers.nodeExecuteAfter).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteBefore).toHaveLength(3);
|
||||
expect(handlers.workflowExecuteAfter).toHaveLength(5);
|
||||
expect(handlers.workflowExecuteResume).toHaveLength(0);
|
||||
expect(handlers.nodeFetchedData).toHaveLength(1);
|
||||
expect(handlers.sendResponse).toHaveLength(0);
|
||||
expect(handlers.sendChunk).toHaveLength(0);
|
||||
@@ -733,6 +734,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
expect(handlers.nodeExecuteAfter).toHaveLength(0);
|
||||
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||
expect(handlers.workflowExecuteResume).toHaveLength(0);
|
||||
expect(handlers.nodeFetchedData).toHaveLength(0);
|
||||
expect(handlers.sendResponse).toHaveLength(0);
|
||||
expect(handlers.sendChunk).toHaveLength(0);
|
||||
@@ -868,6 +870,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
expect(handlers.nodeExecuteAfter).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||
expect(handlers.workflowExecuteResume).toHaveLength(0);
|
||||
expect(handlers.nodeFetchedData).toHaveLength(1);
|
||||
expect(handlers.sendResponse).toHaveLength(0);
|
||||
expect(handlers.sendChunk).toHaveLength(0);
|
||||
@@ -992,6 +995,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
expect(handlers.nodeExecuteAfter).toHaveLength(1);
|
||||
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||
expect(handlers.workflowExecuteResume).toHaveLength(0);
|
||||
expect(handlers.nodeFetchedData).toHaveLength(1);
|
||||
expect(handlers.sendResponse).toHaveLength(0);
|
||||
expect(handlers.sendChunk).toHaveLength(0);
|
||||
|
||||
@@ -57,6 +57,7 @@ class ModulesHooksRegistry {
|
||||
workflow: this.workflowData,
|
||||
runData,
|
||||
newStaticData,
|
||||
executionId: this.executionId,
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return await instance[methodName].call(instance, context);
|
||||
@@ -97,6 +98,21 @@ class ModulesHooksRegistry {
|
||||
workflow: this.workflowData,
|
||||
workflowInstance,
|
||||
executionData,
|
||||
executionId: this.executionId,
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return await instance[methodName].call(instance, context);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'workflowExecuteResume':
|
||||
hooks.addHandler(eventName, async function (workflowInstance, executionData) {
|
||||
const context = {
|
||||
type: 'workflowExecuteResume',
|
||||
workflow: this.workflowData,
|
||||
workflowInstance,
|
||||
executionData,
|
||||
executionId: this.executionId,
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return await instance[methodName].call(instance, context);
|
||||
|
||||
@@ -39,6 +39,7 @@ describe('ExecutionLifecycleHooks', () => {
|
||||
sendResponse: [],
|
||||
workflowExecuteAfter: [],
|
||||
workflowExecuteBefore: [],
|
||||
workflowExecuteResume: [],
|
||||
sendChunk: [],
|
||||
});
|
||||
});
|
||||
@@ -63,6 +64,7 @@ describe('ExecutionLifecycleHooks', () => {
|
||||
},
|
||||
{ hook: 'workflowExecuteBefore', args: [mock<Workflow>(), mock<IRunExecutionData>()] },
|
||||
{ hook: 'workflowExecuteAfter', args: [mock<IRun>(), mock<IDataObject>()] },
|
||||
{ hook: 'workflowExecuteResume', args: [mock<Workflow>(), mock<IRunExecutionData>()] },
|
||||
{ hook: 'sendResponse', args: [mock<IExecuteResponsePromiseData>()] },
|
||||
{ hook: 'nodeFetchedData', args: ['workflow123', mock<INode>()] },
|
||||
];
|
||||
|
||||
@@ -464,6 +464,79 @@ describe('WorkflowExecute', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflowExecuteResume hook', () => {
|
||||
const executionMode = 'manual';
|
||||
const executionOrder = 'v1';
|
||||
const nodeTypes = Helpers.NodeTypes();
|
||||
|
||||
test('should call workflowExecuteResume instead of workflowExecuteBefore when restartExecutionId is set', async () => {
|
||||
// ARRANGE
|
||||
const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' });
|
||||
const node1 = createNodeData({ name: 'node1' });
|
||||
const workflowInstance = new DirectedGraph()
|
||||
.addNodes(trigger, node1)
|
||||
.addConnections({ from: trigger, to: node1 })
|
||||
.toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder } });
|
||||
|
||||
const additionalData = Helpers.WorkflowExecuteAdditionalData(createDeferredPromise<IRun>());
|
||||
// Set restartExecutionId to simulate a resumed execution
|
||||
additionalData.restartExecutionId = 'previous-execution-id';
|
||||
const runHookSpy = jest.spyOn(additionalData.hooks!, 'runHook');
|
||||
|
||||
const workflowExecute = new WorkflowExecute(additionalData, executionMode);
|
||||
|
||||
// ACT
|
||||
await workflowExecute.run({ workflow: workflowInstance, startNode: trigger });
|
||||
|
||||
// ASSERT
|
||||
const workflowHooks = runHookSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
call[0] === 'workflowExecuteBefore' ||
|
||||
call[0] === 'workflowExecuteAfter' ||
|
||||
call[0] === 'workflowExecuteResume',
|
||||
);
|
||||
|
||||
// Should have workflowExecuteResume instead of workflowExecuteBefore
|
||||
expect(workflowHooks.map((hook) => hook[0])).toEqual([
|
||||
'workflowExecuteResume',
|
||||
'workflowExecuteAfter',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should call workflowExecuteBefore when restartExecutionId is not set', async () => {
|
||||
// ARRANGE
|
||||
const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' });
|
||||
const node1 = createNodeData({ name: 'node1' });
|
||||
const workflowInstance = new DirectedGraph()
|
||||
.addNodes(trigger, node1)
|
||||
.addConnections({ from: trigger, to: node1 })
|
||||
.toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder } });
|
||||
|
||||
const additionalData = Helpers.WorkflowExecuteAdditionalData(createDeferredPromise<IRun>());
|
||||
// restartExecutionId is undefined by default
|
||||
const runHookSpy = jest.spyOn(additionalData.hooks!, 'runHook');
|
||||
|
||||
const workflowExecute = new WorkflowExecute(additionalData, executionMode);
|
||||
|
||||
// ACT
|
||||
await workflowExecute.run({ workflow: workflowInstance, startNode: trigger });
|
||||
|
||||
// ASSERT
|
||||
const workflowHooks = runHookSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
call[0] === 'workflowExecuteBefore' ||
|
||||
call[0] === 'workflowExecuteAfter' ||
|
||||
call[0] === 'workflowExecuteResume',
|
||||
);
|
||||
|
||||
// Should have workflowExecuteBefore, not workflowExecuteResume
|
||||
expect(workflowHooks.map((hook) => hook[0])).toEqual([
|
||||
'workflowExecuteBefore',
|
||||
'workflowExecuteAfter',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
//run tests on json files from specified directory, default 'workflows'
|
||||
//workflows must have pinned data that would be used to test output after execution
|
||||
describe('run test workflows', () => {
|
||||
|
||||
@@ -38,6 +38,14 @@ export type ExecutionLifecycleHookHandlers = {
|
||||
) => Promise<void> | void
|
||||
>;
|
||||
|
||||
workflowExecuteResume: Array<
|
||||
(
|
||||
this: ExecutionLifecycleHooks,
|
||||
workflow: Workflow,
|
||||
data?: IRunExecutionData,
|
||||
) => Promise<void> | void
|
||||
>;
|
||||
|
||||
workflowExecuteAfter: Array<
|
||||
(this: ExecutionLifecycleHooks, data: IRun, newStaticData: IDataObject) => Promise<void> | void
|
||||
>;
|
||||
@@ -88,6 +96,7 @@ export class ExecutionLifecycleHooks {
|
||||
sendResponse: [],
|
||||
workflowExecuteAfter: [],
|
||||
workflowExecuteBefore: [],
|
||||
workflowExecuteResume: [],
|
||||
sendChunk: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -1438,6 +1438,8 @@ export class WorkflowExecute {
|
||||
|
||||
if (!this.additionalData.restartExecutionId) {
|
||||
await hooks.runHook('workflowExecuteBefore', [workflow, this.runExecutionData]);
|
||||
} else {
|
||||
await hooks.runHook('workflowExecuteResume', [workflow, this.runExecutionData]);
|
||||
}
|
||||
} catch (error) {
|
||||
const e = error as unknown as ExecutionBaseError;
|
||||
|
||||
Reference in New Issue
Block a user