mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
feat(core): Add workflow.deactivate external hook (#34746)
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import { BadRequestError } from './bad-request.error';
|
||||
|
||||
/**
|
||||
* Error thrown when a workflow fails to deactivate, e.g. because a
|
||||
* `workflow.deactivate` external hook rejected the operation.
|
||||
*/
|
||||
export class WorkflowDeactivationBadRequestError extends BadRequestError {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly meta: { description?: string } = {},
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'WorkflowDeactivationBadRequestError';
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ type ExternalHooksMap = {
|
||||
'workflow.create': [createdWorkflow: IWorkflowBase];
|
||||
'workflow.afterCreate': [createdWorkflow: IWorkflowBase];
|
||||
'workflow.activate': [updatedWorkflow: IWorkflowBase];
|
||||
'workflow.deactivate': [deactivatedWorkflow: IWorkflowBase];
|
||||
'workflow.update': [updatedWorkflow: IWorkflowBase];
|
||||
'workflow.afterUpdate': [updatedWorkflow: IWorkflowBase];
|
||||
'workflow.delete': [workflowId: string];
|
||||
|
||||
@@ -22,6 +22,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ConflictError } from '@/errors/response-errors/conflict.error';
|
||||
import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error';
|
||||
import { WorkflowActivationBadRequestError } from '@/errors/response-errors/workflow-activation-bad-request.error';
|
||||
import { WorkflowDeactivationBadRequestError } from '@/errors/response-errors/workflow-deactivation-bad-request.error';
|
||||
import type { EventService } from '@/events/event.service';
|
||||
import type { ExternalHooks } from '@/external-hooks';
|
||||
import type { RedactionEnforcementService } from '@/modules/redaction/redaction-enforcement.service';
|
||||
@@ -1258,6 +1259,49 @@ describe('WorkflowService', () => {
|
||||
// in-memory teardown is left to the leader, not run here
|
||||
expect(activeWorkflowManagerMock.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('deactivation blocked by hook leaves the workflow published', async () => {
|
||||
const workflow = makeWorkflowEntity({ activeVersionId: PREVIOUS_VERSION_ID });
|
||||
workflowFinderServiceMock.findWorkflowForUser.mockResolvedValue(workflow);
|
||||
|
||||
externalHooksMock.run.mockRejectedValue(new Error('Code freeze in effect'));
|
||||
|
||||
const user = mock<User>();
|
||||
|
||||
await expect(workflowService.deactivateWorkflow(user, WORKFLOW_ID)).rejects.toBeInstanceOf(
|
||||
WorkflowDeactivationBadRequestError,
|
||||
);
|
||||
|
||||
expect(workflow.active).toBe(true);
|
||||
expect(workflow.activeVersionId).toBe(PREVIOUS_VERSION_ID);
|
||||
expect(activeWorkflowManagerMock.remove).not.toHaveBeenCalled();
|
||||
expect(workflowRepositoryMock.update).not.toHaveBeenCalled();
|
||||
expect(workflowPublishHistoryRepositoryMock.addRecord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('hook receives the workflow being deactivated', async () => {
|
||||
const workflow = makeWorkflowEntity({ activeVersionId: PREVIOUS_VERSION_ID });
|
||||
workflowFinderServiceMock.findWorkflowForUser.mockResolvedValue(workflow);
|
||||
|
||||
externalHooksMock.run.mockResolvedValue(undefined);
|
||||
|
||||
const user = mock<User>();
|
||||
|
||||
await workflowService.deactivateWorkflow(user, WORKFLOW_ID);
|
||||
|
||||
expect(externalHooksMock.run).toHaveBeenCalledWith('workflow.deactivate', [workflow]);
|
||||
});
|
||||
|
||||
test('does not run the hook when the workflow is already inactive', async () => {
|
||||
const workflow = makeWorkflowEntity({ active: false, activeVersionId: null });
|
||||
workflowFinderServiceMock.findWorkflowForUser.mockResolvedValue(workflow);
|
||||
|
||||
const user = mock<User>();
|
||||
|
||||
await workflowService.deactivateWorkflow(user, WORKFLOW_ID);
|
||||
|
||||
expect(externalHooksMock.run).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete()', () => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ConflictError } from '@/errors/response-errors/conflict.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { WorkflowActivationBadRequestError } from '@/errors/response-errors/workflow-activation-bad-request.error';
|
||||
import { WorkflowDeactivationBadRequestError } from '@/errors/response-errors/workflow-deactivation-bad-request.error';
|
||||
import { WorkflowValidationError } from '@/errors/response-errors/workflow-validation.error';
|
||||
import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error';
|
||||
import { EventService } from '@/events/event.service';
|
||||
@@ -983,6 +984,14 @@ export class WorkflowService {
|
||||
await this._detectConflicts(workflow, options.expectedChecksum);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.externalHooks.run('workflow.deactivate', [workflow]);
|
||||
} catch (error) {
|
||||
throw new WorkflowDeactivationBadRequestError(ensureError(error).message, {
|
||||
description: getErrorDescription(error),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.globalConfig.workflows.useWorkflowPublicationService) {
|
||||
await this._unpublishViaOutbox(user, workflowId, deactivatedVersionId, workflow.updatedAt);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user