refactor(core): Bring active executions into executions controller (no-changelog) (#8371)

This commit is contained in:
Iván Ovejero
2024-01-23 09:48:50 +01:00
committed by GitHub
parent 913c8c6b0c
commit 49b52c4f1d
22 changed files with 544 additions and 331 deletions

View File

@@ -18,7 +18,7 @@ import type {
Workflow,
WorkflowExecuteMode,
ExecutionStatus,
IExecutionsSummary,
ExecutionSummary,
FeatureFlags,
INodeProperties,
IUserSettings,
@@ -170,8 +170,7 @@ export interface IExecutionFlattedResponse extends IExecutionFlatted {
export interface IExecutionsListResponse {
count: number;
// results: IExecutionShortResponse[];
results: IExecutionsSummary[];
results: ExecutionSummary[];
estimated: boolean;
}
@@ -192,12 +191,6 @@ export interface IExecutionsCurrentSummary {
status?: ExecutionStatus;
}
export interface IExecutionDeleteFilter {
deleteBefore?: Date;
filters?: IDataObject;
ids?: string[];
}
export interface IExecutingWorkflowData {
executionData: IWorkflowExecutionDataProcess;
process?: ChildProcess;

View File

@@ -14,13 +14,10 @@ import cookieParser from 'cookie-parser';
import express from 'express';
import { engine as expressHandlebars } from 'express-handlebars';
import type { ServeStaticOptions } from 'serve-static';
import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { Not, In } from 'typeorm';
import { type Class, InstanceSettings } from 'n8n-core';
import type { ExecutionStatus, IExecutionsSummary, IN8nUISettings } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';
import type { IN8nUISettings } from 'n8n-workflow';
// @ts-ignore
import timezones from 'google-timezones-json';
@@ -39,7 +36,6 @@ import {
} from '@/constants';
import { credentialsController } from '@/credentials/credentials.controller';
import type { CurlHelper } from '@/requests';
import type { ExecutionRequest } from '@/executions/execution.request';
import { registerController } from '@/decorators';
import { AuthController } from '@/controllers/auth.controller';
import { BinaryDataController } from '@/controllers/binaryData.controller';
@@ -58,7 +54,7 @@ import { WorkflowStatisticsController } from '@/controllers/workflowStatistics.c
import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee';
import { ExecutionsController } from '@/executions/executions.controller';
import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi';
import type { ICredentialsOverwrite, IDiagnosticInfo, IExecutionsStopData } from '@/Interfaces';
import type { ICredentialsOverwrite, IDiagnosticInfo } from '@/Interfaces';
import { ActiveExecutions } from '@/ActiveExecutions';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
@@ -76,7 +72,6 @@ import { PostHogClient } from './posthog';
import { eventBus } from './eventbus';
import { InternalHooks } from './InternalHooks';
import { License } from './License';
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
import { SamlController } from './sso/saml/routes/saml.controller.ee';
import { SamlService } from './sso/saml/saml.service.ee';
import { VariablesController } from './environments/variables/variables.controller.ee';
@@ -87,8 +82,6 @@ import {
import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee';
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee';
import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
@@ -100,9 +93,7 @@ import { InvitationController } from './controllers/invitation.controller';
import { CollaborationService } from './collaboration/collaboration.service';
import { RoleController } from './controllers/role.controller';
import { BadRequestError } from './errors/response-errors/bad-request.error';
import { NotFoundError } from './errors/response-errors/not-found.error';
import { OrchestrationService } from '@/services/orchestration.service';
import { WorkflowSharingService } from './workflows/workflowSharing.service';
const exec = promisify(callbackExec);
@@ -408,219 +399,6 @@ export class Server extends AbstractServer {
}),
);
// ----------------------------------------
// Executing Workflows
// ----------------------------------------
// Returns all the currently working executions
this.app.get(
`/${this.restEndpoint}/executions-current`,
ResponseHelper.send(
async (req: ExecutionRequest.GetAllCurrent): Promise<IExecutionsSummary[]> => {
if (config.getEnv('executions.mode') === 'queue') {
const queue = Container.get(Queue);
const currentJobs = await queue.getJobs(['active', 'waiting']);
const currentlyRunningQueueIds = currentJobs.map((job) => job.data.executionId);
const currentlyRunningManualExecutions =
this.activeExecutionsInstance.getActiveExecutions();
const manualExecutionIds = currentlyRunningManualExecutions.map(
(execution) => execution.id,
);
const currentlyRunningExecutionIds =
currentlyRunningQueueIds.concat(manualExecutionIds);
if (!currentlyRunningExecutionIds.length) return [];
const findOptions: FindManyOptions<ExecutionEntity> & {
where: FindOptionsWhere<ExecutionEntity>;
} = {
select: ['id', 'workflowId', 'mode', 'retryOf', 'startedAt', 'stoppedAt', 'status'],
order: { id: 'DESC' },
where: {
id: In(currentlyRunningExecutionIds),
status: Not(In(['finished', 'stopped', 'failed', 'crashed'] as ExecutionStatus[])),
},
};
const sharedWorkflowIds = await Container.get(
WorkflowSharingService,
).getSharedWorkflowIds(req.user);
if (!sharedWorkflowIds.length) return [];
if (req.query.filter) {
const { workflowId, status, finished } = jsonParse<any>(req.query.filter);
if (workflowId && sharedWorkflowIds.includes(workflowId)) {
Object.assign(findOptions.where, { workflowId });
} else {
Object.assign(findOptions.where, { workflowId: In(sharedWorkflowIds) });
}
if (status) {
Object.assign(findOptions.where, { status: In(status) });
}
if (finished) {
Object.assign(findOptions.where, { finished });
}
} else {
Object.assign(findOptions.where, { workflowId: In(sharedWorkflowIds) });
}
const executions =
await Container.get(ExecutionRepository).findMultipleExecutions(findOptions);
if (!executions.length) return [];
return executions.map((execution) => {
if (!execution.status) {
execution.status = getStatusUsingPreviousExecutionStatusMethod(execution);
}
return {
id: execution.id,
workflowId: execution.workflowId,
mode: execution.mode,
retryOf: execution.retryOf !== null ? execution.retryOf : undefined,
startedAt: new Date(execution.startedAt),
status: execution.status ?? null,
stoppedAt: execution.stoppedAt ?? null,
} as IExecutionsSummary;
});
}
const executingWorkflows = this.activeExecutionsInstance.getActiveExecutions();
const returnData: IExecutionsSummary[] = [];
const filter = req.query.filter ? jsonParse<any>(req.query.filter) : {};
const sharedWorkflowIds = await Container.get(
WorkflowSharingService,
).getSharedWorkflowIds(req.user);
for (const data of executingWorkflows) {
if (
(filter.workflowId !== undefined && filter.workflowId !== data.workflowId) ||
(data.workflowId !== undefined && !sharedWorkflowIds.includes(data.workflowId))
) {
continue;
}
returnData.push({
id: data.id,
workflowId: data.workflowId === undefined ? '' : data.workflowId,
mode: data.mode,
retryOf: data.retryOf,
startedAt: new Date(data.startedAt),
status: data.status,
});
}
returnData.sort((a, b) => Number(b.id) - Number(a.id));
return returnData;
},
),
);
// Forces the execution to stop
this.app.post(
`/${this.restEndpoint}/executions-current/:id/stop`,
ResponseHelper.send(async (req: ExecutionRequest.Stop): Promise<IExecutionsStopData> => {
const { id: executionId } = req.params;
const sharedWorkflowIds = await Container.get(WorkflowSharingService).getSharedWorkflowIds(
req.user,
);
if (!sharedWorkflowIds.length) {
throw new NotFoundError('Execution not found');
}
const fullExecutionData = await Container.get(ExecutionRepository).findSingleExecution(
executionId,
{
where: {
workflowId: In(sharedWorkflowIds),
},
},
);
if (!fullExecutionData) {
throw new NotFoundError('Execution not found');
}
if (config.getEnv('executions.mode') === 'queue') {
// Manual executions should still be stoppable, so
// try notifying the `activeExecutions` to stop it.
const result = await this.activeExecutionsInstance.stopExecution(req.params.id);
if (result === undefined) {
// If active execution could not be found check if it is a waiting one
try {
return await this.waitTracker.stopExecution(req.params.id);
} catch (error) {
// Ignore, if it errors as then it is probably a currently running
// execution
}
} else {
return {
mode: result.mode,
startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
status: result.status,
} as IExecutionsStopData;
}
const queue = Container.get(Queue);
const currentJobs = await queue.getJobs(['active', 'waiting']);
const job = currentJobs.find((job) => job.data.executionId === req.params.id);
if (!job) {
this.logger.debug('Could not stop job because it is no longer in queue', {
jobId: req.params.id,
});
} else {
await queue.stopJob(job);
}
const returnData: IExecutionsStopData = {
mode: fullExecutionData.mode,
startedAt: new Date(fullExecutionData.startedAt),
stoppedAt: fullExecutionData.stoppedAt
? new Date(fullExecutionData.stoppedAt)
: undefined,
finished: fullExecutionData.finished,
status: fullExecutionData.status,
};
return returnData;
}
// Stop the execution and wait till it is done and we got the data
const result = await this.activeExecutionsInstance.stopExecution(executionId);
let returnData: IExecutionsStopData;
if (result === undefined) {
// If active execution could not be found check if it is a waiting one
returnData = await this.waitTracker.stopExecution(executionId);
} else {
returnData = {
mode: result.mode,
startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
status: result.status,
};
}
return returnData;
}),
);
// ----------------------------------------
// Options
// ----------------------------------------

View File

@@ -0,0 +1,5 @@
import type { ExecutionRequest } from '@/executions/execution.types';
export namespace StatisticsRequest {
export type GetOne = ExecutionRequest.GetOne;
}

View File

@@ -4,10 +4,10 @@ import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
import { StatisticsNames } from '@db/entities/WorkflowStatistics';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository';
import { ExecutionRequest } from '@/executions/execution.request';
import type { IWorkflowStatisticsDataLoaded } from '@/Interfaces';
import { Logger } from '@/Logger';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { StatisticsRequest } from './workflow-statistics.types';
interface WorkflowStatisticsData<T> {
productionSuccess: T;
@@ -29,7 +29,7 @@ export class WorkflowStatisticsController {
*/
// TODO: move this into a new decorator `@ValidateWorkflowPermission`
@Middleware()
async hasWorkflowAccess(req: ExecutionRequest.Get, res: Response, next: NextFunction) {
async hasWorkflowAccess(req: StatisticsRequest.GetOne, res: Response, next: NextFunction) {
const { user } = req;
const workflowId = req.params.id;
@@ -48,17 +48,17 @@ export class WorkflowStatisticsController {
}
@Get('/:id/counts/')
async getCounts(req: ExecutionRequest.Get): Promise<WorkflowStatisticsData<number>> {
async getCounts(req: StatisticsRequest.GetOne): Promise<WorkflowStatisticsData<number>> {
return await this.getData(req.params.id, 'count', 0);
}
@Get('/:id/times/')
async getTimes(req: ExecutionRequest.Get): Promise<WorkflowStatisticsData<Date | null>> {
async getTimes(req: StatisticsRequest.GetOne): Promise<WorkflowStatisticsData<Date | null>> {
return await this.getData(req.params.id, 'latestEvent', null);
}
@Get('/:id/data-loaded/')
async getDataLoaded(req: ExecutionRequest.Get): Promise<IWorkflowStatisticsDataLoaded> {
async getDataLoaded(req: StatisticsRequest.GetOne): Promise<IWorkflowStatisticsDataLoaded> {
// Get flag
const workflowId = req.params.id;

View File

@@ -23,7 +23,7 @@ import { parse, stringify } from 'flatted';
import {
ApplicationError,
type ExecutionStatus,
type IExecutionsSummary,
type ExecutionSummary,
type IRunExecutionData,
} from 'n8n-workflow';
import { BinaryDataService } from 'n8n-core';
@@ -41,6 +41,7 @@ import { ExecutionEntity } from '../entities/ExecutionEntity';
import { ExecutionMetadata } from '../entities/ExecutionMetadata';
import { ExecutionDataRepository } from './executionData.repository';
import { Logger } from '@/Logger';
import type { GetManyActiveFilter } from '@/executions/execution.types';
function parseFiltersToQueryBuilder(
qb: SelectQueryBuilder<ExecutionEntity>,
@@ -343,7 +344,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
excludedExecutionIds: string[],
accessibleWorkflowIds: string[],
additionalFilters?: { lastId?: string; firstId?: string },
): Promise<IExecutionsSummary[]> {
): Promise<ExecutionSummary[]> {
if (accessibleWorkflowIds.length === 0) {
return [];
}
@@ -657,6 +658,47 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
unflattenData: false,
});
}
async findIfAccessible(executionId: string, accessibleWorkflowIds: string[]) {
return await this.findSingleExecution(executionId, {
where: { workflowId: In(accessibleWorkflowIds) },
});
}
async getManyActive(
activeExecutionIds: string[],
accessibleWorkflowIds: string[],
filter?: GetManyActiveFilter,
) {
const where: FindOptionsWhere<ExecutionEntity> = {
id: In(activeExecutionIds),
status: Not(In(['finished', 'stopped', 'failed', 'crashed'] as ExecutionStatus[])),
};
if (filter) {
const { workflowId, status, finished } = filter;
if (workflowId && accessibleWorkflowIds.includes(workflowId)) {
where.workflowId = workflowId;
} else {
where.workflowId = In(accessibleWorkflowIds);
}
if (status) {
// @ts-ignore
where.status = In(status);
}
if (finished !== undefined) {
where.finished = finished;
}
} else {
where.workflowId = In(accessibleWorkflowIds);
}
return await this.findMultipleExecutions({
select: ['id', 'workflowId', 'mode', 'retryOf', 'startedAt', 'stoppedAt', 'status'],
order: { id: 'DESC' },
where,
});
}
}
export interface IGetExecutionsQueryFilter {

View File

@@ -0,0 +1,134 @@
import { Service } from 'typedi';
import { ActiveExecutions } from '@/ActiveExecutions';
import { Logger } from '@/Logger';
import { Queue } from '@/Queue';
import { WaitTracker } from '@/WaitTracker';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { getStatusUsingPreviousExecutionStatusMethod } from '@/executions/executionHelpers';
import config from '@/config';
import type { ExecutionSummary } from 'n8n-workflow';
import type { IExecutionBase, IExecutionsCurrentSummary } from '@/Interfaces';
import type { GetManyActiveFilter } from './execution.types';
@Service()
export class ActiveExecutionService {
constructor(
private readonly logger: Logger,
private readonly queue: Queue,
private readonly activeExecutions: ActiveExecutions,
private readonly executionRepository: ExecutionRepository,
private readonly waitTracker: WaitTracker,
) {}
private readonly isRegularMode = config.getEnv('executions.mode') === 'regular';
async findOne(executionId: string, accessibleWorkflowIds: string[]) {
return await this.executionRepository.findIfAccessible(executionId, accessibleWorkflowIds);
}
private toSummary(execution: IExecutionsCurrentSummary | IExecutionBase): ExecutionSummary {
return {
id: execution.id,
workflowId: execution.workflowId ?? '',
mode: execution.mode,
retryOf: execution.retryOf !== null ? execution.retryOf : undefined,
startedAt: new Date(execution.startedAt),
status: execution.status,
stoppedAt: 'stoppedAt' in execution ? execution.stoppedAt : undefined,
};
}
// ----------------------------------
// regular mode
// ----------------------------------
async findManyInRegularMode(
filter: GetManyActiveFilter,
accessibleWorkflowIds: string[],
): Promise<ExecutionSummary[]> {
return this.activeExecutions
.getActiveExecutions()
.filter(({ workflowId }) => {
if (filter.workflowId && filter.workflowId !== workflowId) return false;
if (workflowId && !accessibleWorkflowIds.includes(workflowId)) return false;
return true;
})
.map((execution) => this.toSummary(execution))
.sort((a, b) => Number(b.id) - Number(a.id));
}
// ----------------------------------
// queue mode
// ----------------------------------
async findManyInQueueMode(filter: GetManyActiveFilter, accessibleWorkflowIds: string[]) {
const activeManualExecutionIds = this.activeExecutions
.getActiveExecutions()
.map((execution) => execution.id);
const activeJobs = await this.queue.getJobs(['active', 'waiting']);
const activeProductionExecutionIds = activeJobs.map((job) => job.data.executionId);
const activeExecutionIds = activeProductionExecutionIds.concat(activeManualExecutionIds);
if (activeExecutionIds.length === 0) return [];
const activeExecutions = await this.executionRepository.getManyActive(
activeExecutionIds,
accessibleWorkflowIds,
filter,
);
return activeExecutions.map((execution) => {
if (!execution.status) {
// @tech-debt Status should never be nullish
execution.status = getStatusUsingPreviousExecutionStatusMethod(execution);
}
return this.toSummary(execution);
});
}
async stop(execution: IExecutionBase) {
const result = await this.activeExecutions.stopExecution(execution.id);
if (result) {
return {
mode: result.mode,
startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
status: result.status,
};
}
if (!this.isRegularMode) return await this.waitTracker.stopExecution(execution.id);
// queue mode
try {
return await this.waitTracker.stopExecution(execution.id);
} catch {}
const activeJobs = await this.queue.getJobs(['active', 'waiting']);
const job = activeJobs.find(({ data }) => data.executionId === execution.id);
if (!job) {
this.logger.debug('Could not stop job because it is no longer in queue', {
jobId: execution.id,
});
} else {
await this.queue.stopJob(job);
}
return {
mode: execution.mode,
startedAt: new Date(execution.startedAt),
stoppedAt: execution.stoppedAt ? new Date(execution.stoppedAt) : undefined,
finished: execution.finished,
status: execution.status,
};
}
}

View File

@@ -1,29 +0,0 @@
import type { IExecutionDeleteFilter } from '@/Interfaces';
import type { AuthenticatedRequest } from '@/requests';
export declare namespace ExecutionRequest {
namespace QueryParam {
type GetAll = {
filter: string; // '{ waitTill: string; finished: boolean, [other: string]: string }'
limit: string;
lastId: string;
firstId: string;
};
type GetAllCurrent = {
filter: string; // '{ workflowId: string }'
};
}
type GetAll = AuthenticatedRequest<{}, {}, {}, QueryParam.GetAll>;
type Get = AuthenticatedRequest<{ id: string }, {}, {}, { unflattedResponse: 'true' | 'false' }>;
type Delete = AuthenticatedRequest<{}, {}, IExecutionDeleteFilter>;
type Retry = AuthenticatedRequest<{ id: string }, {}, { loadWorkflow: boolean }, {}>;
type Stop = AuthenticatedRequest<{ id: string }>;
type GetAllCurrent = AuthenticatedRequest<{}, {}, {}, QueryParam.GetAllCurrent>;
}

View File

@@ -1,5 +1,5 @@
import { ExecutionService } from './execution.service';
import type { ExecutionRequest } from './execution.request';
import type { ExecutionRequest } from './execution.types';
import type { IExecutionResponse, IExecutionFlattedResponse } from '@/Interfaces';
import { EnterpriseWorkflowService } from '../workflows/workflow.service.ee';
import type { WorkflowWithSharingsAndCredentials } from '@/workflows/workflows.types';
@@ -14,11 +14,11 @@ export class EnterpriseExecutionsService {
private readonly enterpriseWorkflowService: EnterpriseWorkflowService,
) {}
async getExecution(
req: ExecutionRequest.Get,
async findOne(
req: ExecutionRequest.GetOne,
sharedWorkflowIds: string[],
): Promise<IExecutionResponse | IExecutionFlattedResponse | undefined> {
const execution = await this.executionService.getExecution(req, sharedWorkflowIds);
const execution = await this.executionService.findOne(req, sharedWorkflowIds);
if (!execution) return;

View File

@@ -21,7 +21,7 @@ import type {
} from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes';
import { Queue } from '@/Queue';
import type { ExecutionRequest } from './execution.request';
import type { ExecutionRequest } from './execution.types';
import { WorkflowRunner } from '@/WorkflowRunner';
import * as GenericHelpers from '@/GenericHelpers';
import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers';
@@ -78,15 +78,7 @@ export class ExecutionService {
private readonly nodeTypes: NodeTypes,
) {}
async getExecutionsList(req: ExecutionRequest.GetAll, sharedWorkflowIds: string[]) {
if (sharedWorkflowIds.length === 0) {
return {
count: 0,
estimated: false,
results: [],
};
}
async findMany(req: ExecutionRequest.GetMany, sharedWorkflowIds: string[]) {
// parse incoming filter object and remove non-valid fields
let filter: IGetExecutionsQueryFilter | undefined = undefined;
if (req.query.filter) {
@@ -160,8 +152,8 @@ export class ExecutionService {
};
}
async getExecution(
req: ExecutionRequest.Get,
async findOne(
req: ExecutionRequest.GetOne,
sharedWorkflowIds: string[],
): Promise<IExecutionResponse | IExecutionFlattedResponse | undefined> {
if (!sharedWorkflowIds.length) return undefined;
@@ -184,9 +176,7 @@ export class ExecutionService {
return execution;
}
async retryExecution(req: ExecutionRequest.Retry, sharedWorkflowIds: string[]) {
if (!sharedWorkflowIds.length) return false;
async retry(req: ExecutionRequest.Retry, sharedWorkflowIds: string[]) {
const { id: executionId } = req.params;
const execution = (await this.executionRepository.findIfShared(
executionId,
@@ -298,12 +288,7 @@ export class ExecutionService {
return !!executionData.finished;
}
async deleteExecutions(req: ExecutionRequest.Delete, sharedWorkflowIds: string[]) {
if (sharedWorkflowIds.length === 0) {
// return early since without shared workflows there can be no hits
// (note: getSharedWorkflowIds() returns _all_ workflow ids for global owners)
return;
}
async delete(req: ExecutionRequest.Delete, sharedWorkflowIds: string[]) {
const { deleteBefore, ids, filters: requestFiltersRaw } = req.body;
let requestFilters;
if (requestFiltersRaw) {

View File

@@ -0,0 +1,48 @@
import type { ExecutionEntity } from '@/databases/entities/ExecutionEntity';
import type { AuthenticatedRequest } from '@/requests';
import type { ExecutionStatus, IDataObject } from 'n8n-workflow';
export declare namespace ExecutionRequest {
namespace QueryParams {
type GetMany = {
filter: string; // '{ waitTill: string; finished: boolean, [other: string]: string }'
limit: string;
lastId: string;
firstId: string;
};
type GetOne = { unflattedResponse: 'true' | 'false' };
}
namespace BodyParams {
type DeleteFilter = {
deleteBefore?: Date;
filters?: IDataObject;
ids?: string[];
};
}
namespace RouteParams {
type ExecutionId = {
id: ExecutionEntity['id'];
};
}
type GetMany = AuthenticatedRequest<{}, {}, {}, QueryParams.GetMany>;
type GetOne = AuthenticatedRequest<RouteParams.ExecutionId, {}, {}, QueryParams.GetOne>;
type Delete = AuthenticatedRequest<{}, {}, BodyParams.DeleteFilter>;
type Retry = AuthenticatedRequest<RouteParams.ExecutionId, {}, { loadWorkflow: boolean }, {}>;
type Stop = AuthenticatedRequest<RouteParams.ExecutionId>;
type GetManyActive = AuthenticatedRequest<{}, {}, {}, { filter?: string }>;
}
export type GetManyActiveFilter = {
workflowId?: string;
status?: ExecutionStatus;
finished?: boolean;
};

View File

@@ -1,18 +1,26 @@
import { ExecutionRequest } from './execution.request';
import type { GetManyActiveFilter } from './execution.types';
import { ExecutionRequest } from './execution.types';
import { ExecutionService } from './execution.service';
import { Authorized, Get, Post, RestController } from '@/decorators';
import { EnterpriseExecutionsService } from './execution.service.ee';
import { isSharingEnabled } from '@/UserManagement/UserManagementHelper';
import { WorkflowSharingService } from '@/workflows/workflowSharing.service';
import type { User } from '@/databases/entities/User';
import config from '@/config';
import { jsonParse } from 'n8n-workflow';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { ActiveExecutionService } from './active-execution.service';
@Authorized()
@RestController('/executions')
export class ExecutionsController {
private readonly isQueueMode = config.getEnv('executions.mode') === 'queue';
constructor(
private readonly executionService: ExecutionService,
private readonly enterpriseExecutionService: EnterpriseExecutionsService,
private readonly workflowSharingService: WorkflowSharingService,
private readonly activeExecutionService: ActiveExecutionService,
) {}
private async getAccessibleWorkflowIds(user: User) {
@@ -22,32 +30,64 @@ export class ExecutionsController {
}
@Get('/')
async getExecutionsList(req: ExecutionRequest.GetAll) {
async getMany(req: ExecutionRequest.GetMany) {
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
return await this.executionService.getExecutionsList(req, workflowIds);
if (workflowIds.length === 0) return { count: 0, estimated: false, results: [] };
return await this.executionService.findMany(req, workflowIds);
}
@Get('/active')
async getActive(req: ExecutionRequest.GetManyActive) {
const filter = req.query.filter?.length ? jsonParse<GetManyActiveFilter>(req.query.filter) : {};
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
return this.isQueueMode
? await this.activeExecutionService.findManyInQueueMode(filter, workflowIds)
: await this.activeExecutionService.findManyInRegularMode(filter, workflowIds);
}
@Post('/active/:id/stop')
async stop(req: ExecutionRequest.Stop) {
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
const execution = await this.activeExecutionService.findOne(req.params.id, workflowIds);
if (!execution) throw new NotFoundError('Execution not found');
return await this.activeExecutionService.stop(execution);
}
@Get('/:id')
async getExecution(req: ExecutionRequest.Get) {
async getOne(req: ExecutionRequest.GetOne) {
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
return isSharingEnabled()
? await this.enterpriseExecutionService.getExecution(req, workflowIds)
: await this.executionService.getExecution(req, workflowIds);
? await this.enterpriseExecutionService.findOne(req, workflowIds)
: await this.executionService.findOne(req, workflowIds);
}
@Post('/:id/retry')
async retryExecution(req: ExecutionRequest.Retry) {
async retry(req: ExecutionRequest.Retry) {
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
return await this.executionService.retryExecution(req, workflowIds);
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
return await this.executionService.retry(req, workflowIds);
}
@Post('/delete')
async deleteExecutions(req: ExecutionRequest.Delete) {
async delete(req: ExecutionRequest.Delete) {
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
return await this.executionService.deleteExecutions(req, workflowIds);
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
return await this.executionService.delete(req, workflowIds);
}
}