n8n/packages/cli/src/executions/executions.controller.ts
Guillaume Jacquart 8ab168f787
chore(core): Query executions using a single query intead of two (#27081)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-03-30 08:19:32 +00:00

184 lines
6.3 KiB
TypeScript

import type { User, ExecutionSummaries } from '@n8n/db';
import { Get, Patch, Post, RestController } from '@n8n/decorators';
import { PROJECT_OWNER_ROLE_SLUG, type Scope } from '@n8n/permissions';
import { ExecutionService } from './execution.service';
import { EnterpriseExecutionsService } from './execution.service.ee';
import { ExecutionRequest } from './execution.types';
import { parseRangeQuery } from './parse-range-query.middleware';
import { validateExecutionUpdatePayload } from './validation';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { License } from '@/license';
import { RoleService } from '@/services/role.service';
import { isPositiveInteger } from '@/utils';
import { WorkflowSharingService } from '@/workflows/workflow-sharing.service';
@RestController('/executions')
export class ExecutionsController {
constructor(
private readonly executionService: ExecutionService,
private readonly enterpriseExecutionService: EnterpriseExecutionsService,
private readonly workflowSharingService: WorkflowSharingService,
private readonly license: License,
private readonly roleService: RoleService,
) {}
private async getAccessibleWorkflowIds(user: User, scope: Scope) {
if (this.license.isSharingEnabled()) {
return await this.workflowSharingService.getSharedWorkflowIds(user, { scopes: [scope] });
} else {
return await this.workflowSharingService.getSharedWorkflowIds(user, {
workflowRoles: ['workflow:owner'],
projectRoles: [PROJECT_OWNER_ROLE_SLUG],
});
}
}
@Get('/', { middlewares: [parseRangeQuery] })
async getMany(req: ExecutionRequest.GetMany) {
const { rangeQuery: query } = req;
// Build sharing options for the subquery instead of fetching IDs upfront
const scope: Scope = 'workflow:read';
query.user = req.user;
if (this.license.isSharingEnabled()) {
const projectRoles = await this.roleService.rolesWithScope('project', [scope]);
const workflowRoles = await this.roleService.rolesWithScope('workflow', [scope]);
query.sharingOptions = { scopes: [scope], projectRoles, workflowRoles };
} else {
query.sharingOptions = {
workflowRoles: ['workflow:owner'],
projectRoles: [PROJECT_OWNER_ROLE_SLUG],
};
}
if (!this.license.isAdvancedExecutionFiltersEnabled()) {
delete query.metadata;
delete query.annotationTags;
}
const noStatus = !query.status || query.status.length === 0;
const noRange = !query.range.lastId || !query.range.firstId;
if (noStatus && noRange) {
const [executions, concurrentExecutionsCount] = await Promise.all([
this.executionService.findLatestCurrentAndCompleted(query),
this.executionService.getConcurrentExecutionsCount(),
]);
await this.executionService.addScopes(
req.user,
executions.results as ExecutionSummaries.ExecutionSummaryWithScopes[],
);
return {
...executions,
concurrentExecutionsCount,
};
}
const [executions, concurrentExecutionsCount] = await Promise.all([
this.executionService.findRangeWithCount(query),
this.executionService.getConcurrentExecutionsCount(),
]);
await this.executionService.addScopes(
req.user,
executions.results as ExecutionSummaries.ExecutionSummaryWithScopes[],
);
return {
...executions,
concurrentExecutionsCount,
};
}
@Get('/versions/:workflowId')
async getVersions(req: ExecutionRequest.GetVersions) {
const accessibleWorkflowIds = await this.getAccessibleWorkflowIds(req.user, 'workflow:read');
if (!accessibleWorkflowIds.includes(req.params.workflowId)) {
return [];
}
return await this.executionService.getExecutedVersions(req.params.workflowId);
}
@Get('/:id')
async getOne(req: ExecutionRequest.GetOne) {
if (!isPositiveInteger(req.params.id)) {
throw new BadRequestError('Execution ID is not a number');
}
const workflowIds = await this.getAccessibleWorkflowIds(req.user, 'workflow:read');
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
return this.license.isSharingEnabled()
? await this.enterpriseExecutionService.findOne(req, workflowIds)
: await this.executionService.findOne(req, workflowIds);
}
@Post('/:id/stop')
async stop(req: ExecutionRequest.Stop) {
const workflowIds = await this.getAccessibleWorkflowIds(req.user, 'workflow:execute');
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
const executionId = req.params.id;
return await this.executionService.stop(executionId, workflowIds);
}
/**
* Stops executions based on the provided filter
*
* @returns { stopped: number } - The amount of actually stopped executions, potentially lower if some executions finished naturally.
*/
@Post('/stopMany')
async stopMany(req: ExecutionRequest.StopMany) {
const accessibleWorkflowIds = await this.getAccessibleWorkflowIds(req.user, 'workflow:execute');
// Return early to avoid expensive db query
if (accessibleWorkflowIds.length === 0) return { stopped: 0 };
const stopped = await this.executionService.stopMany(req.body.filter, accessibleWorkflowIds);
return { stopped };
}
@Post('/:id/retry')
async retry(req: ExecutionRequest.Retry) {
const workflowIds = await this.getAccessibleWorkflowIds(req.user, 'workflow:execute');
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
return await this.executionService.retry(req, workflowIds);
}
@Post('/delete')
async delete(req: ExecutionRequest.Delete) {
const workflowIds = await this.getAccessibleWorkflowIds(req.user, 'workflow:execute');
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
return await this.executionService.delete(req, workflowIds);
}
@Patch('/:id')
async update(req: ExecutionRequest.Update) {
if (!isPositiveInteger(req.params.id)) {
throw new BadRequestError('Execution ID is not a number');
}
const workflowIds = await this.getAccessibleWorkflowIds(req.user, 'workflow:read');
// Fail fast if no workflows are accessible
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
const { body: payload } = req;
const validatedPayload = validateExecutionUpdatePayload(payload);
await this.executionService.annotate(req.params.id, validatedPayload, workflowIds);
return await this.executionService.findOne(req, workflowIds);
}
}