mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Bring active executions into executions controller (no-changelog) (#8371)
This commit is contained in:
parent
913c8c6b0c
commit
49b52c4f1d
|
@ -16,11 +16,11 @@ describe('Current Workflow Executions', () => {
|
||||||
it('should render executions tab correctly', () => {
|
it('should render executions tab correctly', () => {
|
||||||
createMockExecutions();
|
createMockExecutions();
|
||||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||||
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
|
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
|
||||||
cy.wait(['@getExecutions', '@getCurrentExecutions']);
|
cy.wait(['@getExecutions', '@getActiveExecutions']);
|
||||||
|
|
||||||
executionsTab.getters.executionListItems().should('have.length', 11);
|
executionsTab.getters.executionListItems().should('have.length', 11);
|
||||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
||||||
|
@ -34,7 +34,7 @@ describe('Current Workflow Executions', () => {
|
||||||
|
|
||||||
it('should not redirect back to execution tab when request is not done before leaving the page', () => {
|
it('should not redirect back to execution tab when request is not done before leaving the page', () => {
|
||||||
cy.intercept('GET', '/rest/executions?filter=*');
|
cy.intercept('GET', '/rest/executions?filter=*');
|
||||||
cy.intercept('GET', '/rest/executions-current?filter=*');
|
cy.intercept('GET', '/rest/executions/active?filter=*');
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
executionsTab.actions.switchToEditorTab();
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
@ -63,7 +63,7 @@ describe('Current Workflow Executions', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
|
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
|
||||||
cy.intercept('GET', '/rest/executions-current?filter=*', throttleResponse);
|
cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse);
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
executionsTab.actions.switchToEditorTab();
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
|
|
@ -19,7 +19,7 @@ describe('Debug', () => {
|
||||||
it('should be able to debug executions', () => {
|
it('should be able to debug executions', () => {
|
||||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||||
cy.intercept('GET', '/rest/executions/*').as('getExecution');
|
cy.intercept('GET', '/rest/executions/*').as('getExecution');
|
||||||
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
|
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
|
||||||
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
|
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
|
||||||
|
|
||||||
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
|
||||||
|
@ -41,7 +41,7 @@ describe('Debug', () => {
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
|
||||||
cy.wait(['@getExecutions', '@getCurrentExecutions']);
|
cy.wait(['@getExecutions', '@getActiveExecutions']);
|
||||||
|
|
||||||
executionsTab.getters.executionDebugButton().should('have.text', 'Debug in editor').click();
|
executionsTab.getters.executionDebugButton().should('have.text', 'Debug in editor').click();
|
||||||
cy.url().should('include', '/debug');
|
cy.url().should('include', '/debug');
|
||||||
|
@ -66,7 +66,7 @@ describe('Debug', () => {
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
|
||||||
cy.wait(['@getExecutions', '@getCurrentExecutions']);
|
cy.wait(['@getExecutions', '@getActiveExecutions']);
|
||||||
|
|
||||||
executionsTab.getters.executionListItems().should('have.length', 2).first().click();
|
executionsTab.getters.executionListItems().should('have.length', 2).first().click();
|
||||||
cy.wait(['@getExecution']);
|
cy.wait(['@getExecution']);
|
||||||
|
@ -77,7 +77,7 @@ describe('Debug', () => {
|
||||||
confirmDialog.find('li').should('have.length', 2);
|
confirmDialog.find('li').should('have.length', 2);
|
||||||
confirmDialog.get('.btn--cancel').click();
|
confirmDialog.get('.btn--cancel').click();
|
||||||
|
|
||||||
cy.wait(['@getExecutions', '@getCurrentExecutions']);
|
cy.wait(['@getExecutions', '@getActiveExecutions']);
|
||||||
|
|
||||||
executionsTab.getters.executionListItems().should('have.length', 2).first().click();
|
executionsTab.getters.executionListItems().should('have.length', 2).first().click();
|
||||||
cy.wait(['@getExecution']);
|
cy.wait(['@getExecution']);
|
||||||
|
@ -108,7 +108,7 @@ describe('Debug', () => {
|
||||||
cy.url().should('not.include', '/debug');
|
cy.url().should('not.include', '/debug');
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
cy.wait(['@getExecutions', '@getCurrentExecutions']);
|
cy.wait(['@getExecutions', '@getActiveExecutions']);
|
||||||
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();
|
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();
|
||||||
|
|
||||||
confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible');
|
confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible');
|
||||||
|
@ -130,7 +130,7 @@ describe('Debug', () => {
|
||||||
workflowPage.actions.deleteNode(IF_NODE_NAME);
|
workflowPage.actions.deleteNode(IF_NODE_NAME);
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
cy.wait(['@getExecutions', '@getCurrentExecutions']);
|
cy.wait(['@getExecutions', '@getActiveExecutions']);
|
||||||
executionsTab.getters.executionListItems().should('have.length', 3).first().click();
|
executionsTab.getters.executionListItems().should('have.length', 3).first().click();
|
||||||
cy.wait(['@getExecution']);
|
cy.wait(['@getExecution']);
|
||||||
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();
|
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();
|
||||||
|
|
|
@ -136,10 +136,10 @@ describe('Editor actions should work', () => {
|
||||||
|
|
||||||
it('after switching between Editor and Executions', () => {
|
it('after switching between Editor and Executions', () => {
|
||||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||||
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
|
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
cy.wait(['@getExecutions', '@getCurrentExecutions']);
|
cy.wait(['@getExecutions', '@getActiveExecutions']);
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
executionsTab.actions.switchToEditorTab();
|
executionsTab.actions.switchToEditorTab();
|
||||||
editWorkflowAndDeactivate();
|
editWorkflowAndDeactivate();
|
||||||
|
@ -149,7 +149,7 @@ describe('Editor actions should work', () => {
|
||||||
it('after switching between Editor and Debug', () => {
|
it('after switching between Editor and Debug', () => {
|
||||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||||
cy.intercept('GET', '/rest/executions/*').as('getExecution');
|
cy.intercept('GET', '/rest/executions/*').as('getExecution');
|
||||||
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
|
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
|
||||||
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
|
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
|
||||||
|
|
||||||
editWorkflowAndDeactivate();
|
editWorkflowAndDeactivate();
|
||||||
|
@ -157,7 +157,7 @@ describe('Editor actions should work', () => {
|
||||||
cy.wait(['@postWorkflowRun']);
|
cy.wait(['@postWorkflowRun']);
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
cy.wait(['@getExecutions', '@getCurrentExecutions']);
|
cy.wait(['@getExecutions', '@getActiveExecutions']);
|
||||||
|
|
||||||
executionsTab.getters.executionListItems().should('have.length', 1).first().click();
|
executionsTab.getters.executionListItems().should('have.length', 1).first().click();
|
||||||
cy.wait(['@getExecution']);
|
cy.wait(['@getExecution']);
|
||||||
|
|
|
@ -259,7 +259,7 @@ describe('Workflow Actions', () => {
|
||||||
|
|
||||||
it('should keep endpoint click working when switching between execution and editor tab', () => {
|
it('should keep endpoint click working when switching between execution and editor tab', () => {
|
||||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||||
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
|
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
|
||||||
|
|
||||||
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
|
@ -270,7 +270,7 @@ describe('Workflow Actions', () => {
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
cy.wait(['@getExecutions', '@getCurrentExecutions']);
|
cy.wait(['@getExecutions', '@getActiveExecutions']);
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
executionsTab.actions.switchToEditorTab();
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import type {
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
IExecutionsSummary,
|
ExecutionSummary,
|
||||||
FeatureFlags,
|
FeatureFlags,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
IUserSettings,
|
IUserSettings,
|
||||||
|
@ -170,8 +170,7 @@ export interface IExecutionFlattedResponse extends IExecutionFlatted {
|
||||||
|
|
||||||
export interface IExecutionsListResponse {
|
export interface IExecutionsListResponse {
|
||||||
count: number;
|
count: number;
|
||||||
// results: IExecutionShortResponse[];
|
results: ExecutionSummary[];
|
||||||
results: IExecutionsSummary[];
|
|
||||||
estimated: boolean;
|
estimated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,12 +191,6 @@ export interface IExecutionsCurrentSummary {
|
||||||
status?: ExecutionStatus;
|
status?: ExecutionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExecutionDeleteFilter {
|
|
||||||
deleteBefore?: Date;
|
|
||||||
filters?: IDataObject;
|
|
||||||
ids?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IExecutingWorkflowData {
|
export interface IExecutingWorkflowData {
|
||||||
executionData: IWorkflowExecutionDataProcess;
|
executionData: IWorkflowExecutionDataProcess;
|
||||||
process?: ChildProcess;
|
process?: ChildProcess;
|
||||||
|
|
|
@ -14,13 +14,10 @@ import cookieParser from 'cookie-parser';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { engine as expressHandlebars } from 'express-handlebars';
|
import { engine as expressHandlebars } from 'express-handlebars';
|
||||||
import type { ServeStaticOptions } from 'serve-static';
|
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 Class, InstanceSettings } from 'n8n-core';
|
||||||
|
|
||||||
import type { ExecutionStatus, IExecutionsSummary, IN8nUISettings } from 'n8n-workflow';
|
import type { IN8nUISettings } from 'n8n-workflow';
|
||||||
import { jsonParse } from 'n8n-workflow';
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import timezones from 'google-timezones-json';
|
import timezones from 'google-timezones-json';
|
||||||
|
@ -39,7 +36,6 @@ import {
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { credentialsController } from '@/credentials/credentials.controller';
|
import { credentialsController } from '@/credentials/credentials.controller';
|
||||||
import type { CurlHelper } from '@/requests';
|
import type { CurlHelper } from '@/requests';
|
||||||
import type { ExecutionRequest } from '@/executions/execution.request';
|
|
||||||
import { registerController } from '@/decorators';
|
import { registerController } from '@/decorators';
|
||||||
import { AuthController } from '@/controllers/auth.controller';
|
import { AuthController } from '@/controllers/auth.controller';
|
||||||
import { BinaryDataController } from '@/controllers/binaryData.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 { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee';
|
||||||
import { ExecutionsController } from '@/executions/executions.controller';
|
import { ExecutionsController } from '@/executions/executions.controller';
|
||||||
import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi';
|
import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi';
|
||||||
import type { ICredentialsOverwrite, IDiagnosticInfo, IExecutionsStopData } from '@/Interfaces';
|
import type { ICredentialsOverwrite, IDiagnosticInfo } from '@/Interfaces';
|
||||||
import { ActiveExecutions } from '@/ActiveExecutions';
|
import { ActiveExecutions } from '@/ActiveExecutions';
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
|
@ -76,7 +72,6 @@ import { PostHogClient } from './posthog';
|
||||||
import { eventBus } from './eventbus';
|
import { eventBus } from './eventbus';
|
||||||
import { InternalHooks } from './InternalHooks';
|
import { InternalHooks } from './InternalHooks';
|
||||||
import { License } from './License';
|
import { License } from './License';
|
||||||
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
|
|
||||||
import { SamlController } from './sso/saml/routes/saml.controller.ee';
|
import { SamlController } from './sso/saml/routes/saml.controller.ee';
|
||||||
import { SamlService } from './sso/saml/saml.service.ee';
|
import { SamlService } from './sso/saml/saml.service.ee';
|
||||||
import { VariablesController } from './environments/variables/variables.controller.ee';
|
import { VariablesController } from './environments/variables/variables.controller.ee';
|
||||||
|
@ -87,8 +82,6 @@ import {
|
||||||
import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee';
|
import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee';
|
||||||
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.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 { WorkflowRepository } from '@db/repositories/workflow.repository';
|
||||||
|
|
||||||
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
|
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
|
||||||
|
@ -100,9 +93,7 @@ import { InvitationController } from './controllers/invitation.controller';
|
||||||
import { CollaborationService } from './collaboration/collaboration.service';
|
import { CollaborationService } from './collaboration/collaboration.service';
|
||||||
import { RoleController } from './controllers/role.controller';
|
import { RoleController } from './controllers/role.controller';
|
||||||
import { BadRequestError } from './errors/response-errors/bad-request.error';
|
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 { OrchestrationService } from '@/services/orchestration.service';
|
||||||
import { WorkflowSharingService } from './workflows/workflowSharing.service';
|
|
||||||
|
|
||||||
const exec = promisify(callbackExec);
|
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
|
// Options
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import type { ExecutionRequest } from '@/executions/execution.types';
|
||||||
|
|
||||||
|
export namespace StatisticsRequest {
|
||||||
|
export type GetOne = ExecutionRequest.GetOne;
|
||||||
|
}
|
|
@ -4,10 +4,10 @@ import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
|
||||||
import { StatisticsNames } from '@db/entities/WorkflowStatistics';
|
import { StatisticsNames } from '@db/entities/WorkflowStatistics';
|
||||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||||
import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository';
|
import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository';
|
||||||
import { ExecutionRequest } from '@/executions/execution.request';
|
|
||||||
import type { IWorkflowStatisticsDataLoaded } from '@/Interfaces';
|
import type { IWorkflowStatisticsDataLoaded } from '@/Interfaces';
|
||||||
import { Logger } from '@/Logger';
|
import { Logger } from '@/Logger';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
|
import { StatisticsRequest } from './workflow-statistics.types';
|
||||||
|
|
||||||
interface WorkflowStatisticsData<T> {
|
interface WorkflowStatisticsData<T> {
|
||||||
productionSuccess: T;
|
productionSuccess: T;
|
||||||
|
@ -29,7 +29,7 @@ export class WorkflowStatisticsController {
|
||||||
*/
|
*/
|
||||||
// TODO: move this into a new decorator `@ValidateWorkflowPermission`
|
// TODO: move this into a new decorator `@ValidateWorkflowPermission`
|
||||||
@Middleware()
|
@Middleware()
|
||||||
async hasWorkflowAccess(req: ExecutionRequest.Get, res: Response, next: NextFunction) {
|
async hasWorkflowAccess(req: StatisticsRequest.GetOne, res: Response, next: NextFunction) {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const workflowId = req.params.id;
|
const workflowId = req.params.id;
|
||||||
|
|
||||||
|
@ -48,17 +48,17 @@ export class WorkflowStatisticsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/:id/counts/')
|
@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);
|
return await this.getData(req.params.id, 'count', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/:id/times/')
|
@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);
|
return await this.getData(req.params.id, 'latestEvent', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/:id/data-loaded/')
|
@Get('/:id/data-loaded/')
|
||||||
async getDataLoaded(req: ExecutionRequest.Get): Promise<IWorkflowStatisticsDataLoaded> {
|
async getDataLoaded(req: StatisticsRequest.GetOne): Promise<IWorkflowStatisticsDataLoaded> {
|
||||||
// Get flag
|
// Get flag
|
||||||
const workflowId = req.params.id;
|
const workflowId = req.params.id;
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { parse, stringify } from 'flatted';
|
||||||
import {
|
import {
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
type ExecutionStatus,
|
type ExecutionStatus,
|
||||||
type IExecutionsSummary,
|
type ExecutionSummary,
|
||||||
type IRunExecutionData,
|
type IRunExecutionData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { BinaryDataService } from 'n8n-core';
|
import { BinaryDataService } from 'n8n-core';
|
||||||
|
@ -41,6 +41,7 @@ import { ExecutionEntity } from '../entities/ExecutionEntity';
|
||||||
import { ExecutionMetadata } from '../entities/ExecutionMetadata';
|
import { ExecutionMetadata } from '../entities/ExecutionMetadata';
|
||||||
import { ExecutionDataRepository } from './executionData.repository';
|
import { ExecutionDataRepository } from './executionData.repository';
|
||||||
import { Logger } from '@/Logger';
|
import { Logger } from '@/Logger';
|
||||||
|
import type { GetManyActiveFilter } from '@/executions/execution.types';
|
||||||
|
|
||||||
function parseFiltersToQueryBuilder(
|
function parseFiltersToQueryBuilder(
|
||||||
qb: SelectQueryBuilder<ExecutionEntity>,
|
qb: SelectQueryBuilder<ExecutionEntity>,
|
||||||
|
@ -343,7 +344,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
excludedExecutionIds: string[],
|
excludedExecutionIds: string[],
|
||||||
accessibleWorkflowIds: string[],
|
accessibleWorkflowIds: string[],
|
||||||
additionalFilters?: { lastId?: string; firstId?: string },
|
additionalFilters?: { lastId?: string; firstId?: string },
|
||||||
): Promise<IExecutionsSummary[]> {
|
): Promise<ExecutionSummary[]> {
|
||||||
if (accessibleWorkflowIds.length === 0) {
|
if (accessibleWorkflowIds.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -657,6 +658,47 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
unflattenData: false,
|
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 {
|
export interface IGetExecutionsQueryFilter {
|
||||||
|
|
134
packages/cli/src/executions/active-execution.service.ts
Normal file
134
packages/cli/src/executions/active-execution.service.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>;
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ExecutionService } from './execution.service';
|
import { ExecutionService } from './execution.service';
|
||||||
import type { ExecutionRequest } from './execution.request';
|
import type { ExecutionRequest } from './execution.types';
|
||||||
import type { IExecutionResponse, IExecutionFlattedResponse } from '@/Interfaces';
|
import type { IExecutionResponse, IExecutionFlattedResponse } from '@/Interfaces';
|
||||||
import { EnterpriseWorkflowService } from '../workflows/workflow.service.ee';
|
import { EnterpriseWorkflowService } from '../workflows/workflow.service.ee';
|
||||||
import type { WorkflowWithSharingsAndCredentials } from '@/workflows/workflows.types';
|
import type { WorkflowWithSharingsAndCredentials } from '@/workflows/workflows.types';
|
||||||
|
@ -14,11 +14,11 @@ export class EnterpriseExecutionsService {
|
||||||
private readonly enterpriseWorkflowService: EnterpriseWorkflowService,
|
private readonly enterpriseWorkflowService: EnterpriseWorkflowService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getExecution(
|
async findOne(
|
||||||
req: ExecutionRequest.Get,
|
req: ExecutionRequest.GetOne,
|
||||||
sharedWorkflowIds: string[],
|
sharedWorkflowIds: string[],
|
||||||
): Promise<IExecutionResponse | IExecutionFlattedResponse | undefined> {
|
): Promise<IExecutionResponse | IExecutionFlattedResponse | undefined> {
|
||||||
const execution = await this.executionService.getExecution(req, sharedWorkflowIds);
|
const execution = await this.executionService.findOne(req, sharedWorkflowIds);
|
||||||
|
|
||||||
if (!execution) return;
|
if (!execution) return;
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import type {
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { Queue } from '@/Queue';
|
import { Queue } from '@/Queue';
|
||||||
import type { ExecutionRequest } from './execution.request';
|
import type { ExecutionRequest } from './execution.types';
|
||||||
import { WorkflowRunner } from '@/WorkflowRunner';
|
import { WorkflowRunner } from '@/WorkflowRunner';
|
||||||
import * as GenericHelpers from '@/GenericHelpers';
|
import * as GenericHelpers from '@/GenericHelpers';
|
||||||
import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers';
|
import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers';
|
||||||
|
@ -78,15 +78,7 @@ export class ExecutionService {
|
||||||
private readonly nodeTypes: NodeTypes,
|
private readonly nodeTypes: NodeTypes,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getExecutionsList(req: ExecutionRequest.GetAll, sharedWorkflowIds: string[]) {
|
async findMany(req: ExecutionRequest.GetMany, sharedWorkflowIds: string[]) {
|
||||||
if (sharedWorkflowIds.length === 0) {
|
|
||||||
return {
|
|
||||||
count: 0,
|
|
||||||
estimated: false,
|
|
||||||
results: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse incoming filter object and remove non-valid fields
|
// parse incoming filter object and remove non-valid fields
|
||||||
let filter: IGetExecutionsQueryFilter | undefined = undefined;
|
let filter: IGetExecutionsQueryFilter | undefined = undefined;
|
||||||
if (req.query.filter) {
|
if (req.query.filter) {
|
||||||
|
@ -160,8 +152,8 @@ export class ExecutionService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getExecution(
|
async findOne(
|
||||||
req: ExecutionRequest.Get,
|
req: ExecutionRequest.GetOne,
|
||||||
sharedWorkflowIds: string[],
|
sharedWorkflowIds: string[],
|
||||||
): Promise<IExecutionResponse | IExecutionFlattedResponse | undefined> {
|
): Promise<IExecutionResponse | IExecutionFlattedResponse | undefined> {
|
||||||
if (!sharedWorkflowIds.length) return undefined;
|
if (!sharedWorkflowIds.length) return undefined;
|
||||||
|
@ -184,9 +176,7 @@ export class ExecutionService {
|
||||||
return execution;
|
return execution;
|
||||||
}
|
}
|
||||||
|
|
||||||
async retryExecution(req: ExecutionRequest.Retry, sharedWorkflowIds: string[]) {
|
async retry(req: ExecutionRequest.Retry, sharedWorkflowIds: string[]) {
|
||||||
if (!sharedWorkflowIds.length) return false;
|
|
||||||
|
|
||||||
const { id: executionId } = req.params;
|
const { id: executionId } = req.params;
|
||||||
const execution = (await this.executionRepository.findIfShared(
|
const execution = (await this.executionRepository.findIfShared(
|
||||||
executionId,
|
executionId,
|
||||||
|
@ -298,12 +288,7 @@ export class ExecutionService {
|
||||||
return !!executionData.finished;
|
return !!executionData.finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteExecutions(req: ExecutionRequest.Delete, sharedWorkflowIds: string[]) {
|
async delete(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;
|
|
||||||
}
|
|
||||||
const { deleteBefore, ids, filters: requestFiltersRaw } = req.body;
|
const { deleteBefore, ids, filters: requestFiltersRaw } = req.body;
|
||||||
let requestFilters;
|
let requestFilters;
|
||||||
if (requestFiltersRaw) {
|
if (requestFiltersRaw) {
|
||||||
|
|
48
packages/cli/src/executions/execution.types.ts
Normal file
48
packages/cli/src/executions/execution.types.ts
Normal 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;
|
||||||
|
};
|
|
@ -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 { ExecutionService } from './execution.service';
|
||||||
import { Authorized, Get, Post, RestController } from '@/decorators';
|
import { Authorized, Get, Post, RestController } from '@/decorators';
|
||||||
import { EnterpriseExecutionsService } from './execution.service.ee';
|
import { EnterpriseExecutionsService } from './execution.service.ee';
|
||||||
import { isSharingEnabled } from '@/UserManagement/UserManagementHelper';
|
import { isSharingEnabled } from '@/UserManagement/UserManagementHelper';
|
||||||
import { WorkflowSharingService } from '@/workflows/workflowSharing.service';
|
import { WorkflowSharingService } from '@/workflows/workflowSharing.service';
|
||||||
import type { User } from '@/databases/entities/User';
|
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()
|
@Authorized()
|
||||||
@RestController('/executions')
|
@RestController('/executions')
|
||||||
export class ExecutionsController {
|
export class ExecutionsController {
|
||||||
|
private readonly isQueueMode = config.getEnv('executions.mode') === 'queue';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly executionService: ExecutionService,
|
private readonly executionService: ExecutionService,
|
||||||
private readonly enterpriseExecutionService: EnterpriseExecutionsService,
|
private readonly enterpriseExecutionService: EnterpriseExecutionsService,
|
||||||
private readonly workflowSharingService: WorkflowSharingService,
|
private readonly workflowSharingService: WorkflowSharingService,
|
||||||
|
private readonly activeExecutionService: ActiveExecutionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async getAccessibleWorkflowIds(user: User) {
|
private async getAccessibleWorkflowIds(user: User) {
|
||||||
|
@ -22,32 +30,64 @@ export class ExecutionsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/')
|
@Get('/')
|
||||||
async getExecutionsList(req: ExecutionRequest.GetAll) {
|
async getMany(req: ExecutionRequest.GetMany) {
|
||||||
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
|
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')
|
@Get('/:id')
|
||||||
async getExecution(req: ExecutionRequest.Get) {
|
async getOne(req: ExecutionRequest.GetOne) {
|
||||||
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
|
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
|
||||||
|
|
||||||
|
if (workflowIds.length === 0) throw new NotFoundError('Execution not found');
|
||||||
|
|
||||||
return isSharingEnabled()
|
return isSharingEnabled()
|
||||||
? await this.enterpriseExecutionService.getExecution(req, workflowIds)
|
? await this.enterpriseExecutionService.findOne(req, workflowIds)
|
||||||
: await this.executionService.getExecution(req, workflowIds);
|
: await this.executionService.findOne(req, workflowIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/:id/retry')
|
@Post('/:id/retry')
|
||||||
async retryExecution(req: ExecutionRequest.Retry) {
|
async retry(req: ExecutionRequest.Retry) {
|
||||||
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
|
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')
|
@Post('/delete')
|
||||||
async deleteExecutions(req: ExecutionRequest.Delete) {
|
async delete(req: ExecutionRequest.Delete) {
|
||||||
const workflowIds = await this.getAccessibleWorkflowIds(req.user);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
127
packages/cli/test/unit/active-execution.service.test.ts
Normal file
127
packages/cli/test/unit/active-execution.service.test.ts
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import { mock, mockFn } from 'jest-mock-extended';
|
||||||
|
import { ActiveExecutionService } from '@/executions/active-execution.service';
|
||||||
|
import config from '@/config';
|
||||||
|
import type { ExecutionRepository } from '@db/repositories/execution.repository';
|
||||||
|
import type { ActiveExecutions } from '@/ActiveExecutions';
|
||||||
|
import type { Job, Queue } from '@/Queue';
|
||||||
|
import type { IExecutionBase, IExecutionsCurrentSummary } from '@/Interfaces';
|
||||||
|
import type { WaitTracker } from '@/WaitTracker';
|
||||||
|
|
||||||
|
describe('ActiveExecutionsService', () => {
|
||||||
|
const queue = mock<Queue>();
|
||||||
|
const activeExecutions = mock<ActiveExecutions>();
|
||||||
|
const executionRepository = mock<ExecutionRepository>();
|
||||||
|
const waitTracker = mock<WaitTracker>();
|
||||||
|
|
||||||
|
const jobIds = ['j1', 'j2'];
|
||||||
|
const jobs = jobIds.map((executionId) => mock<Job>({ data: { executionId } }));
|
||||||
|
|
||||||
|
const activeExecutionService = new ActiveExecutionService(
|
||||||
|
mock(),
|
||||||
|
queue,
|
||||||
|
activeExecutions,
|
||||||
|
executionRepository,
|
||||||
|
waitTracker,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getEnv = mockFn<(typeof config)['getEnv']>();
|
||||||
|
config.getEnv = getEnv;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stop()', () => {
|
||||||
|
describe('in regular mode', () => {
|
||||||
|
getEnv.calledWith('executions.mode').mockReturnValue('regular');
|
||||||
|
|
||||||
|
it('should call `ActiveExecutions.stopExecution()`', async () => {
|
||||||
|
const execution = mock<IExecutionBase>({ id: '123' });
|
||||||
|
|
||||||
|
await activeExecutionService.stop(execution);
|
||||||
|
|
||||||
|
expect(activeExecutions.stopExecution).toHaveBeenCalledWith(execution.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call `WaitTracker.stopExecution()` if `ActiveExecutions.stopExecution()` found no execution', async () => {
|
||||||
|
activeExecutions.stopExecution.mockResolvedValue(undefined);
|
||||||
|
const execution = mock<IExecutionBase>({ id: '123' });
|
||||||
|
|
||||||
|
await activeExecutionService.stop(execution);
|
||||||
|
|
||||||
|
expect(waitTracker.stopExecution).toHaveBeenCalledWith(execution.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('in queue mode', () => {
|
||||||
|
it('should call `ActiveExecutions.stopExecution()`', async () => {
|
||||||
|
const execution = mock<IExecutionBase>({ id: '123' });
|
||||||
|
|
||||||
|
await activeExecutionService.stop(execution);
|
||||||
|
|
||||||
|
expect(activeExecutions.stopExecution).toHaveBeenCalledWith(execution.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call `WaitTracker.stopExecution` if `ActiveExecutions.stopExecution()` found no execution', async () => {
|
||||||
|
activeExecutions.stopExecution.mockResolvedValue(undefined);
|
||||||
|
const execution = mock<IExecutionBase>({ id: '123' });
|
||||||
|
|
||||||
|
await activeExecutionService.stop(execution);
|
||||||
|
|
||||||
|
expect(waitTracker.stopExecution).toHaveBeenCalledWith(execution.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findManyInQueueMode()', () => {
|
||||||
|
it('should query for active jobs, waiting jobs, and in-memory executions', async () => {
|
||||||
|
const sharedWorkflowIds = ['123'];
|
||||||
|
const filter = {};
|
||||||
|
const executionIds = ['e1', 'e2'];
|
||||||
|
const summaries = executionIds.map((e) => mock<IExecutionsCurrentSummary>({ id: e }));
|
||||||
|
|
||||||
|
activeExecutions.getActiveExecutions.mockReturnValue(summaries);
|
||||||
|
queue.getJobs.mockResolvedValue(jobs);
|
||||||
|
executionRepository.findMultipleExecutions.mockResolvedValue([]);
|
||||||
|
executionRepository.getManyActive.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await activeExecutionService.findManyInQueueMode(filter, sharedWorkflowIds);
|
||||||
|
|
||||||
|
expect(queue.getJobs).toHaveBeenCalledWith(['active', 'waiting']);
|
||||||
|
|
||||||
|
expect(executionRepository.getManyActive).toHaveBeenCalledWith(
|
||||||
|
jobIds.concat(executionIds),
|
||||||
|
sharedWorkflowIds,
|
||||||
|
filter,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findManyInRegularMode()', () => {
|
||||||
|
it('should return summaries of in-memory executions', async () => {
|
||||||
|
const sharedWorkflowIds = ['123'];
|
||||||
|
const filter = {};
|
||||||
|
const executionIds = ['e1', 'e2'];
|
||||||
|
const summaries = executionIds.map((e) =>
|
||||||
|
mock<IExecutionsCurrentSummary>({ id: e, workflowId: '123', status: 'running' }),
|
||||||
|
);
|
||||||
|
|
||||||
|
activeExecutions.getActiveExecutions.mockReturnValue(summaries);
|
||||||
|
|
||||||
|
const result = await activeExecutionService.findManyInRegularMode(filter, sharedWorkflowIds);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'e1',
|
||||||
|
workflowId: '123',
|
||||||
|
status: 'running',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'e2',
|
||||||
|
workflowId: '123',
|
||||||
|
status: 'running',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { mock, mockFn } from 'jest-mock-extended';
|
||||||
|
import config from '@/config';
|
||||||
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
|
import { ExecutionsController } from '@/executions/executions.controller';
|
||||||
|
import { License } from '@/License';
|
||||||
|
import { mockInstance } from '../../shared/mocking';
|
||||||
|
import type { IExecutionBase } from '@/Interfaces';
|
||||||
|
import type { ActiveExecutionService } from '@/executions/active-execution.service';
|
||||||
|
import type { ExecutionRequest } from '@/executions/execution.types';
|
||||||
|
import type { WorkflowSharingService } from '@/workflows/workflowSharing.service';
|
||||||
|
|
||||||
|
describe('ExecutionsController', () => {
|
||||||
|
const getEnv = mockFn<(typeof config)['getEnv']>();
|
||||||
|
config.getEnv = getEnv;
|
||||||
|
|
||||||
|
mockInstance(License);
|
||||||
|
const activeExecutionService = mock<ActiveExecutionService>();
|
||||||
|
const workflowSharingService = mock<WorkflowSharingService>();
|
||||||
|
|
||||||
|
const req = mock<ExecutionRequest.GetManyActive>({ query: { filter: '{}' } });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getActive()', () => {
|
||||||
|
workflowSharingService.getSharedWorkflowIds.mockResolvedValue(['123']);
|
||||||
|
|
||||||
|
it('should call `ActiveExecutionService.findManyInQueueMode()`', async () => {
|
||||||
|
getEnv.calledWith('executions.mode').mockReturnValue('queue');
|
||||||
|
|
||||||
|
await new ExecutionsController(
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
workflowSharingService,
|
||||||
|
activeExecutionService,
|
||||||
|
).getActive(req);
|
||||||
|
|
||||||
|
expect(activeExecutionService.findManyInQueueMode).toHaveBeenCalled();
|
||||||
|
expect(activeExecutionService.findManyInRegularMode).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call `ActiveExecutionService.findManyInRegularMode()`', async () => {
|
||||||
|
getEnv.calledWith('executions.mode').mockReturnValue('regular');
|
||||||
|
|
||||||
|
await new ExecutionsController(
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
workflowSharingService,
|
||||||
|
activeExecutionService,
|
||||||
|
).getActive(req);
|
||||||
|
|
||||||
|
expect(activeExecutionService.findManyInQueueMode).not.toHaveBeenCalled();
|
||||||
|
expect(activeExecutionService.findManyInRegularMode).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stop()', () => {
|
||||||
|
const req = mock<ExecutionRequest.Stop>({ params: { id: '999' } });
|
||||||
|
const execution = mock<IExecutionBase>();
|
||||||
|
|
||||||
|
it('should 404 when execution is not found or inaccessible for user', async () => {
|
||||||
|
activeExecutionService.findOne.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const promise = new ExecutionsController(
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
workflowSharingService,
|
||||||
|
activeExecutionService,
|
||||||
|
).stop(req);
|
||||||
|
|
||||||
|
await expect(promise).rejects.toThrow(NotFoundError);
|
||||||
|
expect(activeExecutionService.findOne).toHaveBeenCalledWith('999', ['123']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call `ActiveExecutionService.stop()`', async () => {
|
||||||
|
getEnv.calledWith('executions.mode').mockReturnValue('regular');
|
||||||
|
activeExecutionService.findOne.mockResolvedValue(execution);
|
||||||
|
|
||||||
|
await new ExecutionsController(
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
workflowSharingService,
|
||||||
|
activeExecutionService,
|
||||||
|
).stop(req);
|
||||||
|
|
||||||
|
expect(activeExecutionService.stop).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -27,8 +27,8 @@ export async function getActiveWorkflows(context: IRestApiContext) {
|
||||||
return await makeRestApiRequest(context, 'GET', '/active-workflows');
|
return await makeRestApiRequest(context, 'GET', '/active-workflows');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCurrentExecutions(context: IRestApiContext, filter: IDataObject) {
|
export async function getActiveExecutions(context: IRestApiContext, filter: IDataObject) {
|
||||||
return await makeRestApiRequest(context, 'GET', '/executions-current', { filter });
|
return await makeRestApiRequest(context, 'GET', '/executions/active', { filter });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExecutions(
|
export async function getExecutions(
|
||||||
|
|
|
@ -553,7 +553,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
async loadActiveExecutions(): Promise<void> {
|
async loadActiveExecutions(): Promise<void> {
|
||||||
const activeExecutions = isEmpty(this.workflowFilterCurrent.metadata)
|
const activeExecutions = isEmpty(this.workflowFilterCurrent.metadata)
|
||||||
? await this.workflowsStore.getCurrentExecutions(this.workflowFilterCurrent)
|
? await this.workflowsStore.getActiveExecutions(this.workflowFilterCurrent)
|
||||||
: [];
|
: [];
|
||||||
for (const activeExecution of activeExecutions) {
|
for (const activeExecution of activeExecutions) {
|
||||||
if (activeExecution.workflowId && !activeExecution.workflowName) {
|
if (activeExecution.workflowId && !activeExecution.workflowName) {
|
||||||
|
@ -573,7 +573,7 @@ export default defineComponent({
|
||||||
// ever get ids 500, 501, 502 and 503 when they finish
|
// ever get ids 500, 501, 502 and 503 when they finish
|
||||||
const promises = [this.workflowsStore.getPastExecutions(filter, this.requestItemsPerRequest)];
|
const promises = [this.workflowsStore.getPastExecutions(filter, this.requestItemsPerRequest)];
|
||||||
if (isEmpty(filter.metadata)) {
|
if (isEmpty(filter.metadata)) {
|
||||||
promises.push(this.workflowsStore.getCurrentExecutions({}));
|
promises.push(this.workflowsStore.getActiveExecutions({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
|
|
@ -111,7 +111,7 @@ describe('ExecutionsList.vue', () => {
|
||||||
workflowsStore = useWorkflowsStore();
|
workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
vi.spyOn(workflowsStore, 'fetchAllWorkflows').mockResolvedValue(workflowsData);
|
vi.spyOn(workflowsStore, 'fetchAllWorkflows').mockResolvedValue(workflowsData);
|
||||||
vi.spyOn(workflowsStore, 'getCurrentExecutions').mockResolvedValue([]);
|
vi.spyOn(workflowsStore, 'getActiveExecutions').mockResolvedValue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render empty list', async () => {
|
it('should render empty list', async () => {
|
||||||
|
|
|
@ -64,7 +64,7 @@ import { findLast } from 'lodash-es';
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import {
|
import {
|
||||||
getActiveWorkflows,
|
getActiveWorkflows,
|
||||||
getCurrentExecutions,
|
getActiveExecutions,
|
||||||
getExecutionData,
|
getExecutionData,
|
||||||
getExecutions,
|
getExecutions,
|
||||||
getNewWorkflow,
|
getNewWorkflow,
|
||||||
|
@ -1276,7 +1276,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
return await makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/executions', sendData);
|
return await makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/executions', sendData);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getCurrentExecutions(filter: IDataObject): Promise<IExecutionsCurrentSummaryExtended[]> {
|
async getActiveExecutions(filter: IDataObject): Promise<IExecutionsCurrentSummaryExtended[]> {
|
||||||
let sendData = {};
|
let sendData = {};
|
||||||
if (filter) {
|
if (filter) {
|
||||||
sendData = {
|
sendData = {
|
||||||
|
@ -1287,7 +1287,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
return await makeRestApiRequest(
|
return await makeRestApiRequest(
|
||||||
rootStore.getRestApiContext,
|
rootStore.getRestApiContext,
|
||||||
'GET',
|
'GET',
|
||||||
'/executions-current',
|
'/executions/active',
|
||||||
sendData,
|
sendData,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1355,7 +1355,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
return await makeRestApiRequest(
|
return await makeRestApiRequest(
|
||||||
rootStore.getRestApiContext,
|
rootStore.getRestApiContext,
|
||||||
'POST',
|
'POST',
|
||||||
`/executions-current/${executionId}/stop`,
|
`/executions/active/${executionId}/stop`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1370,7 +1370,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
try {
|
try {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
if ((!requestFilter.status || !requestFilter.finished) && isEmpty(requestFilter.metadata)) {
|
if ((!requestFilter.status || !requestFilter.finished) && isEmpty(requestFilter.metadata)) {
|
||||||
activeExecutions = await getCurrentExecutions(rootStore.getRestApiContext, {
|
activeExecutions = await getActiveExecutions(rootStore.getRestApiContext, {
|
||||||
workflowId: requestFilter.workflowId,
|
workflowId: requestFilter.workflowId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2158,7 +2158,7 @@ export interface NodeExecutionWithMetadata extends INodeExecutionData {
|
||||||
pairedItem: IPairedItemData | IPairedItemData[];
|
pairedItem: IPairedItemData | IPairedItemData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExecutionsSummary {
|
export interface ExecutionSummary {
|
||||||
id: string;
|
id: string;
|
||||||
finished?: boolean;
|
finished?: boolean;
|
||||||
mode: WorkflowExecuteMode;
|
mode: WorkflowExecuteMode;
|
||||||
|
|
Loading…
Reference in a new issue