mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(core): Change workflow deletions to soft deletes (#14894)
Adds soft‑deletion support for workflows through a new boolean column `isArchived`. When a workflow is archived we now set `isArchived` flag to true and the workflows stays in the database and is omitted from the default workflow listing query. Archived workflows can be viewed in read-only mode, but they cannot be activated. Archived workflows are still available by ID and can be invoked as sub-executions, so existing Execute Workflow nodes continue to work. Execution engine doesn't care about isArchived flag. Users can restore workflows via Unarchive action at the UI.
This commit is contained in:
@@ -2328,9 +2328,197 @@ describe('POST /workflows/:workflowId/run', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /workflows/:workflowId', () => {
|
||||
test('deletes a workflow owned by the user', async () => {
|
||||
describe('POST /workflows/:workflowId/archive', () => {
|
||||
test('should archive workflow', async () => {
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
const response = await authOwnerAgent
|
||||
.post(`/workflows/${workflow.id}/archive`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
data: { isArchived, versionId },
|
||||
} = response.body;
|
||||
|
||||
expect(isArchived).toBe(true);
|
||||
expect(versionId).not.toBe(workflow.versionId);
|
||||
|
||||
const updatedWorkflow = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
expect(updatedWorkflow).not.toBeNull();
|
||||
expect(updatedWorkflow!.isArchived).toBe(true);
|
||||
});
|
||||
|
||||
test('should deactivate active workflow on archive', async () => {
|
||||
const workflow = await createWorkflow({ active: true }, owner);
|
||||
const response = await authOwnerAgent
|
||||
.post(`/workflows/${workflow.id}/archive`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
data: { isArchived, versionId, active },
|
||||
} = response.body;
|
||||
|
||||
expect(isArchived).toBe(true);
|
||||
expect(active).toBe(false);
|
||||
expect(versionId).not.toBe(workflow.versionId);
|
||||
expect(activeWorkflowManagerLike.remove).toBeCalledWith(workflow.id);
|
||||
|
||||
const updatedWorkflow = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
expect(updatedWorkflow).not.toBeNull();
|
||||
expect(updatedWorkflow!.isArchived).toBe(true);
|
||||
});
|
||||
|
||||
test('should not archive workflow that is already archived', async () => {
|
||||
const workflow = await createWorkflow({ isArchived: true }, owner);
|
||||
const response = await authOwnerAgent
|
||||
.post(`/workflows/${workflow.id}/archive`)
|
||||
.send()
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.message).toBe('Workflow is already archived.');
|
||||
|
||||
const updatedWorkflow = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
expect(updatedWorkflow).not.toBeNull();
|
||||
expect(updatedWorkflow!.isArchived).toBe(true);
|
||||
});
|
||||
|
||||
test('should not archive missing workflow', async () => {
|
||||
const response = await authOwnerAgent.post('/workflows/404/archive').send().expect(403);
|
||||
expect(response.body.message).toBe(
|
||||
'Could not archive the workflow - workflow was not found in your projects',
|
||||
);
|
||||
});
|
||||
|
||||
test('should not archive a workflow that is not owned by the user', async () => {
|
||||
const workflow = await createWorkflow({ isArchived: false }, member);
|
||||
|
||||
await testServer
|
||||
.authAgentFor(anotherMember)
|
||||
.post(`/workflows/${workflow.id}/archive`)
|
||||
.send()
|
||||
.expect(403);
|
||||
|
||||
const workflowsInDb = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
expect(workflowsInDb).not.toBeNull();
|
||||
expect(workflowsInDb!.isArchived).toBe(false);
|
||||
expect(sharedWorkflowsInDb).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("should allow the owner to archive workflows they don't own", async () => {
|
||||
const workflow = await createWorkflow({ isArchived: false }, member);
|
||||
|
||||
const response = await authOwnerAgent
|
||||
.post(`/workflows/${workflow.id}/archive`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
data: { isArchived, versionId },
|
||||
} = response.body;
|
||||
|
||||
expect(isArchived).toBe(true);
|
||||
expect(versionId).not.toBe(workflow.versionId);
|
||||
|
||||
const workflowsInDb = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
expect(workflowsInDb).not.toBeNull();
|
||||
expect(workflowsInDb!.isArchived).toBe(true);
|
||||
expect(sharedWorkflowsInDb).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /workflows/:workflowId/unarchive', () => {
|
||||
test('should unarchive workflow', async () => {
|
||||
const workflow = await createWorkflow({ isArchived: true }, owner);
|
||||
const response = await authOwnerAgent
|
||||
.post(`/workflows/${workflow.id}/unarchive`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
data: { isArchived, versionId },
|
||||
} = response.body;
|
||||
|
||||
expect(isArchived).toBe(false);
|
||||
expect(versionId).not.toBe(workflow.versionId);
|
||||
|
||||
const updatedWorkflow = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
expect(updatedWorkflow).not.toBeNull();
|
||||
expect(updatedWorkflow!.isArchived).toBe(false);
|
||||
});
|
||||
|
||||
test('should not unarchive workflow that is already not archived', async () => {
|
||||
const workflow = await createWorkflow({ isArchived: false }, owner);
|
||||
await authOwnerAgent.post(`/workflows/${workflow.id}/unarchive`).send().expect(400);
|
||||
|
||||
const updatedWorkflow = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
expect(updatedWorkflow).not.toBeNull();
|
||||
expect(updatedWorkflow!.isArchived).toBe(false);
|
||||
});
|
||||
|
||||
test('should not unarchive missing workflow', async () => {
|
||||
const response = await authOwnerAgent.post('/workflows/404/unarchive').send().expect(403);
|
||||
expect(response.body.message).toBe(
|
||||
'Could not unarchive the workflow - workflow was not found in your projects',
|
||||
);
|
||||
});
|
||||
|
||||
test('should not unarchive a workflow that is not owned by the user', async () => {
|
||||
const workflow = await createWorkflow({ isArchived: true }, member);
|
||||
|
||||
await testServer
|
||||
.authAgentFor(anotherMember)
|
||||
.post(`/workflows/${workflow.id}/unarchive`)
|
||||
.send()
|
||||
.expect(403);
|
||||
|
||||
const workflowsInDb = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
expect(workflowsInDb).not.toBeNull();
|
||||
expect(workflowsInDb!.isArchived).toBe(true);
|
||||
expect(sharedWorkflowsInDb).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("should allow the owner to unarchive workflows they don't own", async () => {
|
||||
const workflow = await createWorkflow({ isArchived: true }, member);
|
||||
|
||||
const response = await authOwnerAgent
|
||||
.post(`/workflows/${workflow.id}/unarchive`)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
data: { isArchived, versionId },
|
||||
} = response.body;
|
||||
|
||||
expect(isArchived).toBe(false);
|
||||
expect(versionId).not.toBe(workflow.versionId);
|
||||
|
||||
const workflowsInDb = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
expect(workflowsInDb).not.toBeNull();
|
||||
expect(workflowsInDb!.isArchived).toBe(false);
|
||||
expect(sharedWorkflowsInDb).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /workflows/:workflowId', () => {
|
||||
test('deletes an archived workflow owned by the user', async () => {
|
||||
const workflow = await createWorkflow({ isArchived: true }, owner);
|
||||
|
||||
await authOwnerAgent.delete(`/workflows/${workflow.id}`).send().expect(200);
|
||||
|
||||
@@ -2343,8 +2531,15 @@ describe('DELETE /workflows/:workflowId', () => {
|
||||
expect(sharedWorkflowsInDb).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('deletes a workflow owned by the user, even if the user is just a member', async () => {
|
||||
const workflow = await createWorkflow({}, member);
|
||||
test('should not delete missing workflow', async () => {
|
||||
const response = await authOwnerAgent.delete('/workflows/404').send().expect(403);
|
||||
expect(response.body.message).toBe(
|
||||
'Could not delete the workflow - workflow was not found in your projects',
|
||||
);
|
||||
});
|
||||
|
||||
test('deletes an archived workflow owned by the user, even if the user is just a member', async () => {
|
||||
const workflow = await createWorkflow({ isArchived: true }, member);
|
||||
|
||||
await testServer.authAgentFor(member).delete(`/workflows/${workflow.id}`).send().expect(200);
|
||||
|
||||
@@ -2357,8 +2552,23 @@ describe('DELETE /workflows/:workflowId', () => {
|
||||
expect(sharedWorkflowsInDb).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('does not delete a workflow that is not owned by the user', async () => {
|
||||
const workflow = await createWorkflow({}, member);
|
||||
test('does not delete a workflow that is not archived', async () => {
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
|
||||
const response = await authOwnerAgent.delete(`/workflows/${workflow.id}`).send().expect(400);
|
||||
expect(response.body.message).toBe('Workflow must be archived before it can be deleted.');
|
||||
|
||||
const workflowInDb = await Container.get(WorkflowRepository).findById(workflow.id);
|
||||
const sharedWorkflowsInDb = await Container.get(SharedWorkflowRepository).findBy({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
expect(workflowInDb).not.toBeNull();
|
||||
expect(sharedWorkflowsInDb).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('does not delete an archived workflow that is not owned by the user', async () => {
|
||||
const workflow = await createWorkflow({ isArchived: true }, member);
|
||||
|
||||
await testServer
|
||||
.authAgentFor(anotherMember)
|
||||
@@ -2375,8 +2585,8 @@ describe('DELETE /workflows/:workflowId', () => {
|
||||
expect(sharedWorkflowsInDb).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("allows the owner to delete workflows they don't own", async () => {
|
||||
const workflow = await createWorkflow({}, member);
|
||||
test("allows the owner to delete archived workflows they don't own", async () => {
|
||||
const workflow = await createWorkflow({ isArchived: true }, member);
|
||||
|
||||
await authOwnerAgent.delete(`/workflows/${workflow.id}`).send().expect(200);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user