mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Move ExecutionLifecycleHooks to core (#13042)
This commit is contained in:
parent
cae98e733d
commit
d41ca832dc
|
@ -12,17 +12,14 @@ import type {
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
StartNodeData,
|
StartNodeData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import { Workflow, type ExecutionError } from 'n8n-workflow';
|
||||||
Workflow,
|
|
||||||
WorkflowHooks,
|
|
||||||
type ExecutionError,
|
|
||||||
type IWorkflowExecuteHooks,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import PCancelable from 'p-cancelable';
|
import PCancelable from 'p-cancelable';
|
||||||
|
|
||||||
import { ActiveExecutions } from '@/active-executions';
|
import { ActiveExecutions } from '@/active-executions';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import type { ExecutionEntity } from '@/databases/entities/execution-entity';
|
||||||
import type { User } from '@/databases/entities/user';
|
import type { User } from '@/databases/entities/user';
|
||||||
|
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
||||||
import { ExecutionNotFoundError } from '@/errors/execution-not-found-error';
|
import { ExecutionNotFoundError } from '@/errors/execution-not-found-error';
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
import { PermissionChecker } from '@/user-management/permission-checker';
|
import { PermissionChecker } from '@/user-management/permission-checker';
|
||||||
|
@ -36,25 +33,14 @@ import { setupTestServer } from '@test-integration/utils';
|
||||||
|
|
||||||
let owner: User;
|
let owner: User;
|
||||||
let runner: WorkflowRunner;
|
let runner: WorkflowRunner;
|
||||||
let hookFunctions: IWorkflowExecuteHooks;
|
|
||||||
setupTestServer({ endpointGroups: [] });
|
setupTestServer({ endpointGroups: [] });
|
||||||
|
|
||||||
mockInstance(Telemetry);
|
mockInstance(Telemetry);
|
||||||
|
|
||||||
class Watchers {
|
|
||||||
workflowExecuteAfter = jest.fn();
|
|
||||||
}
|
|
||||||
const watchers = new Watchers();
|
|
||||||
const watchedWorkflowExecuteAfter = jest.spyOn(watchers, 'workflowExecuteAfter');
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
owner = await createUser({ role: 'global:owner' });
|
owner = await createUser({ role: 'global:owner' });
|
||||||
|
|
||||||
runner = Container.get(WorkflowRunner);
|
runner = Container.get(WorkflowRunner);
|
||||||
|
|
||||||
hookFunctions = {
|
|
||||||
workflowExecuteAfter: [watchers.workflowExecuteAfter],
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
@ -67,6 +53,20 @@ beforeEach(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('processError', () => {
|
describe('processError', () => {
|
||||||
|
let workflow: WorkflowEntity;
|
||||||
|
let execution: ExecutionEntity;
|
||||||
|
let hooks: core.ExecutionLifecycleHooks;
|
||||||
|
|
||||||
|
const watcher = mock<{ workflowExecuteAfter: () => Promise<void> }>();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
workflow = await createWorkflow({}, owner);
|
||||||
|
execution = await createExecution({ status: 'success', finished: true }, workflow);
|
||||||
|
hooks = new core.ExecutionLifecycleHooks('webhook', execution.id, workflow);
|
||||||
|
hooks.addHandler('workflowExecuteAfter', watcher.workflowExecuteAfter);
|
||||||
|
});
|
||||||
|
|
||||||
test('processError should return early in Bull stalled edge case', async () => {
|
test('processError should return early in Bull stalled edge case', async () => {
|
||||||
const workflow = await createWorkflow({}, owner);
|
const workflow = await createWorkflow({}, owner);
|
||||||
const execution = await createExecution(
|
const execution = await createExecution(
|
||||||
|
@ -82,9 +82,9 @@ describe('processError', () => {
|
||||||
new Date(),
|
new Date(),
|
||||||
'webhook',
|
'webhook',
|
||||||
execution.id,
|
execution.id,
|
||||||
new WorkflowHooks(hookFunctions, 'webhook', execution.id, workflow),
|
hooks,
|
||||||
);
|
);
|
||||||
expect(watchedWorkflowExecuteAfter).toHaveBeenCalledTimes(0);
|
expect(watcher.workflowExecuteAfter).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('processError should return early if the error is `ExecutionNotFoundError`', async () => {
|
test('processError should return early if the error is `ExecutionNotFoundError`', async () => {
|
||||||
|
@ -95,9 +95,9 @@ describe('processError', () => {
|
||||||
new Date(),
|
new Date(),
|
||||||
'webhook',
|
'webhook',
|
||||||
execution.id,
|
execution.id,
|
||||||
new WorkflowHooks(hookFunctions, 'webhook', execution.id, workflow),
|
hooks,
|
||||||
);
|
);
|
||||||
expect(watchedWorkflowExecuteAfter).toHaveBeenCalledTimes(0);
|
expect(watcher.workflowExecuteAfter).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('processError should process error', async () => {
|
test('processError should process error', async () => {
|
||||||
|
@ -119,9 +119,9 @@ describe('processError', () => {
|
||||||
new Date(),
|
new Date(),
|
||||||
'webhook',
|
'webhook',
|
||||||
execution.id,
|
execution.id,
|
||||||
new WorkflowHooks(hookFunctions, 'webhook', execution.id, workflow),
|
hooks,
|
||||||
);
|
);
|
||||||
expect(watchedWorkflowExecuteAfter).toHaveBeenCalledTimes(1);
|
expect(watcher.workflowExecuteAfter).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { stringify } from 'flatted';
|
import { stringify } from 'flatted';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import { BinaryDataService, ErrorReporter, InstanceSettings, Logger } from 'n8n-core';
|
import {
|
||||||
import { ExpressionError, WorkflowHooks } from 'n8n-workflow';
|
BinaryDataService,
|
||||||
|
ErrorReporter,
|
||||||
|
InstanceSettings,
|
||||||
|
Logger,
|
||||||
|
ExecutionLifecycleHooks,
|
||||||
|
} from 'n8n-core';
|
||||||
|
import { ExpressionError } from 'n8n-workflow';
|
||||||
import type {
|
import type {
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
|
@ -10,6 +16,7 @@ import type {
|
||||||
IRun,
|
IRun,
|
||||||
INode,
|
INode,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
@ -25,10 +32,10 @@ import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.serv
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getWorkflowHooksIntegrated,
|
getLifecycleHooksForSubExecutions,
|
||||||
getWorkflowHooksMain,
|
getLifecycleHooksForRegularMain,
|
||||||
getWorkflowHooksWorkerExecuter,
|
getLifecycleHooksForScalingWorker,
|
||||||
getWorkflowHooksWorkerMain,
|
getLifecycleHooksForScalingMain,
|
||||||
} from '../execution-lifecycle-hooks';
|
} from '../execution-lifecycle-hooks';
|
||||||
|
|
||||||
describe('Execution Lifecycle Hooks', () => {
|
describe('Execution Lifecycle Hooks', () => {
|
||||||
|
@ -79,14 +86,13 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
waitTill: new Date(),
|
waitTill: new Date(),
|
||||||
});
|
});
|
||||||
const expressionError = new ExpressionError('Error');
|
const expressionError = new ExpressionError('Error');
|
||||||
const executionMode = 'manual';
|
|
||||||
const pushRef = 'test-push-ref';
|
const pushRef = 'test-push-ref';
|
||||||
const retryOf = 'test-retry-of';
|
const retryOf = 'test-retry-of';
|
||||||
|
|
||||||
const now = new Date('2025-01-13T18:25:50.267Z');
|
const now = new Date('2025-01-13T18:25:50.267Z');
|
||||||
jest.useFakeTimers({ now });
|
jest.useFakeTimers({ now });
|
||||||
|
|
||||||
let hooks: WorkflowHooks;
|
let lifecycleHooks: ExecutionLifecycleHooks;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
@ -107,7 +113,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
const workflowEventTests = () => {
|
const workflowEventTests = () => {
|
||||||
describe('workflowExecuteBefore', () => {
|
describe('workflowExecuteBefore', () => {
|
||||||
it('should emit workflow-pre-execute events', async () => {
|
it('should emit workflow-pre-execute events', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteBefore', [workflow, runExecutionData]);
|
await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||||
|
|
||||||
expect(eventService.emit).toHaveBeenCalledWith('workflow-pre-execute', {
|
expect(eventService.emit).toHaveBeenCalledWith('workflow-pre-execute', {
|
||||||
executionId,
|
executionId,
|
||||||
|
@ -118,7 +124,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
describe('workflowExecuteAfter', () => {
|
describe('workflowExecuteAfter', () => {
|
||||||
it('should emit workflow-post-execute events', async () => {
|
it('should emit workflow-post-execute events', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||||
|
|
||||||
expect(eventService.emit).toHaveBeenCalledWith('workflow-post-execute', {
|
expect(eventService.emit).toHaveBeenCalledWith('workflow-post-execute', {
|
||||||
executionId,
|
executionId,
|
||||||
|
@ -128,7 +134,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not emit workflow-post-execute events for waiting executions', async () => {
|
it('should not emit workflow-post-execute events for waiting executions', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [waitingRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [waitingRun, {}]);
|
||||||
|
|
||||||
expect(eventService.emit).not.toHaveBeenCalledWith('workflow-post-execute');
|
expect(eventService.emit).not.toHaveBeenCalledWith('workflow-post-execute');
|
||||||
});
|
});
|
||||||
|
@ -138,7 +144,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
const nodeEventsTests = () => {
|
const nodeEventsTests = () => {
|
||||||
describe('nodeExecuteBefore', () => {
|
describe('nodeExecuteBefore', () => {
|
||||||
it('should emit node-pre-execute event', async () => {
|
it('should emit node-pre-execute event', async () => {
|
||||||
await hooks.executeHookFunctions('nodeExecuteBefore', [nodeName]);
|
await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName]);
|
||||||
|
|
||||||
expect(eventService.emit).toHaveBeenCalledWith('node-pre-execute', {
|
expect(eventService.emit).toHaveBeenCalledWith('node-pre-execute', {
|
||||||
executionId,
|
executionId,
|
||||||
|
@ -150,11 +156,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
describe('nodeExecuteAfter', () => {
|
describe('nodeExecuteAfter', () => {
|
||||||
it('should emit node-post-execute event', async () => {
|
it('should emit node-post-execute event', async () => {
|
||||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||||
nodeName,
|
|
||||||
taskData,
|
|
||||||
runExecutionData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(eventService.emit).toHaveBeenCalledWith('node-post-execute', {
|
expect(eventService.emit).toHaveBeenCalledWith('node-post-execute', {
|
||||||
executionId,
|
executionId,
|
||||||
|
@ -168,18 +170,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
const externalHooksTests = () => {
|
const externalHooksTests = () => {
|
||||||
describe('workflowExecuteBefore', () => {
|
describe('workflowExecuteBefore', () => {
|
||||||
it('should run workflow.preExecute hook', async () => {
|
it('should run workflow.preExecute hook', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteBefore', [workflow, runExecutionData]);
|
await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||||
|
|
||||||
expect(externalHooks.run).toHaveBeenCalledWith('workflow.preExecute', [
|
expect(externalHooks.run).toHaveBeenCalledWith('workflow.preExecute', [workflow, 'manual']);
|
||||||
workflow,
|
|
||||||
executionMode,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('workflowExecuteAfter', () => {
|
describe('workflowExecuteAfter', () => {
|
||||||
it('should run workflow.postExecute hook', async () => {
|
it('should run workflow.postExecute hook', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||||
|
|
||||||
expect(externalHooks.run).toHaveBeenCalledWith('workflow.postExecute', [
|
expect(externalHooks.run).toHaveBeenCalledWith('workflow.postExecute', [
|
||||||
successfulRun,
|
successfulRun,
|
||||||
|
@ -193,7 +192,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
const statisticsTests = () => {
|
const statisticsTests = () => {
|
||||||
describe('statistics events', () => {
|
describe('statistics events', () => {
|
||||||
it('workflowExecuteAfter should emit workflowExecutionCompleted statistics event', async () => {
|
it('workflowExecuteAfter should emit workflowExecutionCompleted statistics event', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||||
|
|
||||||
expect(workflowStatisticsService.emit).toHaveBeenCalledWith('workflowExecutionCompleted', {
|
expect(workflowStatisticsService.emit).toHaveBeenCalledWith('workflowExecutionCompleted', {
|
||||||
workflowData,
|
workflowData,
|
||||||
|
@ -202,7 +201,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('nodeFetchedData should handle nodeFetchedData statistics event', async () => {
|
it('nodeFetchedData should handle nodeFetchedData statistics event', async () => {
|
||||||
await hooks.executeHookFunctions('nodeFetchedData', [workflowId, node]);
|
await lifecycleHooks.runHook('nodeFetchedData', [workflowId, node]);
|
||||||
|
|
||||||
expect(workflowStatisticsService.emit).toHaveBeenCalledWith('nodeFetchedData', {
|
expect(workflowStatisticsService.emit).toHaveBeenCalledWith('nodeFetchedData', {
|
||||||
workflowId,
|
workflowId,
|
||||||
|
@ -212,12 +211,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('getWorkflowHooksMain', () => {
|
describe('getLifecycleHooksForRegularMain', () => {
|
||||||
const createHooks = () =>
|
const createHooks = (executionMode: WorkflowExecuteMode = 'manual') =>
|
||||||
getWorkflowHooksMain({ executionMode, workflowData, pushRef, retryOf }, executionId);
|
getLifecycleHooksForRegularMain(
|
||||||
|
{ executionMode, workflowData, pushRef, retryOf },
|
||||||
|
executionId,
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
hooks = createHooks();
|
lifecycleHooks = createHooks();
|
||||||
});
|
});
|
||||||
|
|
||||||
workflowEventTests();
|
workflowEventTests();
|
||||||
|
@ -226,23 +228,23 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
statisticsTests();
|
statisticsTests();
|
||||||
|
|
||||||
it('should setup the correct set of hooks', () => {
|
it('should setup the correct set of hooks', () => {
|
||||||
expect(hooks).toBeInstanceOf(WorkflowHooks);
|
expect(lifecycleHooks).toBeInstanceOf(ExecutionLifecycleHooks);
|
||||||
expect(hooks.mode).toBe('manual');
|
expect(lifecycleHooks.mode).toBe('manual');
|
||||||
expect(hooks.executionId).toBe(executionId);
|
expect(lifecycleHooks.executionId).toBe(executionId);
|
||||||
expect(hooks.workflowData).toEqual(workflowData);
|
expect(lifecycleHooks.workflowData).toEqual(workflowData);
|
||||||
|
|
||||||
const { hookFunctions } = hooks;
|
const { handlers } = lifecycleHooks;
|
||||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(2);
|
expect(handlers.nodeExecuteBefore).toHaveLength(2);
|
||||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(2);
|
expect(handlers.nodeExecuteAfter).toHaveLength(2);
|
||||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(3);
|
expect(handlers.workflowExecuteBefore).toHaveLength(3);
|
||||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(5);
|
expect(handlers.workflowExecuteAfter).toHaveLength(5);
|
||||||
expect(hookFunctions.nodeFetchedData).toHaveLength(1);
|
expect(handlers.nodeFetchedData).toHaveLength(1);
|
||||||
expect(hookFunctions.sendResponse).toHaveLength(0);
|
expect(handlers.sendResponse).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('nodeExecuteBefore', () => {
|
describe('nodeExecuteBefore', () => {
|
||||||
it('should send nodeExecuteBefore push event', async () => {
|
it('should send nodeExecuteBefore push event', async () => {
|
||||||
await hooks.executeHookFunctions('nodeExecuteBefore', [nodeName]);
|
await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName]);
|
||||||
|
|
||||||
expect(push.send).toHaveBeenCalledWith(
|
expect(push.send).toHaveBeenCalledWith(
|
||||||
{ type: 'nodeExecuteBefore', data: { executionId, nodeName } },
|
{ type: 'nodeExecuteBefore', data: { executionId, nodeName } },
|
||||||
|
@ -253,11 +255,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
describe('nodeExecuteAfter', () => {
|
describe('nodeExecuteAfter', () => {
|
||||||
it('should send nodeExecuteAfter push event', async () => {
|
it('should send nodeExecuteAfter push event', async () => {
|
||||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||||
nodeName,
|
|
||||||
taskData,
|
|
||||||
runExecutionData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(push.send).toHaveBeenCalledWith(
|
expect(push.send).toHaveBeenCalledWith(
|
||||||
{ type: 'nodeExecuteAfter', data: { executionId, nodeName, data: taskData } },
|
{ type: 'nodeExecuteAfter', data: { executionId, nodeName, data: taskData } },
|
||||||
|
@ -267,15 +265,11 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
it('should save execution progress when enabled', async () => {
|
it('should save execution progress when enabled', async () => {
|
||||||
workflowData.settings = { saveExecutionProgress: true };
|
workflowData.settings = { saveExecutionProgress: true };
|
||||||
hooks = createHooks();
|
lifecycleHooks = createHooks();
|
||||||
|
|
||||||
expect(hooks.hookFunctions.nodeExecuteAfter).toHaveLength(3);
|
expect(lifecycleHooks.handlers.nodeExecuteAfter).toHaveLength(3);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||||
nodeName,
|
|
||||||
taskData,
|
|
||||||
runExecutionData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(executionRepository.findSingleExecution).toHaveBeenCalledWith(executionId, {
|
expect(executionRepository.findSingleExecution).toHaveBeenCalledWith(executionId, {
|
||||||
includeData: true,
|
includeData: true,
|
||||||
|
@ -285,15 +279,11 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
it('should not save execution progress when disabled', async () => {
|
it('should not save execution progress when disabled', async () => {
|
||||||
workflowData.settings = { saveExecutionProgress: false };
|
workflowData.settings = { saveExecutionProgress: false };
|
||||||
hooks = createHooks();
|
lifecycleHooks = createHooks();
|
||||||
|
|
||||||
expect(hooks.hookFunctions.nodeExecuteAfter).toHaveLength(2);
|
expect(lifecycleHooks.handlers.nodeExecuteAfter).toHaveLength(2);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||||
nodeName,
|
|
||||||
taskData,
|
|
||||||
runExecutionData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(executionRepository.findSingleExecution).not.toHaveBeenCalled();
|
expect(executionRepository.findSingleExecution).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -301,14 +291,14 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
describe('workflowExecuteBefore', () => {
|
describe('workflowExecuteBefore', () => {
|
||||||
it('should send executionStarted push event', async () => {
|
it('should send executionStarted push event', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteBefore', [workflow, runExecutionData]);
|
await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||||
|
|
||||||
expect(push.send).toHaveBeenCalledWith(
|
expect(push.send).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
type: 'executionStarted',
|
type: 'executionStarted',
|
||||||
data: {
|
data: {
|
||||||
executionId,
|
executionId,
|
||||||
mode: executionMode,
|
mode: 'manual',
|
||||||
retryOf,
|
retryOf,
|
||||||
workflowId: 'test-workflow-id',
|
workflowId: 'test-workflow-id',
|
||||||
workflowName: 'Test Workflow',
|
workflowName: 'Test Workflow',
|
||||||
|
@ -321,18 +311,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should run workflow.preExecute external hook', async () => {
|
it('should run workflow.preExecute external hook', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteBefore', [workflow, runExecutionData]);
|
await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||||
|
|
||||||
expect(externalHooks.run).toHaveBeenCalledWith('workflow.preExecute', [
|
expect(externalHooks.run).toHaveBeenCalledWith('workflow.preExecute', [workflow, 'manual']);
|
||||||
workflow,
|
|
||||||
executionMode,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('workflowExecuteAfter', () => {
|
describe('workflowExecuteAfter', () => {
|
||||||
it('should send executionFinished push event', async () => {
|
it('should send executionFinished push event', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||||
expect(push.send).toHaveBeenCalledWith(
|
expect(push.send).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
type: 'executionFinished',
|
type: 'executionFinished',
|
||||||
|
@ -348,7 +335,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send executionWaiting push event', async () => {
|
it('should send executionWaiting push event', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [waitingRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [waitingRun, {}]);
|
||||||
|
|
||||||
expect(push.send).toHaveBeenCalledWith(
|
expect(push.send).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
|
@ -361,17 +348,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
describe('saving static data', () => {
|
describe('saving static data', () => {
|
||||||
it('should skip saving static data for manual executions', async () => {
|
it('should skip saving static data for manual executions', async () => {
|
||||||
hooks.mode = 'manual';
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
|
||||||
|
|
||||||
expect(workflowStaticDataService.saveStaticDataById).not.toHaveBeenCalled();
|
expect(workflowStaticDataService.saveStaticDataById).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save static data for prod executions', async () => {
|
it('should save static data for prod executions', async () => {
|
||||||
hooks.mode = 'trigger';
|
lifecycleHooks = createHooks('trigger');
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||||
|
|
||||||
expect(workflowStaticDataService.saveStaticDataById).toHaveBeenCalledWith(
|
expect(workflowStaticDataService.saveStaticDataById).toHaveBeenCalledWith(
|
||||||
workflowId,
|
workflowId,
|
||||||
|
@ -380,11 +365,12 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle static data saving errors', async () => {
|
it('should handle static data saving errors', async () => {
|
||||||
hooks.mode = 'trigger';
|
lifecycleHooks = createHooks('trigger');
|
||||||
|
|
||||||
const error = new Error('Static data save failed');
|
const error = new Error('Static data save failed');
|
||||||
workflowStaticDataService.saveStaticDataById.mockRejectedValueOnce(error);
|
workflowStaticDataService.saveStaticDataById.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||||
|
|
||||||
expect(errorReporter.error).toHaveBeenCalledWith(error);
|
expect(errorReporter.error).toHaveBeenCalledWith(error);
|
||||||
});
|
});
|
||||||
|
@ -392,7 +378,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
describe('saving execution data', () => {
|
describe('saving execution data', () => {
|
||||||
it('should update execution with proper data', async () => {
|
it('should update execution with proper data', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||||
|
|
||||||
expect(executionRepository.updateExistingExecution).toHaveBeenCalledWith(
|
expect(executionRepository.updateExistingExecution).toHaveBeenCalledWith(
|
||||||
executionId,
|
executionId,
|
||||||
|
@ -406,31 +392,31 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
it('should not delete unfinished executions', async () => {
|
it('should not delete unfinished executions', async () => {
|
||||||
const unfinishedRun = mock<IRun>({ finished: false, status: 'running' });
|
const unfinishedRun = mock<IRun>({ finished: false, status: 'running' });
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [unfinishedRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [unfinishedRun, {}]);
|
||||||
|
|
||||||
expect(executionRepository.hardDelete).not.toHaveBeenCalled();
|
expect(executionRepository.hardDelete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not delete waiting executions', async () => {
|
it('should not delete waiting executions', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [waitingRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [waitingRun, {}]);
|
||||||
|
|
||||||
expect(executionRepository.hardDelete).not.toHaveBeenCalled();
|
expect(executionRepository.hardDelete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should soft delete manual executions when manual saving is disabled', async () => {
|
it('should soft delete manual executions when manual saving is disabled', async () => {
|
||||||
hooks.workflowData.settings = { saveManualExecutions: false };
|
lifecycleHooks.workflowData.settings = { saveManualExecutions: false };
|
||||||
hooks = createHooks();
|
lifecycleHooks = createHooks();
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||||
|
|
||||||
expect(executionRepository.softDelete).toHaveBeenCalledWith(executionId);
|
expect(executionRepository.softDelete).toHaveBeenCalledWith(executionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not soft delete manual executions with waitTill', async () => {
|
it('should not soft delete manual executions with waitTill', async () => {
|
||||||
hooks.workflowData.settings = { saveManualExecutions: false };
|
lifecycleHooks.workflowData.settings = { saveManualExecutions: false };
|
||||||
hooks = createHooks();
|
lifecycleHooks = createHooks();
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [waitingRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [waitingRun, {}]);
|
||||||
|
|
||||||
expect(executionRepository.softDelete).not.toHaveBeenCalled();
|
expect(executionRepository.softDelete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -438,15 +424,14 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
describe('error workflow', () => {
|
describe('error workflow', () => {
|
||||||
it('should not execute error workflow for manual executions', async () => {
|
it('should not execute error workflow for manual executions', async () => {
|
||||||
hooks.mode = 'manual';
|
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
|
||||||
|
|
||||||
expect(workflowExecutionService.executeErrorWorkflow).not.toHaveBeenCalled();
|
expect(workflowExecutionService.executeErrorWorkflow).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute error workflow for failed non-manual executions', async () => {
|
it('should execute error workflow for failed non-manual executions', async () => {
|
||||||
hooks.mode = 'trigger';
|
lifecycleHooks = createHooks('trigger');
|
||||||
|
|
||||||
const errorWorkflow = 'error-workflow-id';
|
const errorWorkflow = 'error-workflow-id';
|
||||||
workflowData.settings = { errorWorkflow };
|
workflowData.settings = { errorWorkflow };
|
||||||
const project = mock<Project>();
|
const project = mock<Project>();
|
||||||
|
@ -454,7 +439,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
.calledWith(workflowId)
|
.calledWith(workflowId)
|
||||||
.mockResolvedValue(project);
|
.mockResolvedValue(project);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||||
|
|
||||||
expect(workflowExecutionService.executeErrorWorkflow).toHaveBeenCalledWith(
|
expect(workflowExecutionService.executeErrorWorkflow).toHaveBeenCalledWith(
|
||||||
errorWorkflow,
|
errorWorkflow,
|
||||||
|
@ -479,7 +464,8 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
it('should restore binary data IDs after workflow execution for webhooks', async () => {
|
it('should restore binary data IDs after workflow execution for webhooks', async () => {
|
||||||
config.set('binaryDataManager.mode', 'filesystem');
|
config.set('binaryDataManager.mode', 'filesystem');
|
||||||
hooks.mode = 'webhook';
|
lifecycleHooks = createHooks('webhook');
|
||||||
|
|
||||||
(successfulRun.data.resultData.runData = {
|
(successfulRun.data.resultData.runData = {
|
||||||
[nodeName]: [
|
[nodeName]: [
|
||||||
{
|
{
|
||||||
|
@ -505,7 +491,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||||
|
|
||||||
expect(binaryDataService.rename).toHaveBeenCalledWith(
|
expect(binaryDataService.rename).toHaveBeenCalledWith(
|
||||||
'workflows/test-workflow-id/executions/temp/binary_data/123',
|
'workflows/test-workflow-id/executions/temp/binary_data/123',
|
||||||
|
@ -516,33 +502,32 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
describe("when pushRef isn't set", () => {
|
describe("when pushRef isn't set", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
hooks = getWorkflowHooksMain({ executionMode, workflowData, retryOf }, executionId);
|
lifecycleHooks = getLifecycleHooksForRegularMain(
|
||||||
|
{ executionMode: 'manual', workflowData, retryOf },
|
||||||
|
executionId,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not setup any push hooks', async () => {
|
it('should not setup any push hooks', async () => {
|
||||||
const { hookFunctions } = hooks;
|
const { handlers } = lifecycleHooks;
|
||||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(1);
|
expect(handlers.nodeExecuteBefore).toHaveLength(1);
|
||||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(1);
|
expect(handlers.nodeExecuteAfter).toHaveLength(1);
|
||||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(2);
|
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(4);
|
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('nodeExecuteBefore', [nodeName]);
|
await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName]);
|
||||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||||
nodeName,
|
await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||||
taskData,
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||||
runExecutionData,
|
|
||||||
]);
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteBefore', [workflow, runExecutionData]);
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
|
||||||
|
|
||||||
expect(push.send).not.toHaveBeenCalled();
|
expect(push.send).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getWorkflowHooksWorkerMain', () => {
|
describe('getLifecycleHooksForScalingMain', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
hooks = getWorkflowHooksWorkerMain(executionMode, executionId, workflowData, {
|
lifecycleHooks = getLifecycleHooksForScalingMain('manual', executionId, workflowData, {
|
||||||
pushRef,
|
pushRef,
|
||||||
retryOf,
|
retryOf,
|
||||||
});
|
});
|
||||||
|
@ -552,28 +537,25 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
externalHooksTests();
|
externalHooksTests();
|
||||||
|
|
||||||
it('should setup the correct set of hooks', () => {
|
it('should setup the correct set of hooks', () => {
|
||||||
expect(hooks).toBeInstanceOf(WorkflowHooks);
|
expect(lifecycleHooks).toBeInstanceOf(ExecutionLifecycleHooks);
|
||||||
expect(hooks.mode).toBe('manual');
|
expect(lifecycleHooks.mode).toBe('manual');
|
||||||
expect(hooks.executionId).toBe(executionId);
|
expect(lifecycleHooks.executionId).toBe(executionId);
|
||||||
expect(hooks.workflowData).toEqual(workflowData);
|
expect(lifecycleHooks.workflowData).toEqual(workflowData);
|
||||||
|
|
||||||
const { hookFunctions } = hooks;
|
const { handlers } = lifecycleHooks;
|
||||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(0);
|
expect(handlers.nodeExecuteBefore).toHaveLength(0);
|
||||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(0);
|
expect(handlers.nodeExecuteAfter).toHaveLength(0);
|
||||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(2);
|
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(4);
|
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||||
expect(hookFunctions.nodeFetchedData).toHaveLength(0);
|
expect(handlers.nodeFetchedData).toHaveLength(0);
|
||||||
expect(hookFunctions.sendResponse).toHaveLength(0);
|
expect(handlers.sendResponse).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('workflowExecuteBefore', () => {
|
describe('workflowExecuteBefore', () => {
|
||||||
it('should run the workflow.preExecute external hook', async () => {
|
it('should run the workflow.preExecute external hook', async () => {
|
||||||
await hooks.executeHookFunctions('workflowExecuteBefore', [workflow, runExecutionData]);
|
await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||||
|
|
||||||
expect(externalHooks.run).toHaveBeenCalledWith('workflow.preExecute', [
|
expect(externalHooks.run).toHaveBeenCalledWith('workflow.preExecute', [workflow, 'manual']);
|
||||||
workflow,
|
|
||||||
executionMode,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -583,12 +565,17 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
saveDataSuccessExecution: 'none',
|
saveDataSuccessExecution: 'none',
|
||||||
saveDataErrorExecution: 'all',
|
saveDataErrorExecution: 'all',
|
||||||
};
|
};
|
||||||
const hooks = getWorkflowHooksWorkerMain('webhook', executionId, workflowData, {
|
const lifecycleHooks = getLifecycleHooksForScalingMain(
|
||||||
pushRef,
|
'webhook',
|
||||||
retryOf,
|
executionId,
|
||||||
});
|
workflowData,
|
||||||
|
{
|
||||||
|
pushRef,
|
||||||
|
retryOf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||||
|
|
||||||
expect(executionRepository.hardDelete).toHaveBeenCalledWith({
|
expect(executionRepository.hardDelete).toHaveBeenCalledWith({
|
||||||
workflowId,
|
workflowId,
|
||||||
|
@ -601,12 +588,17 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
saveDataSuccessExecution: 'all',
|
saveDataSuccessExecution: 'all',
|
||||||
saveDataErrorExecution: 'none',
|
saveDataErrorExecution: 'none',
|
||||||
};
|
};
|
||||||
const hooks = getWorkflowHooksWorkerMain('webhook', executionId, workflowData, {
|
const lifecycleHooks = getLifecycleHooksForScalingMain(
|
||||||
pushRef,
|
'webhook',
|
||||||
retryOf,
|
executionId,
|
||||||
});
|
workflowData,
|
||||||
|
{
|
||||||
|
pushRef,
|
||||||
|
retryOf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||||
|
|
||||||
expect(executionRepository.hardDelete).toHaveBeenCalledWith({
|
expect(executionRepository.hardDelete).toHaveBeenCalledWith({
|
||||||
workflowId,
|
workflowId,
|
||||||
|
@ -616,12 +608,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getWorkflowHooksWorkerExecuter', () => {
|
describe('getLifecycleHooksForScalingWorker', () => {
|
||||||
beforeEach(() => {
|
const createHooks = (executionMode: WorkflowExecuteMode = 'manual') =>
|
||||||
hooks = getWorkflowHooksWorkerExecuter(executionMode, executionId, workflowData, {
|
getLifecycleHooksForScalingWorker(executionMode, executionId, workflowData, {
|
||||||
pushRef,
|
pushRef,
|
||||||
retryOf,
|
retryOf,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
lifecycleHooks = createHooks();
|
||||||
});
|
});
|
||||||
|
|
||||||
nodeEventsTests();
|
nodeEventsTests();
|
||||||
|
@ -629,33 +624,31 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
statisticsTests();
|
statisticsTests();
|
||||||
|
|
||||||
it('should setup the correct set of hooks', () => {
|
it('should setup the correct set of hooks', () => {
|
||||||
expect(hooks).toBeInstanceOf(WorkflowHooks);
|
expect(lifecycleHooks).toBeInstanceOf(ExecutionLifecycleHooks);
|
||||||
expect(hooks.mode).toBe('manual');
|
expect(lifecycleHooks.mode).toBe('manual');
|
||||||
expect(hooks.executionId).toBe(executionId);
|
expect(lifecycleHooks.executionId).toBe(executionId);
|
||||||
expect(hooks.workflowData).toEqual(workflowData);
|
expect(lifecycleHooks.workflowData).toEqual(workflowData);
|
||||||
|
|
||||||
const { hookFunctions } = hooks;
|
const { handlers } = lifecycleHooks;
|
||||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(2);
|
expect(handlers.nodeExecuteBefore).toHaveLength(2);
|
||||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(2);
|
expect(handlers.nodeExecuteAfter).toHaveLength(2);
|
||||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(2);
|
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(4);
|
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||||
expect(hookFunctions.nodeFetchedData).toHaveLength(1);
|
expect(handlers.nodeFetchedData).toHaveLength(1);
|
||||||
expect(hookFunctions.sendResponse).toHaveLength(0);
|
expect(handlers.sendResponse).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('saving static data', () => {
|
describe('saving static data', () => {
|
||||||
it('should skip saving static data for manual executions', async () => {
|
it('should skip saving static data for manual executions', async () => {
|
||||||
hooks.mode = 'manual';
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
|
||||||
|
|
||||||
expect(workflowStaticDataService.saveStaticDataById).not.toHaveBeenCalled();
|
expect(workflowStaticDataService.saveStaticDataById).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save static data for prod executions', async () => {
|
it('should save static data for prod executions', async () => {
|
||||||
hooks.mode = 'trigger';
|
lifecycleHooks = createHooks('trigger');
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||||
|
|
||||||
expect(workflowStaticDataService.saveStaticDataById).toHaveBeenCalledWith(
|
expect(workflowStaticDataService.saveStaticDataById).toHaveBeenCalledWith(
|
||||||
workflowId,
|
workflowId,
|
||||||
|
@ -664,11 +657,11 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle static data saving errors', async () => {
|
it('should handle static data saving errors', async () => {
|
||||||
hooks.mode = 'trigger';
|
lifecycleHooks = createHooks('trigger');
|
||||||
const error = new Error('Static data save failed');
|
const error = new Error('Static data save failed');
|
||||||
workflowStaticDataService.saveStaticDataById.mockRejectedValueOnce(error);
|
workflowStaticDataService.saveStaticDataById.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||||
|
|
||||||
expect(errorReporter.error).toHaveBeenCalledWith(error);
|
expect(errorReporter.error).toHaveBeenCalledWith(error);
|
||||||
});
|
});
|
||||||
|
@ -676,21 +669,19 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
|
|
||||||
describe('error workflow', () => {
|
describe('error workflow', () => {
|
||||||
it('should not execute error workflow for manual executions', async () => {
|
it('should not execute error workflow for manual executions', async () => {
|
||||||
hooks.mode = 'manual';
|
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
|
||||||
|
|
||||||
expect(workflowExecutionService.executeErrorWorkflow).not.toHaveBeenCalled();
|
expect(workflowExecutionService.executeErrorWorkflow).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute error workflow for failed non-manual executions', async () => {
|
it('should execute error workflow for failed non-manual executions', async () => {
|
||||||
hooks.mode = 'trigger';
|
lifecycleHooks = createHooks('trigger');
|
||||||
const errorWorkflow = 'error-workflow-id';
|
const errorWorkflow = 'error-workflow-id';
|
||||||
workflowData.settings = { errorWorkflow };
|
workflowData.settings = { errorWorkflow };
|
||||||
const project = mock<Project>();
|
const project = mock<Project>();
|
||||||
ownershipService.getWorkflowProjectCached.calledWith(workflowId).mockResolvedValue(project);
|
ownershipService.getWorkflowProjectCached.calledWith(workflowId).mockResolvedValue(project);
|
||||||
|
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||||
|
|
||||||
expect(workflowExecutionService.executeErrorWorkflow).toHaveBeenCalledWith(
|
expect(workflowExecutionService.executeErrorWorkflow).toHaveBeenCalledWith(
|
||||||
errorWorkflow,
|
errorWorkflow,
|
||||||
|
@ -714,9 +705,14 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getWorkflowHooksIntegrated', () => {
|
describe('getLifecycleHooksForSubExecutions', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
hooks = getWorkflowHooksIntegrated(executionMode, executionId, workflowData, undefined);
|
lifecycleHooks = getLifecycleHooksForSubExecutions(
|
||||||
|
'manual',
|
||||||
|
executionId,
|
||||||
|
workflowData,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
workflowEventTests();
|
workflowEventTests();
|
||||||
|
@ -725,18 +721,18 @@ describe('Execution Lifecycle Hooks', () => {
|
||||||
statisticsTests();
|
statisticsTests();
|
||||||
|
|
||||||
it('should setup the correct set of hooks', () => {
|
it('should setup the correct set of hooks', () => {
|
||||||
expect(hooks).toBeInstanceOf(WorkflowHooks);
|
expect(lifecycleHooks).toBeInstanceOf(ExecutionLifecycleHooks);
|
||||||
expect(hooks.mode).toBe('manual');
|
expect(lifecycleHooks.mode).toBe('manual');
|
||||||
expect(hooks.executionId).toBe(executionId);
|
expect(lifecycleHooks.executionId).toBe(executionId);
|
||||||
expect(hooks.workflowData).toEqual(workflowData);
|
expect(lifecycleHooks.workflowData).toEqual(workflowData);
|
||||||
|
|
||||||
const { hookFunctions } = hooks;
|
const { handlers } = lifecycleHooks;
|
||||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(1);
|
expect(handlers.nodeExecuteBefore).toHaveLength(1);
|
||||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(1);
|
expect(handlers.nodeExecuteAfter).toHaveLength(1);
|
||||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(2);
|
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(4);
|
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||||
expect(hookFunctions.nodeFetchedData).toHaveLength(1);
|
expect(handlers.nodeFetchedData).toHaveLength(1);
|
||||||
expect(hookFunctions.sendResponse).toHaveLength(0);
|
expect(handlers.sendResponse).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,10 @@
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import { stringify } from 'flatted';
|
import { stringify } from 'flatted';
|
||||||
import { ErrorReporter, Logger, InstanceSettings } from 'n8n-core';
|
import { ErrorReporter, Logger, InstanceSettings, ExecutionLifecycleHooks } from 'n8n-core';
|
||||||
import { WorkflowHooks } from 'n8n-workflow';
|
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
|
||||||
INode,
|
|
||||||
IRun,
|
|
||||||
IRunExecutionData,
|
|
||||||
ITaskData,
|
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
IWorkflowExecuteHooks,
|
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
Workflow,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
|
@ -39,339 +31,255 @@ type HooksSetupParameters = {
|
||||||
retryOf?: string;
|
retryOf?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function mergeHookFunctions(...hookFunctions: IWorkflowExecuteHooks[]): IWorkflowExecuteHooks {
|
function hookFunctionsWorkflowEvents(hooks: ExecutionLifecycleHooks, userId?: string) {
|
||||||
const result: IWorkflowExecuteHooks = {
|
const eventService = Container.get(EventService);
|
||||||
nodeExecuteBefore: [],
|
hooks.addHandler('workflowExecuteBefore', function () {
|
||||||
nodeExecuteAfter: [],
|
const { executionId, workflowData } = this;
|
||||||
workflowExecuteBefore: [],
|
eventService.emit('workflow-pre-execute', { executionId, data: workflowData });
|
||||||
workflowExecuteAfter: [],
|
});
|
||||||
sendResponse: [],
|
hooks.addHandler('workflowExecuteAfter', function (runData) {
|
||||||
nodeFetchedData: [],
|
if (runData.status === 'waiting') return;
|
||||||
};
|
|
||||||
for (const hooks of hookFunctions) {
|
const { executionId, workflowData: workflow } = this;
|
||||||
for (const key in hooks) {
|
eventService.emit('workflow-post-execute', { executionId, runData, workflow, userId });
|
||||||
if (!result[key] || !hooks[key]) continue;
|
});
|
||||||
result[key].push(...hooks[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hookFunctionsWorkflowEvents(userId?: string): IWorkflowExecuteHooks {
|
function hookFunctionsNodeEvents(hooks: ExecutionLifecycleHooks) {
|
||||||
const eventService = Container.get(EventService);
|
const eventService = Container.get(EventService);
|
||||||
return {
|
hooks.addHandler('nodeExecuteBefore', function (nodeName) {
|
||||||
workflowExecuteBefore: [
|
const { executionId, workflowData: workflow } = this;
|
||||||
async function (this: WorkflowHooks): Promise<void> {
|
eventService.emit('node-pre-execute', { executionId, workflow, nodeName });
|
||||||
const { executionId, workflowData } = this;
|
});
|
||||||
eventService.emit('workflow-pre-execute', { executionId, data: workflowData });
|
hooks.addHandler('nodeExecuteAfter', function (nodeName) {
|
||||||
},
|
const { executionId, workflowData: workflow } = this;
|
||||||
],
|
eventService.emit('node-post-execute', { executionId, workflow, nodeName });
|
||||||
workflowExecuteAfter: [
|
});
|
||||||
async function (this: WorkflowHooks, runData: IRun): Promise<void> {
|
|
||||||
if (runData.status === 'waiting') return;
|
|
||||||
|
|
||||||
const { executionId, workflowData: workflow } = this;
|
|
||||||
eventService.emit('workflow-post-execute', { executionId, runData, workflow, userId });
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function hookFunctionsNodeEvents(): IWorkflowExecuteHooks {
|
|
||||||
const eventService = Container.get(EventService);
|
|
||||||
return {
|
|
||||||
nodeExecuteBefore: [
|
|
||||||
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
|
||||||
const { executionId, workflowData: workflow } = this;
|
|
||||||
eventService.emit('node-pre-execute', { executionId, workflow, nodeName });
|
|
||||||
},
|
|
||||||
],
|
|
||||||
nodeExecuteAfter: [
|
|
||||||
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
|
||||||
const { executionId, workflowData: workflow } = this;
|
|
||||||
eventService.emit('node-post-execute', { executionId, workflow, nodeName });
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns hook functions to push data to Editor-UI
|
* Returns hook functions to push data to Editor-UI
|
||||||
*/
|
*/
|
||||||
function hookFunctionsPush({ pushRef, retryOf }: HooksSetupParameters): IWorkflowExecuteHooks {
|
function hookFunctionsPush(
|
||||||
if (!pushRef) return {};
|
hooks: ExecutionLifecycleHooks,
|
||||||
|
{ pushRef, retryOf }: HooksSetupParameters,
|
||||||
|
) {
|
||||||
|
if (!pushRef) return;
|
||||||
const logger = Container.get(Logger);
|
const logger = Container.get(Logger);
|
||||||
const pushInstance = Container.get(Push);
|
const pushInstance = Container.get(Push);
|
||||||
return {
|
hooks.addHandler('nodeExecuteBefore', function (nodeName) {
|
||||||
nodeExecuteBefore: [
|
const { executionId } = this;
|
||||||
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
// Push data to session which started workflow before each
|
||||||
const { executionId } = this;
|
// node which starts rendering
|
||||||
// Push data to session which started workflow before each
|
logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, {
|
||||||
// node which starts rendering
|
executionId,
|
||||||
logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, {
|
pushRef,
|
||||||
executionId,
|
workflowId: this.workflowData.id,
|
||||||
pushRef,
|
});
|
||||||
workflowId: this.workflowData.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
pushInstance.send({ type: 'nodeExecuteBefore', data: { executionId, nodeName } }, pushRef);
|
pushInstance.send({ type: 'nodeExecuteBefore', data: { executionId, nodeName } }, pushRef);
|
||||||
},
|
});
|
||||||
],
|
hooks.addHandler('nodeExecuteAfter', function (nodeName, data) {
|
||||||
nodeExecuteAfter: [
|
const { executionId } = this;
|
||||||
async function (this: WorkflowHooks, nodeName: string, data: ITaskData): Promise<void> {
|
// Push data to session which started workflow after each rendered node
|
||||||
const { executionId } = this;
|
logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, {
|
||||||
// Push data to session which started workflow after each rendered node
|
executionId,
|
||||||
logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, {
|
pushRef,
|
||||||
executionId,
|
workflowId: this.workflowData.id,
|
||||||
pushRef,
|
});
|
||||||
workflowId: this.workflowData.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
pushInstance.send(
|
pushInstance.send({ type: 'nodeExecuteAfter', data: { executionId, nodeName, data } }, pushRef);
|
||||||
{ type: 'nodeExecuteAfter', data: { executionId, nodeName, data } },
|
});
|
||||||
pushRef,
|
hooks.addHandler('workflowExecuteBefore', function (_workflow, data) {
|
||||||
);
|
const { executionId } = this;
|
||||||
},
|
const { id: workflowId, name: workflowName } = this.workflowData;
|
||||||
],
|
logger.debug('Executing hook (hookFunctionsPush)', {
|
||||||
workflowExecuteBefore: [
|
executionId,
|
||||||
async function (this: WorkflowHooks, _workflow, data): Promise<void> {
|
pushRef,
|
||||||
const { executionId } = this;
|
workflowId,
|
||||||
const { id: workflowId, name: workflowName } = this.workflowData;
|
});
|
||||||
logger.debug('Executing hook (hookFunctionsPush)', {
|
// Push data to session which started the workflow
|
||||||
|
pushInstance.send(
|
||||||
|
{
|
||||||
|
type: 'executionStarted',
|
||||||
|
data: {
|
||||||
executionId,
|
executionId,
|
||||||
pushRef,
|
mode: this.mode,
|
||||||
|
startedAt: new Date(),
|
||||||
|
retryOf,
|
||||||
workflowId,
|
workflowId,
|
||||||
});
|
workflowName,
|
||||||
// Push data to session which started the workflow
|
flattedRunData: data?.resultData.runData
|
||||||
pushInstance.send(
|
? stringify(data.resultData.runData)
|
||||||
{
|
: stringify({}),
|
||||||
type: 'executionStarted',
|
},
|
||||||
data: {
|
|
||||||
executionId,
|
|
||||||
mode: this.mode,
|
|
||||||
startedAt: new Date(),
|
|
||||||
retryOf,
|
|
||||||
workflowId,
|
|
||||||
workflowName,
|
|
||||||
flattedRunData: data?.resultData.runData
|
|
||||||
? stringify(data.resultData.runData)
|
|
||||||
: stringify({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pushRef,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
],
|
pushRef,
|
||||||
workflowExecuteAfter: [
|
);
|
||||||
async function (this: WorkflowHooks, fullRunData: IRun): Promise<void> {
|
});
|
||||||
const { executionId } = this;
|
hooks.addHandler('workflowExecuteAfter', function (fullRunData) {
|
||||||
const { id: workflowId } = this.workflowData;
|
const { executionId } = this;
|
||||||
logger.debug('Executing hook (hookFunctionsPush)', {
|
const { id: workflowId } = this.workflowData;
|
||||||
executionId,
|
logger.debug('Executing hook (hookFunctionsPush)', {
|
||||||
pushRef,
|
executionId,
|
||||||
workflowId,
|
pushRef,
|
||||||
});
|
workflowId,
|
||||||
|
});
|
||||||
|
|
||||||
const { status } = fullRunData;
|
const { status } = fullRunData;
|
||||||
if (status === 'waiting') {
|
if (status === 'waiting') {
|
||||||
pushInstance.send({ type: 'executionWaiting', data: { executionId } }, pushRef);
|
pushInstance.send({ type: 'executionWaiting', data: { executionId } }, pushRef);
|
||||||
} else {
|
} else {
|
||||||
const rawData = stringify(fullRunData.data);
|
const rawData = stringify(fullRunData.data);
|
||||||
pushInstance.send(
|
pushInstance.send(
|
||||||
{ type: 'executionFinished', data: { executionId, workflowId, status, rawData } },
|
{ type: 'executionFinished', data: { executionId, workflowId, status, rawData } },
|
||||||
pushRef,
|
pushRef,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hookFunctionsExternalHooks(): IWorkflowExecuteHooks {
|
function hookFunctionsExternalHooks(hooks: ExecutionLifecycleHooks) {
|
||||||
const externalHooks = Container.get(ExternalHooks);
|
const externalHooks = Container.get(ExternalHooks);
|
||||||
return {
|
hooks.addHandler('workflowExecuteBefore', async function (workflow) {
|
||||||
workflowExecuteBefore: [
|
await externalHooks.run('workflow.preExecute', [workflow, this.mode]);
|
||||||
async function (this: WorkflowHooks, workflow: Workflow): Promise<void> {
|
});
|
||||||
await externalHooks.run('workflow.preExecute', [workflow, this.mode]);
|
hooks.addHandler('workflowExecuteAfter', async function (fullRunData) {
|
||||||
},
|
await externalHooks.run('workflow.postExecute', [
|
||||||
],
|
fullRunData,
|
||||||
workflowExecuteAfter: [
|
this.workflowData,
|
||||||
async function (this: WorkflowHooks, fullRunData: IRun) {
|
this.executionId,
|
||||||
await externalHooks.run('workflow.postExecute', [
|
]);
|
||||||
fullRunData,
|
});
|
||||||
this.workflowData,
|
|
||||||
this.executionId,
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hookFunctionsSaveProgress({ saveSettings }: HooksSetupParameters): IWorkflowExecuteHooks {
|
function hookFunctionsSaveProgress(
|
||||||
if (!saveSettings.progress) return {};
|
hooks: ExecutionLifecycleHooks,
|
||||||
return {
|
{ saveSettings }: HooksSetupParameters,
|
||||||
nodeExecuteAfter: [
|
) {
|
||||||
async function (
|
if (!saveSettings.progress) return;
|
||||||
this: WorkflowHooks,
|
hooks.addHandler('nodeExecuteAfter', async function (nodeName, data, executionData) {
|
||||||
nodeName: string,
|
await saveExecutionProgress(
|
||||||
data: ITaskData,
|
this.workflowData.id,
|
||||||
executionData: IRunExecutionData,
|
this.executionId,
|
||||||
): Promise<void> {
|
nodeName,
|
||||||
await saveExecutionProgress(
|
data,
|
||||||
this.workflowData.id,
|
executionData,
|
||||||
this.executionId,
|
);
|
||||||
nodeName,
|
});
|
||||||
data,
|
|
||||||
executionData,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This should ideally be added before any other `workflowExecuteAfter` hook to ensure all hooks get the same execution status */
|
/** This should ideally be added before any other `workflowExecuteAfter` hook to ensure all hooks get the same execution status */
|
||||||
function hookFunctionsFinalizeExecutionStatus(): IWorkflowExecuteHooks {
|
function hookFunctionsFinalizeExecutionStatus(hooks: ExecutionLifecycleHooks) {
|
||||||
return {
|
hooks.addHandler('workflowExecuteAfter', (fullRunData) => {
|
||||||
workflowExecuteAfter: [
|
fullRunData.status = determineFinalExecutionStatus(fullRunData);
|
||||||
async function (fullRunData: IRun) {
|
});
|
||||||
fullRunData.status = determineFinalExecutionStatus(fullRunData);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hookFunctionsStatistics(): IWorkflowExecuteHooks {
|
function hookFunctionsStatistics(hooks: ExecutionLifecycleHooks) {
|
||||||
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
||||||
return {
|
hooks.addHandler('nodeFetchedData', (workflowId, node) => {
|
||||||
nodeFetchedData: [
|
workflowStatisticsService.emit('nodeFetchedData', { workflowId, node });
|
||||||
async (workflowId: string, node: INode) => {
|
});
|
||||||
workflowStatisticsService.emit('nodeFetchedData', { workflowId, node });
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns hook functions to save workflow execution and call error workflow
|
* Returns hook functions to save workflow execution and call error workflow
|
||||||
*/
|
*/
|
||||||
function hookFunctionsSave({
|
function hookFunctionsSave(
|
||||||
pushRef,
|
hooks: ExecutionLifecycleHooks,
|
||||||
retryOf,
|
{ pushRef, retryOf, saveSettings }: HooksSetupParameters,
|
||||||
saveSettings,
|
) {
|
||||||
}: HooksSetupParameters): IWorkflowExecuteHooks {
|
|
||||||
const logger = Container.get(Logger);
|
const logger = Container.get(Logger);
|
||||||
const errorReporter = Container.get(ErrorReporter);
|
const errorReporter = Container.get(ErrorReporter);
|
||||||
const executionRepository = Container.get(ExecutionRepository);
|
const executionRepository = Container.get(ExecutionRepository);
|
||||||
const workflowStaticDataService = Container.get(WorkflowStaticDataService);
|
const workflowStaticDataService = Container.get(WorkflowStaticDataService);
|
||||||
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
||||||
return {
|
hooks.addHandler('workflowExecuteAfter', async function (fullRunData, newStaticData) {
|
||||||
workflowExecuteAfter: [
|
logger.debug('Executing hook (hookFunctionsSave)', {
|
||||||
async function (
|
executionId: this.executionId,
|
||||||
this: WorkflowHooks,
|
workflowId: this.workflowData.id,
|
||||||
fullRunData: IRun,
|
});
|
||||||
newStaticData: IDataObject,
|
|
||||||
): Promise<void> {
|
await restoreBinaryDataId(fullRunData, this.executionId, this.mode);
|
||||||
logger.debug('Executing hook (hookFunctionsSave)', {
|
|
||||||
executionId: this.executionId,
|
const isManualMode = this.mode === 'manual';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isManualMode && isWorkflowIdValid(this.workflowData.id) && newStaticData) {
|
||||||
|
// Workflow is saved so update in database
|
||||||
|
try {
|
||||||
|
await workflowStaticDataService.saveStaticDataById(this.workflowData.id, newStaticData);
|
||||||
|
} catch (e) {
|
||||||
|
errorReporter.error(e);
|
||||||
|
logger.error(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`,
|
||||||
|
{ executionId: this.executionId, workflowId: this.workflowData.id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isManualMode && !saveSettings.manual && !fullRunData.waitTill) {
|
||||||
|
/**
|
||||||
|
* When manual executions are not being saved, we only soft-delete
|
||||||
|
* the execution so that the user can access its binary data
|
||||||
|
* while building their workflow.
|
||||||
|
*
|
||||||
|
* The manual execution and its binary data will be hard-deleted
|
||||||
|
* on the next pruning cycle after the grace period set by
|
||||||
|
* `EXECUTIONS_DATA_HARD_DELETE_BUFFER`.
|
||||||
|
*/
|
||||||
|
await executionRepository.softDelete(this.executionId);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldNotSave =
|
||||||
|
(fullRunData.status === 'success' && !saveSettings.success) ||
|
||||||
|
(fullRunData.status !== 'success' && !saveSettings.error);
|
||||||
|
|
||||||
|
if (shouldNotSave && !fullRunData.waitTill && !isManualMode) {
|
||||||
|
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, this.executionId, retryOf);
|
||||||
|
|
||||||
|
await executionRepository.hardDelete({
|
||||||
workflowId: this.workflowData.id,
|
workflowId: this.workflowData.id,
|
||||||
|
executionId: this.executionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await restoreBinaryDataId(fullRunData, this.executionId, this.mode);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isManualMode = this.mode === 'manual';
|
// Although it is treated as IWorkflowBase here, it's being instantiated elsewhere with properties that may be sensitive
|
||||||
|
// As a result, we should create an IWorkflowBase object with only the data we want to save in it.
|
||||||
|
const fullExecutionData = prepareExecutionDataForDbUpdate({
|
||||||
|
runData: fullRunData,
|
||||||
|
workflowData: this.workflowData,
|
||||||
|
workflowStatusFinal: fullRunData.status,
|
||||||
|
retryOf,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
// When going into the waiting state, store the pushRef in the execution-data
|
||||||
if (!isManualMode && isWorkflowIdValid(this.workflowData.id) && newStaticData) {
|
if (fullRunData.waitTill && isManualMode) {
|
||||||
// Workflow is saved so update in database
|
fullExecutionData.data.pushRef = pushRef;
|
||||||
try {
|
}
|
||||||
await workflowStaticDataService.saveStaticDataById(
|
|
||||||
this.workflowData.id,
|
|
||||||
newStaticData,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
errorReporter.error(e);
|
|
||||||
logger.error(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`,
|
|
||||||
{ executionId: this.executionId, workflowId: this.workflowData.id },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isManualMode && !saveSettings.manual && !fullRunData.waitTill) {
|
await updateExistingExecution({
|
||||||
/**
|
executionId: this.executionId,
|
||||||
* When manual executions are not being saved, we only soft-delete
|
workflowId: this.workflowData.id,
|
||||||
* the execution so that the user can access its binary data
|
executionData: fullExecutionData,
|
||||||
* while building their workflow.
|
});
|
||||||
*
|
|
||||||
* The manual execution and its binary data will be hard-deleted
|
|
||||||
* on the next pruning cycle after the grace period set by
|
|
||||||
* `EXECUTIONS_DATA_HARD_DELETE_BUFFER`.
|
|
||||||
*/
|
|
||||||
await executionRepository.softDelete(this.executionId);
|
|
||||||
|
|
||||||
return;
|
if (!isManualMode) {
|
||||||
}
|
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, this.executionId, retryOf);
|
||||||
|
}
|
||||||
const shouldNotSave =
|
} finally {
|
||||||
(fullRunData.status === 'success' && !saveSettings.success) ||
|
workflowStatisticsService.emit('workflowExecutionCompleted', {
|
||||||
(fullRunData.status !== 'success' && !saveSettings.error);
|
workflowData: this.workflowData,
|
||||||
|
fullRunData,
|
||||||
if (shouldNotSave && !fullRunData.waitTill && !isManualMode) {
|
});
|
||||||
executeErrorWorkflow(
|
}
|
||||||
this.workflowData,
|
});
|
||||||
fullRunData,
|
|
||||||
this.mode,
|
|
||||||
this.executionId,
|
|
||||||
retryOf,
|
|
||||||
);
|
|
||||||
|
|
||||||
await executionRepository.hardDelete({
|
|
||||||
workflowId: this.workflowData.id,
|
|
||||||
executionId: this.executionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Although it is treated as IWorkflowBase here, it's being instantiated elsewhere with properties that may be sensitive
|
|
||||||
// As a result, we should create an IWorkflowBase object with only the data we want to save in it.
|
|
||||||
const fullExecutionData = prepareExecutionDataForDbUpdate({
|
|
||||||
runData: fullRunData,
|
|
||||||
workflowData: this.workflowData,
|
|
||||||
workflowStatusFinal: fullRunData.status,
|
|
||||||
retryOf,
|
|
||||||
});
|
|
||||||
|
|
||||||
// When going into the waiting state, store the pushRef in the execution-data
|
|
||||||
if (fullRunData.waitTill && isManualMode) {
|
|
||||||
fullExecutionData.data.pushRef = pushRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateExistingExecution({
|
|
||||||
executionId: this.executionId,
|
|
||||||
workflowId: this.workflowData.id,
|
|
||||||
executionData: fullExecutionData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isManualMode) {
|
|
||||||
executeErrorWorkflow(
|
|
||||||
this.workflowData,
|
|
||||||
fullRunData,
|
|
||||||
this.mode,
|
|
||||||
this.executionId,
|
|
||||||
retryOf,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
workflowStatisticsService.emit('workflowExecutionCompleted', {
|
|
||||||
workflowData: this.workflowData,
|
|
||||||
fullRunData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -379,224 +287,196 @@ function hookFunctionsSave({
|
||||||
* for running with queues. Manual executions should never run on queues as
|
* for running with queues. Manual executions should never run on queues as
|
||||||
* they are always executed in the main process.
|
* they are always executed in the main process.
|
||||||
*/
|
*/
|
||||||
function hookFunctionsSaveWorker({
|
function hookFunctionsSaveWorker(
|
||||||
pushRef,
|
hooks: ExecutionLifecycleHooks,
|
||||||
retryOf,
|
{ pushRef, retryOf }: HooksSetupParameters,
|
||||||
}: HooksSetupParameters): IWorkflowExecuteHooks {
|
) {
|
||||||
const logger = Container.get(Logger);
|
const logger = Container.get(Logger);
|
||||||
const errorReporter = Container.get(ErrorReporter);
|
const errorReporter = Container.get(ErrorReporter);
|
||||||
const workflowStaticDataService = Container.get(WorkflowStaticDataService);
|
const workflowStaticDataService = Container.get(WorkflowStaticDataService);
|
||||||
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
||||||
return {
|
hooks.addHandler('workflowExecuteAfter', async function (fullRunData, newStaticData) {
|
||||||
workflowExecuteAfter: [
|
logger.debug('Executing hook (hookFunctionsSaveWorker)', {
|
||||||
async function (
|
executionId: this.executionId,
|
||||||
this: WorkflowHooks,
|
workflowId: this.workflowData.id,
|
||||||
fullRunData: IRun,
|
});
|
||||||
newStaticData: IDataObject,
|
|
||||||
): Promise<void> {
|
|
||||||
logger.debug('Executing hook (hookFunctionsSaveWorker)', {
|
|
||||||
executionId: this.executionId,
|
|
||||||
workflowId: this.workflowData.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isManualMode = this.mode === 'manual';
|
const isManualMode = this.mode === 'manual';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isManualMode && isWorkflowIdValid(this.workflowData.id) && newStaticData) {
|
||||||
|
// Workflow is saved so update in database
|
||||||
try {
|
try {
|
||||||
if (!isManualMode && isWorkflowIdValid(this.workflowData.id) && newStaticData) {
|
await workflowStaticDataService.saveStaticDataById(this.workflowData.id, newStaticData);
|
||||||
// Workflow is saved so update in database
|
} catch (e) {
|
||||||
try {
|
errorReporter.error(e);
|
||||||
await workflowStaticDataService.saveStaticDataById(
|
logger.error(
|
||||||
this.workflowData.id,
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
newStaticData,
|
`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`,
|
||||||
);
|
{ workflowId: this.workflowData.id },
|
||||||
} catch (e) {
|
);
|
||||||
errorReporter.error(e);
|
|
||||||
logger.error(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`,
|
|
||||||
{ workflowId: this.workflowData.id },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isManualMode &&
|
|
||||||
fullRunData.status !== 'success' &&
|
|
||||||
fullRunData.status !== 'waiting'
|
|
||||||
) {
|
|
||||||
executeErrorWorkflow(
|
|
||||||
this.workflowData,
|
|
||||||
fullRunData,
|
|
||||||
this.mode,
|
|
||||||
this.executionId,
|
|
||||||
retryOf,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Although it is treated as IWorkflowBase here, it's being instantiated elsewhere with properties that may be sensitive
|
|
||||||
// As a result, we should create an IWorkflowBase object with only the data we want to save in it.
|
|
||||||
const fullExecutionData = prepareExecutionDataForDbUpdate({
|
|
||||||
runData: fullRunData,
|
|
||||||
workflowData: this.workflowData,
|
|
||||||
workflowStatusFinal: fullRunData.status,
|
|
||||||
retryOf,
|
|
||||||
});
|
|
||||||
|
|
||||||
// When going into the waiting state, store the pushRef in the execution-data
|
|
||||||
if (fullRunData.waitTill && isManualMode) {
|
|
||||||
fullExecutionData.data.pushRef = pushRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateExistingExecution({
|
|
||||||
executionId: this.executionId,
|
|
||||||
workflowId: this.workflowData.id,
|
|
||||||
executionData: fullExecutionData,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
workflowStatisticsService.emit('workflowExecutionCompleted', {
|
|
||||||
workflowData: this.workflowData,
|
|
||||||
fullRunData,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
],
|
|
||||||
};
|
if (!isManualMode && fullRunData.status !== 'success' && fullRunData.status !== 'waiting') {
|
||||||
|
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, this.executionId, retryOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Although it is treated as IWorkflowBase here, it's being instantiated elsewhere with properties that may be sensitive
|
||||||
|
// As a result, we should create an IWorkflowBase object with only the data we want to save in it.
|
||||||
|
const fullExecutionData = prepareExecutionDataForDbUpdate({
|
||||||
|
runData: fullRunData,
|
||||||
|
workflowData: this.workflowData,
|
||||||
|
workflowStatusFinal: fullRunData.status,
|
||||||
|
retryOf,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When going into the waiting state, store the pushRef in the execution-data
|
||||||
|
if (fullRunData.waitTill && isManualMode) {
|
||||||
|
fullExecutionData.data.pushRef = pushRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateExistingExecution({
|
||||||
|
executionId: this.executionId,
|
||||||
|
workflowId: this.workflowData.id,
|
||||||
|
executionData: fullExecutionData,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
workflowStatisticsService.emit('workflowExecutionCompleted', {
|
||||||
|
workflowData: this.workflowData,
|
||||||
|
fullRunData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns WorkflowHooks instance for running integrated workflows
|
* Returns ExecutionLifecycleHooks instance for running integrated workflows
|
||||||
* (Workflows which get started inside of another workflow)
|
* (Workflows which get started inside of another workflow)
|
||||||
*/
|
*/
|
||||||
export function getWorkflowHooksIntegrated(
|
export function getLifecycleHooksForSubExecutions(
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
executionId: string,
|
executionId: string,
|
||||||
workflowData: IWorkflowBase,
|
workflowData: IWorkflowBase,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
): WorkflowHooks {
|
): ExecutionLifecycleHooks {
|
||||||
|
const hooks = new ExecutionLifecycleHooks(mode, executionId, workflowData);
|
||||||
const saveSettings = toSaveSettings(workflowData.settings);
|
const saveSettings = toSaveSettings(workflowData.settings);
|
||||||
const hookFunctions = mergeHookFunctions(
|
hookFunctionsWorkflowEvents(hooks, userId);
|
||||||
hookFunctionsWorkflowEvents(userId),
|
hookFunctionsNodeEvents(hooks);
|
||||||
hookFunctionsNodeEvents(),
|
hookFunctionsFinalizeExecutionStatus(hooks);
|
||||||
hookFunctionsFinalizeExecutionStatus(),
|
hookFunctionsSave(hooks, { saveSettings });
|
||||||
hookFunctionsSave({ saveSettings }),
|
hookFunctionsSaveProgress(hooks, { saveSettings });
|
||||||
hookFunctionsSaveProgress({ saveSettings }),
|
hookFunctionsStatistics(hooks);
|
||||||
hookFunctionsStatistics(),
|
hookFunctionsExternalHooks(hooks);
|
||||||
hookFunctionsExternalHooks(),
|
return hooks;
|
||||||
);
|
|
||||||
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns WorkflowHooks instance for worker in scaling mode.
|
* Returns ExecutionLifecycleHooks instance for worker in scaling mode.
|
||||||
*/
|
*/
|
||||||
export function getWorkflowHooksWorkerExecuter(
|
export function getLifecycleHooksForScalingWorker(
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
executionId: string,
|
executionId: string,
|
||||||
workflowData: IWorkflowBase,
|
workflowData: IWorkflowBase,
|
||||||
{ pushRef, retryOf }: Omit<HooksSetupParameters, 'saveSettings'> = {},
|
{ pushRef, retryOf }: Omit<HooksSetupParameters, 'saveSettings'> = {},
|
||||||
): WorkflowHooks {
|
): ExecutionLifecycleHooks {
|
||||||
|
const hooks = new ExecutionLifecycleHooks(mode, executionId, workflowData);
|
||||||
const saveSettings = toSaveSettings(workflowData.settings);
|
const saveSettings = toSaveSettings(workflowData.settings);
|
||||||
const optionalParameters = { pushRef, retryOf, saveSettings };
|
const optionalParameters = { pushRef, retryOf, saveSettings };
|
||||||
const toMerge = [
|
hookFunctionsNodeEvents(hooks);
|
||||||
hookFunctionsNodeEvents(),
|
hookFunctionsFinalizeExecutionStatus(hooks);
|
||||||
hookFunctionsFinalizeExecutionStatus(),
|
hookFunctionsSaveWorker(hooks, optionalParameters);
|
||||||
hookFunctionsSaveWorker(optionalParameters),
|
hookFunctionsSaveProgress(hooks, optionalParameters);
|
||||||
hookFunctionsSaveProgress(optionalParameters),
|
hookFunctionsStatistics(hooks);
|
||||||
hookFunctionsStatistics(),
|
hookFunctionsExternalHooks(hooks);
|
||||||
hookFunctionsExternalHooks(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (mode === 'manual' && Container.get(InstanceSettings).isWorker) {
|
if (mode === 'manual' && Container.get(InstanceSettings).isWorker) {
|
||||||
toMerge.push(hookFunctionsPush(optionalParameters));
|
hookFunctionsPush(hooks, optionalParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hookFunctions = mergeHookFunctions(...toMerge);
|
return hooks;
|
||||||
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns WorkflowHooks instance for main process if workflow runs via worker
|
* Returns ExecutionLifecycleHooks instance for main process if workflow runs via worker
|
||||||
*/
|
*/
|
||||||
export function getWorkflowHooksWorkerMain(
|
export function getLifecycleHooksForScalingMain(
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
executionId: string,
|
executionId: string,
|
||||||
workflowData: IWorkflowBase,
|
workflowData: IWorkflowBase,
|
||||||
{ pushRef, retryOf }: Omit<HooksSetupParameters, 'saveSettings'> = {},
|
{ pushRef, retryOf }: Omit<HooksSetupParameters, 'saveSettings'> = {},
|
||||||
): WorkflowHooks {
|
): ExecutionLifecycleHooks {
|
||||||
|
const hooks = new ExecutionLifecycleHooks(mode, executionId, workflowData);
|
||||||
const saveSettings = toSaveSettings(workflowData.settings);
|
const saveSettings = toSaveSettings(workflowData.settings);
|
||||||
const optionalParameters = { pushRef, retryOf, saveSettings };
|
const optionalParameters = { pushRef, retryOf, saveSettings };
|
||||||
const executionRepository = Container.get(ExecutionRepository);
|
const executionRepository = Container.get(ExecutionRepository);
|
||||||
const hookFunctions = mergeHookFunctions(
|
|
||||||
hookFunctionsWorkflowEvents(),
|
|
||||||
hookFunctionsSaveProgress(optionalParameters),
|
|
||||||
hookFunctionsExternalHooks(),
|
|
||||||
hookFunctionsFinalizeExecutionStatus(),
|
|
||||||
{
|
|
||||||
workflowExecuteAfter: [
|
|
||||||
async function (this: WorkflowHooks, fullRunData: IRun): Promise<void> {
|
|
||||||
// Don't delete executions before they are finished
|
|
||||||
if (!fullRunData.finished) return;
|
|
||||||
|
|
||||||
const isManualMode = this.mode === 'manual';
|
hookFunctionsWorkflowEvents(hooks);
|
||||||
|
hookFunctionsSaveProgress(hooks, optionalParameters);
|
||||||
|
hookFunctionsExternalHooks(hooks);
|
||||||
|
hookFunctionsFinalizeExecutionStatus(hooks);
|
||||||
|
|
||||||
if (isManualMode && !saveSettings.manual && !fullRunData.waitTill) {
|
hooks.addHandler('workflowExecuteAfter', async function (fullRunData) {
|
||||||
/**
|
// Don't delete executions before they are finished
|
||||||
* When manual executions are not being saved, we only soft-delete
|
if (!fullRunData.finished) return;
|
||||||
* the execution so that the user can access its binary data
|
|
||||||
* while building their workflow.
|
|
||||||
*
|
|
||||||
* The manual execution and its binary data will be hard-deleted
|
|
||||||
* on the next pruning cycle after the grace period set by
|
|
||||||
* `EXECUTIONS_DATA_HARD_DELETE_BUFFER`.
|
|
||||||
*/
|
|
||||||
await executionRepository.softDelete(this.executionId);
|
|
||||||
|
|
||||||
return;
|
const isManualMode = this.mode === 'manual';
|
||||||
}
|
|
||||||
|
|
||||||
const shouldNotSave =
|
if (isManualMode && !saveSettings.manual && !fullRunData.waitTill) {
|
||||||
(fullRunData.status === 'success' && !saveSettings.success) ||
|
/**
|
||||||
(fullRunData.status !== 'success' && !saveSettings.error);
|
* When manual executions are not being saved, we only soft-delete
|
||||||
|
* the execution so that the user can access its binary data
|
||||||
|
* while building their workflow.
|
||||||
|
*
|
||||||
|
* The manual execution and its binary data will be hard-deleted
|
||||||
|
* on the next pruning cycle after the grace period set by
|
||||||
|
* `EXECUTIONS_DATA_HARD_DELETE_BUFFER`.
|
||||||
|
*/
|
||||||
|
await executionRepository.softDelete(this.executionId);
|
||||||
|
|
||||||
if (!isManualMode && shouldNotSave && !fullRunData.waitTill) {
|
return;
|
||||||
await executionRepository.hardDelete({
|
}
|
||||||
workflowId: this.workflowData.id,
|
|
||||||
executionId: this.executionId,
|
const shouldNotSave =
|
||||||
});
|
(fullRunData.status === 'success' && !saveSettings.success) ||
|
||||||
}
|
(fullRunData.status !== 'success' && !saveSettings.error);
|
||||||
},
|
|
||||||
],
|
if (!isManualMode && shouldNotSave && !fullRunData.waitTill) {
|
||||||
},
|
await executionRepository.hardDelete({
|
||||||
);
|
workflowId: this.workflowData.id,
|
||||||
|
executionId: this.executionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// When running with worker mode, main process executes
|
// When running with worker mode, main process executes
|
||||||
// Only workflowExecuteBefore + workflowExecuteAfter
|
// Only workflowExecuteBefore + workflowExecuteAfter
|
||||||
// So to avoid confusion, we are removing other hooks.
|
// So to avoid confusion, we are removing other hooks.
|
||||||
hookFunctions.nodeExecuteBefore = [];
|
hooks.handlers.nodeExecuteBefore = [];
|
||||||
hookFunctions.nodeExecuteAfter = [];
|
hooks.handlers.nodeExecuteAfter = [];
|
||||||
|
|
||||||
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData);
|
return hooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns WorkflowHooks instance for running the main workflow
|
* Returns ExecutionLifecycleHooks instance for running the main workflow
|
||||||
*/
|
*/
|
||||||
export function getWorkflowHooksMain(
|
export function getLifecycleHooksForRegularMain(
|
||||||
data: IWorkflowExecutionDataProcess,
|
data: IWorkflowExecutionDataProcess,
|
||||||
executionId: string,
|
executionId: string,
|
||||||
): WorkflowHooks {
|
): ExecutionLifecycleHooks {
|
||||||
const { pushRef, retryOf } = data;
|
const { pushRef, retryOf, executionMode, workflowData } = data;
|
||||||
const saveSettings = toSaveSettings(data.workflowData.settings);
|
const hooks = new ExecutionLifecycleHooks(executionMode, executionId, workflowData);
|
||||||
|
const saveSettings = toSaveSettings(workflowData.settings);
|
||||||
const optionalParameters = { pushRef, retryOf: retryOf ?? undefined, saveSettings };
|
const optionalParameters = { pushRef, retryOf: retryOf ?? undefined, saveSettings };
|
||||||
const hookFunctions = mergeHookFunctions(
|
hookFunctionsWorkflowEvents(hooks);
|
||||||
hookFunctionsWorkflowEvents(),
|
hookFunctionsNodeEvents(hooks);
|
||||||
hookFunctionsNodeEvents(),
|
hookFunctionsFinalizeExecutionStatus(hooks);
|
||||||
hookFunctionsFinalizeExecutionStatus(),
|
hookFunctionsSave(hooks, optionalParameters);
|
||||||
hookFunctionsSave(optionalParameters),
|
hookFunctionsPush(hooks, optionalParameters);
|
||||||
hookFunctionsPush(optionalParameters),
|
hookFunctionsSaveProgress(hooks, optionalParameters);
|
||||||
hookFunctionsSaveProgress(optionalParameters),
|
hookFunctionsStatistics(hooks);
|
||||||
hookFunctionsStatistics(),
|
hookFunctionsExternalHooks(hooks);
|
||||||
hookFunctionsExternalHooks(),
|
return hooks;
|
||||||
);
|
|
||||||
return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { ARTIFICIAL_TASK_DATA } from '@/constants';
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import { NodeCrashedError } from '@/errors/node-crashed.error';
|
import { NodeCrashedError } from '@/errors/node-crashed.error';
|
||||||
import { WorkflowCrashedError } from '@/errors/workflow-crashed.error';
|
import { WorkflowCrashedError } from '@/errors/workflow-crashed.error';
|
||||||
import { getWorkflowHooksMain } from '@/execution-lifecycle/execution-lifecycle-hooks';
|
import { getLifecycleHooksForRegularMain } from '@/execution-lifecycle/execution-lifecycle-hooks';
|
||||||
import type { IExecutionResponse } from '@/interfaces';
|
import type { IExecutionResponse } from '@/interfaces';
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ export class ExecutionRecoveryService {
|
||||||
private async runHooks(execution: IExecutionResponse) {
|
private async runHooks(execution: IExecutionResponse) {
|
||||||
execution.data ??= { resultData: { runData: {} } };
|
execution.data ??= { resultData: { runData: {} } };
|
||||||
|
|
||||||
const externalHooks = getWorkflowHooksMain(
|
const lifecycleHooks = getLifecycleHooksForRegularMain(
|
||||||
{
|
{
|
||||||
userId: '',
|
userId: '',
|
||||||
workflowData: execution.workflowData,
|
workflowData: execution.workflowData,
|
||||||
|
@ -196,6 +196,6 @@ export class ExecutionRecoveryService {
|
||||||
status: execution.status,
|
status: execution.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
await externalHooks.executeHookFunctions('workflowExecuteAfter', [run]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [run]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import type PCancelable from 'p-cancelable';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
import { getWorkflowHooksWorkerExecuter } from '@/execution-lifecycle/execution-lifecycle-hooks';
|
import { getLifecycleHooksForScalingWorker } from '@/execution-lifecycle/execution-lifecycle-hooks';
|
||||||
import { ManualExecutionService } from '@/manual-execution.service';
|
import { ManualExecutionService } from '@/manual-execution.service';
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
||||||
|
@ -131,30 +131,29 @@ export class JobProcessor {
|
||||||
|
|
||||||
const { pushRef } = job.data;
|
const { pushRef } = job.data;
|
||||||
|
|
||||||
additionalData.hooks = getWorkflowHooksWorkerExecuter(
|
const lifecycleHooks = getLifecycleHooksForScalingWorker(
|
||||||
execution.mode,
|
execution.mode,
|
||||||
job.data.executionId,
|
job.data.executionId,
|
||||||
execution.workflowData,
|
execution.workflowData,
|
||||||
{ retryOf: execution.retryOf ?? undefined, pushRef },
|
{ retryOf: execution.retryOf ?? undefined, pushRef },
|
||||||
);
|
);
|
||||||
|
additionalData.hooks = lifecycleHooks;
|
||||||
|
|
||||||
if (pushRef) {
|
if (pushRef) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
additionalData.sendDataToUI = WorkflowExecuteAdditionalData.sendDataToUI.bind({ pushRef });
|
additionalData.sendDataToUI = WorkflowExecuteAdditionalData.sendDataToUI.bind({ pushRef });
|
||||||
}
|
}
|
||||||
|
|
||||||
additionalData.hooks.hookFunctions.sendResponse = [
|
lifecycleHooks.addHandler('sendResponse', async (response): Promise<void> => {
|
||||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
const msg: RespondToWebhookMessage = {
|
||||||
const msg: RespondToWebhookMessage = {
|
kind: 'respond-to-webhook',
|
||||||
kind: 'respond-to-webhook',
|
executionId,
|
||||||
executionId,
|
response: this.encodeWebhookResponse(response),
|
||||||
response: this.encodeWebhookResponse(response),
|
workerId: this.instanceSettings.hostId,
|
||||||
workerId: this.instanceSettings.hostId,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
await job.progress(msg);
|
await job.progress(msg);
|
||||||
},
|
});
|
||||||
];
|
|
||||||
|
|
||||||
additionalData.executionId = executionId;
|
additionalData.executionId = executionId;
|
||||||
|
|
||||||
|
@ -206,7 +205,7 @@ export class JobProcessor {
|
||||||
data: { resultData: { error, runData: {} } },
|
data: { resultData: { error, runData: {} } },
|
||||||
};
|
};
|
||||||
|
|
||||||
await additionalData.hooks.executeHookFunctions('workflowExecuteAfter', [runData]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [runData]);
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
@ -37,7 +37,7 @@ import { ExecutionRepository } from '@/databases/repositories/execution.reposito
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
import { EventService } from '@/events/event.service';
|
import { EventService } from '@/events/event.service';
|
||||||
import type { AiEventMap, AiEventPayload } from '@/events/maps/ai.event-map';
|
import type { AiEventMap, AiEventPayload } from '@/events/maps/ai.event-map';
|
||||||
import { getWorkflowHooksIntegrated } from '@/execution-lifecycle/execution-lifecycle-hooks';
|
import { getLifecycleHooksForSubExecutions } from '@/execution-lifecycle/execution-lifecycle-hooks';
|
||||||
import type { UpdateExecutionPayload } from '@/interfaces';
|
import type { UpdateExecutionPayload } from '@/interfaces';
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
|
@ -217,7 +217,7 @@ async function startExecution(
|
||||||
// Create new additionalData to have different workflow loaded and to call
|
// Create new additionalData to have different workflow loaded and to call
|
||||||
// different webhooks
|
// different webhooks
|
||||||
const additionalDataIntegrated = await getBase();
|
const additionalDataIntegrated = await getBase();
|
||||||
additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(
|
additionalDataIntegrated.hooks = getLifecycleHooksForSubExecutions(
|
||||||
runData.executionMode,
|
runData.executionMode,
|
||||||
executionId,
|
executionId,
|
||||||
workflowData,
|
workflowData,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-shadow */
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
|
import type { ExecutionLifecycleHooks } from 'n8n-core';
|
||||||
import { ErrorReporter, InstanceSettings, Logger, WorkflowExecute } from 'n8n-core';
|
import { ErrorReporter, InstanceSettings, Logger, WorkflowExecute } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
|
@ -11,7 +12,6 @@ import type {
|
||||||
IPinData,
|
IPinData,
|
||||||
IRun,
|
IRun,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
WorkflowHooks,
|
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { ExecutionCancelledError, Workflow } from 'n8n-workflow';
|
import { ExecutionCancelledError, Workflow } from 'n8n-workflow';
|
||||||
|
@ -22,9 +22,9 @@ import config from '@/config';
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import { ExecutionNotFoundError } from '@/errors/execution-not-found-error';
|
import { ExecutionNotFoundError } from '@/errors/execution-not-found-error';
|
||||||
import {
|
import {
|
||||||
getWorkflowHooksMain,
|
getLifecycleHooksForRegularMain,
|
||||||
getWorkflowHooksWorkerExecuter,
|
getLifecycleHooksForScalingWorker,
|
||||||
getWorkflowHooksWorkerMain,
|
getLifecycleHooksForScalingMain,
|
||||||
} from '@/execution-lifecycle/execution-lifecycle-hooks';
|
} from '@/execution-lifecycle/execution-lifecycle-hooks';
|
||||||
import { ManualExecutionService } from '@/manual-execution.service';
|
import { ManualExecutionService } from '@/manual-execution.service';
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
|
@ -61,7 +61,7 @@ export class WorkflowRunner {
|
||||||
startedAt: Date,
|
startedAt: Date,
|
||||||
executionMode: WorkflowExecuteMode,
|
executionMode: WorkflowExecuteMode,
|
||||||
executionId: string,
|
executionId: string,
|
||||||
hooks?: WorkflowHooks,
|
hooks?: ExecutionLifecycleHooks,
|
||||||
) {
|
) {
|
||||||
// This means the execution was probably cancelled and has already
|
// This means the execution was probably cancelled and has already
|
||||||
// been cleaned up.
|
// been cleaned up.
|
||||||
|
@ -116,9 +116,7 @@ export class WorkflowRunner {
|
||||||
// set the execution to failed.
|
// set the execution to failed.
|
||||||
this.activeExecutions.finalizeExecution(executionId, fullRunData);
|
this.activeExecutions.finalizeExecution(executionId, fullRunData);
|
||||||
|
|
||||||
if (hooks) {
|
await hooks?.runHook('workflowExecuteAfter', [fullRunData]);
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [fullRunData]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run the workflow
|
/** Run the workflow
|
||||||
|
@ -140,12 +138,9 @@ export class WorkflowRunner {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Create a failed execution with the data for the node, save it and abort execution
|
// Create a failed execution with the data for the node, save it and abort execution
|
||||||
const runData = generateFailedExecutionFromError(data.executionMode, error, error.node);
|
const runData = generateFailedExecutionFromError(data.executionMode, error, error.node);
|
||||||
const workflowHooks = getWorkflowHooksMain(data, executionId);
|
const lifecycleHooks = getLifecycleHooksForRegularMain(data, executionId);
|
||||||
await workflowHooks.executeHookFunctions('workflowExecuteBefore', [
|
await lifecycleHooks.runHook('workflowExecuteBefore', [undefined, data.executionData]);
|
||||||
undefined,
|
await lifecycleHooks.runHook('workflowExecuteAfter', [runData]);
|
||||||
data.executionData,
|
|
||||||
]);
|
|
||||||
await workflowHooks.executeHookFunctions('workflowExecuteAfter', [runData]);
|
|
||||||
responsePromise?.reject(error);
|
responsePromise?.reject(error);
|
||||||
this.activeExecutions.finalizeExecution(executionId);
|
this.activeExecutions.finalizeExecution(executionId);
|
||||||
return executionId;
|
return executionId;
|
||||||
|
@ -250,13 +245,12 @@ export class WorkflowRunner {
|
||||||
await this.executionRepository.setRunning(executionId); // write
|
await this.executionRepository.setRunning(executionId); // write
|
||||||
|
|
||||||
try {
|
try {
|
||||||
additionalData.hooks = getWorkflowHooksMain(data, executionId);
|
const lifecycleHooks = getLifecycleHooksForRegularMain(data, executionId);
|
||||||
|
additionalData.hooks = lifecycleHooks;
|
||||||
|
|
||||||
additionalData.hooks.hookFunctions.sendResponse = [
|
lifecycleHooks.addHandler('sendResponse', (response) => {
|
||||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
this.activeExecutions.resolveResponsePromise(executionId, response);
|
||||||
this.activeExecutions.resolveResponsePromise(executionId, response);
|
});
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
additionalData.setExecutionStatus = WorkflowExecuteAdditionalData.setExecutionStatus.bind({
|
additionalData.setExecutionStatus = WorkflowExecuteAdditionalData.setExecutionStatus.bind({
|
||||||
executionId,
|
executionId,
|
||||||
|
@ -347,27 +341,32 @@ export class WorkflowRunner {
|
||||||
// TODO: For realtime jobs should probably also not do retry or not retry if they are older than x seconds.
|
// TODO: For realtime jobs should probably also not do retry or not retry if they are older than x seconds.
|
||||||
// Check if they get retried by default and how often.
|
// Check if they get retried by default and how often.
|
||||||
let job: Job;
|
let job: Job;
|
||||||
let hooks: WorkflowHooks;
|
let lifecycleHooks: ExecutionLifecycleHooks;
|
||||||
try {
|
try {
|
||||||
job = await this.scalingService.addJob(jobData, { priority: realtime ? 50 : 100 });
|
job = await this.scalingService.addJob(jobData, { priority: realtime ? 50 : 100 });
|
||||||
|
|
||||||
hooks = getWorkflowHooksWorkerMain(data.executionMode, executionId, data.workflowData, {
|
lifecycleHooks = getLifecycleHooksForScalingMain(
|
||||||
retryOf: data.retryOf ?? undefined,
|
data.executionMode,
|
||||||
});
|
executionId,
|
||||||
|
data.workflowData,
|
||||||
|
{
|
||||||
|
retryOf: data.retryOf ?? undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Normally also workflow should be supplied here but as it only used for sending
|
// Normally also workflow should be supplied here but as it only used for sending
|
||||||
// data to editor-UI is not needed.
|
// data to editor-UI is not needed.
|
||||||
await hooks.executeHookFunctions('workflowExecuteBefore', [undefined, data.executionData]);
|
await lifecycleHooks.runHook('workflowExecuteBefore', [undefined, data.executionData]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
// We use "getLifecycleHooksForScalingWorker" as "getLifecycleHooksForScalingMain" does not contain the
|
||||||
// "workflowExecuteAfter" which we require.
|
// "workflowExecuteAfter" which we require.
|
||||||
const hooks = getWorkflowHooksWorkerExecuter(
|
const lifecycleHooks = getLifecycleHooksForScalingWorker(
|
||||||
data.executionMode,
|
data.executionMode,
|
||||||
executionId,
|
executionId,
|
||||||
data.workflowData,
|
data.workflowData,
|
||||||
{ retryOf: data.retryOf ?? undefined },
|
{ retryOf: data.retryOf ?? undefined },
|
||||||
);
|
);
|
||||||
await this.processError(error, new Date(), data.executionMode, executionId, hooks);
|
await this.processError(error, new Date(), data.executionMode, executionId, lifecycleHooks);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,9 +376,9 @@ export class WorkflowRunner {
|
||||||
onCancel(async () => {
|
onCancel(async () => {
|
||||||
await this.scalingService.stopJob(job);
|
await this.scalingService.stopJob(job);
|
||||||
|
|
||||||
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
// We use "getLifecycleHooksForScalingWorker" as "getLifecycleHooksForScalingMain" does not contain the
|
||||||
// "workflowExecuteAfter" which we require.
|
// "workflowExecuteAfter" which we require.
|
||||||
const hooksWorker = getWorkflowHooksWorkerExecuter(
|
const lifecycleHooks = getLifecycleHooksForScalingWorker(
|
||||||
data.executionMode,
|
data.executionMode,
|
||||||
executionId,
|
executionId,
|
||||||
data.workflowData,
|
data.workflowData,
|
||||||
|
@ -387,7 +386,13 @@ export class WorkflowRunner {
|
||||||
);
|
);
|
||||||
|
|
||||||
const error = new ExecutionCancelledError(executionId);
|
const error = new ExecutionCancelledError(executionId);
|
||||||
await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker);
|
await this.processError(
|
||||||
|
error,
|
||||||
|
new Date(),
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
lifecycleHooks,
|
||||||
|
);
|
||||||
|
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
@ -402,16 +407,22 @@ export class WorkflowRunner {
|
||||||
error = new MaxStalledCountError(error);
|
error = new MaxStalledCountError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
// We use "getLifecycleHooksForScalingWorker" as "getLifecycleHooksForScalingMain" does not contain the
|
||||||
// "workflowExecuteAfter" which we require.
|
// "workflowExecuteAfter" which we require.
|
||||||
const hooks = getWorkflowHooksWorkerExecuter(
|
const lifecycleHooks = getLifecycleHooksForScalingWorker(
|
||||||
data.executionMode,
|
data.executionMode,
|
||||||
executionId,
|
executionId,
|
||||||
data.workflowData,
|
data.workflowData,
|
||||||
{ retryOf: data.retryOf ?? undefined },
|
{ retryOf: data.retryOf ?? undefined },
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.processError(error, new Date(), data.executionMode, executionId, hooks);
|
await this.processError(
|
||||||
|
error,
|
||||||
|
new Date(),
|
||||||
|
data.executionMode,
|
||||||
|
executionId,
|
||||||
|
lifecycleHooks,
|
||||||
|
);
|
||||||
|
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
|
@ -437,7 +448,7 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
// Normally also static data should be supplied here but as it only used for sending
|
// Normally also static data should be supplied here but as it only used for sending
|
||||||
// data to editor-UI is not needed.
|
// data to editor-UI is not needed.
|
||||||
await hooks.executeHookFunctions('workflowExecuteAfter', [runData]);
|
await lifecycleHooks.runHook('workflowExecuteAfter', [runData]);
|
||||||
|
|
||||||
resolve(runData);
|
resolve(runData);
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,10 +6,10 @@ import type {
|
||||||
IRequestOptions,
|
IRequestOptions,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowHooks,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
|
|
||||||
|
import type { ExecutionLifecycleHooks } from '@/execution-engine/execution-lifecycle-hooks';
|
||||||
import {
|
import {
|
||||||
copyInputItems,
|
copyInputItems,
|
||||||
invokeAxios,
|
invokeAxios,
|
||||||
|
@ -21,12 +21,12 @@ describe('NodeExecuteFunctions', () => {
|
||||||
describe('proxyRequestToAxios', () => {
|
describe('proxyRequestToAxios', () => {
|
||||||
const baseUrl = 'http://example.de';
|
const baseUrl = 'http://example.de';
|
||||||
const workflow = mock<Workflow>();
|
const workflow = mock<Workflow>();
|
||||||
const hooks = mock<WorkflowHooks>();
|
const hooks = mock<ExecutionLifecycleHooks>();
|
||||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({ hooks });
|
const additionalData = mock<IWorkflowExecuteAdditionalData>({ hooks });
|
||||||
const node = mock<INode>();
|
const node = mock<INode>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
hooks.executeHookFunctions.mockClear();
|
hooks.runHook.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should rethrow an error with `status` property', async () => {
|
test('should rethrow an error with `status` property', async () => {
|
||||||
|
@ -42,10 +42,7 @@ describe('NodeExecuteFunctions', () => {
|
||||||
test('should not throw if the response status is 200', async () => {
|
test('should not throw if the response status is 200', async () => {
|
||||||
nock(baseUrl).get('/test').reply(200);
|
nock(baseUrl).get('/test').reply(200);
|
||||||
await proxyRequestToAxios(workflow, additionalData, node, `${baseUrl}/test`);
|
await proxyRequestToAxios(workflow, additionalData, node, `${baseUrl}/test`);
|
||||||
expect(hooks.executeHookFunctions).toHaveBeenCalledWith('nodeFetchedData', [
|
expect(hooks.runHook).toHaveBeenCalledWith('nodeFetchedData', [workflow.id, node]);
|
||||||
workflow.id,
|
|
||||||
node,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw if the response status is 403', async () => {
|
test('should throw if the response status is 403', async () => {
|
||||||
|
@ -65,7 +62,7 @@ describe('NodeExecuteFunctions', () => {
|
||||||
expect(error.config).toBeUndefined();
|
expect(error.config).toBeUndefined();
|
||||||
expect(error.message).toEqual('403 - "Forbidden"');
|
expect(error.message).toEqual('403 - "Forbidden"');
|
||||||
}
|
}
|
||||||
expect(hooks.executeHookFunctions).not.toHaveBeenCalled();
|
expect(hooks.runHook).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not throw if the response status is 404, but `simple` option is set to `false`', async () => {
|
test('should not throw if the response status is 404, but `simple` option is set to `false`', async () => {
|
||||||
|
@ -76,10 +73,7 @@ describe('NodeExecuteFunctions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response).toEqual('Not Found');
|
expect(response).toEqual('Not Found');
|
||||||
expect(hooks.executeHookFunctions).toHaveBeenCalledWith('nodeFetchedData', [
|
expect(hooks.runHook).toHaveBeenCalledWith('nodeFetchedData', [workflow.id, node]);
|
||||||
workflow.id,
|
|
||||||
node,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return full response when `resolveWithFullResponse` is set to true', async () => {
|
test('should return full response when `resolveWithFullResponse` is set to true', async () => {
|
||||||
|
@ -96,10 +90,7 @@ describe('NodeExecuteFunctions', () => {
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
statusMessage: 'Not Found',
|
statusMessage: 'Not Found',
|
||||||
});
|
});
|
||||||
expect(hooks.executeHookFunctions).toHaveBeenCalledWith('nodeFetchedData', [
|
expect(hooks.runHook).toHaveBeenCalledWith('nodeFetchedData', [workflow.id, node]);
|
||||||
workflow.id,
|
|
||||||
node,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('redirects', () => {
|
describe('redirects', () => {
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type {
|
||||||
|
IDataObject,
|
||||||
|
IExecuteResponsePromiseData,
|
||||||
|
INode,
|
||||||
|
IRun,
|
||||||
|
IRunExecutionData,
|
||||||
|
ITaskData,
|
||||||
|
IWorkflowBase,
|
||||||
|
Workflow,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExecutionLifecycleHookName,
|
||||||
|
ExecutionLifecyleHookHandlers,
|
||||||
|
} from '../execution-lifecycle-hooks';
|
||||||
|
import { ExecutionLifecycleHooks } from '../execution-lifecycle-hooks';
|
||||||
|
|
||||||
|
describe('ExecutionLifecycleHooks', () => {
|
||||||
|
const executionId = '123';
|
||||||
|
const workflowData = mock<IWorkflowBase>();
|
||||||
|
|
||||||
|
let hooks: ExecutionLifecycleHooks;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
hooks = new ExecutionLifecycleHooks('internal', executionId, workflowData);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor()', () => {
|
||||||
|
it('should initialize with correct properties', () => {
|
||||||
|
expect(hooks.mode).toBe('internal');
|
||||||
|
expect(hooks.executionId).toBe(executionId);
|
||||||
|
expect(hooks.workflowData).toBe(workflowData);
|
||||||
|
expect(hooks.handlers).toEqual({
|
||||||
|
nodeExecuteAfter: [],
|
||||||
|
nodeExecuteBefore: [],
|
||||||
|
nodeFetchedData: [],
|
||||||
|
sendResponse: [],
|
||||||
|
workflowExecuteAfter: [],
|
||||||
|
workflowExecuteBefore: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addHandler()', () => {
|
||||||
|
const hooksHandlers =
|
||||||
|
mock<{
|
||||||
|
[K in keyof ExecutionLifecyleHookHandlers]: ExecutionLifecyleHookHandlers[K][number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const testCases: Array<{
|
||||||
|
hook: ExecutionLifecycleHookName;
|
||||||
|
args: Parameters<ExecutionLifecyleHookHandlers[keyof ExecutionLifecyleHookHandlers][number]>;
|
||||||
|
}> = [
|
||||||
|
{ hook: 'nodeExecuteBefore', args: ['testNode'] },
|
||||||
|
{
|
||||||
|
hook: 'nodeExecuteAfter',
|
||||||
|
args: ['testNode', mock<ITaskData>(), mock<IRunExecutionData>()],
|
||||||
|
},
|
||||||
|
{ hook: 'workflowExecuteBefore', args: [mock<Workflow>(), mock<IRunExecutionData>()] },
|
||||||
|
{ hook: 'workflowExecuteAfter', args: [mock<IRun>(), mock<IDataObject>()] },
|
||||||
|
{ hook: 'sendResponse', args: [mock<IExecuteResponsePromiseData>()] },
|
||||||
|
{ hook: 'nodeFetchedData', args: ['workflow123', mock<INode>()] },
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(testCases)(
|
||||||
|
'should add handlers to $hook hook and call them',
|
||||||
|
async ({ hook, args }) => {
|
||||||
|
hooks.addHandler(hook, hooksHandlers[hook]);
|
||||||
|
await hooks.runHook(hook, args);
|
||||||
|
expect(hooksHandlers[hook]).toHaveBeenCalledWith(...args);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runHook()', () => {
|
||||||
|
it('should execute multiple hooks in order', async () => {
|
||||||
|
const executionOrder: string[] = [];
|
||||||
|
const hook1 = jest.fn().mockImplementation(async () => {
|
||||||
|
executionOrder.push('hook1');
|
||||||
|
});
|
||||||
|
const hook2 = jest.fn().mockImplementation(async () => {
|
||||||
|
executionOrder.push('hook2');
|
||||||
|
});
|
||||||
|
|
||||||
|
hooks.addHandler('nodeExecuteBefore', hook1, hook2);
|
||||||
|
await hooks.runHook('nodeExecuteBefore', ['testNode']);
|
||||||
|
|
||||||
|
expect(executionOrder).toEqual(['hook1', 'hook2']);
|
||||||
|
expect(hook1).toHaveBeenCalled();
|
||||||
|
expect(hook2).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should maintain correct "this" context', async () => {
|
||||||
|
const hook = jest.fn().mockImplementation(async function (this: ExecutionLifecycleHooks) {
|
||||||
|
expect(this.executionId).toBe(executionId);
|
||||||
|
expect(this.mode).toBe('internal');
|
||||||
|
});
|
||||||
|
|
||||||
|
hooks.addHandler('nodeExecuteBefore', hook);
|
||||||
|
await hooks.runHook('nodeExecuteBefore', ['testNode']);
|
||||||
|
|
||||||
|
expect(hook).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors in hooks', async () => {
|
||||||
|
const errorHook = jest.fn().mockRejectedValue(new Error('Hook failed'));
|
||||||
|
hooks.addHandler('nodeExecuteBefore', errorHook);
|
||||||
|
|
||||||
|
await expect(hooks.runHook('nodeExecuteBefore', ['testNode'])).rejects.toThrow('Hook failed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,10 +9,10 @@ import type {
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypes,
|
INodeTypes,
|
||||||
ITriggerFunctions,
|
ITriggerFunctions,
|
||||||
WorkflowHooks,
|
|
||||||
IRun,
|
IRun,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { ExecutionLifecycleHooks } from '../execution-lifecycle-hooks';
|
||||||
import { TriggersAndPollers } from '../triggers-and-pollers';
|
import { TriggersAndPollers } from '../triggers-and-pollers';
|
||||||
|
|
||||||
describe('TriggersAndPollers', () => {
|
describe('TriggersAndPollers', () => {
|
||||||
|
@ -23,15 +23,8 @@ describe('TriggersAndPollers', () => {
|
||||||
});
|
});
|
||||||
const nodeTypes = mock<INodeTypes>();
|
const nodeTypes = mock<INodeTypes>();
|
||||||
const workflow = mock<Workflow>({ nodeTypes });
|
const workflow = mock<Workflow>({ nodeTypes });
|
||||||
const hookFunctions = mock<WorkflowHooks['hookFunctions']>({
|
const hooks = new ExecutionLifecycleHooks('internal', '123', mock());
|
||||||
sendResponse: [],
|
const additionalData = mock<IWorkflowExecuteAdditionalData>({ hooks });
|
||||||
workflowExecuteAfter: [],
|
|
||||||
});
|
|
||||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({
|
|
||||||
hooks: {
|
|
||||||
hookFunctions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const triggersAndPollers = new TriggersAndPollers();
|
const triggersAndPollers = new TriggersAndPollers();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -98,8 +91,7 @@ describe('TriggersAndPollers', () => {
|
||||||
|
|
||||||
getMockTriggerFunctions()?.emit?.(mockEmitData, responsePromise);
|
getMockTriggerFunctions()?.emit?.(mockEmitData, responsePromise);
|
||||||
|
|
||||||
expect(hookFunctions.sendResponse?.length).toBe(1);
|
await hooks.runHook('sendResponse', [{ testResponse: true }]);
|
||||||
await hookFunctions.sendResponse![0]?.({ testResponse: true });
|
|
||||||
expect(responsePromise.resolve).toHaveBeenCalledWith({ testResponse: true });
|
expect(responsePromise.resolve).toHaveBeenCalledWith({ testResponse: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -111,10 +103,10 @@ describe('TriggersAndPollers', () => {
|
||||||
await runTriggerHelper('manual');
|
await runTriggerHelper('manual');
|
||||||
getMockTriggerFunctions()?.emit?.(mockEmitData, responsePromise, donePromise);
|
getMockTriggerFunctions()?.emit?.(mockEmitData, responsePromise, donePromise);
|
||||||
|
|
||||||
await hookFunctions.sendResponse![0]?.({ testResponse: true });
|
await hooks.runHook('sendResponse', [{ testResponse: true }]);
|
||||||
expect(responsePromise.resolve).toHaveBeenCalledWith({ testResponse: true });
|
expect(responsePromise.resolve).toHaveBeenCalledWith({ testResponse: true });
|
||||||
|
|
||||||
await hookFunctions.workflowExecuteAfter?.[0]?.(mockRunData, {});
|
await hooks.runHook('workflowExecuteAfter', [mockRunData, {}]);
|
||||||
expect(donePromise.resolve).toHaveBeenCalledWith(mockRunData);
|
expect(donePromise.resolve).toHaveBeenCalledWith(mockRunData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,6 +44,7 @@ import {
|
||||||
import * as Helpers from '@test/helpers';
|
import * as Helpers from '@test/helpers';
|
||||||
import { legacyWorkflowExecuteTests, v1WorkflowExecuteTests } from '@test/helpers/constants';
|
import { legacyWorkflowExecuteTests, v1WorkflowExecuteTests } from '@test/helpers/constants';
|
||||||
|
|
||||||
|
import type { ExecutionLifecycleHooks } from '../execution-lifecycle-hooks';
|
||||||
import { DirectedGraph } from '../partial-execution-utils';
|
import { DirectedGraph } from '../partial-execution-utils';
|
||||||
import * as partialExecutionUtils from '../partial-execution-utils';
|
import * as partialExecutionUtils from '../partial-execution-utils';
|
||||||
import { createNodeData, toITaskData } from '../partial-execution-utils/__tests__/helpers';
|
import { createNodeData, toITaskData } from '../partial-execution-utils/__tests__/helpers';
|
||||||
|
@ -1211,6 +1212,7 @@ describe('WorkflowExecute', () => {
|
||||||
|
|
||||||
let runExecutionData: IRunExecutionData;
|
let runExecutionData: IRunExecutionData;
|
||||||
let workflowExecute: WorkflowExecute;
|
let workflowExecute: WorkflowExecute;
|
||||||
|
let additionalData: IWorkflowExecuteAdditionalData;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
runExecutionData = {
|
runExecutionData = {
|
||||||
|
@ -1224,9 +1226,12 @@ describe('WorkflowExecute', () => {
|
||||||
waitingExecutionSource: null,
|
waitingExecutionSource: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
workflowExecute = new WorkflowExecute(mock(), 'manual', runExecutionData);
|
additionalData = mock();
|
||||||
|
additionalData.hooks = mock<ExecutionLifecycleHooks>();
|
||||||
|
|
||||||
jest.spyOn(workflowExecute, 'executeHook').mockResolvedValue(undefined);
|
workflowExecute = new WorkflowExecute(additionalData, 'manual', runExecutionData);
|
||||||
|
|
||||||
|
jest.spyOn(additionalData.hooks, 'runHook').mockResolvedValue(undefined);
|
||||||
jest.spyOn(workflowExecute, 'moveNodeMetadata').mockImplementation();
|
jest.spyOn(workflowExecute, 'moveNodeMetadata').mockImplementation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1294,7 +1299,7 @@ describe('WorkflowExecute', () => {
|
||||||
// Verify static data handling
|
// Verify static data handling
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(workflowExecute.moveNodeMetadata).toHaveBeenCalled();
|
expect(workflowExecute.moveNodeMetadata).toHaveBeenCalled();
|
||||||
expect(workflowExecute.executeHook).toHaveBeenCalledWith('workflowExecuteAfter', [
|
expect(additionalData.hooks?.runHook).toHaveBeenCalledWith('workflowExecuteAfter', [
|
||||||
result,
|
result,
|
||||||
workflow.staticData,
|
workflow.staticData,
|
||||||
]);
|
]);
|
||||||
|
|
119
packages/core/src/execution-engine/execution-lifecycle-hooks.ts
Normal file
119
packages/core/src/execution-engine/execution-lifecycle-hooks.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import type {
|
||||||
|
IDataObject,
|
||||||
|
IExecuteResponsePromiseData,
|
||||||
|
INode,
|
||||||
|
IRun,
|
||||||
|
IRunExecutionData,
|
||||||
|
ITaskData,
|
||||||
|
IWorkflowBase,
|
||||||
|
Workflow,
|
||||||
|
WorkflowExecuteMode,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export type ExecutionLifecyleHookHandlers = {
|
||||||
|
nodeExecuteBefore: Array<
|
||||||
|
(this: ExecutionLifecycleHooks, nodeName: string) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
|
||||||
|
nodeExecuteAfter: Array<
|
||||||
|
(
|
||||||
|
this: ExecutionLifecycleHooks,
|
||||||
|
nodeName: string,
|
||||||
|
data: ITaskData,
|
||||||
|
executionData: IRunExecutionData,
|
||||||
|
) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
|
||||||
|
workflowExecuteBefore: Array<
|
||||||
|
(
|
||||||
|
this: ExecutionLifecycleHooks,
|
||||||
|
workflow: Workflow,
|
||||||
|
data?: IRunExecutionData,
|
||||||
|
) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
|
||||||
|
workflowExecuteAfter: Array<
|
||||||
|
(this: ExecutionLifecycleHooks, data: IRun, newStaticData: IDataObject) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** Used by trigger and webhook nodes to respond back to the request */
|
||||||
|
sendResponse: Array<
|
||||||
|
(this: ExecutionLifecycleHooks, response: IExecuteResponsePromiseData) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executed after a node fetches data
|
||||||
|
* - For a webhook node, after the node had been run.
|
||||||
|
* - For a http-request node, or any other node that makes http requests that still use the deprecated request* methods, after every successful http request
|
||||||
|
s */
|
||||||
|
nodeFetchedData: Array<
|
||||||
|
(this: ExecutionLifecycleHooks, workflowId: string, node: INode) => Promise<void> | void
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExecutionLifecycleHookName = keyof ExecutionLifecyleHookHandlers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains hooks that trigger at specific events in an execution's lifecycle. Every hook has an array of callbacks to run.
|
||||||
|
*
|
||||||
|
* Common use cases include:
|
||||||
|
* - Saving execution progress to database
|
||||||
|
* - Pushing execution status updates to the frontend
|
||||||
|
* - Recording workflow statistics
|
||||||
|
* - Running external hooks for execution events
|
||||||
|
* - Error and Cancellation handling and cleanup
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const hooks = new ExecutionLifecycleHooks(mode, executionId, workflowData);
|
||||||
|
* hooks.add('workflowExecuteAfter, async function(fullRunData) {
|
||||||
|
* await saveToDatabase(executionId, fullRunData);
|
||||||
|
*});
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class ExecutionLifecycleHooks {
|
||||||
|
readonly handlers: ExecutionLifecyleHookHandlers = {
|
||||||
|
nodeExecuteAfter: [],
|
||||||
|
nodeExecuteBefore: [],
|
||||||
|
nodeFetchedData: [],
|
||||||
|
sendResponse: [],
|
||||||
|
workflowExecuteAfter: [],
|
||||||
|
workflowExecuteBefore: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly mode: WorkflowExecuteMode,
|
||||||
|
readonly executionId: string,
|
||||||
|
readonly workflowData: IWorkflowBase,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
addHandler<Hook extends keyof ExecutionLifecyleHookHandlers>(
|
||||||
|
hookName: Hook,
|
||||||
|
...handlers: Array<ExecutionLifecyleHookHandlers[Hook][number]>
|
||||||
|
): void {
|
||||||
|
// @ts-expect-error FIX THIS
|
||||||
|
this.handlers[hookName].push(...handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runHook<
|
||||||
|
Hook extends keyof ExecutionLifecyleHookHandlers,
|
||||||
|
Params extends unknown[] = Parameters<
|
||||||
|
Exclude<ExecutionLifecyleHookHandlers[Hook], undefined>[number]
|
||||||
|
>,
|
||||||
|
>(hookName: Hook, parameters: Params) {
|
||||||
|
const hooks = this.handlers[hookName];
|
||||||
|
for (const hookFunction of hooks) {
|
||||||
|
const typedHookFunction = hookFunction as unknown as (
|
||||||
|
this: ExecutionLifecycleHooks,
|
||||||
|
...args: Params
|
||||||
|
) => Promise<void>;
|
||||||
|
await typedHookFunction.apply(this, parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'n8n-workflow' {
|
||||||
|
interface IWorkflowExecuteAdditionalData {
|
||||||
|
hooks?: ExecutionLifecycleHooks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,3 +5,4 @@ export * from './node-execution-context';
|
||||||
export * from './partial-execution-utils';
|
export * from './partial-execution-utils';
|
||||||
export * from './node-execution-context/utils/execution-metadata';
|
export * from './node-execution-context/utils/execution-metadata';
|
||||||
export * from './workflow-execute';
|
export * from './workflow-execute';
|
||||||
|
export { ExecutionLifecycleHooks } from './execution-lifecycle-hooks';
|
||||||
|
|
|
@ -194,7 +194,7 @@ export class ExecuteContext extends BaseExecuteContext implements IExecuteFuncti
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendResponse(response: IExecuteResponsePromiseData): Promise<void> {
|
async sendResponse(response: IExecuteResponsePromiseData): Promise<void> {
|
||||||
await this.additionalData.hooks?.executeHookFunctions('sendResponse', [response]);
|
await this.additionalData.hooks?.runHook('sendResponse', [response]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated use ISupplyDataFunctions.addInputData */
|
/** @deprecated use ISupplyDataFunctions.addInputData */
|
||||||
|
|
|
@ -258,12 +258,12 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
|
||||||
}
|
}
|
||||||
|
|
||||||
runExecutionData.resultData.runData[nodeName][currentNodeRunIndex] = taskData;
|
runExecutionData.resultData.runData[nodeName][currentNodeRunIndex] = taskData;
|
||||||
await additionalData.hooks?.executeHookFunctions('nodeExecuteBefore', [nodeName]);
|
await additionalData.hooks?.runHook('nodeExecuteBefore', [nodeName]);
|
||||||
} else {
|
} else {
|
||||||
// Outputs
|
// Outputs
|
||||||
taskData.executionTime = new Date().getTime() - taskData.startTime;
|
taskData.executionTime = new Date().getTime() - taskData.startTime;
|
||||||
|
|
||||||
await additionalData.hooks?.executeHookFunctions('nodeExecuteAfter', [
|
await additionalData.hooks?.runHook('nodeExecuteAfter', [
|
||||||
nodeName,
|
nodeName,
|
||||||
taskData,
|
taskData,
|
||||||
this.runExecutionData,
|
this.runExecutionData,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
||||||
IExecuteResponsePromiseData,
|
IExecuteResponsePromiseData,
|
||||||
IRun,
|
IRun,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
|
||||||
import type { IGetExecuteTriggerFunctions } from './interfaces';
|
import type { IGetExecuteTriggerFunctions } from './interfaces';
|
||||||
|
|
||||||
|
@ -47,46 +48,34 @@ export class TriggersAndPollers {
|
||||||
|
|
||||||
// Add the manual trigger response which resolves when the first time data got emitted
|
// Add the manual trigger response which resolves when the first time data got emitted
|
||||||
triggerResponse!.manualTriggerResponse = new Promise((resolve, reject) => {
|
triggerResponse!.manualTriggerResponse = new Promise((resolve, reject) => {
|
||||||
|
const { hooks } = additionalData;
|
||||||
|
assert.ok(hooks, 'Execution lifecycle hooks are not defined');
|
||||||
|
|
||||||
triggerFunctions.emit = (
|
triggerFunctions.emit = (
|
||||||
(resolveEmit) =>
|
data: INodeExecutionData[][],
|
||||||
(
|
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||||
data: INodeExecutionData[][],
|
donePromise?: IDeferredPromise<IRun>,
|
||||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
) => {
|
||||||
donePromise?: IDeferredPromise<IRun>,
|
if (responsePromise) {
|
||||||
) => {
|
hooks.addHandler('sendResponse', (response) => responsePromise.resolve(response));
|
||||||
additionalData.hooks!.hookFunctions.sendResponse = [
|
|
||||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
|
||||||
if (responsePromise) {
|
|
||||||
responsePromise.resolve(response);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (donePromise) {
|
|
||||||
additionalData.hooks!.hookFunctions.workflowExecuteAfter?.unshift(
|
|
||||||
async (runData: IRun): Promise<void> => {
|
|
||||||
return donePromise.resolve(runData);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveEmit(data);
|
|
||||||
}
|
}
|
||||||
)(resolve);
|
|
||||||
|
if (donePromise) {
|
||||||
|
hooks.addHandler('workflowExecuteAfter', (runData) => donePromise.resolve(runData));
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(data);
|
||||||
|
};
|
||||||
|
|
||||||
triggerFunctions.emitError = (
|
triggerFunctions.emitError = (
|
||||||
(rejectEmit) =>
|
error: Error,
|
||||||
(error: Error, responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>) => {
|
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||||
additionalData.hooks!.hookFunctions.sendResponse = [
|
) => {
|
||||||
async (): Promise<void> => {
|
if (responsePromise) {
|
||||||
if (responsePromise) {
|
hooks.addHandler('sendResponse', () => responsePromise.reject(error));
|
||||||
responsePromise.reject(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
rejectEmit(error);
|
|
||||||
}
|
}
|
||||||
)(reject);
|
reject(error);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return triggerResponse;
|
return triggerResponse;
|
||||||
|
|
|
@ -405,19 +405,6 @@ export class WorkflowExecute {
|
||||||
return this.processRunExecutionData(graph.toWorkflow({ ...workflow }));
|
return this.processRunExecutionData(graph.toWorkflow({ ...workflow }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the hook with the given name
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
async executeHook(hookName: string, parameters: any[]): Promise<void> {
|
|
||||||
if (this.additionalData.hooks === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.additionalData.hooks.executeHookFunctions(hookName, parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges temporary execution metadata into the final runData structure.
|
* Merges temporary execution metadata into the final runData structure.
|
||||||
* During workflow execution, metadata is collected in a temporary location
|
* During workflow execution, metadata is collected in a temporary location
|
||||||
|
@ -1207,11 +1194,14 @@ export class WorkflowExecute {
|
||||||
|
|
||||||
this.status = 'running';
|
this.status = 'running';
|
||||||
|
|
||||||
|
const { hooks, executionId } = this.additionalData;
|
||||||
|
assert.ok(hooks, 'Failed to run workflow due to missing execution lifecycle hooks');
|
||||||
|
|
||||||
if (!this.runExecutionData.executionData) {
|
if (!this.runExecutionData.executionData) {
|
||||||
throw new ApplicationError('Failed to run workflow due to missing execution data', {
|
throw new ApplicationError('Failed to run workflow due to missing execution data', {
|
||||||
extra: {
|
extra: {
|
||||||
workflowId: workflow.id,
|
workflowId: workflow.id,
|
||||||
executionid: this.additionalData.executionId,
|
executionId,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1269,14 +1259,14 @@ export class WorkflowExecute {
|
||||||
this.status = 'canceled';
|
this.status = 'canceled';
|
||||||
this.abortController.abort();
|
this.abortController.abort();
|
||||||
const fullRunData = this.getFullRunData(startedAt);
|
const fullRunData = this.getFullRunData(startedAt);
|
||||||
void this.executeHook('workflowExecuteAfter', [fullRunData]);
|
void hooks.runHook('workflowExecuteAfter', [fullRunData]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
const returnPromise = (async () => {
|
const returnPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
if (!this.additionalData.restartExecutionId) {
|
if (!this.additionalData.restartExecutionId) {
|
||||||
await this.executeHook('workflowExecuteBefore', [workflow, this.runExecutionData]);
|
await hooks.runHook('workflowExecuteBefore', [workflow, this.runExecutionData]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const e = error as unknown as ExecutionBaseError;
|
const e = error as unknown as ExecutionBaseError;
|
||||||
|
@ -1360,7 +1350,7 @@ export class WorkflowExecute {
|
||||||
node: executionNode.name,
|
node: executionNode.name,
|
||||||
workflowId: workflow.id,
|
workflowId: workflow.id,
|
||||||
});
|
});
|
||||||
await this.executeHook('nodeExecuteBefore', [executionNode.name]);
|
await hooks.runHook('nodeExecuteBefore', [executionNode.name]);
|
||||||
|
|
||||||
// Get the index of the current run
|
// Get the index of the current run
|
||||||
runIndex = 0;
|
runIndex = 0;
|
||||||
|
@ -1651,7 +1641,7 @@ export class WorkflowExecute {
|
||||||
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
|
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
|
||||||
// Only execute the nodeExecuteAfter hook if the node did not get aborted
|
// Only execute the nodeExecuteAfter hook if the node did not get aborted
|
||||||
if (!this.isCancelled) {
|
if (!this.isCancelled) {
|
||||||
await this.executeHook('nodeExecuteAfter', [
|
await hooks.runHook('nodeExecuteAfter', [
|
||||||
executionNode.name,
|
executionNode.name,
|
||||||
taskData,
|
taskData,
|
||||||
this.runExecutionData,
|
this.runExecutionData,
|
||||||
|
@ -1693,7 +1683,7 @@ export class WorkflowExecute {
|
||||||
this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
|
this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
|
||||||
|
|
||||||
if (this.runExecutionData.waitTill) {
|
if (this.runExecutionData.waitTill) {
|
||||||
await this.executeHook('nodeExecuteAfter', [
|
await hooks.runHook('nodeExecuteAfter', [
|
||||||
executionNode.name,
|
executionNode.name,
|
||||||
taskData,
|
taskData,
|
||||||
this.runExecutionData,
|
this.runExecutionData,
|
||||||
|
@ -1712,7 +1702,7 @@ export class WorkflowExecute {
|
||||||
) {
|
) {
|
||||||
// Before stopping, make sure we are executing hooks so
|
// Before stopping, make sure we are executing hooks so
|
||||||
// That frontend is notified for example for manual executions.
|
// That frontend is notified for example for manual executions.
|
||||||
await this.executeHook('nodeExecuteAfter', [
|
await hooks.runHook('nodeExecuteAfter', [
|
||||||
executionNode.name,
|
executionNode.name,
|
||||||
taskData,
|
taskData,
|
||||||
this.runExecutionData,
|
this.runExecutionData,
|
||||||
|
@ -1822,7 +1812,7 @@ export class WorkflowExecute {
|
||||||
// Execute hooks now to make sure that all hooks are executed properly
|
// Execute hooks now to make sure that all hooks are executed properly
|
||||||
// Await is needed to make sure that we don't fall into concurrency problems
|
// Await is needed to make sure that we don't fall into concurrency problems
|
||||||
// When saving node execution data
|
// When saving node execution data
|
||||||
await this.executeHook('nodeExecuteAfter', [
|
await hooks.runHook('nodeExecuteAfter', [
|
||||||
executionNode.name,
|
executionNode.name,
|
||||||
taskData,
|
taskData,
|
||||||
this.runExecutionData,
|
this.runExecutionData,
|
||||||
|
@ -2025,7 +2015,7 @@ export class WorkflowExecute {
|
||||||
|
|
||||||
this.moveNodeMetadata();
|
this.moveNodeMetadata();
|
||||||
|
|
||||||
await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]).catch(
|
await hooks.runHook('workflowExecuteAfter', [fullRunData, newStaticData]).catch(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('There was a problem running hook "workflowExecuteAfter"', error);
|
console.error('There was a problem running hook "workflowExecuteAfter"', error);
|
||||||
|
@ -2118,7 +2108,10 @@ export class WorkflowExecute {
|
||||||
this.moveNodeMetadata();
|
this.moveNodeMetadata();
|
||||||
// Prevent from running the hook if the error is an abort error as it was already handled
|
// Prevent from running the hook if the error is an abort error as it was already handled
|
||||||
if (!this.isCancelled) {
|
if (!this.isCancelled) {
|
||||||
await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]);
|
await this.additionalData.hooks?.runHook('workflowExecuteAfter', [
|
||||||
|
fullRunData,
|
||||||
|
newStaticData,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closeFunction) {
|
if (closeFunction) {
|
||||||
|
|
|
@ -263,7 +263,7 @@ export async function proxyRequestToAxios(
|
||||||
} else if (body === '') {
|
} else if (body === '') {
|
||||||
body = axiosConfig.responseType === 'arraybuffer' ? Buffer.alloc(0) : undefined;
|
body = axiosConfig.responseType === 'arraybuffer' ? Buffer.alloc(0) : undefined;
|
||||||
}
|
}
|
||||||
await additionalData?.hooks?.executeHookFunctions('nodeFetchedData', [workflow?.id, node]);
|
await additionalData?.hooks?.runHook('nodeFetchedData', [workflow?.id, node]);
|
||||||
return configObject.resolveWithFullResponse
|
return configObject.resolveWithFullResponse
|
||||||
? {
|
? {
|
||||||
body,
|
body,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import type {
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypes,
|
INodeTypes,
|
||||||
IRun,
|
IRun,
|
||||||
ITaskData,
|
|
||||||
IVersionedNodeType,
|
IVersionedNodeType,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
|
@ -14,10 +13,11 @@ import type {
|
||||||
WorkflowTestData,
|
WorkflowTestData,
|
||||||
INodeTypeData,
|
INodeTypeData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { ApplicationError, NodeHelpers, WorkflowHooks } from 'n8n-workflow';
|
import { ApplicationError, NodeHelpers } from 'n8n-workflow';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { UnrecognizedNodeTypeError } from '@/errors';
|
import { UnrecognizedNodeTypeError } from '@/errors';
|
||||||
|
import { ExecutionLifecycleHooks } from '@/execution-engine/execution-lifecycle-hooks';
|
||||||
|
|
||||||
import { predefinedNodesTypes } from './constants';
|
import { predefinedNodesTypes } from './constants';
|
||||||
|
|
||||||
|
@ -53,22 +53,12 @@ export function WorkflowExecuteAdditionalData(
|
||||||
waitPromise: IDeferredPromise<IRun>,
|
waitPromise: IDeferredPromise<IRun>,
|
||||||
nodeExecutionOrder: string[],
|
nodeExecutionOrder: string[],
|
||||||
): IWorkflowExecuteAdditionalData {
|
): IWorkflowExecuteAdditionalData {
|
||||||
const hookFunctions = {
|
const hooks = new ExecutionLifecycleHooks('trigger', '1', mock());
|
||||||
nodeExecuteAfter: [
|
hooks.addHandler('nodeExecuteAfter', (nodeName) => {
|
||||||
async (nodeName: string, _data: ITaskData): Promise<void> => {
|
nodeExecutionOrder.push(nodeName);
|
||||||
nodeExecutionOrder.push(nodeName);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
workflowExecuteAfter: [
|
|
||||||
async (fullRunData: IRun): Promise<void> => {
|
|
||||||
waitPromise.resolve(fullRunData);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return mock<IWorkflowExecuteAdditionalData>({
|
|
||||||
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', mock()),
|
|
||||||
});
|
});
|
||||||
|
hooks.addHandler('workflowExecuteAfter', (fullRunData) => waitPromise.resolve(fullRunData));
|
||||||
|
return mock<IWorkflowExecuteAdditionalData>({ hooks });
|
||||||
}
|
}
|
||||||
|
|
||||||
const preparePinData = (pinData: IDataObject) => {
|
const preparePinData = (pinData: IDataObject) => {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
Credentials,
|
Credentials,
|
||||||
UnrecognizedNodeTypeError,
|
UnrecognizedNodeTypeError,
|
||||||
constructExecutionMetaData,
|
constructExecutionMetaData,
|
||||||
|
ExecutionLifecycleHooks,
|
||||||
} from 'n8n-core';
|
} from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
CredentialLoadingDetails,
|
CredentialLoadingDetails,
|
||||||
|
@ -28,14 +29,13 @@ import type {
|
||||||
INodeTypeData,
|
INodeTypeData,
|
||||||
INodeTypes,
|
INodeTypes,
|
||||||
IRun,
|
IRun,
|
||||||
ITaskData,
|
|
||||||
IVersionedNodeType,
|
IVersionedNodeType,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeLoadingDetails,
|
NodeLoadingDetails,
|
||||||
WorkflowTestData,
|
WorkflowTestData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { ApplicationError, ICredentialsHelper, NodeHelpers, WorkflowHooks } from 'n8n-workflow';
|
import { ApplicationError, ICredentialsHelper, NodeHelpers } from 'n8n-workflow';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
@ -155,22 +155,15 @@ export function WorkflowExecuteAdditionalData(
|
||||||
waitPromise: IDeferredPromise<IRun>,
|
waitPromise: IDeferredPromise<IRun>,
|
||||||
nodeExecutionOrder: string[],
|
nodeExecutionOrder: string[],
|
||||||
): IWorkflowExecuteAdditionalData {
|
): IWorkflowExecuteAdditionalData {
|
||||||
const hookFunctions = {
|
const hooks = new ExecutionLifecycleHooks('trigger', '1', mock());
|
||||||
nodeExecuteAfter: [
|
hooks.addHandler('nodeExecuteAfter', (nodeName) => {
|
||||||
async (nodeName: string, _data: ITaskData): Promise<void> => {
|
nodeExecutionOrder.push(nodeName);
|
||||||
nodeExecutionOrder.push(nodeName);
|
});
|
||||||
},
|
hooks.addHandler('workflowExecuteAfter', (fullRunData) => waitPromise.resolve(fullRunData));
|
||||||
],
|
|
||||||
workflowExecuteAfter: [
|
|
||||||
async (fullRunData: IRun): Promise<void> => {
|
|
||||||
waitPromise.resolve(fullRunData);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return mock<IWorkflowExecuteAdditionalData>({
|
return mock<IWorkflowExecuteAdditionalData>({
|
||||||
credentialsHelper: new CredentialsHelper(),
|
credentialsHelper: new CredentialsHelper(),
|
||||||
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', mock()),
|
hooks,
|
||||||
// Get from node.parameters
|
// Get from node.parameters
|
||||||
currentNodeParameters: undefined,
|
currentNodeParameters: undefined,
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { mock } from 'jest-mock-extended';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
import { PollContext, returnJsonArray, type InstanceSettings } from 'n8n-core';
|
import { PollContext, returnJsonArray } from 'n8n-core';
|
||||||
|
import type { InstanceSettings, ExecutionLifecycleHooks } from 'n8n-core';
|
||||||
import { ScheduledTaskManager } from 'n8n-core/dist/execution-engine/scheduled-task-manager';
|
import { ScheduledTaskManager } from 'n8n-core/dist/execution-engine/scheduled-task-manager';
|
||||||
import type {
|
import type {
|
||||||
IBinaryData,
|
IBinaryData,
|
||||||
|
@ -19,7 +20,6 @@ import type {
|
||||||
NodeTypeAndVersion,
|
NodeTypeAndVersion,
|
||||||
VersionedNodeType,
|
VersionedNodeType,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowHooks,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
type MockDeepPartial<T> = Parameters<typeof mock<T>>[0];
|
type MockDeepPartial<T> = Parameters<typeof mock<T>>[0];
|
||||||
|
@ -212,7 +212,7 @@ export async function testPollingTriggerNode(
|
||||||
return options as IHttpRequestOptions;
|
return options as IHttpRequestOptions;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
hooks: mock<WorkflowHooks>(),
|
hooks: mock<ExecutionLifecycleHooks>(),
|
||||||
}),
|
}),
|
||||||
mode,
|
mode,
|
||||||
'init',
|
'init',
|
||||||
|
|
|
@ -24,7 +24,6 @@ import type { ExecutionStatus } from './ExecutionStatus';
|
||||||
import type { Result } from './result';
|
import type { Result } from './result';
|
||||||
import type { Workflow } from './Workflow';
|
import type { Workflow } from './Workflow';
|
||||||
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
|
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
|
||||||
import type { WorkflowHooks } from './WorkflowHooks';
|
|
||||||
|
|
||||||
export interface IAdditionalCredentialOptions {
|
export interface IAdditionalCredentialOptions {
|
||||||
oauth2?: IOAuth2Options;
|
oauth2?: IOAuth2Options;
|
||||||
|
@ -2237,17 +2236,6 @@ export interface IWorkflowCredentials {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowExecuteHooks {
|
|
||||||
[key: string]: Array<(...args: any[]) => Promise<void>> | undefined;
|
|
||||||
nodeExecuteAfter?: Array<
|
|
||||||
(nodeName: string, data: ITaskData, executionData: IRunExecutionData) => Promise<void>
|
|
||||||
>;
|
|
||||||
nodeExecuteBefore?: Array<(nodeName: string) => Promise<void>>;
|
|
||||||
workflowExecuteAfter?: Array<(data: IRun, newStaticData: IDataObject) => Promise<void>>;
|
|
||||||
workflowExecuteBefore?: Array<(workflow?: Workflow, data?: IRunExecutionData) => Promise<void>>;
|
|
||||||
sendResponse?: Array<(response: IExecuteResponsePromiseData) => Promise<void>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IWorkflowExecutionDataProcess {
|
export interface IWorkflowExecutionDataProcess {
|
||||||
destinationNode?: string;
|
destinationNode?: string;
|
||||||
restartExecutionId?: string;
|
restartExecutionId?: string;
|
||||||
|
@ -2325,7 +2313,6 @@ export interface IWorkflowExecuteAdditionalData {
|
||||||
) => Promise<ExecuteWorkflowData>;
|
) => Promise<ExecuteWorkflowData>;
|
||||||
executionId?: string;
|
executionId?: string;
|
||||||
restartExecutionId?: string;
|
restartExecutionId?: string;
|
||||||
hooks?: WorkflowHooks;
|
|
||||||
httpResponse?: express.Response;
|
httpResponse?: express.Response;
|
||||||
httpRequest?: express.Request;
|
httpRequest?: express.Request;
|
||||||
restApiUrl: string;
|
restApiUrl: string;
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import type { IWorkflowBase, IWorkflowExecuteHooks, WorkflowExecuteMode } from './Interfaces';
|
|
||||||
|
|
||||||
export class WorkflowHooks {
|
|
||||||
mode: WorkflowExecuteMode;
|
|
||||||
|
|
||||||
workflowData: IWorkflowBase;
|
|
||||||
|
|
||||||
executionId: string;
|
|
||||||
|
|
||||||
hookFunctions: IWorkflowExecuteHooks;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
hookFunctions: IWorkflowExecuteHooks,
|
|
||||||
mode: WorkflowExecuteMode,
|
|
||||||
executionId: string,
|
|
||||||
workflowData: IWorkflowBase,
|
|
||||||
) {
|
|
||||||
this.hookFunctions = hookFunctions;
|
|
||||||
this.mode = mode;
|
|
||||||
this.executionId = executionId;
|
|
||||||
this.workflowData = workflowData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
async executeHookFunctions(hookName: string, parameters: any[]) {
|
|
||||||
const hooks = this.hookFunctions[hookName];
|
|
||||||
if (hooks !== undefined && Array.isArray(hooks)) {
|
|
||||||
for (const hookFunction of hooks) {
|
|
||||||
await hookFunction.apply(this, parameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,7 +18,6 @@ export * from './NodeHelpers';
|
||||||
export * from './Workflow';
|
export * from './Workflow';
|
||||||
export * from './WorkflowDataProxy';
|
export * from './WorkflowDataProxy';
|
||||||
export * from './WorkflowDataProxyEnvProvider';
|
export * from './WorkflowDataProxyEnvProvider';
|
||||||
export * from './WorkflowHooks';
|
|
||||||
export * from './VersionedNodeType';
|
export * from './VersionedNodeType';
|
||||||
export * from './TypeValidation';
|
export * from './TypeValidation';
|
||||||
export * from './result';
|
export * from './result';
|
||||||
|
|
Loading…
Reference in a new issue