feat(editor): Refactor and unify executions views (no-changelog) (#8538)

This commit is contained in:
Alex Grozav
2024-04-19 07:50:18 +02:00
committed by GitHub
parent eab01876ab
commit a3eea3ac5e
65 changed files with 3601 additions and 2960 deletions

View File

@@ -2,16 +2,19 @@ import { Service } from 'typedi';
import { validate as jsonSchemaValidate } from 'jsonschema';
import type {
IWorkflowBase,
JsonObject,
ExecutionError,
INode,
IRunExecutionData,
WorkflowExecuteMode,
ExecutionStatus,
} from 'n8n-workflow';
import {
ApplicationError,
ExecutionStatusList,
Workflow,
WorkflowOperationError,
} from 'n8n-workflow';
import { ApplicationError, jsonParse, Workflow, WorkflowOperationError } from 'n8n-workflow';
import { ActiveExecutions } from '@/ActiveExecutions';
import config from '@/config';
import type {
ExecutionPayload,
IExecutionFlattedResponse,
@@ -21,9 +24,8 @@ import type {
} from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes';
import { Queue } from '@/Queue';
import type { ExecutionRequest } from './execution.types';
import type { ExecutionRequest, ExecutionSummaries } from './execution.types';
import { WorkflowRunner } from '@/WorkflowRunner';
import * as GenericHelpers from '@/GenericHelpers';
import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers';
import type { IGetExecutionsQueryFilter } from '@db/repositories/execution.repository';
import { ExecutionRepository } from '@db/repositories/execution.repository';
@@ -31,8 +33,11 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { Logger } from '@/Logger';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import config from '@/config';
import { WaitTracker } from '@/WaitTracker';
import type { ExecutionEntity } from '@/databases/entities/ExecutionEntity';
const schemaGetExecutionsQueryFilter = {
export const schemaGetExecutionsQueryFilter = {
$id: '/IGetExecutionsQueryFilter',
type: 'object',
properties: {
@@ -65,7 +70,9 @@ const schemaGetExecutionsQueryFilter = {
},
};
const allowedExecutionsQueryFilterFields = Object.keys(schemaGetExecutionsQueryFilter.properties);
export const allowedExecutionsQueryFilterFields = Object.keys(
schemaGetExecutionsQueryFilter.properties,
);
@Service()
export class ExecutionService {
@@ -76,83 +83,10 @@ export class ExecutionService {
private readonly executionRepository: ExecutionRepository,
private readonly workflowRepository: WorkflowRepository,
private readonly nodeTypes: NodeTypes,
private readonly waitTracker: WaitTracker,
private readonly workflowRunner: WorkflowRunner,
) {}
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) {
try {
const filterJson: JsonObject = jsonParse(req.query.filter);
if (filterJson) {
Object.keys(filterJson).map((key) => {
if (!allowedExecutionsQueryFilterFields.includes(key)) delete filterJson[key];
});
if (jsonSchemaValidate(filterJson, schemaGetExecutionsQueryFilter).valid) {
filter = filterJson as IGetExecutionsQueryFilter;
}
}
} catch (error) {
this.logger.error('Failed to parse filter', {
userId: req.user.id,
filter: req.query.filter,
});
throw new InternalServerError('Parameter "filter" contained invalid JSON string.');
}
}
// safeguard against querying workflowIds not shared with the user
const workflowId = filter?.workflowId?.toString();
if (workflowId !== undefined && !sharedWorkflowIds.includes(workflowId)) {
this.logger.verbose(
`User ${req.user.id} attempted to query non-shared workflow ${workflowId}`,
);
return {
count: 0,
estimated: false,
results: [],
};
}
const limit = req.query.limit
? parseInt(req.query.limit, 10)
: GenericHelpers.DEFAULT_EXECUTIONS_GET_ALL_LIMIT;
const executingWorkflowIds: string[] = [];
if (config.getEnv('executions.mode') === 'queue') {
const currentJobs = await this.queue.getJobs(['active', 'waiting']);
executingWorkflowIds.push(...currentJobs.map(({ data }) => data.executionId));
}
// We may have manual executions even with queue so we must account for these.
executingWorkflowIds.push(...this.activeExecutions.getActiveExecutions().map(({ id }) => id));
const { count, estimated } = await this.executionRepository.countExecutions(
filter,
sharedWorkflowIds,
executingWorkflowIds,
req.user.hasGlobalScope('workflow:list'),
);
const formattedExecutions = await this.executionRepository.searchExecutions(
filter,
limit,
executingWorkflowIds,
sharedWorkflowIds,
{
lastId: req.query.lastId,
firstId: req.query.firstId,
},
);
return {
count,
results: formattedExecutions,
estimated,
};
}
async findOne(
req: ExecutionRequest.GetOne,
sharedWorkflowIds: string[],
@@ -384,4 +318,112 @@ export class ExecutionService {
await this.executionRepository.createNewExecution(fullExecutionData);
}
// ----------------------------------
// new API
// ----------------------------------
private readonly isRegularMode = config.getEnv('executions.mode') === 'regular';
/**
* Find summaries of executions that satisfy a query.
*
* Return also the total count of all executions that satisfy the query,
* and whether the total is an estimate or not.
*/
async findRangeWithCount(query: ExecutionSummaries.RangeQuery) {
const results = await this.executionRepository.findManyByRangeQuery(query);
if (config.getEnv('database.type') === 'postgresdb') {
const liveRows = await this.executionRepository.getLiveExecutionRowsOnPostgres();
if (liveRows === -1) return { count: -1, estimated: false, results };
if (liveRows > 100_000) {
// likely too high to fetch exact count fast
return { count: liveRows, estimated: true, results };
}
}
const { range: _, ...countQuery } = query;
const count = await this.executionRepository.fetchCount({ ...countQuery, kind: 'count' });
return { results, count, estimated: false };
}
/**
* Find summaries of active and finished executions that satisfy a query.
*
* Return also the total count of all finished executions that satisfy the query,
* and whether the total is an estimate or not. Active executions are excluded
* from the total and count for pagination purposes.
*/
async findAllRunningAndLatest(query: ExecutionSummaries.RangeQuery) {
const currentlyRunningStatuses: ExecutionStatus[] = ['new', 'running'];
const allStatuses = new Set(ExecutionStatusList);
currentlyRunningStatuses.forEach((status) => allStatuses.delete(status));
const notRunningStatuses: ExecutionStatus[] = Array.from(allStatuses);
const [activeResult, finishedResult] = await Promise.all([
this.findRangeWithCount({ ...query, status: currentlyRunningStatuses }),
this.findRangeWithCount({
...query,
status: notRunningStatuses,
order: { stoppedAt: 'DESC' },
}),
]);
return {
results: activeResult.results.concat(finishedResult.results),
count: finishedResult.count,
estimated: finishedResult.estimated,
};
}
/**
* Stop an active execution.
*/
async stop(executionId: string) {
const execution = await this.executionRepository.findOneBy({ id: executionId });
if (!execution) throw new NotFoundError('Execution not found');
const stopResult = await this.activeExecutions.stopExecution(execution.id);
if (stopResult) return this.toExecutionStopResult(execution);
if (this.isRegularMode) {
return await this.waitTracker.stopExecution(execution.id);
}
// queue mode
try {
return await this.waitTracker.stopExecution(execution.id);
} catch {
// @TODO: Why are we swallowing this error in queue mode?
}
const activeJobs = await this.queue.getJobs(['active', 'waiting']);
const job = activeJobs.find(({ data }) => data.executionId === execution.id);
if (job) {
await this.queue.stopJob(job);
} else {
this.logger.debug('Job to stop no longer in queue', { jobId: execution.id });
}
return this.toExecutionStopResult(execution);
}
private toExecutionStopResult(execution: ExecutionEntity) {
return {
mode: execution.mode,
startedAt: new Date(execution.startedAt),
stoppedAt: execution.stoppedAt ? new Date(execution.stoppedAt) : undefined,
finished: execution.finished,
status: execution.status,
};
}
}