From 8db0b48ee18da45c0e8ff721fb2c85471baebc1f Mon Sep 17 00:00:00 2001 From: Ali Elkhateeb Date: Fri, 7 Aug 2026 11:13:11 +0300 Subject: [PATCH] refactor(API): Route public API executions through shared execution services (no-changelog) (#35582) --- .../__tests__/execution.repository.test.ts | 42 +++-- .../annotation-tag-mapping.repository.ee.ts | 4 + .../src/repositories/execution.repository.ts | 46 ++--- packages/cli/eslint.config.mjs | 3 - .../execution-persistence.integration.test.ts | 7 +- .../__tests__/execution-persistence.test.ts | 15 +- .../__tests__/execution.service.test.ts | 175 +++++++++++++++++- .../src/executions/execution-persistence.ts | 50 +++-- .../cli/src/executions/execution.service.ts | 173 ++++++++++++++++- .../handlers/executions/executions.handler.ts | 142 ++++---------- .../handlers/executions/executions.service.ts | 63 ------- .../integration/public-api/executions.test.ts | 52 +++--- .../test/integration/shared/utils/index.ts | 2 - 13 files changed, 488 insertions(+), 286 deletions(-) delete mode 100644 packages/cli/src/public-api/v1/handlers/executions/executions.service.ts diff --git a/packages/@n8n/db/src/repositories/__tests__/execution.repository.test.ts b/packages/@n8n/db/src/repositories/__tests__/execution.repository.test.ts index f22692417d8..327b793a06a 100644 --- a/packages/@n8n/db/src/repositories/__tests__/execution.repository.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/execution.repository.test.ts @@ -32,43 +32,43 @@ describe('ExecutionRepository', () => { vi.resetAllMocks(); }); - describe('getExecutionsCountForPublicApi', () => { + describe('countInWorkflows', () => { test('should get executions matching all filter parameters', async () => { const mockCount = 20; - const params = { + const workflowIds = ['3', '4']; + const options = { limit: 10, lastId: '3', - workflowIds: ['3', '4'], }; entityManager.count.mockResolvedValueOnce(mockCount); - const result = await executionRepository.getExecutionsCountForPublicApi(params); + const result = await executionRepository.countInWorkflows(workflowIds, options); expect(entityManager.count).toHaveBeenCalledWith(ExecutionEntity, { where: { - id: LessThan(params.lastId), - workflowId: In(params.workflowIds), + id: LessThan(options.lastId), + workflowId: In(workflowIds), }, - take: params.limit, + take: options.limit, }); expect(result).toBe(mockCount); }); test('should get executions matching the workflowIds filter', async () => { const mockCount = 12; - const params = { + const workflowIds = ['7', '8']; + const options = { limit: 10, - workflowIds: ['7', '8'], }; entityManager.count.mockResolvedValueOnce(mockCount); - const result = await executionRepository.getExecutionsCountForPublicApi(params); + const result = await executionRepository.countInWorkflows(workflowIds, options); expect(entityManager.count).toHaveBeenCalledWith(ExecutionEntity, { where: { - workflowId: In(params.workflowIds), + workflowId: In(workflowIds), }, - take: params.limit, + take: options.limit, }); expect(result).toBe(mockCount); }); @@ -86,19 +86,21 @@ describe('ExecutionRepository', () => { 'should find with id less than "$lastId" and not in "$excludedExecutionsIds"', async ({ lastId, excludedExecutionsIds, expectedIdCondition }) => { const mockCount = 15; - const params = { + const workflowIds = ['wf-1']; + const options = { limit: 10, ...(lastId ? { lastId } : {}), ...(excludedExecutionsIds ? { excludedExecutionsIds } : {}), }; entityManager.count.mockResolvedValueOnce(mockCount); - const result = await executionRepository.getExecutionsCountForPublicApi(params); + const result = await executionRepository.countInWorkflows(workflowIds, options); expect(entityManager.count).toHaveBeenCalledWith(ExecutionEntity, { where: { + workflowId: In(workflowIds), ...(expectedIdCondition ? { id: expectedIdCondition } : {}), }, - take: params.limit, + take: options.limit, }); expect(result).toBe(mockCount); }, @@ -119,15 +121,16 @@ describe('ExecutionRepository', () => { `('should retrieve all $filterStatus executions', async ({ filterStatus, entityStatus }) => { const limit = 10; const mockCount = 20; + const workflowIds = ['wf-1']; entityManager.count.mockResolvedValueOnce(mockCount); - const result = await executionRepository.getExecutionsCountForPublicApi({ + const result = await executionRepository.countInWorkflows(workflowIds, { limit, status: filterStatus, }); expect(entityManager.count).toHaveBeenCalledWith(ExecutionEntity, { - where: { status: entityStatus }, + where: { status: entityStatus, workflowId: In(workflowIds) }, take: limit, }); @@ -137,12 +140,13 @@ describe('ExecutionRepository', () => { test('should find all executions without status filter', async () => { const limit = 10; const mockCount = 20; + const workflowIds = ['wf-1']; entityManager.count.mockResolvedValueOnce(mockCount); - const result = await executionRepository.getExecutionsCountForPublicApi({ limit }); + const result = await executionRepository.countInWorkflows(workflowIds, { limit }); expect(entityManager.count).toHaveBeenCalledWith(ExecutionEntity, { - where: {}, + where: { workflowId: In(workflowIds) }, take: limit, }); diff --git a/packages/@n8n/db/src/repositories/annotation-tag-mapping.repository.ee.ts b/packages/@n8n/db/src/repositories/annotation-tag-mapping.repository.ee.ts index 2ec4e0eacac..c3b9a35fd06 100644 --- a/packages/@n8n/db/src/repositories/annotation-tag-mapping.repository.ee.ts +++ b/packages/@n8n/db/src/repositories/annotation-tag-mapping.repository.ee.ts @@ -16,6 +16,10 @@ export class AnnotationTagMappingRepository extends Repository { await tx.delete(AnnotationTagMapping, { annotationId }); + if (tagIds.length === 0) { + return; + } + const tagMappings = tagIds.map((tagId) => ({ annotationId, tagId, diff --git a/packages/@n8n/db/src/repositories/execution.repository.ts b/packages/@n8n/db/src/repositories/execution.repository.ts index 9f2e4adf4dc..b39ba677a4a 100644 --- a/packages/@n8n/db/src/repositories/execution.repository.ts +++ b/packages/@n8n/db/src/repositories/execution.repository.ts @@ -628,19 +628,19 @@ export class ExecutionRepository extends Repository { }); } - async getExecutionsCountForPublicApi(params: { - limit: number; - lastId?: string; - workflowIds?: string[]; - status?: ExecutionStatus; - excludedExecutionsIds?: string[]; - }): Promise { - const executionsCount = await this.count({ - where: this.getFindExecutionsForPublicApiCondition(params), - take: params.limit, + async countInWorkflows( + workflowIds: string[], + options: { + limit: number; + lastId?: string; + status?: ExecutionStatus; + excludedExecutionsIds?: string[]; + }, + ): Promise { + return await this.count({ + where: this.getFindManyInWorkflowsCondition(workflowIds, options), + take: options.limit, }); - - return executionsCount; } private getStatusCondition(status?: ExecutionStatus) { @@ -667,19 +667,21 @@ export class ExecutionRepository extends Repository { return condition; } - getFindExecutionsForPublicApiCondition(params: { - lastId?: string; - workflowIds?: string[]; - status?: ExecutionStatus; - excludedExecutionsIds?: string[]; - }) { + getFindManyInWorkflowsCondition( + workflowIds: string[], + options: { + lastId?: string; + status?: ExecutionStatus; + excludedExecutionsIds?: string[]; + } = {}, + ) { const where: FindOptionsWhere = { ...this.getIdCondition({ - lastId: params.lastId, - excludedExecutionsIds: params.excludedExecutionsIds, + lastId: options.lastId, + excludedExecutionsIds: options.excludedExecutionsIds, }), - ...this.getStatusCondition(params.status), - ...(params.workflowIds && { workflowId: In(params.workflowIds) }), + ...this.getStatusCondition(options.status), + workflowId: In(workflowIds), }; return where; diff --git a/packages/cli/eslint.config.mjs b/packages/cli/eslint.config.mjs index 0241639ad66..cc924eb3db1 100644 --- a/packages/cli/eslint.config.mjs +++ b/packages/cli/eslint.config.mjs @@ -123,8 +123,6 @@ export default defineConfig( './src/public-api/v1/handlers/data-tables/data-tables.service.ts', './src/public-api/v1/handlers/discover/discover.handler.ts', './src/public-api/v1/handlers/evaluations/evaluations.handler.ts', - './src/public-api/v1/handlers/executions/executions.handler.ts', - './src/public-api/v1/handlers/executions/executions.service.ts', './src/public-api/v1/handlers/projects/projects.handler.ts', './src/public-api/v1/handlers/users/users.handler.ee.ts', './src/public-api/v1/handlers/users/users.service.ee.ts', @@ -249,7 +247,6 @@ export default defineConfig( './src/eventbus/message-event-bus/message-event-bus.ts', './src/evaluation.ee/evaluation-collection.service.ts', './src/evaluation.ee/test-runner/test-runner.service.ee.ts', - './src/public-api/v1/handlers/executions/executions.handler.ts', './src/public-api/v1/handlers/tags/tags.handler.ts', './src/public-api/v1/handlers/users/users.service.ee.ts', './src/public-api/v1/handlers/workflows/workflows.handler.ts', diff --git a/packages/cli/src/executions/__tests__/execution-persistence.integration.test.ts b/packages/cli/src/executions/__tests__/execution-persistence.integration.test.ts index 7649cf48486..4d59c04f7b6 100644 --- a/packages/cli/src/executions/__tests__/execution-persistence.integration.test.ts +++ b/packages/cli/src/executions/__tests__/execution-persistence.integration.test.ts @@ -420,12 +420,13 @@ describe('ExecutionPersistence', () => { expect(execution?.data?.resultData?.runData).toHaveProperty('bigNode'); }); - it('getExecutionsForPublicApi omits oversized data and flags it', async () => { + it('findManyInWorkflows omits oversized data and flags it', async () => { const executionPersistence = Container.get(ExecutionPersistence); const { workflow } = await createSizedExecution(2 * ONE_MB); - const executions = (await executionPersistence.getExecutionsForPublicApi( - { limit: 10, includeData: true, workflowIds: [workflow.id] }, + const executions = (await executionPersistence.findManyInWorkflows( + [workflow.id], + { limit: 10, includeData: true }, ONE_MB, )) as IExecutionResponse[]; diff --git a/packages/cli/src/executions/__tests__/execution-persistence.test.ts b/packages/cli/src/executions/__tests__/execution-persistence.test.ts index 58326693cd7..a1eb22f34a9 100644 --- a/packages/cli/src/executions/__tests__/execution-persistence.test.ts +++ b/packages/cli/src/executions/__tests__/execution-persistence.test.ts @@ -1694,7 +1694,7 @@ describe('ExecutionPersistence', () => { }); }); - describe('getExecutionsForPublicApi', () => { + describe('findManyInWorkflows', () => { const wf = 'wf-1'; const where = { workflowId: wf }; const publicApiSelect = [ @@ -1711,18 +1711,19 @@ describe('ExecutionPersistence', () => { ]; beforeEach(() => { - executionRepository.getFindExecutionsForPublicApiCondition.mockReturnValue(where); + executionRepository.getFindManyInWorkflowsCondition.mockReturnValue(where); }); it('should query per the repository where condition, without data when not requested', async () => { const executionPersistence = createPersistenceService('db'); executionRepository.findMultipleExecutions.mockResolvedValue([]); - const params = { limit: 10, workflowIds: [wf] }; + const options = { limit: 10 }; - await executionPersistence.getExecutionsForPublicApi(params); + await executionPersistence.findManyInWorkflows([wf], options); - expect(executionRepository.getFindExecutionsForPublicApiCondition).toHaveBeenCalledWith( - params, + expect(executionRepository.getFindManyInWorkflowsCondition).toHaveBeenCalledWith( + [wf], + options, ); expect(executionRepository.findMultipleExecutions).toHaveBeenCalledWith( { select: publicApiSelect, where, order: { id: 'DESC' }, take: 10 }, @@ -1755,7 +1756,7 @@ describe('ExecutionPersistence', () => { ]), ); - const result = await executionPersistence.getExecutionsForPublicApi({ + const result = await executionPersistence.findManyInWorkflows([wf], { limit: 10, includeData: true, }); diff --git a/packages/cli/src/executions/__tests__/execution.service.test.ts b/packages/cli/src/executions/__tests__/execution.service.test.ts index 68084c3fc49..d3551455c20 100644 --- a/packages/cli/src/executions/__tests__/execution.service.test.ts +++ b/packages/cli/src/executions/__tests__/execution.service.test.ts @@ -1,6 +1,9 @@ import { mockInstance } from '@n8n/backend-test-utils'; import { GlobalConfig } from '@n8n/config'; import type { + AnnotationTagMappingRepository, + ExecutionAnnotationRepository, + IExecutionBase, IExecutionDb, IExecutionResponse, ExecutionRepository, @@ -10,6 +13,7 @@ import type { } from '@n8n/db'; import type { WorkflowHistory } from '@n8n/db'; import { Container } from '@n8n/di'; +import { QueryFailedError } from '@n8n/typeorm'; import { mock } from 'vitest-mock-extended'; import type { IRun, IRunData, IRunExecutionData, ITaskData } from 'n8n-workflow'; import { ManualExecutionCancelledError, WorkflowOperationError } from 'n8n-workflow'; @@ -18,6 +22,7 @@ import type { ActiveExecutions } from '@/active-executions'; import type { ConcurrencyControlService } from '@/concurrency/concurrency-control.service'; import { AbortedExecutionRetryError } from '@/errors/aborted-execution-retry.error'; import { MissingExecutionStopError } from '@/errors/missing-execution-stop.error'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { MissingExecutionDataError } from '@/executions/execution-data/missing-execution-data.error'; import type { ExecutionPersistence } from '@/executions/execution-persistence'; @@ -34,6 +39,8 @@ import type { WorkflowRunner } from '@/workflow-runner'; describe('ExecutionService', () => { const scalingService = mockInstance(ScalingService); const activeExecutions = mock(); + const executionAnnotationRepository = mock(); + const annotationTagMappingRepository = mock(); const executionRepository = mock(); const executionPersistence = mock(); const workflowHistoryRepository = mock(); @@ -48,8 +55,8 @@ describe('ExecutionService', () => { globalConfig, mock(), activeExecutions, - mock(), - mock(), + executionAnnotationRepository, + annotationTagMappingRepository, executionRepository, executionPersistence, workflowHistoryRepository, @@ -81,7 +88,7 @@ describe('ExecutionService', () => { * Arrange */ const execution = mock({ id: '123', data: { resultData: {} } }); - executionPersistence.findIfSharedUnflatten.mockResolvedValue(execution); + executionPersistence.findOneInWorkflows.mockResolvedValue(execution); executionRedactionServiceProxy.processExecution.mockResolvedValue(execution); const req = mock({ @@ -108,7 +115,7 @@ describe('ExecutionService', () => { * Arrange */ const execution = mock({ id: '123', data: { resultData: {} } }); - executionPersistence.findIfSharedUnflatten.mockResolvedValue(execution); + executionPersistence.findOneInWorkflows.mockResolvedValue(execution); executionRedactionServiceProxy.processExecution.mockResolvedValue(execution); const req = mock({ @@ -131,7 +138,7 @@ describe('ExecutionService', () => { }); it('should surface missing execution data as a user-facing not-found error', async () => { - executionPersistence.findIfSharedUnflatten.mockRejectedValue( + executionPersistence.findOneInWorkflows.mockRejectedValue( new MissingExecutionDataError({ workflowId: 'workflow-1', executionId: '123' }), ); @@ -147,7 +154,7 @@ describe('ExecutionService', () => { it('should rethrow errors other than missing execution data unchanged', async () => { const error = new Error('boom'); - executionPersistence.findIfSharedUnflatten.mockRejectedValue(error); + executionPersistence.findOneInWorkflows.mockRejectedValue(error); const req = mock({ params: { id: '123' }, query: {} }); @@ -856,4 +863,160 @@ describe('ExecutionService', () => { expect(result).toEqual([]); }); }); + + describe('findManyAndCount', () => { + it('should exclude live running executions when excludeRunning is true', async () => { + activeExecutions.getActiveExecutions.mockReturnValue([ + { id: 'run-1', status: 'running' }, + { id: 'wait-1', status: 'waiting' }, + ] as never); + executionPersistence.findManyInWorkflows.mockResolvedValue([ + mock({ id: '10' }), + ]); + executionRepository.countInWorkflows.mockResolvedValue(0); + + await executionService.findManyAndCount(['wf-1'], { + limit: 10, + excludeRunning: true, + }); + + expect(executionPersistence.findManyInWorkflows).toHaveBeenCalledWith( + ['wf-1'], + expect.objectContaining({ excludedExecutionsIds: ['run-1'] }), + undefined, + ); + }); + + it('should not exclude running executions when excludeRunning is false', async () => { + executionPersistence.findManyInWorkflows.mockResolvedValue([]); + executionRepository.countInWorkflows.mockResolvedValue(0); + + await executionService.findManyAndCount(['wf-1'], { + limit: 10, + excludeRunning: false, + }); + + expect(executionPersistence.findManyInWorkflows).toHaveBeenCalledWith( + ['wf-1'], + expect.objectContaining({ excludedExecutionsIds: undefined }), + undefined, + ); + expect(activeExecutions.getActiveExecutions).not.toHaveBeenCalled(); + }); + }); + + describe('deleteOne', () => { + it('should reject deleting a running execution', async () => { + executionPersistence.findOneInWorkflows.mockResolvedValue( + mock({ id: '1', status: 'running', workflowId: 'wf-1' }), + ); + + await expect(executionService.deleteOne('1', ['wf-1'])).rejects.toThrow(BadRequestError); + expect(executionPersistence.hardDelete).not.toHaveBeenCalled(); + }); + + it('should remove from concurrency control when deleting a new execution', async () => { + const execution = mock({ + id: '1', + status: 'new', + mode: 'manual', + workflowId: 'wf-1', + storedAt: 'db', + }); + executionPersistence.findOneInWorkflows.mockResolvedValue(execution); + + await executionService.deleteOne('1', ['wf-1']); + + expect(concurrencyControl.remove).toHaveBeenCalledWith({ + executionId: '1', + mode: 'manual', + }); + expect(executionPersistence.hardDelete).toHaveBeenCalledWith({ + workflowId: 'wf-1', + executionId: '1', + storedAt: 'db', + }); + }); + + it('should throw NotFoundError when execution is inaccessible', async () => { + executionPersistence.findOneInWorkflows.mockResolvedValue(undefined); + + await expect(executionService.deleteOne('1', ['wf-1'])).rejects.toThrow(NotFoundError); + }); + }); + + describe('getExecutionTags', () => { + it('should return mapped tags for an accessible execution', async () => { + executionPersistence.findOneInWorkflows.mockResolvedValue(mock({ id: '1' })); + executionAnnotationRepository.findOne.mockResolvedValue({ + tags: [ + { + id: 'tag-1', + name: 'Important', + createdAt: new Date('2025-01-01'), + updatedAt: new Date('2025-01-02'), + }, + ], + } as never); + + const result = await executionService.getExecutionTags('1', ['wf-1']); + + expect(result).toEqual([ + { + id: 'tag-1', + name: 'Important', + createdAt: new Date('2025-01-01'), + updatedAt: new Date('2025-01-02'), + }, + ]); + }); + + it('should throw NotFoundError when execution is inaccessible', async () => { + executionPersistence.findOneInWorkflows.mockResolvedValue(undefined); + + await expect(executionService.getExecutionTags('1', ['wf-1'])).rejects.toThrow(NotFoundError); + }); + }); + + describe('updateExecutionTags', () => { + it('should overwrite tags and return the updated list', async () => { + executionPersistence.findOneInWorkflows.mockResolvedValue(mock({ id: '1' })); + executionAnnotationRepository.findOneOrFail + .mockResolvedValueOnce({ id: 42 } as never) + .mockResolvedValueOnce({ + tags: [ + { + id: 'tag-1', + name: 'A', + createdAt: new Date('2025-01-01'), + updatedAt: new Date('2025-01-01'), + }, + ], + } as never); + + const result = await executionService.updateExecutionTags('1', ['tag-1'], ['wf-1']); + + expect(annotationTagMappingRepository.overwriteTags).toHaveBeenCalledWith(42, ['tag-1']); + expect(result).toEqual([ + { + id: 'tag-1', + name: 'A', + createdAt: new Date('2025-01-01'), + updatedAt: new Date('2025-01-01'), + }, + ]); + }); + + it('should map QueryFailedError to NotFoundError for missing tags', async () => { + executionPersistence.findOneInWorkflows.mockResolvedValue(mock({ id: '1' })); + executionAnnotationRepository.findOneOrFail.mockResolvedValue({ id: 42 } as never); + annotationTagMappingRepository.overwriteTags.mockRejectedValue( + new QueryFailedError('INSERT', [], new Error('FK')), + ); + + await expect( + executionService.updateExecutionTags('1', ['missing'], ['wf-1']), + ).rejects.toThrow('Some tags not found'); + }); + }); }); diff --git a/packages/cli/src/executions/execution-persistence.ts b/packages/cli/src/executions/execution-persistence.ts index b9d583ba168..05a6a102576 100644 --- a/packages/cli/src/executions/execution-persistence.ts +++ b/packages/cli/src/executions/execution-persistence.ts @@ -521,43 +521,37 @@ export class ExecutionPersistence { }); } - /** Find an execution scoped to shared workflows, with unflattened data and annotation (a display read). */ - async findIfSharedUnflatten( + /** + * Find one execution scoped to the given workflow IDs (display read). + * Defaults: include data + annotation, unflattened. + */ + async findOneInWorkflows( executionId: string, - sharedWorkflowIds: string[], - maxDataSizeBytes?: number, - ) { - return await this.findSingleExecution(executionId, { - where: { workflowId: In(sharedWorkflowIds) }, - includeData: true, - unflattenData: true, - includeAnnotation: true, - maxDataSizeBytes, - }); - } - - /** Find an execution scoped to the given workflows for the public API (a display read). */ - async getExecutionInWorkflowsForPublicApi( - id: string, workflowIds: string[], - includeData?: boolean, - maxDataSizeBytes?: number, - ): Promise { - return await this.findSingleExecution(id, { + options: { + includeData?: boolean; + includeAnnotation?: boolean; + maxDataSizeBytes?: number; + } = {}, + ): Promise { + const { includeData = true, includeAnnotation = true, maxDataSizeBytes } = options; + + return await this.findSingleExecution(executionId, { where: { workflowId: In(workflowIds) }, includeData, unflattenData: true, + includeAnnotation, maxDataSizeBytes, }); } - /** Find executions scoped to the given workflows for the public API, with data per `storedAt`. */ - async getExecutionsForPublicApi( - params: { + /** Find executions scoped to the given workflows, with data per `storedAt`. */ + async findManyInWorkflows( + workflowIds: string[], + options: { limit: number; includeData?: boolean; lastId?: string; - workflowIds?: string[]; status?: ExecutionStatus; excludedExecutionsIds?: string[]; }, @@ -577,11 +571,11 @@ export class ExecutionPersistence { 'finished', 'status', ], - where: this.executionRepository.getFindExecutionsForPublicApiCondition(params), + where: this.executionRepository.getFindManyInWorkflowsCondition(workflowIds, options), order: { id: 'DESC' }, - take: params.limit, + take: options.limit, }, - { includeData: params.includeData, unflattenData: true, maxDataSizeBytes }, + { includeData: options.includeData, unflattenData: true, maxDataSizeBytes }, ); } diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index 233afa15508..69a1c0acc79 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -4,6 +4,7 @@ import { GlobalConfig } from '@n8n/config'; import type { CreateExecutionPayload, ExecutionSummaries, + IExecutionBase, IExecutionResponse, IGetExecutionsQueryFilter, User, @@ -18,6 +19,7 @@ import { } from '@n8n/db'; import { Service } from '@n8n/di'; import type { Scope } from '@n8n/permissions'; +import { QueryFailedError } from '@n8n/typeorm'; import { ensureError } from '@n8n/utils/errors/ensure-error'; import { stringify } from 'flatted'; import { validate as jsonSchemaValidate } from 'jsonschema'; @@ -45,6 +47,7 @@ import { ConcurrencyControlService } from '@/concurrency/concurrency-control.ser import { AbortedExecutionRetryError } from '@/errors/aborted-execution-retry.error'; import { MissingExecutionStopError } from '@/errors/missing-execution-stop.error'; import { QueuedExecutionRetryError } from '@/errors/queued-execution-retry.error'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ConflictError } from '@/errors/response-errors/conflict.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; @@ -148,6 +151,13 @@ export class ExecutionService { return { scopes: [scope], projectRoles, workflowRoles }; } + /** + * Editor/internal GET: load an execution for display, apply redaction, and + * return flatted `data` (`IExecutionFlattedResponse`). + * + * Prefer this for the private executions API. For a domain entity with + * caller-controlled options, use {@link findOneInWorkflows}. + */ async findOne( req: ExecutionRequest.GetOne | ExecutionRequest.Update, sharedWorkflowIds: string[], @@ -155,12 +165,12 @@ export class ExecutionService { if (!sharedWorkflowIds.length) return undefined; const { id: executionId } = req.params; - let execution: IExecutionResponse | undefined; + let execution: IExecutionResponse | IExecutionBase | undefined; try { - execution = await this.executionPersistence.findIfSharedUnflatten( + execution = await this.executionPersistence.findOneInWorkflows( executionId, sharedWorkflowIds, - this.globalConfig.executions.maxDisplaySize, + { maxDataSizeBytes: this.globalConfig.executions.maxDisplaySize }, ); } catch (error) { if (error instanceof MissingExecutionDataError) { @@ -179,6 +189,10 @@ export class ExecutionService { return undefined; } + if (!('data' in execution)) { + throw new UnexpectedError('Expected execution data for display read'); + } + let redactExecutionData: boolean | undefined; const redactQuery = ExecutionRedactionQueryDtoSchema.safeParse(req.query); if (redactQuery.success) { @@ -762,6 +776,159 @@ export class ExecutionService { } } + /** + * Load one execution scoped to `workflowIds` as a domain entity. + * Options control data/annotation inclusion; no redaction or flatting. + * + * Prefer this for public API and service-to-service loads. For the editor + * flatted response, use {@link findOne}. + */ + async findOneInWorkflows( + executionId: string, + workflowIds: string[], + options?: { + includeData?: boolean; + includeAnnotation?: boolean; + maxDataSizeBytes?: number; + }, + ) { + return await this.executionPersistence.findOneInWorkflows(executionId, workflowIds, options); + } + + async findManyAndCount( + workflowIds: string[], + options: { + limit: number; + includeData?: boolean; + lastId?: string; + status?: ExecutionStatus; + excludeRunning?: boolean; + maxDataSizeBytes?: number; + }, + ): Promise<{ executions: IExecutionBase[]; count: number }> { + const excludedExecutionsIds = options.excludeRunning + ? this.activeExecutions + .getActiveExecutions() + .filter(({ status }) => status === 'running') + .map(({ id }) => id) + : undefined; + + const listOptions = { + limit: options.limit, + includeData: options.includeData, + lastId: options.lastId, + status: options.status, + excludedExecutionsIds, + }; + + const executions = await this.executionPersistence.findManyInWorkflows( + workflowIds, + listOptions, + options.maxDataSizeBytes, + ); + + const newLastId = executions.length === 0 ? '0' : executions.at(-1)!.id; + const count = await this.executionRepository.countInWorkflows(workflowIds, { + ...listOptions, + lastId: newLastId, + }); + + return { executions, count }; + } + + async deleteOne(executionId: string, sharedWorkflowIds: string[]) { + const execution = await this.findOneInWorkflows(executionId, sharedWorkflowIds, { + includeData: false, + includeAnnotation: false, + }); + + if (!execution) { + throw new NotFoundError('Not Found'); + } + + if (execution.status === 'running') { + throw new BadRequestError('Cannot delete a running execution'); + } + + if (execution.status === 'new') { + this.concurrencyControl.remove({ + executionId: execution.id, + mode: execution.mode, + }); + } + + await this.executionPersistence.hardDelete({ + workflowId: execution.workflowId, + executionId: execution.id, + storedAt: execution.storedAt, + }); + + return execution; + } + + async getExecutionTags(executionId: string, sharedWorkflowIds: string[]) { + const execution = await this.findOneInWorkflows(executionId, sharedWorkflowIds, { + includeData: false, + includeAnnotation: false, + }); + + if (!execution) { + throw new NotFoundError('Not Found'); + } + + const annotation = await this.executionAnnotationRepository.findOne({ + where: { execution: { id: executionId } }, + relations: ['tags'], + }); + + return (annotation?.tags ?? []).map(({ id, name, createdAt, updatedAt }) => ({ + id, + name, + createdAt, + updatedAt, + })); + } + + async updateExecutionTags(executionId: string, tagIds: string[], sharedWorkflowIds: string[]) { + const execution = await this.findOneInWorkflows(executionId, sharedWorkflowIds, { + includeData: false, + includeAnnotation: false, + }); + + if (!execution) { + throw new NotFoundError('Not Found'); + } + + await this.executionAnnotationRepository.upsert({ execution: { id: executionId } }, [ + 'execution', + ]); + + const annotation = await this.executionAnnotationRepository.findOneOrFail({ + where: { execution: { id: executionId } }, + }); + + try { + await this.annotationTagMappingRepository.overwriteTags(annotation.id, tagIds); + } catch (error) { + if (error instanceof QueryFailedError) { + throw new NotFoundError('Some tags not found'); + } + throw error; + } + + const updatedAnnotation = await this.executionAnnotationRepository.findOneOrFail({ + where: { execution: { id: executionId } }, + relations: ['tags'], + }); + + return (updatedAnnotation.tags ?? []).map(({ id, name, createdAt, updatedAt }) => ({ + id, + name, + createdAt, + updatedAt, + })); + } + async getExecutedVersions( workflowId: string, ): Promise> { diff --git a/packages/cli/src/public-api/v1/handlers/executions/executions.handler.ts b/packages/cli/src/public-api/v1/handlers/executions/executions.handler.ts index e53003b8aa4..7ea8046da0b 100644 --- a/packages/cli/src/public-api/v1/handlers/executions/executions.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/executions/executions.handler.ts @@ -1,27 +1,20 @@ import { ExecutionRedactionQueryDtoSchema } from '@n8n/api-types'; import { ExecutionsConfig } from '@n8n/config'; import type { IExecutionBase } from '@n8n/db'; -import { ExecutionRepository } from '@n8n/db'; import { Container } from '@n8n/di'; -import { QueryFailedError } from '@n8n/typeorm'; import { type ExecutionStatus, replaceCircularReferences } from 'n8n-workflow'; -import { ActiveExecutions } from '@/active-executions'; -import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service'; import { AbortedExecutionRetryError } from '@/errors/aborted-execution-retry.error'; import { MissingExecutionStopError } from '@/errors/missing-execution-stop.error'; import { QueuedExecutionRetryError } from '@/errors/queued-execution-retry.error'; -import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ConflictError } from '@/errors/response-errors/conflict.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { EventService } from '@/events/event.service'; -import { ExecutionPersistence } from '@/executions/execution-persistence'; import type { RedactableExecution } from '@/executions/execution-redaction'; import { ExecutionRedactionServiceProxy } from '@/executions/execution-redaction-proxy.service'; import { ExecutionService } from '@/executions/execution.service'; import { WorkflowSharingService } from '@/workflows/workflow-sharing.service'; -import { getExecutionTags, mapAnnotationTags, updateExecutionTags } from './executions.service'; import type { ExecutionRequest } from '../../../types'; import type { PublicAPIEndpoint } from '../../shared/handler.types'; import { publicApiScope, validCursor } from '../../shared/middlewares/global.middleware'; @@ -63,43 +56,15 @@ const executionHandlers: ExecutionHandlers = { WorkflowSharingService, ).getSharedWorkflowIdsForScopes(req.user, ['workflow:delete']); - // user does not have workflows hence no executions - // or the execution they are trying to access belongs to a workflow they do not own if (!sharedWorkflowsIds.length) { throw new NotFoundError('Not Found'); } const { id } = req.params; - // look for the execution on the workflow the user owns - const execution = await Container.get( - ExecutionPersistence, - ).getExecutionInWorkflowsForPublicApi(id, sharedWorkflowsIds, false); + const execution = await Container.get(ExecutionService).deleteOne(id, sharedWorkflowsIds); - if (!execution) { - throw new NotFoundError('Not Found'); - } - - if (execution.status === 'running') { - throw new BadRequestError('Cannot delete a running execution'); - } - - if (execution.status === 'new') { - Container.get(ConcurrencyControlService).remove({ - executionId: execution.id, - mode: execution.mode, - }); - } - - await Container.get(ExecutionPersistence).hardDelete({ - workflowId: execution.workflowId, - executionId: execution.id, - storedAt: execution.storedAt, - }); - - execution.id = id; - - return res.json(replaceCircularReferences(execution)); + return res.json(replaceCircularReferences({ ...execution, id })); }, ], getExecution: [ @@ -109,8 +74,6 @@ const executionHandlers: ExecutionHandlers = { WorkflowSharingService, ).getSharedWorkflowIdsForScopes(req.user, ['workflow:read']); - // user does not have workflows hence no executions - // or the execution they are trying to access belongs to a workflow they do not own if (!sharedWorkflowsIds.length) { throw new NotFoundError('Not Found'); } @@ -124,10 +87,15 @@ const executionHandlers: ExecutionHandlers = { ? 0 : Container.get(ExecutionsConfig).maxDisplaySize; - // look for the execution on the workflow the user owns - const execution = await Container.get( - ExecutionPersistence, - ).getExecutionInWorkflowsForPublicApi(id, sharedWorkflowsIds, includeData, maxDataSizeBytes); + const execution = await Container.get(ExecutionService).findOneInWorkflows( + id, + sharedWorkflowsIds, + { + includeData, + includeAnnotation: false, + maxDataSizeBytes, + }, + ); if (!execution) { throw new NotFoundError('Not Found'); @@ -173,45 +141,27 @@ const executionHandlers: ExecutionHandlers = { WorkflowSharingService, ).getSharedWorkflowIdsForScopes(req.user, ['workflow:read'], projectId); - // user does not have workflows hence no executions - // or the execution they are trying to access belongs to a workflow they do not own if (!sharedWorkflowsIds.length || (workflowId && !sharedWorkflowsIds.includes(workflowId))) { return res.status(200).json({ data: [], nextCursor: null }); } - // Collect genuinely running executions to exclude from the default listing. - // The active executions list also retains `waiting` executions (persisted and - // resumable); filter by status so waiting executions are still listed. - const runningExecutionsIds = Container.get(ActiveExecutions) - .getActiveExecutions() - .filter(({ status }) => status === 'running') - .map(({ id }) => id); - - const filters: Parameters< - typeof ExecutionPersistence.prototype.getExecutionsForPublicApi - >[0] = { - status, - limit, - lastId, - includeData, - workflowIds: workflowId ? [workflowId] : sharedWorkflowsIds, - - // for backward compatibility `running` executions are always excluded - // unless the user explicitly filters by `running` status - excludedExecutionsIds: status !== 'running' ? runningExecutionsIds : undefined, - }; - - const executions = await Container.get(ExecutionPersistence).getExecutionsForPublicApi( - filters, - ignoreDataSizeLimit ? 0 : Container.get(ExecutionsConfig).maxDisplaySize, + const { executions, count } = await Container.get(ExecutionService).findManyAndCount( + workflowId ? [workflowId] : sharedWorkflowsIds, + { + status, + limit, + lastId, + includeData, + // for backward compatibility `running` executions are always excluded + // unless the user explicitly filters by `running` status + excludeRunning: status !== 'running', + maxDataSizeBytes: ignoreDataSizeLimit + ? 0 + : Container.get(ExecutionsConfig).maxDisplaySize, + }, ); - const newLastId = !executions.length ? '0' : executions.slice(-1)[0].id; - - filters.lastId = newLastId; - - const count = - await Container.get(ExecutionRepository).getExecutionsCountForPublicApi(filters); + const newLastId = executions.length === 0 ? '0' : executions.at(-1)!.id; if (includeData) { const redactQuery = ExecutionRedactionQueryDtoSchema.safeParse(req.query); @@ -253,8 +203,6 @@ const executionHandlers: ExecutionHandlers = { WorkflowSharingService, ).getSharedWorkflowIdsForScopes(req.user, ['workflow:execute']); - // user does not have workflows hence no executions - // or the execution they are trying to access belongs to a workflow they do not own if (!sharedWorkflowsIds.length) { throw new NotFoundError('Not Found'); } @@ -279,7 +227,6 @@ const executionHandlers: ExecutionHandlers = { getExecutionTags: [ publicApiScope('executionTags:list'), async (req, res) => { - const { id } = req.params; const sharedWorkflowsIds = await Container.get( WorkflowSharingService, ).getSharedWorkflowIdsForScopes(req.user, ['workflow:read']); @@ -288,15 +235,10 @@ const executionHandlers: ExecutionHandlers = { throw new NotFoundError('Not Found'); } - const execution = await Container.get( - ExecutionPersistence, - ).getExecutionInWorkflowsForPublicApi(id, sharedWorkflowsIds, false); - - if (!execution) { - throw new NotFoundError('Not Found'); - } - - const tags = await getExecutionTags(id); + const tags = await Container.get(ExecutionService).getExecutionTags( + req.params.id, + sharedWorkflowsIds, + ); return res.json(tags); }, @@ -304,7 +246,6 @@ const executionHandlers: ExecutionHandlers = { updateExecutionTags: [ publicApiScope('executionTags:update'), async (req, res) => { - const { id } = req.params; const newTagIds = req.body.map((tag) => tag.id); const sharedWorkflowsIds = await Container.get( WorkflowSharingService, @@ -314,22 +255,14 @@ const executionHandlers: ExecutionHandlers = { throw new NotFoundError('Not Found'); } - const execution = await Container.get( - ExecutionPersistence, - ).getExecutionInWorkflowsForPublicApi(id, sharedWorkflowsIds, false); - - if (!execution) { - throw new NotFoundError('Not Found'); - } - try { - const updatedTags = await updateExecutionTags(id, newTagIds); - const tags = mapAnnotationTags(updatedTags); + const tags = await Container.get(ExecutionService).updateExecutionTags( + req.params.id, + newTagIds, + sharedWorkflowsIds, + ); return res.json(tags); } catch (error) { - if (error instanceof QueryFailedError) { - throw new NotFoundError('Some tags not found'); - } return handleError(error); } }, @@ -341,8 +274,6 @@ const executionHandlers: ExecutionHandlers = { WorkflowSharingService, ).getSharedWorkflowIdsForScopes(req.user, ['workflow:execute']); - // user does not have workflows hence no executions - // or the execution they are trying to access belongs to a workflow they do not own if (!sharedWorkflowsIds.length) { throw new NotFoundError('Not Found'); } @@ -363,7 +294,6 @@ const executionHandlers: ExecutionHandlers = { async (req, res) => { const { status: rawStatus, workflowId, startedAfter, startedBefore } = req.body; const status: ExecutionStatus[] = rawStatus.map((x) => (x === 'queued' ? 'new' : x)); - // Validate that status is provided and not empty if (!status || status.length === 0) { return res.status(400).json({ message: @@ -378,12 +308,10 @@ const executionHandlers: ExecutionHandlers = { WorkflowSharingService, ).getSharedWorkflowIdsForScopes(req.user, ['workflow:execute']); - // Return early to avoid expensive db query if (!sharedWorkflowsIds.length) { return res.json({ stopped: 0 }); } - // If workflowId is provided, validate user has access to it if (workflowId && workflowId !== 'all' && !sharedWorkflowsIds.includes(workflowId)) { throw new NotFoundError('Workflow not found or not accessible'); } diff --git a/packages/cli/src/public-api/v1/handlers/executions/executions.service.ts b/packages/cli/src/public-api/v1/handlers/executions/executions.service.ts deleted file mode 100644 index 04c8bb5d541..00000000000 --- a/packages/cli/src/public-api/v1/handlers/executions/executions.service.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - AnnotationTagMapping, - ExecutionAnnotation, - ExecutionAnnotationRepository, - type AnnotationTagEntity, -} from '@n8n/db'; -import { Container } from '@n8n/di'; - -export function mapAnnotationTags(tags: AnnotationTagEntity[]) { - return tags.map(({ id: tagId, name, createdAt, updatedAt }) => ({ - id: tagId, - name, - createdAt, - updatedAt, - })); -} - -export async function getExecutionTags(executionId: string) { - const annotation = await Container.get(ExecutionAnnotationRepository).findOne({ - where: { execution: { id: executionId } }, - relations: ['tags'], - }); - - return mapAnnotationTags(annotation?.tags ?? []); -} - -export async function updateExecutionTags( - executionId: string, - newTagIds: string[], -): Promise { - const { manager: dbManager } = Container.get(ExecutionAnnotationRepository); - return await dbManager.transaction(async (transactionManager) => { - // Upsert annotation (create if it doesn't exist) - await transactionManager.upsert(ExecutionAnnotation, { execution: { id: executionId } }, [ - 'execution', - ]); - - const annotation = await transactionManager.findOneOrFail(ExecutionAnnotation, { - where: { execution: { id: executionId } }, - }); - - // Overwrite tags - await transactionManager.delete(AnnotationTagMapping, { - annotationId: annotation.id, - }); - - if (newTagIds.length > 0) { - const tagMappings = newTagIds.map((tagId) => ({ - annotationId: annotation.id, - tagId, - })); - await transactionManager.insert(AnnotationTagMapping, tagMappings); - } - - // Fetch updated tags to return - const updatedAnnotation = await transactionManager.findOneOrFail(ExecutionAnnotation, { - where: { execution: { id: executionId } }, - relations: ['tags'], - }); - - return updatedAnnotation.tags ?? []; - }); -} diff --git a/packages/cli/test/integration/public-api/executions.test.ts b/packages/cli/test/integration/public-api/executions.test.ts index a4173f765a7..6d0b0bfcbe1 100644 --- a/packages/cli/test/integration/public-api/executions.test.ts +++ b/packages/cli/test/integration/public-api/executions.test.ts @@ -274,6 +274,8 @@ describe('DELETE /executions/:id', () => { } = response.body; expect(id).toBeDefined(); + expect(typeof id).toBe('number'); + expect(id).toBe(Number(execution.id)); expect(finished).toBe(true); expect(mode).toEqual(execution.mode); expect(retrySuccessId).toBeNull(); @@ -298,7 +300,7 @@ describe('POST /executions/:id/retry', () => { test('should retry an execution', async () => { const mockedExecutionResponse = { status: 'waiting' } as any; const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).retry) + .spyOn(Container.get(ExecutionService), 'retry') .mockResolvedValue(mockedExecutionResponse); const workflow = await createWorkflow({}, user1); @@ -323,7 +325,7 @@ describe('POST /executions/:id/retry', () => { test('should return 409 when trying to retry a queued execution', async () => { const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).retry) + .spyOn(Container.get(ExecutionService), 'retry') .mockRejectedValue(new QueuedExecutionRetryError()); const workflow = await createWorkflow({}, user1); @@ -341,7 +343,7 @@ describe('POST /executions/:id/retry', () => { test('should return 409 when trying to retry an aborted execution without execution data', async () => { const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).retry) + .spyOn(Container.get(ExecutionService), 'retry') .mockRejectedValue(new AbortedExecutionRetryError()); const workflow = await createWorkflow({}, user1); @@ -367,7 +369,7 @@ describe('POST /executions/:id/retry', () => { test('should return 404 when user only has read access to the workflow via project viewer role', async () => { testServer.license.enable('feat:sharing'); - const executionServiceSpy = vi.mocked(Container.get(ExecutionService).retry); + const executionServiceSpy = vi.spyOn(Container.get(ExecutionService), 'retry'); const project = await createTeamProject('project with viewer', owner); await linkUserToProject(user1, project, 'project:viewer'); @@ -389,7 +391,7 @@ describe('POST /executions/:id/retry', () => { const mockedExecutionResponse = { status: 'waiting' } as any; const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).retry) + .spyOn(Container.get(ExecutionService), 'retry') .mockResolvedValue(mockedExecutionResponse); const project = await createTeamProject('project with editor', owner); @@ -408,7 +410,7 @@ describe('POST /executions/:id/retry', () => { test('should return 409 when trying to retry a finished execution', async () => { const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).retry) + .spyOn(Container.get(ExecutionService), 'retry') .mockRejectedValue(new ConflictError('The execution succeeded, so it cannot be retried.')); const workflow = await createWorkflow({}, user1); @@ -916,11 +918,13 @@ describe('POST /executions/:id/stop', () => { finished: false, status: 'canceled', } as any; - const executionServiceSpy = vi.mocked(Container.get(ExecutionService).stop).mockResolvedValue({ - ...mockedStopResponse, - startedAt: new Date(mockedStopResponse.startedAt), - stoppedAt: new Date(mockedStopResponse.stoppedAt), - }); + const executionServiceSpy = vi + .spyOn(Container.get(ExecutionService), 'stop') + .mockResolvedValue({ + ...mockedStopResponse, + startedAt: new Date(mockedStopResponse.startedAt), + stoppedAt: new Date(mockedStopResponse.stoppedAt), + }); const workflow = await createWorkflow({}, user1); const execution = await createExecution({ status: 'running', finished: false }, workflow); @@ -967,11 +971,13 @@ describe('POST /executions/:id/stop', () => { finished: false, status: 'canceled', } as any; - const executionServiceSpy = vi.mocked(Container.get(ExecutionService).stop).mockResolvedValue({ - ...mockedStopResponse, - startedAt: new Date(mockedStopResponse.startedAt), - stoppedAt: new Date(mockedStopResponse.stoppedAt), - }); + const executionServiceSpy = vi + .spyOn(Container.get(ExecutionService), 'stop') + .mockResolvedValue({ + ...mockedStopResponse, + startedAt: new Date(mockedStopResponse.startedAt), + stoppedAt: new Date(mockedStopResponse.stoppedAt), + }); const workflow = await createWorkflow({}, user1); const execution = await createExecution({ status: 'running', finished: false }, workflow); @@ -1010,7 +1016,7 @@ describe('POST /executions/stop', () => { test('should stop multiple running executions', async () => { const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).stopMany) + .spyOn(Container.get(ExecutionService), 'stopMany') .mockResolvedValue(3); await createWorkflow({}, user1); @@ -1036,7 +1042,7 @@ describe('POST /executions/stop', () => { test('should stop executions filtered by workflowId', async () => { const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).stopMany) + .spyOn(Container.get(ExecutionService), 'stopMany') .mockResolvedValue(2); const workflow = await createWorkflow({}, user1); @@ -1062,7 +1068,7 @@ describe('POST /executions/stop', () => { test('should stop executions with date filters', async () => { const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).stopMany) + .spyOn(Container.get(ExecutionService), 'stopMany') .mockResolvedValue(1); await createWorkflow({}, user1); @@ -1095,7 +1101,7 @@ describe('POST /executions/stop', () => { const workflow = await createWorkflow({}, user1); const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).stopMany) + .spyOn(Container.get(ExecutionService), 'stopMany') .mockResolvedValue(1); // User1 should be able to stop executions in their own workflow @@ -1111,7 +1117,7 @@ describe('POST /executions/stop', () => { }); test('should return 0 stopped when user has no workflows', async () => { - const executionServiceSpy = vi.mocked(Container.get(ExecutionService).stopMany); + const executionServiceSpy = vi.spyOn(Container.get(ExecutionService), 'stopMany'); // Create a new user with no workflows const userWithNoWorkflows = await createMemberWithApiKey(); @@ -1134,7 +1140,7 @@ describe('POST /executions/stop', () => { await createManyWorkflows(2, {}, owner); const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).stopMany) + .spyOn(Container.get(ExecutionService), 'stopMany') .mockResolvedValue(5); const response = await authOwnerAgent @@ -1151,7 +1157,7 @@ describe('POST /executions/stop', () => { testServer.license.enable('feat:sharing'); const executionServiceSpy = vi - .mocked(Container.get(ExecutionService).stopMany) + .spyOn(Container.get(ExecutionService), 'stopMany') .mockResolvedValue(2); const [workflow1, workflow2] = await createManyWorkflows(2, {}, user1); diff --git a/packages/cli/test/integration/shared/utils/index.ts b/packages/cli/test/integration/shared/utils/index.ts index 3442f348b77..31350c5d317 100644 --- a/packages/cli/test/integration/shared/utils/index.ts +++ b/packages/cli/test/integration/shared/utils/index.ts @@ -27,7 +27,6 @@ import { v4 as uuid } from 'uuid'; import { mock } from 'vitest-mock-extended'; import { AUTH_COOKIE_NAME } from '@/constants'; -import { ExecutionService } from '@/executions/execution.service'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { Push } from '@/push'; @@ -48,7 +47,6 @@ export async function initActiveWorkflowManager() { }); mockInstance(Push); - mockInstance(ExecutionService); const { ActiveWorkflowManager } = await import('@/active-workflow-manager.js'); const activeWorkflowManager = Container.get(ActiveWorkflowManager); await activeWorkflowManager.init();