mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-23 21:06:51 +02:00
1147 lines
34 KiB
TypeScript
1147 lines
34 KiB
TypeScript
import {
|
|
createTeamProject,
|
|
createWorkflow,
|
|
linkUserToProject,
|
|
testDb,
|
|
} from '@n8n/backend-test-utils';
|
|
import { GlobalConfig } from '@n8n/config';
|
|
import type { ExecutionSummaries, User } from '@n8n/db';
|
|
import { ExecutionMetadataRepository, ExecutionRepository, WorkflowRepository } from '@n8n/db';
|
|
import { Container } from '@n8n/di';
|
|
import { mock } from 'jest-mock-extended';
|
|
|
|
import { ExecutionService } from '@/executions/execution.service';
|
|
|
|
import { annotateExecution, createAnnotationTags, createExecution } from './shared/db/executions';
|
|
import { createMember, createOwner } from './shared/db/users';
|
|
|
|
describe('ExecutionService', () => {
|
|
let executionService: ExecutionService;
|
|
let executionRepository: ExecutionRepository;
|
|
let member: User;
|
|
let owner: User;
|
|
const globalConfig = Container.get(GlobalConfig);
|
|
|
|
beforeAll(async () => {
|
|
await testDb.init();
|
|
|
|
executionRepository = Container.get(ExecutionRepository);
|
|
|
|
executionService = new ExecutionService(
|
|
globalConfig,
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
executionRepository,
|
|
mock(),
|
|
mock(),
|
|
Container.get(WorkflowRepository),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
);
|
|
|
|
owner = await createOwner();
|
|
member = await createMember();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
globalConfig.executions.concurrency.productionLimit = -1;
|
|
globalConfig.executions.mode = 'regular';
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await testDb.truncate(['ExecutionEntity']);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await testDb.terminate();
|
|
});
|
|
|
|
describe('findRangeWithCount', () => {
|
|
test('should return execution summaries', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
status: ['success'],
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
const summaryShape = {
|
|
id: expect.any(String),
|
|
workflowId: expect.any(String),
|
|
mode: expect.any(String),
|
|
retryOf: null,
|
|
status: expect.any(String),
|
|
createdAt: expect.any(String),
|
|
startedAt: expect.any(String),
|
|
stoppedAt: expect.any(String),
|
|
waitTill: null,
|
|
retrySuccessId: null,
|
|
workflowName: expect.any(String),
|
|
annotation: {
|
|
tags: expect.arrayContaining([]),
|
|
vote: null,
|
|
},
|
|
};
|
|
|
|
expect(output.count).toBe(2);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual([summaryShape, summaryShape]);
|
|
});
|
|
|
|
test('should limit executions', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
status: ['success'],
|
|
range: { limit: 2 },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(3);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toHaveLength(2);
|
|
});
|
|
|
|
test('should retrieve executions before `lastId`, excluding it', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
]);
|
|
|
|
const [firstId, secondId] = await executionRepository.getAllIds();
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20, lastId: secondId },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(4);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ id: firstId })]),
|
|
);
|
|
});
|
|
|
|
test('should retrieve executions after `firstId`, excluding it', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
]);
|
|
|
|
const [firstId, secondId, thirdId, fourthId] = await executionRepository.getAllIds();
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20, firstId },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(4);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ id: fourthId }),
|
|
expect.objectContaining({ id: thirdId }),
|
|
expect.objectContaining({ id: secondId }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
test('should filter executions by `status`', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'waiting' }, workflow),
|
|
createExecution({ status: 'waiting' }, workflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
status: ['success'],
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(2);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual([
|
|
expect.objectContaining({ status: 'success' }),
|
|
expect.objectContaining({ status: 'success' }),
|
|
]);
|
|
});
|
|
|
|
test('should filter executions by `workflowId`', async () => {
|
|
const firstWorkflow = await createWorkflow({}, owner);
|
|
const secondWorkflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, firstWorkflow),
|
|
createExecution({ status: 'success' }, secondWorkflow),
|
|
createExecution({ status: 'success' }, secondWorkflow),
|
|
createExecution({ status: 'success' }, secondWorkflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
workflowId: firstWorkflow.id,
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(1);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ workflowId: firstWorkflow.id })]),
|
|
);
|
|
});
|
|
|
|
test('should filter executions by `startedBefore`', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ startedAt: new Date('2020-06-01') }, workflow),
|
|
createExecution({ startedAt: new Date('2020-12-31') }, workflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
startedBefore: '2020-07-01',
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(1);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual([
|
|
expect.objectContaining({ startedAt: '2020-06-01T00:00:00.000Z' }),
|
|
]);
|
|
});
|
|
|
|
test('should filter executions by `startedAfter`', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ startedAt: new Date('2020-06-01') }, workflow),
|
|
createExecution({ startedAt: new Date('2020-12-31') }, workflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
startedAfter: '2020-07-01',
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(1);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual([
|
|
expect.objectContaining({ startedAt: '2020-12-31T00:00:00.000Z' }),
|
|
]);
|
|
});
|
|
|
|
test('should filter executions by `metadata` with an exact match by default', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
const key = 'myKey';
|
|
const value = 'myValue';
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success', metadata: [{ key, value }] }, workflow),
|
|
createExecution({ status: 'error', metadata: [{ key, value: `${value}2` }] }, workflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
metadata: [{ key, value, exactMatch: true }],
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output).toEqual({
|
|
count: 1,
|
|
estimated: false,
|
|
results: [expect.objectContaining({ status: 'success' })],
|
|
});
|
|
});
|
|
|
|
test('should filter executions by `metadata` with a partial match', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
const key = 'myKey';
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success', metadata: [{ key, value: 'myValue' }] }, workflow),
|
|
createExecution({ status: 'error', metadata: [{ key, value: 'var' }] }, workflow),
|
|
createExecution({ status: 'success', metadata: [{ key, value: 'evaluation' }] }, workflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
metadata: [{ key, value: 'val', exactMatch: false }],
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output).toEqual({
|
|
count: 2,
|
|
estimated: false,
|
|
results: [
|
|
expect.objectContaining({ status: 'success' }),
|
|
expect.objectContaining({ status: 'success' }),
|
|
],
|
|
});
|
|
});
|
|
|
|
test('should filter executions by `projectId`', async () => {
|
|
const firstProject = await createTeamProject();
|
|
const secondProject = await createTeamProject();
|
|
|
|
const firstWorkflow = await createWorkflow(undefined, firstProject);
|
|
const secondWorkflow = await createWorkflow(undefined, secondProject);
|
|
|
|
await createExecution({ status: 'success' }, firstWorkflow);
|
|
await createExecution({ status: 'success' }, firstWorkflow);
|
|
await createExecution({ status: 'success' }, secondWorkflow); // to filter out
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
projectId: firstProject.id,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output).toEqual({
|
|
count: 2,
|
|
estimated: false,
|
|
results: expect.arrayContaining([
|
|
expect.objectContaining({ workflowId: firstWorkflow.id }),
|
|
expect.objectContaining({ workflowId: firstWorkflow.id }),
|
|
// execution for workflow in second project was filtered out
|
|
]),
|
|
});
|
|
});
|
|
|
|
test('should filter executions by `projectId` and expected `status`', async () => {
|
|
const firstProject = await createTeamProject();
|
|
const secondProject = await createTeamProject();
|
|
|
|
const firstWorkflow = await createWorkflow(undefined, firstProject);
|
|
const secondWorkflow = await createWorkflow(undefined, secondProject);
|
|
|
|
await createExecution({ status: 'success' }, firstWorkflow);
|
|
await createExecution({ status: 'error' }, firstWorkflow);
|
|
await createExecution({ status: 'success' }, secondWorkflow);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
projectId: firstProject.id,
|
|
status: ['error'],
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output).toEqual({
|
|
count: 1,
|
|
estimated: false,
|
|
results: expect.arrayContaining([
|
|
expect.objectContaining({ workflowId: firstWorkflow.id, status: 'error' }),
|
|
]),
|
|
});
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
name: 'waitTill',
|
|
filter: { waitTill: true },
|
|
matchingParams: { waitTill: new Date() },
|
|
nonMatchingParams: { waitTill: undefined },
|
|
},
|
|
{
|
|
name: 'metadata',
|
|
filter: { metadata: [{ key: 'testKey', value: 'testValue' }] },
|
|
matchingParams: { metadata: [{ key: 'testKey', value: 'testValue' }] },
|
|
nonMatchingParams: { metadata: [{ key: 'otherKey', value: 'otherValue' }] },
|
|
},
|
|
{
|
|
name: 'startedAfter',
|
|
filter: { startedAfter: '2023-01-01' },
|
|
matchingParams: { startedAt: new Date('2023-06-01') },
|
|
nonMatchingParams: { startedAt: new Date('2022-01-01') },
|
|
},
|
|
{
|
|
name: 'startedBefore',
|
|
filter: { startedBefore: '2023-12-31' },
|
|
matchingParams: { startedAt: new Date('2023-06-01') },
|
|
nonMatchingParams: { startedAt: new Date('2024-01-01') },
|
|
},
|
|
])(
|
|
'should filter executions by `projectId` and expected `$name`',
|
|
async ({ filter, matchingParams, nonMatchingParams }) => {
|
|
const firstProject = await createTeamProject();
|
|
const secondProject = await createTeamProject();
|
|
|
|
const firstWorkflow = await createWorkflow(undefined, firstProject);
|
|
const secondWorkflow = await createWorkflow(undefined, secondProject);
|
|
|
|
await Promise.all([
|
|
createExecution(matchingParams, firstWorkflow),
|
|
createExecution(nonMatchingParams, secondWorkflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
projectId: firstProject.id,
|
|
...filter,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output).toEqual({
|
|
count: 1,
|
|
estimated: false,
|
|
results: expect.arrayContaining([
|
|
expect.objectContaining({ workflowId: firstWorkflow.id }),
|
|
]),
|
|
});
|
|
},
|
|
);
|
|
|
|
test('should exclude executions by inaccessible `workflowId`', async () => {
|
|
const accessibleWorkflow = await createWorkflow({}, member);
|
|
const inaccessibleWorkflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, accessibleWorkflow),
|
|
createExecution({ status: 'success' }, inaccessibleWorkflow),
|
|
createExecution({ status: 'success' }, inaccessibleWorkflow),
|
|
createExecution({ status: 'success' }, inaccessibleWorkflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
workflowId: inaccessibleWorkflow.id,
|
|
user: member,
|
|
sharingOptions: {
|
|
workflowRoles: ['workflow:owner'],
|
|
projectRoles: ['project:personalOwner'],
|
|
},
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(0);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual([]);
|
|
});
|
|
|
|
test('should support advanced filters', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([createExecution({}, workflow), createExecution({}, workflow)]);
|
|
|
|
const [firstId, secondId] = await executionRepository.getAllIds();
|
|
|
|
const executionMetadataRepository = Container.get(ExecutionMetadataRepository);
|
|
|
|
await executionMetadataRepository.save({
|
|
key: 'key1',
|
|
value: 'value1',
|
|
execution: { id: firstId },
|
|
});
|
|
|
|
await executionMetadataRepository.save({
|
|
key: 'key2',
|
|
value: 'value2',
|
|
execution: { id: secondId },
|
|
});
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
metadata: [{ key: 'key1', value: 'value1' }],
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(1);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual([expect.objectContaining({ id: firstId })]);
|
|
});
|
|
});
|
|
|
|
describe('findRangeWithCount — subquery approach', () => {
|
|
test('should scope results to user accessible workflows', async () => {
|
|
const workflow1 = await createWorkflow({}, member);
|
|
const workflow2 = await createWorkflow({}, member);
|
|
const inaccessibleWorkflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, workflow1),
|
|
createExecution({ status: 'success' }, workflow1),
|
|
createExecution({ status: 'error' }, workflow2),
|
|
createExecution({ status: 'success' }, inaccessibleWorkflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: member,
|
|
sharingOptions: {
|
|
workflowRoles: ['workflow:owner'],
|
|
projectRoles: ['project:personalOwner'],
|
|
},
|
|
};
|
|
|
|
const result = await executionService.findRangeWithCount(query);
|
|
|
|
// member owns workflow1 and workflow2 → sees 3 executions, not the inaccessible one
|
|
expect(result.count).toBe(3);
|
|
const workflowIds = result.results.map((r) => r.workflowId);
|
|
expect(workflowIds).toContain(workflow1.id);
|
|
expect(workflowIds).toContain(workflow2.id);
|
|
expect(workflowIds).not.toContain(inaccessibleWorkflow.id);
|
|
});
|
|
|
|
test('should filter by status correctly', async () => {
|
|
const workflow = await createWorkflow({}, member);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'error' }, workflow),
|
|
]);
|
|
|
|
const arrayQuery: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
status: ['success'],
|
|
user: owner,
|
|
};
|
|
|
|
const subqueryQuery: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
status: ['success'],
|
|
user: member,
|
|
sharingOptions: {
|
|
workflowRoles: ['workflow:owner'],
|
|
projectRoles: ['project:personalOwner'],
|
|
},
|
|
};
|
|
|
|
const [arrayResult, subqueryResult] = await Promise.all([
|
|
executionService.findRangeWithCount(arrayQuery),
|
|
executionService.findRangeWithCount(subqueryQuery),
|
|
]);
|
|
|
|
expect(arrayResult.count).toBe(2);
|
|
expect(subqueryResult.count).toBe(2);
|
|
expect(subqueryResult.results.map((r) => r.id)).toEqual(arrayResult.results.map((r) => r.id));
|
|
});
|
|
|
|
test('should filter by workflowId correctly', async () => {
|
|
const workflow1 = await createWorkflow({}, member);
|
|
const workflow2 = await createWorkflow({}, member);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, workflow1),
|
|
createExecution({ status: 'success' }, workflow2),
|
|
createExecution({ status: 'success' }, workflow2),
|
|
]);
|
|
|
|
const arrayQuery: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
workflowId: workflow1.id,
|
|
user: owner,
|
|
};
|
|
|
|
const subqueryQuery: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
workflowId: workflow1.id,
|
|
user: member,
|
|
sharingOptions: {
|
|
workflowRoles: ['workflow:owner'],
|
|
projectRoles: ['project:personalOwner'],
|
|
},
|
|
};
|
|
|
|
const [arrayResult, subqueryResult] = await Promise.all([
|
|
executionService.findRangeWithCount(arrayQuery),
|
|
executionService.findRangeWithCount(subqueryQuery),
|
|
]);
|
|
|
|
expect(arrayResult.count).toBe(1);
|
|
expect(subqueryResult.count).toBe(1);
|
|
expect(subqueryResult.results[0].workflowId).toBe(workflow1.id);
|
|
});
|
|
|
|
test('should work with team project', async () => {
|
|
const teamProject = await createTeamProject();
|
|
const personalWorkflow = await createWorkflow({}, owner);
|
|
const teamWorkflow = await createWorkflow({}, teamProject);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, personalWorkflow),
|
|
createExecution({ status: 'success' }, teamWorkflow),
|
|
]);
|
|
|
|
const arrayQuery: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const arrayResult = await executionService.findRangeWithCount(arrayQuery);
|
|
expect(arrayResult.count).toBe(2);
|
|
});
|
|
|
|
test('should work with sharing-enabled roles (team project admin)', async () => {
|
|
// Simulates the isSharingEnabled() === true path in the controller
|
|
const teamProject = await createTeamProject(undefined, member);
|
|
const personalWorkflow = await createWorkflow({}, member);
|
|
const teamWorkflow = await createWorkflow({}, teamProject);
|
|
const inaccessibleWorkflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, personalWorkflow),
|
|
createExecution({ status: 'success' }, teamWorkflow),
|
|
createExecution({ status: 'error' }, teamWorkflow),
|
|
createExecution({ status: 'success' }, inaccessibleWorkflow),
|
|
]);
|
|
|
|
// Sharing-enabled roles: member can see workflows they own OR are admin/editor of
|
|
const sharingEnabledQuery: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: member,
|
|
sharingOptions: {
|
|
workflowRoles: ['workflow:owner', 'workflow:editor'],
|
|
projectRoles: ['project:personalOwner', 'project:admin', 'project:editor'],
|
|
},
|
|
};
|
|
|
|
const result = await executionService.findRangeWithCount(sharingEnabledQuery);
|
|
|
|
// member owns personalWorkflow and is admin of teamProject → sees 3 executions
|
|
expect(result.count).toBe(3);
|
|
const workflowIds = result.results.map((r) => r.workflowId);
|
|
expect(workflowIds).toContain(personalWorkflow.id);
|
|
expect(workflowIds).toContain(teamWorkflow.id);
|
|
expect(workflowIds).not.toContain(inaccessibleWorkflow.id);
|
|
});
|
|
|
|
test('should work with sharing-enabled roles (team project editor)', async () => {
|
|
// member is linked as project:editor to a team project they didn't create
|
|
const teamProject = await createTeamProject();
|
|
await linkUserToProject(member, teamProject, 'project:editor');
|
|
const teamWorkflow = await createWorkflow({}, teamProject);
|
|
const personalWorkflow = await createWorkflow({}, member);
|
|
const inaccessibleWorkflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, teamWorkflow),
|
|
createExecution({ status: 'success' }, personalWorkflow),
|
|
createExecution({ status: 'success' }, inaccessibleWorkflow),
|
|
]);
|
|
|
|
const sharingEnabledQuery: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: member,
|
|
sharingOptions: {
|
|
workflowRoles: ['workflow:owner', 'workflow:editor'],
|
|
projectRoles: ['project:personalOwner', 'project:admin', 'project:editor'],
|
|
},
|
|
};
|
|
|
|
const result = await executionService.findRangeWithCount(sharingEnabledQuery);
|
|
|
|
// member owns personalWorkflow and is editor in teamProject → sees 2 executions
|
|
expect(result.count).toBe(2);
|
|
const workflowIds = result.results.map((r) => r.workflowId);
|
|
expect(workflowIds).toContain(teamWorkflow.id);
|
|
expect(workflowIds).toContain(personalWorkflow.id);
|
|
expect(workflowIds).not.toContain(inaccessibleWorkflow.id);
|
|
});
|
|
});
|
|
|
|
describe('findLatestCurrentAndCompleted — subquery approach', () => {
|
|
test('should return same results as array approach', async () => {
|
|
const workflow = await createWorkflow({}, member);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'running', stoppedAt: undefined }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'error' }, workflow),
|
|
]);
|
|
|
|
const arrayQuery: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const subqueryQuery: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: member,
|
|
sharingOptions: {
|
|
workflowRoles: ['workflow:owner'],
|
|
projectRoles: ['project:personalOwner'],
|
|
},
|
|
};
|
|
|
|
const [arrayResult, subqueryResult] = await Promise.all([
|
|
executionService.findLatestCurrentAndCompleted(arrayQuery),
|
|
executionService.findLatestCurrentAndCompleted(subqueryQuery),
|
|
]);
|
|
|
|
expect(arrayResult.count).toBe(subqueryResult.count);
|
|
expect(arrayResult.results).toHaveLength(subqueryResult.results.length);
|
|
|
|
const arrayIds = arrayResult.results.map((r) => r.id).sort();
|
|
const subqueryIds = subqueryResult.results.map((r) => r.id).sort();
|
|
expect(subqueryIds).toEqual(arrayIds);
|
|
});
|
|
});
|
|
|
|
describe('getConcurrentExecutionsCount', () => {
|
|
test('should return concurrentExecutionsCount when concurrency is enabled', async () => {
|
|
globalConfig.executions.concurrency.productionLimit = 4;
|
|
|
|
const workflow = await createWorkflow({}, owner);
|
|
const concurrentExecutionsData = await Promise.all([
|
|
createExecution({ status: 'running', mode: 'webhook' }, workflow),
|
|
createExecution({ status: 'running', mode: 'trigger' }, workflow),
|
|
]);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'crashed' }, workflow),
|
|
createExecution({ status: 'new' }, workflow),
|
|
createExecution({ status: 'running', mode: 'manual' }, workflow),
|
|
]);
|
|
|
|
const output = await executionService.getConcurrentExecutionsCount();
|
|
expect(output).toEqual(concurrentExecutionsData.length);
|
|
});
|
|
|
|
test('should set concurrentExecutionsCount to -1 when concurrency is disabled', async () => {
|
|
globalConfig.executions.concurrency.productionLimit = -1;
|
|
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'running', mode: 'webhook' }, workflow),
|
|
createExecution({ status: 'running', mode: 'trigger' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'crashed' }, workflow),
|
|
createExecution({ status: 'new' }, workflow),
|
|
createExecution({ status: 'running', mode: 'manual' }, workflow),
|
|
]);
|
|
|
|
const output = await executionService.getConcurrentExecutionsCount();
|
|
|
|
expect(output).toEqual(-1);
|
|
});
|
|
|
|
test('should set concurrentExecutionsCount to -1 in queue mode', async () => {
|
|
globalConfig.executions.mode = 'queue';
|
|
globalConfig.executions.concurrency.productionLimit = 4;
|
|
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'running', mode: 'webhook' }, workflow),
|
|
createExecution({ status: 'running', mode: 'trigger' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'crashed' }, workflow),
|
|
createExecution({ status: 'new' }, workflow),
|
|
createExecution({ status: 'running', mode: 'manual' }, workflow),
|
|
]);
|
|
|
|
const output = await executionService.getConcurrentExecutionsCount();
|
|
|
|
expect(output).toEqual(-1);
|
|
});
|
|
});
|
|
|
|
describe('findLatestCurrentAndCompleted', () => {
|
|
test('should return latest current and completed executions', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
const totalCompleted = 21;
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'running' }, workflow),
|
|
createExecution({ status: 'running' }, workflow),
|
|
createExecution({ status: 'running' }, workflow),
|
|
...new Array(totalCompleted)
|
|
.fill(null)
|
|
.map(async () => await createExecution({ status: 'success' }, workflow)),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findLatestCurrentAndCompleted(query);
|
|
|
|
expect(output.results).toHaveLength(23); // 3 current + 20 completed (excludes 21st)
|
|
expect(output.count).toBe(totalCompleted); // 21 finished, excludes current
|
|
expect(output.estimated).toBe(false);
|
|
});
|
|
|
|
test('should handle zero current executions', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
const totalFinished = 5;
|
|
|
|
await Promise.all(
|
|
new Array(totalFinished)
|
|
.fill(null)
|
|
.map(async () => await createExecution({ status: 'success' }, workflow)),
|
|
);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findLatestCurrentAndCompleted(query);
|
|
|
|
expect(output.results).toHaveLength(totalFinished); // 5 finished
|
|
expect(output.count).toBe(totalFinished); // 5 finished, excludes active
|
|
expect(output.estimated).toBe(false);
|
|
});
|
|
|
|
test('should handle zero completed executions', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'running' }, workflow),
|
|
createExecution({ status: 'running' }, workflow),
|
|
createExecution({ status: 'running' }, workflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findLatestCurrentAndCompleted(query);
|
|
|
|
expect(output.results).toHaveLength(3); // 3 finished
|
|
expect(output.count).toBe(0); // 0 finished, excludes active
|
|
expect(output.estimated).toBe(false);
|
|
});
|
|
|
|
test('should handle zero executions', async () => {
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findLatestCurrentAndCompleted(query);
|
|
|
|
expect(output.results).toHaveLength(0);
|
|
expect(output.count).toBe(0);
|
|
expect(output.estimated).toBe(false);
|
|
});
|
|
|
|
test('should prioritize `running` over `new` executions', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
await Promise.all([
|
|
createExecution({ status: 'new' }, workflow),
|
|
createExecution({ status: 'new' }, workflow),
|
|
createExecution({ status: 'running' }, workflow),
|
|
createExecution({ status: 'running' }, workflow),
|
|
createExecution({ status: 'new' }, workflow),
|
|
createExecution({ status: 'new' }, workflow),
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
range: { limit: 2 },
|
|
user: owner,
|
|
};
|
|
|
|
const { results } = await executionService.findLatestCurrentAndCompleted(query);
|
|
|
|
expect(results).toHaveLength(2);
|
|
expect(results[0].status).toBe('running');
|
|
expect(results[1].status).toBe('running');
|
|
});
|
|
});
|
|
|
|
describe('annotation', () => {
|
|
const summaryShape = {
|
|
id: expect.any(String),
|
|
workflowId: expect.any(String),
|
|
mode: expect.any(String),
|
|
retryOf: null,
|
|
status: expect.any(String),
|
|
createdAt: expect.any(String),
|
|
startedAt: expect.any(String),
|
|
stoppedAt: expect.any(String),
|
|
waitTill: null,
|
|
retrySuccessId: null,
|
|
workflowName: expect.any(String),
|
|
};
|
|
|
|
afterEach(async () => {
|
|
await testDb.truncate(['AnnotationTagEntity', 'ExecutionAnnotation']);
|
|
});
|
|
|
|
test('should add and retrieve annotation', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
const execution1 = await createExecution({ status: 'success' }, workflow);
|
|
const execution2 = await createExecution({ status: 'success' }, workflow);
|
|
|
|
const annotationTags = await createAnnotationTags(['tag1', 'tag2', 'tag3']);
|
|
|
|
await annotateExecution(
|
|
execution1.id,
|
|
{ vote: 'up', tags: [annotationTags[0].id, annotationTags[1].id] },
|
|
[workflow.id],
|
|
);
|
|
await annotateExecution(execution2.id, { vote: 'down', tags: [annotationTags[2].id] }, [
|
|
workflow.id,
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
status: ['success'],
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(2);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
...summaryShape,
|
|
annotation: {
|
|
tags: [expect.objectContaining({ name: 'tag3' })],
|
|
vote: 'down',
|
|
},
|
|
},
|
|
{
|
|
...summaryShape,
|
|
annotation: {
|
|
tags: expect.arrayContaining([
|
|
expect.objectContaining({ name: 'tag1' }),
|
|
expect.objectContaining({ name: 'tag2' }),
|
|
]),
|
|
vote: 'up',
|
|
},
|
|
},
|
|
]),
|
|
);
|
|
});
|
|
|
|
test('should update annotation', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
const execution = await createExecution({ status: 'success' }, workflow);
|
|
|
|
const annotationTags = await createAnnotationTags(['tag1', 'tag2', 'tag3']);
|
|
|
|
await annotateExecution(execution.id, { vote: 'up', tags: [annotationTags[0].id] }, [
|
|
workflow.id,
|
|
]);
|
|
|
|
await annotateExecution(execution.id, { vote: 'down', tags: [annotationTags[1].id] }, [
|
|
workflow.id,
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
status: ['success'],
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(1);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual([
|
|
{
|
|
...summaryShape,
|
|
annotation: {
|
|
tags: [expect.objectContaining({ name: 'tag2' })],
|
|
vote: 'down',
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('should filter by annotation tags', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
const executions = await Promise.all([
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
]);
|
|
|
|
const annotationTags = await createAnnotationTags(['tag1', 'tag2', 'tag3']);
|
|
|
|
await annotateExecution(
|
|
executions[0].id,
|
|
{ vote: 'up', tags: [annotationTags[0].id, annotationTags[1].id] },
|
|
[workflow.id],
|
|
);
|
|
await annotateExecution(executions[1].id, { vote: 'down', tags: [annotationTags[2].id] }, [
|
|
workflow.id,
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
status: ['success'],
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
annotationTags: [annotationTags[0].id],
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(1);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual([
|
|
{
|
|
...summaryShape,
|
|
annotation: {
|
|
tags: expect.arrayContaining([
|
|
expect.objectContaining({ name: 'tag1' }),
|
|
expect.objectContaining({ name: 'tag2' }),
|
|
]),
|
|
vote: 'up',
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('should filter by annotation vote', async () => {
|
|
const workflow = await createWorkflow({}, owner);
|
|
|
|
const executions = await Promise.all([
|
|
createExecution({ status: 'success' }, workflow),
|
|
createExecution({ status: 'success' }, workflow),
|
|
]);
|
|
|
|
const annotationTags = await createAnnotationTags(['tag1', 'tag2', 'tag3']);
|
|
|
|
await annotateExecution(
|
|
executions[0].id,
|
|
{ vote: 'up', tags: [annotationTags[0].id, annotationTags[1].id] },
|
|
[workflow.id],
|
|
);
|
|
await annotateExecution(executions[1].id, { vote: 'down', tags: [annotationTags[2].id] }, [
|
|
workflow.id,
|
|
]);
|
|
|
|
const query: ExecutionSummaries.RangeQuery = {
|
|
kind: 'range',
|
|
status: ['success'],
|
|
range: { limit: 20 },
|
|
user: owner,
|
|
vote: 'up',
|
|
};
|
|
|
|
const output = await executionService.findRangeWithCount(query);
|
|
|
|
expect(output.count).toBe(1);
|
|
expect(output.estimated).toBe(false);
|
|
expect(output.results).toEqual([
|
|
{
|
|
...summaryShape,
|
|
annotation: {
|
|
tags: expect.arrayContaining([
|
|
expect.objectContaining({ name: 'tag1' }),
|
|
expect.objectContaining({ name: 'tag2' }),
|
|
]),
|
|
vote: 'up',
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
});
|