diff --git a/packages/cli/src/executions/__tests__/execution-data.service.test.ts b/packages/cli/src/executions/__tests__/execution-data.service.test.ts new file mode 100644 index 0000000000000..d579279af0a61 --- /dev/null +++ b/packages/cli/src/executions/__tests__/execution-data.service.test.ts @@ -0,0 +1,47 @@ +import { mock } from 'jest-mock-extended'; +import { NodeOperationError } from 'n8n-workflow'; +import type { INode, WorkflowExecuteMode } from 'n8n-workflow'; + +import { ExecutionDataService } from '../execution-data.service'; + +describe('ExecutionDataService', () => { + const service = new ExecutionDataService(); + + describe('generateFailedExecutionFromError', () => { + const mode: WorkflowExecuteMode = 'manual'; + const node = mock({ name: 'Test Node' }); + const error = new NodeOperationError(node, 'Test error message'); + + it('should generate a failed execution with error details', () => { + const startTime = Date.now(); + + const result = service.generateFailedExecutionFromError(mode, error, node, startTime); + + expect(result.mode).toBe(mode); + expect(result.status).toBe('error'); + expect(result.startedAt).toBeInstanceOf(Date); + expect(result.stoppedAt).toBeInstanceOf(Date); + expect(result.data.resultData.error?.message).toBe(error.message); + + const taskData = result.data.resultData.runData[node.name][0]; + expect(taskData.error?.message).toBe(error.message); + expect(taskData.startTime).toBe(startTime); + expect(taskData.executionStatus).toBe('error'); + expect(result.data.resultData.lastNodeExecuted).toBe(node.name); + expect(result.data.executionData?.nodeExecutionStack[0].node).toEqual(node); + }); + + it('should generate a failed execution without node details if node is undefined', () => { + const result = service.generateFailedExecutionFromError(mode, error, undefined); + + expect(result.mode).toBe(mode); + expect(result.status).toBe('error'); + expect(result.startedAt).toBeInstanceOf(Date); + expect(result.stoppedAt).toBeInstanceOf(Date); + expect(result.data.resultData.error?.message).toBe(error.message); + expect(result.data.resultData.runData).toEqual({}); + expect(result.data.resultData.lastNodeExecuted).toBeUndefined(); + expect(result.data.executionData).toBeUndefined(); + }); + }); +}); diff --git a/packages/cli/src/executions/__tests__/execution.service.test.ts b/packages/cli/src/executions/__tests__/execution.service.test.ts index 11e373603c081..e425c1e588eeb 100644 --- a/packages/cli/src/executions/__tests__/execution.service.test.ts +++ b/packages/cli/src/executions/__tests__/execution.service.test.ts @@ -1,6 +1,5 @@ import { mock } from 'jest-mock-extended'; -import type { INode, WorkflowExecuteMode } from 'n8n-workflow'; -import { NodeOperationError, WorkflowOperationError } from 'n8n-workflow'; +import { WorkflowOperationError } from 'n8n-workflow'; import type { ActiveExecutions } from '@/active-executions'; import type { ConcurrencyControlService } from '@/concurrency/concurrency-control.service'; @@ -278,47 +277,4 @@ describe('ExecutionService', () => { }); }); }); - - describe('generateFailedExecutionFromError', () => { - const mode: WorkflowExecuteMode = 'manual'; - const node = mock({ name: 'Test Node' }); - const error = new NodeOperationError(node, 'Test error message'); - - it('should generate a failed execution with error details', () => { - const startTime = Date.now(); - - const result = executionService.generateFailedExecutionFromError( - mode, - error, - node, - startTime, - ); - - expect(result.mode).toBe(mode); - expect(result.status).toBe('error'); - expect(result.startedAt).toBeInstanceOf(Date); - expect(result.stoppedAt).toBeInstanceOf(Date); - expect(result.data.resultData.error?.message).toBe(error.message); - - const taskData = result.data.resultData.runData[node.name][0]; - expect(taskData.error?.message).toBe(error.message); - expect(taskData.startTime).toBe(startTime); - expect(taskData.executionStatus).toBe('error'); - expect(result.data.resultData.lastNodeExecuted).toBe(node.name); - expect(result.data.executionData?.nodeExecutionStack[0].node).toEqual(node); - }); - - it('should generate a failed execution without node details if node is undefined', () => { - const result = executionService.generateFailedExecutionFromError(mode, error, undefined); - - expect(result.mode).toBe(mode); - expect(result.status).toBe('error'); - expect(result.startedAt).toBeInstanceOf(Date); - expect(result.stoppedAt).toBeInstanceOf(Date); - expect(result.data.resultData.error?.message).toBe(error.message); - expect(result.data.resultData.runData).toEqual({}); - expect(result.data.resultData.lastNodeExecuted).toBeUndefined(); - expect(result.data.executionData).toBeUndefined(); - }); - }); }); diff --git a/packages/cli/src/executions/execution-data.service.ts b/packages/cli/src/executions/execution-data.service.ts new file mode 100644 index 0000000000000..9e23e39d1dc84 --- /dev/null +++ b/packages/cli/src/executions/execution-data.service.ts @@ -0,0 +1,62 @@ +import { Service } from '@n8n/di'; +import type { ExecutionError, INode, IRun, WorkflowExecuteMode } from 'n8n-workflow'; + +@Service() +export class ExecutionDataService { + generateFailedExecutionFromError( + mode: WorkflowExecuteMode, + error: ExecutionError, + node: INode | undefined, + startTime = Date.now(), + ): IRun { + const executionError = { + ...error, + message: error.message, + stack: error.stack, + }; + const returnData: IRun = { + data: { + resultData: { + error: executionError, + runData: {}, + }, + }, + finished: false, + mode, + startedAt: new Date(), + stoppedAt: new Date(), + status: 'error', + }; + + if (node) { + returnData.data.startData = { + destinationNode: node.name, + runNodeFilter: [node.name], + }; + returnData.data.resultData.lastNodeExecuted = node.name; + returnData.data.resultData.runData[node.name] = [ + { + startTime, + executionTime: 0, + executionStatus: 'error', + error: executionError, + source: [], + }, + ]; + returnData.data.executionData = { + contextData: {}, + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + nodeExecutionStack: [ + { + node, + data: {}, + source: null, + }, + ], + }; + } + return returnData; + } +} diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index aa0a970ef0e04..c4a8188f0ece5 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -10,7 +10,6 @@ import type { IWorkflowBase, WorkflowExecuteMode, IWorkflowExecutionDataProcess, - IRun, } from 'n8n-workflow'; import { ExecutionStatusList, @@ -525,61 +524,4 @@ export class ExecutionService { await this.annotationTagMappingRepository.overwriteTags(annotation.id, updateData.tags); } } - - generateFailedExecutionFromError( - mode: WorkflowExecuteMode, - error: ExecutionError, - node: INode | undefined, - startTime = Date.now(), - ): IRun { - const executionError = { - ...error, - message: error.message, - stack: error.stack, - }; - const returnData: IRun = { - data: { - resultData: { - error: executionError, - runData: {}, - }, - }, - finished: false, - mode, - startedAt: new Date(), - stoppedAt: new Date(), - status: 'error', - }; - - if (node) { - returnData.data.startData = { - destinationNode: node.name, - runNodeFilter: [node.name], - }; - returnData.data.resultData.lastNodeExecuted = node.name; - returnData.data.resultData.runData[node.name] = [ - { - startTime, - executionTime: 0, - executionStatus: 'error', - error: executionError, - source: [], - }, - ]; - returnData.data.executionData = { - contextData: {}, - metadata: {}, - waitingExecution: {}, - waitingExecutionSource: {}, - nodeExecutionStack: [ - { - node, - data: {}, - source: null, - }, - ], - }; - } - return returnData; - } } diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index 4d1df2e633d47..0155f985ec1bd 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -37,7 +37,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository import { EventService } from '@/events/event.service'; import type { AiEventMap, AiEventPayload } from '@/events/maps/ai.event-map'; import { getLifecycleHooksForSubExecutions } from '@/execution-lifecycle/execution-lifecycle-hooks'; -import { ExecutionService } from '@/executions/execution.service'; +import { ExecutionDataService } from '@/executions/execution-data.service'; import { CredentialsPermissionChecker, SubworkflowPolicyChecker, @@ -262,7 +262,7 @@ async function startExecution( data = await execution; } catch (error) { const executionError = error as ExecutionError; - const fullRunData = Container.get(ExecutionService).generateFailedExecutionFromError( + const fullRunData = Container.get(ExecutionDataService).generateFailedExecutionFromError( runData.executionMode, executionError, 'node' in executionError ? executionError.node : undefined, diff --git a/packages/cli/src/workflow-runner.ts b/packages/cli/src/workflow-runner.ts index 54e656c97c2af..bf6b51d9c293d 100644 --- a/packages/cli/src/workflow-runner.ts +++ b/packages/cli/src/workflow-runner.ts @@ -27,7 +27,7 @@ import { getLifecycleHooksForScalingWorker, getLifecycleHooksForScalingMain, } from '@/execution-lifecycle/execution-lifecycle-hooks'; -import { ExecutionService } from '@/executions/execution.service'; +import { ExecutionDataService } from '@/executions/execution-data.service'; import { CredentialsPermissionChecker } from '@/executions/pre-execution-checks'; import { ManualExecutionService } from '@/manual-execution.service'; import { NodeTypes } from '@/node-types'; @@ -52,7 +52,7 @@ export class WorkflowRunner { private readonly credentialsPermissionChecker: CredentialsPermissionChecker, private readonly instanceSettings: InstanceSettings, private readonly manualExecutionService: ManualExecutionService, - private readonly executionService: ExecutionService, + private readonly executionDataService: ExecutionDataService, ) {} /** The process did error */ @@ -137,7 +137,7 @@ export class WorkflowRunner { await this.credentialsPermissionChecker.check(workflowId, nodes); } catch (error) { // Create a failed execution with the data for the node, save it and abort execution - const runData = this.executionService.generateFailedExecutionFromError( + const runData = this.executionDataService.generateFailedExecutionFromError( data.executionMode, error, error.node, diff --git a/packages/cli/src/workflows/workflow-execution.service.ts b/packages/cli/src/workflows/workflow-execution.service.ts index c12babfc5d5ba..62cc6fb361e0c 100644 --- a/packages/cli/src/workflows/workflow-execution.service.ts +++ b/packages/cli/src/workflows/workflow-execution.service.ts @@ -21,7 +21,7 @@ import type { Project } from '@/databases/entities/project'; import type { User } from '@/databases/entities/user'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import { ExecutionService } from '@/executions/execution.service'; +import { ExecutionDataService } from '@/executions/execution-data.service'; import { SubworkflowPolicyChecker } from '@/executions/pre-execution-checks'; import type { CreateExecutionPayload, IWorkflowErrorData } from '@/interfaces'; import { NodeTypes } from '@/node-types'; @@ -42,7 +42,7 @@ export class WorkflowExecutionService { private readonly workflowRunner: WorkflowRunner, private readonly globalConfig: GlobalConfig, private readonly subworkflowPolicyChecker: SubworkflowPolicyChecker, - private readonly executionService: ExecutionService, + private readonly executionDataService: ExecutionDataService, ) {} async runWorkflow( @@ -274,7 +274,7 @@ export class WorkflowExecutionService { ); // Create a fake execution and save it to DB. - const fakeExecution = this.executionService.generateFailedExecutionFromError( + const fakeExecution = this.executionDataService.generateFailedExecutionFromError( 'error', errorWorkflowPermissionError, initialNode,