mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(API): Add running status query on the executions public api endpoint (#19205)
Co-authored-by: Konstantin Tieber <46342664+konstantintieber@users.noreply.github.com>
This commit is contained in:
@@ -1,31 +1,14 @@
|
|||||||
import { Container, type Constructable } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import { DataSource, EntityManager, In, LessThan, type EntityMetadata } from '@n8n/typeorm';
|
import { In, LessThan, And, Not } from '@n8n/typeorm';
|
||||||
import { mock } from 'jest-mock-extended';
|
|
||||||
import type { Class } from 'n8n-core';
|
|
||||||
import type { DeepPartial } from 'ts-essentials';
|
|
||||||
|
|
||||||
import { ExecutionEntity } from '../../entities';
|
import { ExecutionEntity } from '../../entities';
|
||||||
|
import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager';
|
||||||
import { ExecutionRepository } from '../execution.repository';
|
import { ExecutionRepository } from '../execution.repository';
|
||||||
|
|
||||||
const mockInstance = <T>(
|
/**
|
||||||
serviceClass: Constructable<T>,
|
* TODO: add tests for all the other methods
|
||||||
data: DeepPartial<T> | undefined = undefined,
|
* TODO: getExecutionsForPublicApi -> add test cases for the `includeData` toggle
|
||||||
) => {
|
*/
|
||||||
const instance = mock<T>(data);
|
|
||||||
Container.set(serviceClass, instance);
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockEntityManager = (entityClass: Class) => {
|
|
||||||
const entityManager = mockInstance(EntityManager);
|
|
||||||
const dataSource = mockInstance(DataSource, {
|
|
||||||
manager: entityManager,
|
|
||||||
getMetadata: () => mock<EntityMetadata>({ target: entityClass }),
|
|
||||||
});
|
|
||||||
Object.assign(entityManager, { connection: dataSource });
|
|
||||||
return entityManager;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('ExecutionRepository', () => {
|
describe('ExecutionRepository', () => {
|
||||||
const entityManager = mockEntityManager(ExecutionEntity);
|
const entityManager = mockEntityManager(ExecutionEntity);
|
||||||
const executionRepository = Container.get(ExecutionRepository);
|
const executionRepository = Container.get(ExecutionRepository);
|
||||||
@@ -35,10 +18,29 @@ describe('ExecutionRepository', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getExecutionsForPublicApi', () => {
|
describe('getExecutionsForPublicApi', () => {
|
||||||
test('should get executions matching the filter parameters', async () => {
|
const defaultLimit = 10;
|
||||||
const limit = 10;
|
const defaultQuery = {
|
||||||
|
select: [
|
||||||
|
'id',
|
||||||
|
'mode',
|
||||||
|
'retryOf',
|
||||||
|
'retrySuccessId',
|
||||||
|
'startedAt',
|
||||||
|
'stoppedAt',
|
||||||
|
'workflowId',
|
||||||
|
'waitTill',
|
||||||
|
'finished',
|
||||||
|
'status',
|
||||||
|
],
|
||||||
|
where: {},
|
||||||
|
order: { id: 'DESC' },
|
||||||
|
take: defaultLimit,
|
||||||
|
relations: ['executionData'],
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should get executions matching all filter parameters', async () => {
|
||||||
const params = {
|
const params = {
|
||||||
limit: 10,
|
limit: defaultLimit,
|
||||||
lastId: '3',
|
lastId: '3',
|
||||||
workflowIds: ['3', '4'],
|
workflowIds: ['3', '4'],
|
||||||
};
|
};
|
||||||
@@ -48,64 +50,89 @@ describe('ExecutionRepository', () => {
|
|||||||
const result = await executionRepository.getExecutionsForPublicApi(params);
|
const result = await executionRepository.getExecutionsForPublicApi(params);
|
||||||
|
|
||||||
expect(entityManager.find).toHaveBeenCalledWith(ExecutionEntity, {
|
expect(entityManager.find).toHaveBeenCalledWith(ExecutionEntity, {
|
||||||
select: [
|
...defaultQuery,
|
||||||
'id',
|
|
||||||
'mode',
|
|
||||||
'retryOf',
|
|
||||||
'retrySuccessId',
|
|
||||||
'startedAt',
|
|
||||||
'stoppedAt',
|
|
||||||
'workflowId',
|
|
||||||
'waitTill',
|
|
||||||
'finished',
|
|
||||||
'status',
|
|
||||||
],
|
|
||||||
where: {
|
where: {
|
||||||
id: LessThan(params.lastId),
|
id: LessThan(params.lastId),
|
||||||
workflowId: In(params.workflowIds),
|
workflowId: In(params.workflowIds),
|
||||||
},
|
},
|
||||||
order: { id: 'DESC' },
|
|
||||||
take: limit,
|
|
||||||
relations: ['executionData'],
|
|
||||||
});
|
});
|
||||||
expect(result.length).toBe(mockEntities.length);
|
expect(result.length).toBe(mockEntities.length);
|
||||||
expect(result[0].id).toEqual(mockEntities[0].id);
|
expect(result[0].id).toEqual(mockEntities[0].id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should get executions matching the workflowIds filter', async () => {
|
||||||
|
const params = {
|
||||||
|
limit: 10,
|
||||||
|
workflowIds: ['3', '4'],
|
||||||
|
};
|
||||||
|
const mockEntities = [{ id: '1' }, { id: '2' }];
|
||||||
|
|
||||||
|
entityManager.find.mockResolvedValueOnce(mockEntities);
|
||||||
|
const result = await executionRepository.getExecutionsForPublicApi(params);
|
||||||
|
|
||||||
|
expect(entityManager.find).toHaveBeenCalledWith(ExecutionEntity, {
|
||||||
|
...defaultQuery,
|
||||||
|
where: {
|
||||||
|
workflowId: In(params.workflowIds),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.length).toBe(mockEntities.length);
|
||||||
|
expect(result[0].id).toEqual(mockEntities[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with id filters', () => {
|
||||||
|
test.each`
|
||||||
|
lastId | excludedExecutionsIds | expectedIdCondition
|
||||||
|
${'5'} | ${['2', '3']} | ${And(LessThan('5'), Not(In(['2', '3'])))}
|
||||||
|
${'5'} | ${[]} | ${LessThan('5')}
|
||||||
|
${'5'} | ${undefined} | ${LessThan('5')}
|
||||||
|
${undefined} | ${['2', '3']} | ${Not(In(['2', '3']))}
|
||||||
|
${undefined} | ${[]} | ${undefined}
|
||||||
|
${undefined} | ${undefined} | ${undefined}
|
||||||
|
`(
|
||||||
|
'should find with id less than "$lastId" and not in "$excludedExecutionsIds"',
|
||||||
|
async ({ lastId, excludedExecutionsIds, expectedIdCondition }) => {
|
||||||
|
const params = {
|
||||||
|
limit: defaultLimit,
|
||||||
|
...(lastId ? { lastId } : {}),
|
||||||
|
...(excludedExecutionsIds ? { excludedExecutionsIds } : {}),
|
||||||
|
};
|
||||||
|
const mockEntities = [{ id: '1' }, { id: '2' }];
|
||||||
|
entityManager.find.mockResolvedValueOnce(mockEntities);
|
||||||
|
const result = await executionRepository.getExecutionsForPublicApi(params);
|
||||||
|
|
||||||
|
expect(entityManager.find).toHaveBeenCalledWith(ExecutionEntity, {
|
||||||
|
...defaultQuery,
|
||||||
|
where: {
|
||||||
|
...(expectedIdCondition ? { id: expectedIdCondition } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.length).toBe(mockEntities.length);
|
||||||
|
expect(result[0].id).toEqual(mockEntities[0].id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('with status filter', () => {
|
describe('with status filter', () => {
|
||||||
test.each`
|
test.each`
|
||||||
filterStatus | entityStatus
|
filterStatus | entityStatus
|
||||||
${'canceled'} | ${'canceled'}
|
${'canceled'} | ${'canceled'}
|
||||||
${'error'} | ${In(['error', 'crashed'])}
|
${'error'} | ${In(['error', 'crashed'])}
|
||||||
|
${'running'} | ${'running'}
|
||||||
${'success'} | ${'success'}
|
${'success'} | ${'success'}
|
||||||
${'waiting'} | ${'waiting'}
|
${'waiting'} | ${'waiting'}
|
||||||
`('should find all "$filterStatus" executions', async ({ filterStatus, entityStatus }) => {
|
`('should find all "$filterStatus" executions', async ({ filterStatus, entityStatus }) => {
|
||||||
const limit = 10;
|
|
||||||
const mockEntities = [{ id: '1' }, { id: '2' }];
|
const mockEntities = [{ id: '1' }, { id: '2' }];
|
||||||
|
|
||||||
entityManager.find.mockResolvedValueOnce(mockEntities);
|
entityManager.find.mockResolvedValueOnce(mockEntities);
|
||||||
const result = await executionRepository.getExecutionsForPublicApi({
|
const result = await executionRepository.getExecutionsForPublicApi({
|
||||||
limit,
|
limit: defaultLimit,
|
||||||
status: filterStatus,
|
status: filterStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(entityManager.find).toHaveBeenCalledWith(ExecutionEntity, {
|
expect(entityManager.find).toHaveBeenCalledWith(ExecutionEntity, {
|
||||||
select: [
|
...defaultQuery,
|
||||||
'id',
|
|
||||||
'mode',
|
|
||||||
'retryOf',
|
|
||||||
'retrySuccessId',
|
|
||||||
'startedAt',
|
|
||||||
'stoppedAt',
|
|
||||||
'workflowId',
|
|
||||||
'waitTill',
|
|
||||||
'finished',
|
|
||||||
'status',
|
|
||||||
],
|
|
||||||
where: { status: entityStatus },
|
where: { status: entityStatus },
|
||||||
order: { id: 'DESC' },
|
|
||||||
take: limit,
|
|
||||||
relations: ['executionData'],
|
|
||||||
});
|
});
|
||||||
expect(result.length).toBe(mockEntities.length);
|
expect(result.length).toBe(mockEntities.length);
|
||||||
expect(result[0].id).toEqual(mockEntities[0].id);
|
expect(result[0].id).toEqual(mockEntities[0].id);
|
||||||
@@ -115,37 +142,21 @@ describe('ExecutionRepository', () => {
|
|||||||
filterStatus
|
filterStatus
|
||||||
${'crashed'}
|
${'crashed'}
|
||||||
${'new'}
|
${'new'}
|
||||||
${'running'}
|
|
||||||
${'unknown'}
|
${'unknown'}
|
||||||
`(
|
`(
|
||||||
'should find all executions and ignore status filter "$filterStatus"',
|
'should find all executions and ignore status filter "$filterStatus"',
|
||||||
async ({ filterStatus }) => {
|
async ({ filterStatus }) => {
|
||||||
const limit = 10;
|
|
||||||
const mockEntities = [{ id: '1' }, { id: '2' }];
|
const mockEntities = [{ id: '1' }, { id: '2' }];
|
||||||
|
|
||||||
entityManager.find.mockResolvedValueOnce(mockEntities);
|
entityManager.find.mockResolvedValueOnce(mockEntities);
|
||||||
const result = await executionRepository.getExecutionsForPublicApi({
|
const result = await executionRepository.getExecutionsForPublicApi({
|
||||||
limit,
|
limit: defaultLimit,
|
||||||
status: filterStatus,
|
status: filterStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(entityManager.find).toHaveBeenCalledWith(ExecutionEntity, {
|
expect(entityManager.find).toHaveBeenCalledWith(ExecutionEntity, {
|
||||||
select: [
|
...defaultQuery,
|
||||||
'id',
|
|
||||||
'mode',
|
|
||||||
'retryOf',
|
|
||||||
'retrySuccessId',
|
|
||||||
'startedAt',
|
|
||||||
'stoppedAt',
|
|
||||||
'workflowId',
|
|
||||||
'waitTill',
|
|
||||||
'finished',
|
|
||||||
'status',
|
|
||||||
],
|
|
||||||
where: {},
|
where: {},
|
||||||
order: { id: 'DESC' },
|
|
||||||
take: limit,
|
|
||||||
relations: ['executionData'],
|
|
||||||
});
|
});
|
||||||
expect(result.length).toBe(mockEntities.length);
|
expect(result.length).toBe(mockEntities.length);
|
||||||
expect(result[0].id).toEqual(mockEntities[0].id);
|
expect(result[0].id).toEqual(mockEntities[0].id);
|
||||||
@@ -155,8 +166,7 @@ describe('ExecutionRepository', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getExecutionsCountForPublicApi', () => {
|
describe('getExecutionsCountForPublicApi', () => {
|
||||||
test('should get executions matching the filter parameters', async () => {
|
test('should get executions matching all filter parameters', async () => {
|
||||||
const limit = 10;
|
|
||||||
const mockCount = 20;
|
const mockCount = 20;
|
||||||
const params = {
|
const params = {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
@@ -172,16 +182,68 @@ describe('ExecutionRepository', () => {
|
|||||||
id: LessThan(params.lastId),
|
id: LessThan(params.lastId),
|
||||||
workflowId: In(params.workflowIds),
|
workflowId: In(params.workflowIds),
|
||||||
},
|
},
|
||||||
take: limit,
|
take: params.limit,
|
||||||
});
|
});
|
||||||
expect(result).toBe(mockCount);
|
expect(result).toBe(mockCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should get executions matching the workflowIds filter', async () => {
|
||||||
|
const mockCount = 12;
|
||||||
|
const params = {
|
||||||
|
limit: 10,
|
||||||
|
workflowIds: ['7', '8'],
|
||||||
|
};
|
||||||
|
|
||||||
|
entityManager.count.mockResolvedValueOnce(mockCount);
|
||||||
|
const result = await executionRepository.getExecutionsCountForPublicApi(params);
|
||||||
|
|
||||||
|
expect(entityManager.count).toHaveBeenCalledWith(ExecutionEntity, {
|
||||||
|
where: {
|
||||||
|
workflowId: In(params.workflowIds),
|
||||||
|
},
|
||||||
|
take: params.limit,
|
||||||
|
});
|
||||||
|
expect(result).toBe(mockCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with id filters', () => {
|
||||||
|
test.each`
|
||||||
|
lastId | excludedExecutionsIds | expectedIdCondition
|
||||||
|
${'5'} | ${['2', '3']} | ${And(LessThan('5'), Not(In(['2', '3'])))}
|
||||||
|
${'5'} | ${[]} | ${LessThan('5')}
|
||||||
|
${'5'} | ${undefined} | ${LessThan('5')}
|
||||||
|
${undefined} | ${['2', '3']} | ${Not(In(['2', '3']))}
|
||||||
|
${undefined} | ${[]} | ${undefined}
|
||||||
|
${undefined} | ${undefined} | ${undefined}
|
||||||
|
`(
|
||||||
|
'should find with id less than "$lastId" and not in "$excludedExecutionsIds"',
|
||||||
|
async ({ lastId, excludedExecutionsIds, expectedIdCondition }) => {
|
||||||
|
const mockCount = 15;
|
||||||
|
const params = {
|
||||||
|
limit: 10,
|
||||||
|
...(lastId ? { lastId } : {}),
|
||||||
|
...(excludedExecutionsIds ? { excludedExecutionsIds } : {}),
|
||||||
|
};
|
||||||
|
entityManager.count.mockResolvedValueOnce(mockCount);
|
||||||
|
const result = await executionRepository.getExecutionsCountForPublicApi(params);
|
||||||
|
|
||||||
|
expect(entityManager.count).toHaveBeenCalledWith(ExecutionEntity, {
|
||||||
|
where: {
|
||||||
|
...(expectedIdCondition ? { id: expectedIdCondition } : {}),
|
||||||
|
},
|
||||||
|
take: params.limit,
|
||||||
|
});
|
||||||
|
expect(result).toBe(mockCount);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('with status filter', () => {
|
describe('with status filter', () => {
|
||||||
test.each`
|
test.each`
|
||||||
filterStatus | entityStatus
|
filterStatus | entityStatus
|
||||||
${'canceled'} | ${'canceled'}
|
${'canceled'} | ${'canceled'}
|
||||||
${'error'} | ${In(['error', 'crashed'])}
|
${'error'} | ${In(['error', 'crashed'])}
|
||||||
|
${'running'} | ${'running'}
|
||||||
${'success'} | ${'success'}
|
${'success'} | ${'success'}
|
||||||
${'waiting'} | ${'waiting'}
|
${'waiting'} | ${'waiting'}
|
||||||
`('should retrieve all $filterStatus executions', async ({ filterStatus, entityStatus }) => {
|
`('should retrieve all $filterStatus executions', async ({ filterStatus, entityStatus }) => {
|
||||||
@@ -206,7 +268,6 @@ describe('ExecutionRepository', () => {
|
|||||||
filterStatus
|
filterStatus
|
||||||
${'crashed'}
|
${'crashed'}
|
||||||
${'new'}
|
${'new'}
|
||||||
${'running'}
|
|
||||||
${'unknown'}
|
${'unknown'}
|
||||||
`(
|
`(
|
||||||
'should find all executions and ignore status filter "$filterStatus"',
|
'should find all executions and ignore status filter "$filterStatus"',
|
||||||
|
|||||||
@@ -17,38 +17,38 @@ import {
|
|||||||
LessThanOrEqual,
|
LessThanOrEqual,
|
||||||
MoreThanOrEqual,
|
MoreThanOrEqual,
|
||||||
Not,
|
Not,
|
||||||
Raw,
|
|
||||||
Repository,
|
Repository,
|
||||||
|
And,
|
||||||
} from '@n8n/typeorm';
|
} from '@n8n/typeorm';
|
||||||
import { DateUtils } from '@n8n/typeorm/util/DateUtils';
|
import { DateUtils } from '@n8n/typeorm/util/DateUtils';
|
||||||
import { parse, stringify } from 'flatted';
|
import { parse, stringify } from 'flatted';
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import { BinaryDataService, ErrorReporter } from 'n8n-core';
|
import { BinaryDataService, ErrorReporter } from 'n8n-core';
|
||||||
import { ExecutionCancelledError, UnexpectedError } from 'n8n-workflow';
|
|
||||||
import type {
|
import type {
|
||||||
AnnotationVote,
|
AnnotationVote,
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
ExecutionSummary,
|
ExecutionSummary,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { ExecutionCancelledError, UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { ExecutionDataRepository } from './execution-data.repository';
|
import { ExecutionDataRepository } from './execution-data.repository';
|
||||||
import {
|
import {
|
||||||
|
AnnotationTagEntity,
|
||||||
|
AnnotationTagMapping,
|
||||||
|
ExecutionAnnotation,
|
||||||
|
ExecutionData,
|
||||||
ExecutionEntity,
|
ExecutionEntity,
|
||||||
ExecutionMetadata,
|
ExecutionMetadata,
|
||||||
ExecutionData,
|
|
||||||
ExecutionAnnotation,
|
|
||||||
AnnotationTagMapping,
|
|
||||||
WorkflowEntity,
|
|
||||||
SharedWorkflow,
|
SharedWorkflow,
|
||||||
AnnotationTagEntity,
|
WorkflowEntity,
|
||||||
} from '../entities';
|
} from '../entities';
|
||||||
import type {
|
import type {
|
||||||
CreateExecutionPayload,
|
CreateExecutionPayload,
|
||||||
IExecutionFlattedDb,
|
|
||||||
IExecutionBase,
|
|
||||||
IExecutionResponse,
|
|
||||||
ExecutionSummaries,
|
ExecutionSummaries,
|
||||||
|
IExecutionBase,
|
||||||
|
IExecutionFlattedDb,
|
||||||
|
IExecutionResponse,
|
||||||
} from '../entities/types-db';
|
} from '../entities/types-db';
|
||||||
import { separate } from '../utils/separate';
|
import { separate } from '../utils/separate';
|
||||||
|
|
||||||
@@ -631,27 +631,22 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getExecutionsCountForPublicApi(data: {
|
async getExecutionsCountForPublicApi(params: {
|
||||||
limit: number;
|
limit: number;
|
||||||
lastId?: string;
|
lastId?: string;
|
||||||
workflowIds?: string[];
|
workflowIds?: string[];
|
||||||
status?: ExecutionStatus;
|
status?: ExecutionStatus;
|
||||||
excludedWorkflowIds?: string[];
|
excludedExecutionsIds?: string[];
|
||||||
}): Promise<number> {
|
}): Promise<number> {
|
||||||
const executionsCount = await this.count({
|
const executionsCount = await this.count({
|
||||||
where: {
|
where: this.getFindExecutionsForPublicApiCondition(params),
|
||||||
...(data.lastId && { id: LessThan(data.lastId) }),
|
take: params.limit,
|
||||||
...(data.status && { ...this.getStatusCondition(data.status) }),
|
|
||||||
...(data.workflowIds && { workflowId: In(data.workflowIds) }),
|
|
||||||
...(data.excludedWorkflowIds && { workflowId: Not(In(data.excludedWorkflowIds)) }),
|
|
||||||
},
|
|
||||||
take: data.limit,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return executionsCount;
|
return executionsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getStatusCondition(status: ExecutionStatus) {
|
private getStatusCondition(status?: ExecutionStatus) {
|
||||||
const condition: Pick<FindOptionsWhere<IExecutionFlattedDb>, 'status'> = {};
|
const condition: Pick<FindOptionsWhere<IExecutionFlattedDb>, 'status'> = {};
|
||||||
|
|
||||||
if (status === 'success') {
|
if (status === 'success') {
|
||||||
@@ -662,11 +657,45 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
|||||||
condition.status = In(['error', 'crashed']);
|
condition.status = In(['error', 'crashed']);
|
||||||
} else if (status === 'canceled') {
|
} else if (status === 'canceled') {
|
||||||
condition.status = 'canceled';
|
condition.status = 'canceled';
|
||||||
|
} else if (status === 'running') {
|
||||||
|
condition.status = 'running';
|
||||||
}
|
}
|
||||||
|
|
||||||
return condition;
|
return condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getIdCondition(params: { lastId?: string; excludedExecutionsIds?: string[] }) {
|
||||||
|
const condition: Pick<FindOptionsWhere<IExecutionFlattedDb>, 'id'> = {};
|
||||||
|
|
||||||
|
if (params.lastId && params.excludedExecutionsIds?.length) {
|
||||||
|
condition.id = And(LessThan(params.lastId), Not(In(params.excludedExecutionsIds)));
|
||||||
|
} else if (params.lastId) {
|
||||||
|
condition.id = LessThan(params.lastId);
|
||||||
|
} else if (params.excludedExecutionsIds?.length) {
|
||||||
|
condition.id = Not(In(params.excludedExecutionsIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
return condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFindExecutionsForPublicApiCondition(params: {
|
||||||
|
lastId?: string;
|
||||||
|
workflowIds?: string[];
|
||||||
|
status?: ExecutionStatus;
|
||||||
|
excludedExecutionsIds?: string[];
|
||||||
|
}) {
|
||||||
|
const where: FindOptionsWhere<IExecutionFlattedDb> = {
|
||||||
|
...this.getIdCondition({
|
||||||
|
lastId: params.lastId,
|
||||||
|
excludedExecutionsIds: params.excludedExecutionsIds,
|
||||||
|
}),
|
||||||
|
...this.getStatusCondition(params.status),
|
||||||
|
...(params.workflowIds && { workflowId: In(params.workflowIds) }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
async getExecutionsForPublicApi(params: {
|
async getExecutionsForPublicApi(params: {
|
||||||
limit: number;
|
limit: number;
|
||||||
includeData?: boolean;
|
includeData?: boolean;
|
||||||
@@ -675,26 +704,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
|||||||
status?: ExecutionStatus;
|
status?: ExecutionStatus;
|
||||||
excludedExecutionsIds?: string[];
|
excludedExecutionsIds?: string[];
|
||||||
}): Promise<IExecutionBase[]> {
|
}): Promise<IExecutionBase[]> {
|
||||||
let where: FindOptionsWhere<IExecutionFlattedDb> = {};
|
const where = this.getFindExecutionsForPublicApiCondition(params);
|
||||||
|
|
||||||
if (params.lastId && params.excludedExecutionsIds?.length) {
|
|
||||||
where.id = Raw((id) => `${id} < :lastId AND ${id} NOT IN (:...excludedExecutionsIds)`, {
|
|
||||||
lastId: params.lastId,
|
|
||||||
excludedExecutionsIds: params.excludedExecutionsIds,
|
|
||||||
});
|
|
||||||
} else if (params.lastId) {
|
|
||||||
where.id = LessThan(params.lastId);
|
|
||||||
} else if (params.excludedExecutionsIds?.length) {
|
|
||||||
where.id = Not(In(params.excludedExecutionsIds));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.status) {
|
|
||||||
where = { ...where, ...this.getStatusCondition(params.status) };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.workflowIds) {
|
|
||||||
where = { ...where, workflowId: In(params.workflowIds) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.findMultipleExecutions(
|
return await this.findMultipleExecutions(
|
||||||
{
|
{
|
||||||
|
|||||||
15
packages/@n8n/db/src/utils/test-utils/mock-entity-manager.ts
Normal file
15
packages/@n8n/db/src/utils/test-utils/mock-entity-manager.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { DataSource, EntityManager, type EntityMetadata } from '@n8n/typeorm';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type { Class } from 'n8n-core';
|
||||||
|
|
||||||
|
import { mockInstance } from './mock-instance';
|
||||||
|
|
||||||
|
export const mockEntityManager = (entityClass: Class) => {
|
||||||
|
const entityManager = mockInstance(EntityManager);
|
||||||
|
const dataSource = mockInstance(DataSource, {
|
||||||
|
manager: entityManager,
|
||||||
|
getMetadata: () => mock<EntityMetadata>({ target: entityClass }),
|
||||||
|
});
|
||||||
|
Object.assign(entityManager, { connection: dataSource });
|
||||||
|
return entityManager;
|
||||||
|
};
|
||||||
12
packages/@n8n/db/src/utils/test-utils/mock-instance.ts
Normal file
12
packages/@n8n/db/src/utils/test-utils/mock-instance.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Container, type Constructable } from '@n8n/di';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type { DeepPartial } from 'ts-essentials';
|
||||||
|
|
||||||
|
export const mockInstance = <T>(
|
||||||
|
serviceClass: Constructable<T>,
|
||||||
|
data: DeepPartial<T> | undefined = undefined,
|
||||||
|
) => {
|
||||||
|
const instance = mock<T>(data);
|
||||||
|
Container.set(serviceClass, instance);
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
@@ -114,19 +114,23 @@ export = {
|
|||||||
return res.status(200).json({ data: [], nextCursor: null });
|
return res.status(200).json({ data: [], nextCursor: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
// get running workflows so we exclude them from the result
|
// get running executions so we exclude them from the result
|
||||||
const runningExecutionsIds = Container.get(ActiveExecutions)
|
const runningExecutionsIds = Container.get(ActiveExecutions)
|
||||||
.getActiveExecutions()
|
.getActiveExecutions()
|
||||||
.map(({ id }) => id);
|
.map(({ id }) => id);
|
||||||
|
|
||||||
const filters = {
|
const filters: Parameters<typeof ExecutionRepository.prototype.getExecutionsForPublicApi>[0] =
|
||||||
status,
|
{
|
||||||
limit,
|
status,
|
||||||
lastId,
|
limit,
|
||||||
includeData,
|
lastId,
|
||||||
workflowIds: workflowId ? [workflowId] : sharedWorkflowsIds,
|
includeData,
|
||||||
excludedExecutionsIds: runningExecutionsIds,
|
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 =
|
const executions =
|
||||||
await Container.get(ExecutionRepository).getExecutionsForPublicApi(filters);
|
await Container.get(ExecutionRepository).getExecutionsForPublicApi(filters);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ get:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: ['canceled', 'error', 'success', 'waiting']
|
enum: ['canceled', 'error', 'running', 'success', 'waiting']
|
||||||
- name: workflowId
|
- name: workflowId
|
||||||
in: query
|
in: query
|
||||||
description: Workflow to filter the executions by.
|
description: Workflow to filter the executions by.
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ properties:
|
|||||||
stoppedAt:
|
stoppedAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
description: The time at which the execution stopped. Will only be null for executions that still have the status 'running'.
|
||||||
workflowId:
|
workflowId:
|
||||||
type: number
|
type: number
|
||||||
example: '1000'
|
example: '1000'
|
||||||
|
|||||||
@@ -402,12 +402,13 @@ describe('GET /executions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('with query status', () => {
|
describe('with query status', () => {
|
||||||
type AllowedQueryStatus = 'success' | 'error' | 'canceled' | 'waiting';
|
type AllowedQueryStatus = 'canceled' | 'error' | 'running' | 'success' | 'waiting';
|
||||||
test.each`
|
test.each`
|
||||||
queryStatus | entityStatus
|
queryStatus | entityStatus
|
||||||
${'canceled'} | ${'canceled'}
|
${'canceled'} | ${'canceled'}
|
||||||
${'error'} | ${'error'}
|
${'error'} | ${'error'}
|
||||||
${'error'} | ${'crashed'}
|
${'error'} | ${'crashed'}
|
||||||
|
${'running'} | ${'running'}
|
||||||
${'success'} | ${'success'}
|
${'success'} | ${'success'}
|
||||||
${'waiting'} | ${'waiting'}
|
${'waiting'} | ${'waiting'}
|
||||||
`(
|
`(
|
||||||
@@ -419,6 +420,10 @@ describe('GET /executions', () => {
|
|||||||
const workflow = await createWorkflow({}, owner);
|
const workflow = await createWorkflow({}, owner);
|
||||||
|
|
||||||
await createdExecutionWithStatus(workflow, queryStatus === 'success' ? 'error' : 'success');
|
await createdExecutionWithStatus(workflow, queryStatus === 'success' ? 'error' : 'success');
|
||||||
|
if (queryStatus !== 'running') {
|
||||||
|
// ensure there is a running execution that gets excluded unless filtering by `running`
|
||||||
|
await createdExecutionWithStatus(workflow, 'running');
|
||||||
|
}
|
||||||
|
|
||||||
const expectedExecution = await createdExecutionWithStatus(workflow, entityStatus);
|
const expectedExecution = await createdExecutionWithStatus(workflow, entityStatus);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user