mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
refactor(core): Move ExecutionLifecycleHooks to core (#13042)
This commit is contained in:
committed by
GitHub
parent
cae98e733d
commit
d41ca832dc
@@ -1,7 +1,13 @@
|
||||
import { stringify } from 'flatted';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { BinaryDataService, ErrorReporter, InstanceSettings, Logger } from 'n8n-core';
|
||||
import { ExpressionError, WorkflowHooks } from 'n8n-workflow';
|
||||
import {
|
||||
BinaryDataService,
|
||||
ErrorReporter,
|
||||
InstanceSettings,
|
||||
Logger,
|
||||
ExecutionLifecycleHooks,
|
||||
} from 'n8n-core';
|
||||
import { ExpressionError } from 'n8n-workflow';
|
||||
import type {
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
@@ -10,6 +16,7 @@ import type {
|
||||
IRun,
|
||||
INode,
|
||||
IWorkflowBase,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import config from '@/config';
|
||||
@@ -25,10 +32,10 @@ import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.serv
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
import {
|
||||
getWorkflowHooksIntegrated,
|
||||
getWorkflowHooksMain,
|
||||
getWorkflowHooksWorkerExecuter,
|
||||
getWorkflowHooksWorkerMain,
|
||||
getLifecycleHooksForSubExecutions,
|
||||
getLifecycleHooksForRegularMain,
|
||||
getLifecycleHooksForScalingWorker,
|
||||
getLifecycleHooksForScalingMain,
|
||||
} from '../execution-lifecycle-hooks';
|
||||
|
||||
describe('Execution Lifecycle Hooks', () => {
|
||||
@@ -79,14 +86,13 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
waitTill: new Date(),
|
||||
});
|
||||
const expressionError = new ExpressionError('Error');
|
||||
const executionMode = 'manual';
|
||||
const pushRef = 'test-push-ref';
|
||||
const retryOf = 'test-retry-of';
|
||||
|
||||
const now = new Date('2025-01-13T18:25:50.267Z');
|
||||
jest.useFakeTimers({ now });
|
||||
|
||||
let hooks: WorkflowHooks;
|
||||
let lifecycleHooks: ExecutionLifecycleHooks;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -107,7 +113,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
const workflowEventTests = () => {
|
||||
describe('workflowExecuteBefore', () => {
|
||||
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', {
|
||||
executionId,
|
||||
@@ -118,7 +124,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
describe('workflowExecuteAfter', () => {
|
||||
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', {
|
||||
executionId,
|
||||
@@ -128,7 +134,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
@@ -138,7 +144,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
const nodeEventsTests = () => {
|
||||
describe('nodeExecuteBefore', () => {
|
||||
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', {
|
||||
executionId,
|
||||
@@ -150,11 +156,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
describe('nodeExecuteAfter', () => {
|
||||
it('should emit node-post-execute event', async () => {
|
||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
||||
nodeName,
|
||||
taskData,
|
||||
runExecutionData,
|
||||
]);
|
||||
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||
|
||||
expect(eventService.emit).toHaveBeenCalledWith('node-post-execute', {
|
||||
executionId,
|
||||
@@ -168,18 +170,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
const externalHooksTests = () => {
|
||||
describe('workflowExecuteBefore', () => {
|
||||
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', [
|
||||
workflow,
|
||||
executionMode,
|
||||
]);
|
||||
expect(externalHooks.run).toHaveBeenCalledWith('workflow.preExecute', [workflow, 'manual']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflowExecuteAfter', () => {
|
||||
it('should run workflow.postExecute hook', async () => {
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||
|
||||
expect(externalHooks.run).toHaveBeenCalledWith('workflow.postExecute', [
|
||||
successfulRun,
|
||||
@@ -193,7 +192,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
const statisticsTests = () => {
|
||||
describe('statistics events', () => {
|
||||
it('workflowExecuteAfter should emit workflowExecutionCompleted statistics event', async () => {
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||
|
||||
expect(workflowStatisticsService.emit).toHaveBeenCalledWith('workflowExecutionCompleted', {
|
||||
workflowData,
|
||||
@@ -202,7 +201,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
});
|
||||
|
||||
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', {
|
||||
workflowId,
|
||||
@@ -212,12 +211,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
});
|
||||
};
|
||||
|
||||
describe('getWorkflowHooksMain', () => {
|
||||
const createHooks = () =>
|
||||
getWorkflowHooksMain({ executionMode, workflowData, pushRef, retryOf }, executionId);
|
||||
describe('getLifecycleHooksForRegularMain', () => {
|
||||
const createHooks = (executionMode: WorkflowExecuteMode = 'manual') =>
|
||||
getLifecycleHooksForRegularMain(
|
||||
{ executionMode, workflowData, pushRef, retryOf },
|
||||
executionId,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
hooks = createHooks();
|
||||
lifecycleHooks = createHooks();
|
||||
});
|
||||
|
||||
workflowEventTests();
|
||||
@@ -226,23 +228,23 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
statisticsTests();
|
||||
|
||||
it('should setup the correct set of hooks', () => {
|
||||
expect(hooks).toBeInstanceOf(WorkflowHooks);
|
||||
expect(hooks.mode).toBe('manual');
|
||||
expect(hooks.executionId).toBe(executionId);
|
||||
expect(hooks.workflowData).toEqual(workflowData);
|
||||
expect(lifecycleHooks).toBeInstanceOf(ExecutionLifecycleHooks);
|
||||
expect(lifecycleHooks.mode).toBe('manual');
|
||||
expect(lifecycleHooks.executionId).toBe(executionId);
|
||||
expect(lifecycleHooks.workflowData).toEqual(workflowData);
|
||||
|
||||
const { hookFunctions } = hooks;
|
||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(2);
|
||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(2);
|
||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(3);
|
||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(5);
|
||||
expect(hookFunctions.nodeFetchedData).toHaveLength(1);
|
||||
expect(hookFunctions.sendResponse).toHaveLength(0);
|
||||
const { handlers } = lifecycleHooks;
|
||||
expect(handlers.nodeExecuteBefore).toHaveLength(2);
|
||||
expect(handlers.nodeExecuteAfter).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteBefore).toHaveLength(3);
|
||||
expect(handlers.workflowExecuteAfter).toHaveLength(5);
|
||||
expect(handlers.nodeFetchedData).toHaveLength(1);
|
||||
expect(handlers.sendResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
describe('nodeExecuteBefore', () => {
|
||||
it('should send nodeExecuteBefore push event', async () => {
|
||||
await hooks.executeHookFunctions('nodeExecuteBefore', [nodeName]);
|
||||
await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName]);
|
||||
|
||||
expect(push.send).toHaveBeenCalledWith(
|
||||
{ type: 'nodeExecuteBefore', data: { executionId, nodeName } },
|
||||
@@ -253,11 +255,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
describe('nodeExecuteAfter', () => {
|
||||
it('should send nodeExecuteAfter push event', async () => {
|
||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
||||
nodeName,
|
||||
taskData,
|
||||
runExecutionData,
|
||||
]);
|
||||
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||
|
||||
expect(push.send).toHaveBeenCalledWith(
|
||||
{ type: 'nodeExecuteAfter', data: { executionId, nodeName, data: taskData } },
|
||||
@@ -267,15 +265,11 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
it('should save execution progress when enabled', async () => {
|
||||
workflowData.settings = { saveExecutionProgress: true };
|
||||
hooks = createHooks();
|
||||
lifecycleHooks = createHooks();
|
||||
|
||||
expect(hooks.hookFunctions.nodeExecuteAfter).toHaveLength(3);
|
||||
expect(lifecycleHooks.handlers.nodeExecuteAfter).toHaveLength(3);
|
||||
|
||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
||||
nodeName,
|
||||
taskData,
|
||||
runExecutionData,
|
||||
]);
|
||||
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||
|
||||
expect(executionRepository.findSingleExecution).toHaveBeenCalledWith(executionId, {
|
||||
includeData: true,
|
||||
@@ -285,15 +279,11 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
it('should not save execution progress when disabled', async () => {
|
||||
workflowData.settings = { saveExecutionProgress: false };
|
||||
hooks = createHooks();
|
||||
lifecycleHooks = createHooks();
|
||||
|
||||
expect(hooks.hookFunctions.nodeExecuteAfter).toHaveLength(2);
|
||||
expect(lifecycleHooks.handlers.nodeExecuteAfter).toHaveLength(2);
|
||||
|
||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
||||
nodeName,
|
||||
taskData,
|
||||
runExecutionData,
|
||||
]);
|
||||
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||
|
||||
expect(executionRepository.findSingleExecution).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -301,14 +291,14 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
describe('workflowExecuteBefore', () => {
|
||||
it('should send executionStarted push event', async () => {
|
||||
await hooks.executeHookFunctions('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||
await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||
|
||||
expect(push.send).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'executionStarted',
|
||||
data: {
|
||||
executionId,
|
||||
mode: executionMode,
|
||||
mode: 'manual',
|
||||
retryOf,
|
||||
workflowId: 'test-workflow-id',
|
||||
workflowName: 'Test Workflow',
|
||||
@@ -321,18 +311,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
});
|
||||
|
||||
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', [
|
||||
workflow,
|
||||
executionMode,
|
||||
]);
|
||||
expect(externalHooks.run).toHaveBeenCalledWith('workflow.preExecute', [workflow, 'manual']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflowExecuteAfter', () => {
|
||||
it('should send executionFinished push event', async () => {
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||
expect(push.send).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'executionFinished',
|
||||
@@ -348,7 +335,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
});
|
||||
|
||||
it('should send executionWaiting push event', async () => {
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [waitingRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [waitingRun, {}]);
|
||||
|
||||
expect(push.send).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -361,17 +348,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
describe('saving static data', () => {
|
||||
it('should skip saving static data for manual executions', async () => {
|
||||
hooks.mode = 'manual';
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||
|
||||
expect(workflowStaticDataService.saveStaticDataById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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(
|
||||
workflowId,
|
||||
@@ -380,11 +365,12 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
});
|
||||
|
||||
it('should handle static data saving errors', async () => {
|
||||
hooks.mode = 'trigger';
|
||||
lifecycleHooks = createHooks('trigger');
|
||||
|
||||
const error = new Error('Static data save failed');
|
||||
workflowStaticDataService.saveStaticDataById.mockRejectedValueOnce(error);
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||
|
||||
expect(errorReporter.error).toHaveBeenCalledWith(error);
|
||||
});
|
||||
@@ -392,7 +378,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
describe('saving execution data', () => {
|
||||
it('should update execution with proper data', async () => {
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||
|
||||
expect(executionRepository.updateExistingExecution).toHaveBeenCalledWith(
|
||||
executionId,
|
||||
@@ -406,31 +392,31 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
it('should not delete unfinished executions', async () => {
|
||||
const unfinishedRun = mock<IRun>({ finished: false, status: 'running' });
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [unfinishedRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [unfinishedRun, {}]);
|
||||
|
||||
expect(executionRepository.hardDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not delete waiting executions', async () => {
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [waitingRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [waitingRun, {}]);
|
||||
|
||||
expect(executionRepository.hardDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should soft delete manual executions when manual saving is disabled', async () => {
|
||||
hooks.workflowData.settings = { saveManualExecutions: false };
|
||||
hooks = createHooks();
|
||||
lifecycleHooks.workflowData.settings = { saveManualExecutions: false };
|
||||
lifecycleHooks = createHooks();
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||
|
||||
expect(executionRepository.softDelete).toHaveBeenCalledWith(executionId);
|
||||
});
|
||||
|
||||
it('should not soft delete manual executions with waitTill', async () => {
|
||||
hooks.workflowData.settings = { saveManualExecutions: false };
|
||||
hooks = createHooks();
|
||||
lifecycleHooks.workflowData.settings = { saveManualExecutions: false };
|
||||
lifecycleHooks = createHooks();
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [waitingRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [waitingRun, {}]);
|
||||
|
||||
expect(executionRepository.softDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -438,15 +424,14 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
describe('error workflow', () => {
|
||||
it('should not execute error workflow for manual executions', async () => {
|
||||
hooks.mode = 'manual';
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||
|
||||
expect(workflowExecutionService.executeErrorWorkflow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should execute error workflow for failed non-manual executions', async () => {
|
||||
hooks.mode = 'trigger';
|
||||
lifecycleHooks = createHooks('trigger');
|
||||
|
||||
const errorWorkflow = 'error-workflow-id';
|
||||
workflowData.settings = { errorWorkflow };
|
||||
const project = mock<Project>();
|
||||
@@ -454,7 +439,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
.calledWith(workflowId)
|
||||
.mockResolvedValue(project);
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||
|
||||
expect(workflowExecutionService.executeErrorWorkflow).toHaveBeenCalledWith(
|
||||
errorWorkflow,
|
||||
@@ -479,7 +464,8 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
it('should restore binary data IDs after workflow execution for webhooks', async () => {
|
||||
config.set('binaryDataManager.mode', 'filesystem');
|
||||
hooks.mode = 'webhook';
|
||||
lifecycleHooks = createHooks('webhook');
|
||||
|
||||
(successfulRun.data.resultData.runData = {
|
||||
[nodeName]: [
|
||||
{
|
||||
@@ -505,7 +491,7 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||
|
||||
expect(binaryDataService.rename).toHaveBeenCalledWith(
|
||||
'workflows/test-workflow-id/executions/temp/binary_data/123',
|
||||
@@ -516,33 +502,32 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
describe("when pushRef isn't set", () => {
|
||||
beforeEach(() => {
|
||||
hooks = getWorkflowHooksMain({ executionMode, workflowData, retryOf }, executionId);
|
||||
lifecycleHooks = getLifecycleHooksForRegularMain(
|
||||
{ executionMode: 'manual', workflowData, retryOf },
|
||||
executionId,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not setup any push hooks', async () => {
|
||||
const { hookFunctions } = hooks;
|
||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(1);
|
||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(1);
|
||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(4);
|
||||
const { handlers } = lifecycleHooks;
|
||||
expect(handlers.nodeExecuteBefore).toHaveLength(1);
|
||||
expect(handlers.nodeExecuteAfter).toHaveLength(1);
|
||||
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||
|
||||
await hooks.executeHookFunctions('nodeExecuteBefore', [nodeName]);
|
||||
await hooks.executeHookFunctions('nodeExecuteAfter', [
|
||||
nodeName,
|
||||
taskData,
|
||||
runExecutionData,
|
||||
]);
|
||||
await hooks.executeHookFunctions('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
||||
await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName]);
|
||||
await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
|
||||
await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||
|
||||
expect(push.send).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkflowHooksWorkerMain', () => {
|
||||
describe('getLifecycleHooksForScalingMain', () => {
|
||||
beforeEach(() => {
|
||||
hooks = getWorkflowHooksWorkerMain(executionMode, executionId, workflowData, {
|
||||
lifecycleHooks = getLifecycleHooksForScalingMain('manual', executionId, workflowData, {
|
||||
pushRef,
|
||||
retryOf,
|
||||
});
|
||||
@@ -552,28 +537,25 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
externalHooksTests();
|
||||
|
||||
it('should setup the correct set of hooks', () => {
|
||||
expect(hooks).toBeInstanceOf(WorkflowHooks);
|
||||
expect(hooks.mode).toBe('manual');
|
||||
expect(hooks.executionId).toBe(executionId);
|
||||
expect(hooks.workflowData).toEqual(workflowData);
|
||||
expect(lifecycleHooks).toBeInstanceOf(ExecutionLifecycleHooks);
|
||||
expect(lifecycleHooks.mode).toBe('manual');
|
||||
expect(lifecycleHooks.executionId).toBe(executionId);
|
||||
expect(lifecycleHooks.workflowData).toEqual(workflowData);
|
||||
|
||||
const { hookFunctions } = hooks;
|
||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(0);
|
||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(0);
|
||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(4);
|
||||
expect(hookFunctions.nodeFetchedData).toHaveLength(0);
|
||||
expect(hookFunctions.sendResponse).toHaveLength(0);
|
||||
const { handlers } = lifecycleHooks;
|
||||
expect(handlers.nodeExecuteBefore).toHaveLength(0);
|
||||
expect(handlers.nodeExecuteAfter).toHaveLength(0);
|
||||
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||
expect(handlers.nodeFetchedData).toHaveLength(0);
|
||||
expect(handlers.sendResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
describe('workflowExecuteBefore', () => {
|
||||
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', [
|
||||
workflow,
|
||||
executionMode,
|
||||
]);
|
||||
expect(externalHooks.run).toHaveBeenCalledWith('workflow.preExecute', [workflow, 'manual']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -583,12 +565,17 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
saveDataSuccessExecution: 'none',
|
||||
saveDataErrorExecution: 'all',
|
||||
};
|
||||
const hooks = getWorkflowHooksWorkerMain('webhook', executionId, workflowData, {
|
||||
pushRef,
|
||||
retryOf,
|
||||
});
|
||||
const lifecycleHooks = getLifecycleHooksForScalingMain(
|
||||
'webhook',
|
||||
executionId,
|
||||
workflowData,
|
||||
{
|
||||
pushRef,
|
||||
retryOf,
|
||||
},
|
||||
);
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);
|
||||
|
||||
expect(executionRepository.hardDelete).toHaveBeenCalledWith({
|
||||
workflowId,
|
||||
@@ -601,12 +588,17 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
saveDataSuccessExecution: 'all',
|
||||
saveDataErrorExecution: 'none',
|
||||
};
|
||||
const hooks = getWorkflowHooksWorkerMain('webhook', executionId, workflowData, {
|
||||
pushRef,
|
||||
retryOf,
|
||||
});
|
||||
const lifecycleHooks = getLifecycleHooksForScalingMain(
|
||||
'webhook',
|
||||
executionId,
|
||||
workflowData,
|
||||
{
|
||||
pushRef,
|
||||
retryOf,
|
||||
},
|
||||
);
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||
|
||||
expect(executionRepository.hardDelete).toHaveBeenCalledWith({
|
||||
workflowId,
|
||||
@@ -616,12 +608,15 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkflowHooksWorkerExecuter', () => {
|
||||
beforeEach(() => {
|
||||
hooks = getWorkflowHooksWorkerExecuter(executionMode, executionId, workflowData, {
|
||||
describe('getLifecycleHooksForScalingWorker', () => {
|
||||
const createHooks = (executionMode: WorkflowExecuteMode = 'manual') =>
|
||||
getLifecycleHooksForScalingWorker(executionMode, executionId, workflowData, {
|
||||
pushRef,
|
||||
retryOf,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
lifecycleHooks = createHooks();
|
||||
});
|
||||
|
||||
nodeEventsTests();
|
||||
@@ -629,33 +624,31 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
statisticsTests();
|
||||
|
||||
it('should setup the correct set of hooks', () => {
|
||||
expect(hooks).toBeInstanceOf(WorkflowHooks);
|
||||
expect(hooks.mode).toBe('manual');
|
||||
expect(hooks.executionId).toBe(executionId);
|
||||
expect(hooks.workflowData).toEqual(workflowData);
|
||||
expect(lifecycleHooks).toBeInstanceOf(ExecutionLifecycleHooks);
|
||||
expect(lifecycleHooks.mode).toBe('manual');
|
||||
expect(lifecycleHooks.executionId).toBe(executionId);
|
||||
expect(lifecycleHooks.workflowData).toEqual(workflowData);
|
||||
|
||||
const { hookFunctions } = hooks;
|
||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(2);
|
||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(2);
|
||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(4);
|
||||
expect(hookFunctions.nodeFetchedData).toHaveLength(1);
|
||||
expect(hookFunctions.sendResponse).toHaveLength(0);
|
||||
const { handlers } = lifecycleHooks;
|
||||
expect(handlers.nodeExecuteBefore).toHaveLength(2);
|
||||
expect(handlers.nodeExecuteAfter).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||
expect(handlers.nodeFetchedData).toHaveLength(1);
|
||||
expect(handlers.sendResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
describe('saving static data', () => {
|
||||
it('should skip saving static data for manual executions', async () => {
|
||||
hooks.mode = 'manual';
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||
|
||||
expect(workflowStaticDataService.saveStaticDataById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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(
|
||||
workflowId,
|
||||
@@ -664,11 +657,11 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
});
|
||||
|
||||
it('should handle static data saving errors', async () => {
|
||||
hooks.mode = 'trigger';
|
||||
lifecycleHooks = createHooks('trigger');
|
||||
const error = new Error('Static data save failed');
|
||||
workflowStaticDataService.saveStaticDataById.mockRejectedValueOnce(error);
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [successfulRun, staticData]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, staticData]);
|
||||
|
||||
expect(errorReporter.error).toHaveBeenCalledWith(error);
|
||||
});
|
||||
@@ -676,21 +669,19 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
|
||||
describe('error workflow', () => {
|
||||
it('should not execute error workflow for manual executions', async () => {
|
||||
hooks.mode = 'manual';
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||
|
||||
expect(workflowExecutionService.executeErrorWorkflow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should execute error workflow for failed non-manual executions', async () => {
|
||||
hooks.mode = 'trigger';
|
||||
lifecycleHooks = createHooks('trigger');
|
||||
const errorWorkflow = 'error-workflow-id';
|
||||
workflowData.settings = { errorWorkflow };
|
||||
const project = mock<Project>();
|
||||
ownershipService.getWorkflowProjectCached.calledWith(workflowId).mockResolvedValue(project);
|
||||
|
||||
await hooks.executeHookFunctions('workflowExecuteAfter', [failedRun, {}]);
|
||||
await lifecycleHooks.runHook('workflowExecuteAfter', [failedRun, {}]);
|
||||
|
||||
expect(workflowExecutionService.executeErrorWorkflow).toHaveBeenCalledWith(
|
||||
errorWorkflow,
|
||||
@@ -714,9 +705,14 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkflowHooksIntegrated', () => {
|
||||
describe('getLifecycleHooksForSubExecutions', () => {
|
||||
beforeEach(() => {
|
||||
hooks = getWorkflowHooksIntegrated(executionMode, executionId, workflowData, undefined);
|
||||
lifecycleHooks = getLifecycleHooksForSubExecutions(
|
||||
'manual',
|
||||
executionId,
|
||||
workflowData,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
workflowEventTests();
|
||||
@@ -725,18 +721,18 @@ describe('Execution Lifecycle Hooks', () => {
|
||||
statisticsTests();
|
||||
|
||||
it('should setup the correct set of hooks', () => {
|
||||
expect(hooks).toBeInstanceOf(WorkflowHooks);
|
||||
expect(hooks.mode).toBe('manual');
|
||||
expect(hooks.executionId).toBe(executionId);
|
||||
expect(hooks.workflowData).toEqual(workflowData);
|
||||
expect(lifecycleHooks).toBeInstanceOf(ExecutionLifecycleHooks);
|
||||
expect(lifecycleHooks.mode).toBe('manual');
|
||||
expect(lifecycleHooks.executionId).toBe(executionId);
|
||||
expect(lifecycleHooks.workflowData).toEqual(workflowData);
|
||||
|
||||
const { hookFunctions } = hooks;
|
||||
expect(hookFunctions.nodeExecuteBefore).toHaveLength(1);
|
||||
expect(hookFunctions.nodeExecuteAfter).toHaveLength(1);
|
||||
expect(hookFunctions.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(hookFunctions.workflowExecuteAfter).toHaveLength(4);
|
||||
expect(hookFunctions.nodeFetchedData).toHaveLength(1);
|
||||
expect(hookFunctions.sendResponse).toHaveLength(0);
|
||||
const { handlers } = lifecycleHooks;
|
||||
expect(handlers.nodeExecuteBefore).toHaveLength(1);
|
||||
expect(handlers.nodeExecuteAfter).toHaveLength(1);
|
||||
expect(handlers.workflowExecuteBefore).toHaveLength(2);
|
||||
expect(handlers.workflowExecuteAfter).toHaveLength(4);
|
||||
expect(handlers.nodeFetchedData).toHaveLength(1);
|
||||
expect(handlers.sendResponse).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { Container } from '@n8n/di';
|
||||
import { stringify } from 'flatted';
|
||||
import { ErrorReporter, Logger, InstanceSettings } from 'n8n-core';
|
||||
import { WorkflowHooks } from 'n8n-workflow';
|
||||
import { ErrorReporter, Logger, InstanceSettings, ExecutionLifecycleHooks } from 'n8n-core';
|
||||
import type {
|
||||
IDataObject,
|
||||
INode,
|
||||
IRun,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
IWorkflowBase,
|
||||
IWorkflowExecuteHooks,
|
||||
WorkflowExecuteMode,
|
||||
IWorkflowExecutionDataProcess,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||
@@ -39,339 +31,255 @@ type HooksSetupParameters = {
|
||||
retryOf?: string;
|
||||
};
|
||||
|
||||
function mergeHookFunctions(...hookFunctions: IWorkflowExecuteHooks[]): IWorkflowExecuteHooks {
|
||||
const result: IWorkflowExecuteHooks = {
|
||||
nodeExecuteBefore: [],
|
||||
nodeExecuteAfter: [],
|
||||
workflowExecuteBefore: [],
|
||||
workflowExecuteAfter: [],
|
||||
sendResponse: [],
|
||||
nodeFetchedData: [],
|
||||
};
|
||||
for (const hooks of hookFunctions) {
|
||||
for (const key in hooks) {
|
||||
if (!result[key] || !hooks[key]) continue;
|
||||
result[key].push(...hooks[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
function hookFunctionsWorkflowEvents(hooks: ExecutionLifecycleHooks, userId?: string) {
|
||||
const eventService = Container.get(EventService);
|
||||
hooks.addHandler('workflowExecuteBefore', function () {
|
||||
const { executionId, workflowData } = this;
|
||||
eventService.emit('workflow-pre-execute', { executionId, data: workflowData });
|
||||
});
|
||||
hooks.addHandler('workflowExecuteAfter', function (runData) {
|
||||
if (runData.status === 'waiting') return;
|
||||
|
||||
const { executionId, workflowData: workflow } = this;
|
||||
eventService.emit('workflow-post-execute', { executionId, runData, workflow, userId });
|
||||
});
|
||||
}
|
||||
|
||||
function hookFunctionsWorkflowEvents(userId?: string): IWorkflowExecuteHooks {
|
||||
function hookFunctionsNodeEvents(hooks: ExecutionLifecycleHooks) {
|
||||
const eventService = Container.get(EventService);
|
||||
return {
|
||||
workflowExecuteBefore: [
|
||||
async function (this: WorkflowHooks): Promise<void> {
|
||||
const { executionId, workflowData } = this;
|
||||
eventService.emit('workflow-pre-execute', { executionId, data: workflowData });
|
||||
},
|
||||
],
|
||||
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 });
|
||||
},
|
||||
],
|
||||
};
|
||||
hooks.addHandler('nodeExecuteBefore', function (nodeName) {
|
||||
const { executionId, workflowData: workflow } = this;
|
||||
eventService.emit('node-pre-execute', { executionId, workflow, nodeName });
|
||||
});
|
||||
hooks.addHandler('nodeExecuteAfter', function (nodeName) {
|
||||
const { executionId, workflowData: workflow } = this;
|
||||
eventService.emit('node-post-execute', { executionId, workflow, nodeName });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns hook functions to push data to Editor-UI
|
||||
*/
|
||||
function hookFunctionsPush({ pushRef, retryOf }: HooksSetupParameters): IWorkflowExecuteHooks {
|
||||
if (!pushRef) return {};
|
||||
function hookFunctionsPush(
|
||||
hooks: ExecutionLifecycleHooks,
|
||||
{ pushRef, retryOf }: HooksSetupParameters,
|
||||
) {
|
||||
if (!pushRef) return;
|
||||
const logger = Container.get(Logger);
|
||||
const pushInstance = Container.get(Push);
|
||||
return {
|
||||
nodeExecuteBefore: [
|
||||
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
||||
const { executionId } = this;
|
||||
// Push data to session which started workflow before each
|
||||
// node which starts rendering
|
||||
logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, {
|
||||
executionId,
|
||||
pushRef,
|
||||
workflowId: this.workflowData.id,
|
||||
});
|
||||
hooks.addHandler('nodeExecuteBefore', function (nodeName) {
|
||||
const { executionId } = this;
|
||||
// Push data to session which started workflow before each
|
||||
// node which starts rendering
|
||||
logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, {
|
||||
executionId,
|
||||
pushRef,
|
||||
workflowId: this.workflowData.id,
|
||||
});
|
||||
|
||||
pushInstance.send({ type: 'nodeExecuteBefore', data: { executionId, nodeName } }, pushRef);
|
||||
},
|
||||
],
|
||||
nodeExecuteAfter: [
|
||||
async function (this: WorkflowHooks, nodeName: string, data: ITaskData): Promise<void> {
|
||||
const { executionId } = this;
|
||||
// Push data to session which started workflow after each rendered node
|
||||
logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, {
|
||||
executionId,
|
||||
pushRef,
|
||||
workflowId: this.workflowData.id,
|
||||
});
|
||||
pushInstance.send({ type: 'nodeExecuteBefore', data: { executionId, nodeName } }, pushRef);
|
||||
});
|
||||
hooks.addHandler('nodeExecuteAfter', function (nodeName, data) {
|
||||
const { executionId } = this;
|
||||
// Push data to session which started workflow after each rendered node
|
||||
logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, {
|
||||
executionId,
|
||||
pushRef,
|
||||
workflowId: this.workflowData.id,
|
||||
});
|
||||
|
||||
pushInstance.send(
|
||||
{ type: 'nodeExecuteAfter', data: { executionId, nodeName, data } },
|
||||
pushRef,
|
||||
);
|
||||
},
|
||||
],
|
||||
workflowExecuteBefore: [
|
||||
async function (this: WorkflowHooks, _workflow, data): Promise<void> {
|
||||
const { executionId } = this;
|
||||
const { id: workflowId, name: workflowName } = this.workflowData;
|
||||
logger.debug('Executing hook (hookFunctionsPush)', {
|
||||
pushInstance.send({ 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)', {
|
||||
executionId,
|
||||
pushRef,
|
||||
workflowId,
|
||||
});
|
||||
// Push data to session which started the workflow
|
||||
pushInstance.send(
|
||||
{
|
||||
type: 'executionStarted',
|
||||
data: {
|
||||
executionId,
|
||||
pushRef,
|
||||
mode: this.mode,
|
||||
startedAt: new Date(),
|
||||
retryOf,
|
||||
workflowId,
|
||||
});
|
||||
// Push data to session which started the workflow
|
||||
pushInstance.send(
|
||||
{
|
||||
type: 'executionStarted',
|
||||
data: {
|
||||
executionId,
|
||||
mode: this.mode,
|
||||
startedAt: new Date(),
|
||||
retryOf,
|
||||
workflowId,
|
||||
workflowName,
|
||||
flattedRunData: data?.resultData.runData
|
||||
? stringify(data.resultData.runData)
|
||||
: stringify({}),
|
||||
},
|
||||
},
|
||||
pushRef,
|
||||
);
|
||||
workflowName,
|
||||
flattedRunData: data?.resultData.runData
|
||||
? stringify(data.resultData.runData)
|
||||
: stringify({}),
|
||||
},
|
||||
},
|
||||
],
|
||||
workflowExecuteAfter: [
|
||||
async function (this: WorkflowHooks, fullRunData: IRun): Promise<void> {
|
||||
const { executionId } = this;
|
||||
const { id: workflowId } = this.workflowData;
|
||||
logger.debug('Executing hook (hookFunctionsPush)', {
|
||||
executionId,
|
||||
pushRef,
|
||||
workflowId,
|
||||
});
|
||||
pushRef,
|
||||
);
|
||||
});
|
||||
hooks.addHandler('workflowExecuteAfter', function (fullRunData) {
|
||||
const { executionId } = this;
|
||||
const { id: workflowId } = this.workflowData;
|
||||
logger.debug('Executing hook (hookFunctionsPush)', {
|
||||
executionId,
|
||||
pushRef,
|
||||
workflowId,
|
||||
});
|
||||
|
||||
const { status } = fullRunData;
|
||||
if (status === 'waiting') {
|
||||
pushInstance.send({ type: 'executionWaiting', data: { executionId } }, pushRef);
|
||||
} else {
|
||||
const rawData = stringify(fullRunData.data);
|
||||
pushInstance.send(
|
||||
{ type: 'executionFinished', data: { executionId, workflowId, status, rawData } },
|
||||
pushRef,
|
||||
);
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
const { status } = fullRunData;
|
||||
if (status === 'waiting') {
|
||||
pushInstance.send({ type: 'executionWaiting', data: { executionId } }, pushRef);
|
||||
} else {
|
||||
const rawData = stringify(fullRunData.data);
|
||||
pushInstance.send(
|
||||
{ type: 'executionFinished', data: { executionId, workflowId, status, rawData } },
|
||||
pushRef,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function hookFunctionsExternalHooks(): IWorkflowExecuteHooks {
|
||||
function hookFunctionsExternalHooks(hooks: ExecutionLifecycleHooks) {
|
||||
const externalHooks = Container.get(ExternalHooks);
|
||||
return {
|
||||
workflowExecuteBefore: [
|
||||
async function (this: WorkflowHooks, workflow: Workflow): Promise<void> {
|
||||
await externalHooks.run('workflow.preExecute', [workflow, this.mode]);
|
||||
},
|
||||
],
|
||||
workflowExecuteAfter: [
|
||||
async function (this: WorkflowHooks, fullRunData: IRun) {
|
||||
await externalHooks.run('workflow.postExecute', [
|
||||
fullRunData,
|
||||
this.workflowData,
|
||||
this.executionId,
|
||||
]);
|
||||
},
|
||||
],
|
||||
};
|
||||
hooks.addHandler('workflowExecuteBefore', async function (workflow) {
|
||||
await externalHooks.run('workflow.preExecute', [workflow, this.mode]);
|
||||
});
|
||||
hooks.addHandler('workflowExecuteAfter', async function (fullRunData) {
|
||||
await externalHooks.run('workflow.postExecute', [
|
||||
fullRunData,
|
||||
this.workflowData,
|
||||
this.executionId,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
function hookFunctionsSaveProgress({ saveSettings }: HooksSetupParameters): IWorkflowExecuteHooks {
|
||||
if (!saveSettings.progress) return {};
|
||||
return {
|
||||
nodeExecuteAfter: [
|
||||
async function (
|
||||
this: WorkflowHooks,
|
||||
nodeName: string,
|
||||
data: ITaskData,
|
||||
executionData: IRunExecutionData,
|
||||
): Promise<void> {
|
||||
await saveExecutionProgress(
|
||||
this.workflowData.id,
|
||||
this.executionId,
|
||||
nodeName,
|
||||
data,
|
||||
executionData,
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
function hookFunctionsSaveProgress(
|
||||
hooks: ExecutionLifecycleHooks,
|
||||
{ saveSettings }: HooksSetupParameters,
|
||||
) {
|
||||
if (!saveSettings.progress) return;
|
||||
hooks.addHandler('nodeExecuteAfter', async function (nodeName, data, executionData) {
|
||||
await saveExecutionProgress(
|
||||
this.workflowData.id,
|
||||
this.executionId,
|
||||
nodeName,
|
||||
data,
|
||||
executionData,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** This should ideally be added before any other `workflowExecuteAfter` hook to ensure all hooks get the same execution status */
|
||||
function hookFunctionsFinalizeExecutionStatus(): IWorkflowExecuteHooks {
|
||||
return {
|
||||
workflowExecuteAfter: [
|
||||
async function (fullRunData: IRun) {
|
||||
fullRunData.status = determineFinalExecutionStatus(fullRunData);
|
||||
},
|
||||
],
|
||||
};
|
||||
function hookFunctionsFinalizeExecutionStatus(hooks: ExecutionLifecycleHooks) {
|
||||
hooks.addHandler('workflowExecuteAfter', (fullRunData) => {
|
||||
fullRunData.status = determineFinalExecutionStatus(fullRunData);
|
||||
});
|
||||
}
|
||||
|
||||
function hookFunctionsStatistics(): IWorkflowExecuteHooks {
|
||||
function hookFunctionsStatistics(hooks: ExecutionLifecycleHooks) {
|
||||
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
||||
return {
|
||||
nodeFetchedData: [
|
||||
async (workflowId: string, node: INode) => {
|
||||
workflowStatisticsService.emit('nodeFetchedData', { workflowId, node });
|
||||
},
|
||||
],
|
||||
};
|
||||
hooks.addHandler('nodeFetchedData', (workflowId, node) => {
|
||||
workflowStatisticsService.emit('nodeFetchedData', { workflowId, node });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns hook functions to save workflow execution and call error workflow
|
||||
*/
|
||||
function hookFunctionsSave({
|
||||
pushRef,
|
||||
retryOf,
|
||||
saveSettings,
|
||||
}: HooksSetupParameters): IWorkflowExecuteHooks {
|
||||
function hookFunctionsSave(
|
||||
hooks: ExecutionLifecycleHooks,
|
||||
{ pushRef, retryOf, saveSettings }: HooksSetupParameters,
|
||||
) {
|
||||
const logger = Container.get(Logger);
|
||||
const errorReporter = Container.get(ErrorReporter);
|
||||
const executionRepository = Container.get(ExecutionRepository);
|
||||
const workflowStaticDataService = Container.get(WorkflowStaticDataService);
|
||||
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
||||
return {
|
||||
workflowExecuteAfter: [
|
||||
async function (
|
||||
this: WorkflowHooks,
|
||||
fullRunData: IRun,
|
||||
newStaticData: IDataObject,
|
||||
): Promise<void> {
|
||||
logger.debug('Executing hook (hookFunctionsSave)', {
|
||||
executionId: this.executionId,
|
||||
hooks.addHandler('workflowExecuteAfter', async function (fullRunData, newStaticData) {
|
||||
logger.debug('Executing hook (hookFunctionsSave)', {
|
||||
executionId: this.executionId,
|
||||
workflowId: this.workflowData.id,
|
||||
});
|
||||
|
||||
await restoreBinaryDataId(fullRunData, this.executionId, this.mode);
|
||||
|
||||
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,
|
||||
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 {
|
||||
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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
// When going into the waiting state, store the pushRef in the execution-data
|
||||
if (fullRunData.waitTill && isManualMode) {
|
||||
fullExecutionData.data.pushRef = pushRef;
|
||||
}
|
||||
|
||||
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);
|
||||
await updateExistingExecution({
|
||||
executionId: this.executionId,
|
||||
workflowId: this.workflowData.id,
|
||||
executionData: fullExecutionData,
|
||||
});
|
||||
|
||||
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,
|
||||
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,
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
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
|
||||
* they are always executed in the main process.
|
||||
*/
|
||||
function hookFunctionsSaveWorker({
|
||||
pushRef,
|
||||
retryOf,
|
||||
}: HooksSetupParameters): IWorkflowExecuteHooks {
|
||||
function hookFunctionsSaveWorker(
|
||||
hooks: ExecutionLifecycleHooks,
|
||||
{ pushRef, retryOf }: HooksSetupParameters,
|
||||
) {
|
||||
const logger = Container.get(Logger);
|
||||
const errorReporter = Container.get(ErrorReporter);
|
||||
const workflowStaticDataService = Container.get(WorkflowStaticDataService);
|
||||
const workflowStatisticsService = Container.get(WorkflowStatisticsService);
|
||||
return {
|
||||
workflowExecuteAfter: [
|
||||
async function (
|
||||
this: WorkflowHooks,
|
||||
fullRunData: IRun,
|
||||
newStaticData: IDataObject,
|
||||
): Promise<void> {
|
||||
logger.debug('Executing hook (hookFunctionsSaveWorker)', {
|
||||
executionId: this.executionId,
|
||||
workflowId: this.workflowData.id,
|
||||
});
|
||||
hooks.addHandler('workflowExecuteAfter', async function (fullRunData, newStaticData) {
|
||||
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 {
|
||||
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}" (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,
|
||||
});
|
||||
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}" (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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns WorkflowHooks instance for running integrated workflows
|
||||
* Returns ExecutionLifecycleHooks instance for running integrated workflows
|
||||
* (Workflows which get started inside of another workflow)
|
||||
*/
|
||||
export function getWorkflowHooksIntegrated(
|
||||
export function getLifecycleHooksForSubExecutions(
|
||||
mode: WorkflowExecuteMode,
|
||||
executionId: string,
|
||||
workflowData: IWorkflowBase,
|
||||
userId?: string,
|
||||
): WorkflowHooks {
|
||||
): ExecutionLifecycleHooks {
|
||||
const hooks = new ExecutionLifecycleHooks(mode, executionId, workflowData);
|
||||
const saveSettings = toSaveSettings(workflowData.settings);
|
||||
const hookFunctions = mergeHookFunctions(
|
||||
hookFunctionsWorkflowEvents(userId),
|
||||
hookFunctionsNodeEvents(),
|
||||
hookFunctionsFinalizeExecutionStatus(),
|
||||
hookFunctionsSave({ saveSettings }),
|
||||
hookFunctionsSaveProgress({ saveSettings }),
|
||||
hookFunctionsStatistics(),
|
||||
hookFunctionsExternalHooks(),
|
||||
);
|
||||
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData);
|
||||
hookFunctionsWorkflowEvents(hooks, userId);
|
||||
hookFunctionsNodeEvents(hooks);
|
||||
hookFunctionsFinalizeExecutionStatus(hooks);
|
||||
hookFunctionsSave(hooks, { saveSettings });
|
||||
hookFunctionsSaveProgress(hooks, { saveSettings });
|
||||
hookFunctionsStatistics(hooks);
|
||||
hookFunctionsExternalHooks(hooks);
|
||||
return hooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns WorkflowHooks instance for worker in scaling mode.
|
||||
* Returns ExecutionLifecycleHooks instance for worker in scaling mode.
|
||||
*/
|
||||
export function getWorkflowHooksWorkerExecuter(
|
||||
export function getLifecycleHooksForScalingWorker(
|
||||
mode: WorkflowExecuteMode,
|
||||
executionId: string,
|
||||
workflowData: IWorkflowBase,
|
||||
{ pushRef, retryOf }: Omit<HooksSetupParameters, 'saveSettings'> = {},
|
||||
): WorkflowHooks {
|
||||
): ExecutionLifecycleHooks {
|
||||
const hooks = new ExecutionLifecycleHooks(mode, executionId, workflowData);
|
||||
const saveSettings = toSaveSettings(workflowData.settings);
|
||||
const optionalParameters = { pushRef, retryOf, saveSettings };
|
||||
const toMerge = [
|
||||
hookFunctionsNodeEvents(),
|
||||
hookFunctionsFinalizeExecutionStatus(),
|
||||
hookFunctionsSaveWorker(optionalParameters),
|
||||
hookFunctionsSaveProgress(optionalParameters),
|
||||
hookFunctionsStatistics(),
|
||||
hookFunctionsExternalHooks(),
|
||||
];
|
||||
hookFunctionsNodeEvents(hooks);
|
||||
hookFunctionsFinalizeExecutionStatus(hooks);
|
||||
hookFunctionsSaveWorker(hooks, optionalParameters);
|
||||
hookFunctionsSaveProgress(hooks, optionalParameters);
|
||||
hookFunctionsStatistics(hooks);
|
||||
hookFunctionsExternalHooks(hooks);
|
||||
|
||||
if (mode === 'manual' && Container.get(InstanceSettings).isWorker) {
|
||||
toMerge.push(hookFunctionsPush(optionalParameters));
|
||||
hookFunctionsPush(hooks, optionalParameters);
|
||||
}
|
||||
|
||||
const hookFunctions = mergeHookFunctions(...toMerge);
|
||||
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData);
|
||||
return hooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
executionId: string,
|
||||
workflowData: IWorkflowBase,
|
||||
{ pushRef, retryOf }: Omit<HooksSetupParameters, 'saveSettings'> = {},
|
||||
): WorkflowHooks {
|
||||
): ExecutionLifecycleHooks {
|
||||
const hooks = new ExecutionLifecycleHooks(mode, executionId, workflowData);
|
||||
const saveSettings = toSaveSettings(workflowData.settings);
|
||||
const optionalParameters = { pushRef, retryOf, saveSettings };
|
||||
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) {
|
||||
/**
|
||||
* 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);
|
||||
hooks.addHandler('workflowExecuteAfter', async function (fullRunData) {
|
||||
// Don't delete executions before they are finished
|
||||
if (!fullRunData.finished) return;
|
||||
|
||||
return;
|
||||
}
|
||||
const isManualMode = this.mode === 'manual';
|
||||
|
||||
const shouldNotSave =
|
||||
(fullRunData.status === 'success' && !saveSettings.success) ||
|
||||
(fullRunData.status !== 'success' && !saveSettings.error);
|
||||
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);
|
||||
|
||||
if (!isManualMode && shouldNotSave && !fullRunData.waitTill) {
|
||||
await executionRepository.hardDelete({
|
||||
workflowId: this.workflowData.id,
|
||||
executionId: this.executionId,
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
// Only workflowExecuteBefore + workflowExecuteAfter
|
||||
// So to avoid confusion, we are removing other hooks.
|
||||
hookFunctions.nodeExecuteBefore = [];
|
||||
hookFunctions.nodeExecuteAfter = [];
|
||||
hooks.handlers.nodeExecuteBefore = [];
|
||||
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,
|
||||
executionId: string,
|
||||
): WorkflowHooks {
|
||||
const { pushRef, retryOf } = data;
|
||||
const saveSettings = toSaveSettings(data.workflowData.settings);
|
||||
): ExecutionLifecycleHooks {
|
||||
const { pushRef, retryOf, executionMode, workflowData } = data;
|
||||
const hooks = new ExecutionLifecycleHooks(executionMode, executionId, workflowData);
|
||||
const saveSettings = toSaveSettings(workflowData.settings);
|
||||
const optionalParameters = { pushRef, retryOf: retryOf ?? undefined, saveSettings };
|
||||
const hookFunctions = mergeHookFunctions(
|
||||
hookFunctionsWorkflowEvents(),
|
||||
hookFunctionsNodeEvents(),
|
||||
hookFunctionsFinalizeExecutionStatus(),
|
||||
hookFunctionsSave(optionalParameters),
|
||||
hookFunctionsPush(optionalParameters),
|
||||
hookFunctionsSaveProgress(optionalParameters),
|
||||
hookFunctionsStatistics(),
|
||||
hookFunctionsExternalHooks(),
|
||||
);
|
||||
return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData);
|
||||
hookFunctionsWorkflowEvents(hooks);
|
||||
hookFunctionsNodeEvents(hooks);
|
||||
hookFunctionsFinalizeExecutionStatus(hooks);
|
||||
hookFunctionsSave(hooks, optionalParameters);
|
||||
hookFunctionsPush(hooks, optionalParameters);
|
||||
hookFunctionsSaveProgress(hooks, optionalParameters);
|
||||
hookFunctionsStatistics(hooks);
|
||||
hookFunctionsExternalHooks(hooks);
|
||||
return hooks;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user