diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts index e68df512a3..0dc03d1c02 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts @@ -2,7 +2,6 @@ import express from 'express'; import { FindConditions, FindManyOptions, In } from 'typeorm'; -import * as Db from '@/Db'; import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; import config from '@/config'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; @@ -28,6 +27,7 @@ import { getWorkflowIdsViaTags, parseTagNames, } from './workflows.service'; +import { WorkflowsService } from '@/workflows/workflows.services'; export = { createWorkflow: [ @@ -58,27 +58,16 @@ export = { deleteWorkflow: [ authorize(['owner', 'member']), async (req: WorkflowRequest.Get, res: express.Response): Promise => { - const { id } = req.params; + const { id: workflowId } = req.params; - const sharedWorkflow = await getSharedWorkflow(req.user, id); - - if (!sharedWorkflow) { + const workflow = await WorkflowsService.delete(req.user, workflowId); + if (!workflow) { // user trying to access a workflow he does not own // or workflow does not exist return res.status(404).json({ message: 'Not Found' }); } - if (sharedWorkflow.workflow.active) { - // deactivate before deleting - await ActiveWorkflowRunner.getInstance().remove(id); - } - - await Db.collections.Workflow.delete(id); - - void InternalHooksManager.getInstance().onWorkflowDeleted(req.user, id, true); - await ExternalHooks().run('workflow.afterDelete', [id]); - - return res.json(sharedWorkflow.workflow); + return res.json(workflow); }, ], getWorkflow: [ diff --git a/packages/cli/src/databases/migrations/mysqldb/1673268682475-DeleteExecutionsWithWorkflows.ts b/packages/cli/src/databases/migrations/mysqldb/1673268682475-DeleteExecutionsWithWorkflows.ts new file mode 100644 index 0000000000..6db58e780a --- /dev/null +++ b/packages/cli/src/databases/migrations/mysqldb/1673268682475-DeleteExecutionsWithWorkflows.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers'; +import config from '@/config'; + +export class DeleteExecutionsWithWorkflows1673268682475 implements MigrationInterface { + name = 'DeleteExecutionsWithWorkflows1673268682475'; + public async up(queryRunner: QueryRunner): Promise { + logMigrationStart(this.name); + const tablePrefix = config.getEnv('database.tablePrefix'); + + await queryRunner.query(`ALTER TABLE \`${tablePrefix}execution_entity\` MODIFY workflowId INT`); + + const workflowIds: Array<{ id: number }> = await queryRunner.query(` + SELECT id FROM \`${tablePrefix}execution_entity\` + `); + + await queryRunner.query( + `DELETE FROM \`${tablePrefix}execution_entity\` + WHERE workflowId IS NOT NULL + ${workflowIds.length ? `AND workflowId NOT IN (${workflowIds.map(({ id }) => id).join()})` : ''}`, + ); + + await queryRunner.query( + `ALTER TABLE \`${tablePrefix}execution_entity\` + ADD CONSTRAINT \`FK_${tablePrefix}execution_entity_workflowId\` + FOREIGN KEY (\`workflowId\`) REFERENCES \`${tablePrefix}workflow_entity\`(\`id\`) + ON DELETE CASCADE`, + ); + + logMigrationEnd(this.name); + } + + public async down(queryRunner: QueryRunner): Promise { + const tablePrefix = config.getEnv('database.tablePrefix'); + await queryRunner.query( + `ALTER TABLE \`${tablePrefix}execution_entity\` + DROP FOREIGN KEY \`FK_${tablePrefix}execution_entity_workflowId\``, + ); + + await queryRunner.query( + `ALTER TABLE \`${tablePrefix}execution_entity\` MODIFY workflowId varchar(255);`, + ); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index d055535598..299d47dcb0 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -28,6 +28,7 @@ import { AddWorkflowVersionIdColumn1669739707125 } from './1669739707125-AddWork import { AddTriggerCountColumn1669823906994 } from './1669823906994-AddTriggerCountColumn'; import { RemoveWorkflowDataLoadedFlag1671726148420 } from './1671726148420-RemoveWorkflowDataLoadedFlag'; import { MessageEventBusDestinations1671535397530 } from './1671535397530-MessageEventBusDestinations'; +import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-DeleteExecutionsWithWorkflows'; export const mysqlMigrations = [ InitialMigration1588157391238, @@ -60,4 +61,5 @@ export const mysqlMigrations = [ AddTriggerCountColumn1669823906994, RemoveWorkflowDataLoadedFlag1671726148420, MessageEventBusDestinations1671535397530, + DeleteExecutionsWithWorkflows1673268682475, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/1673268682475-DeleteExecutionsWithWorkflows.ts b/packages/cli/src/databases/migrations/postgresdb/1673268682475-DeleteExecutionsWithWorkflows.ts new file mode 100644 index 0000000000..a8389e2a94 --- /dev/null +++ b/packages/cli/src/databases/migrations/postgresdb/1673268682475-DeleteExecutionsWithWorkflows.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers'; +import config from '@/config'; + +export class DeleteExecutionsWithWorkflows1673268682475 implements MigrationInterface { + name = 'DeleteExecutionsWithWorkflows1673268682475'; + public async up(queryRunner: QueryRunner): Promise { + logMigrationStart(this.name); + const tablePrefix = config.getEnv('database.tablePrefix'); + + await queryRunner.query( + `ALTER TABLE ${tablePrefix}execution_entity + ALTER COLUMN "workflowId" TYPE INTEGER USING "workflowId"::integer`, + ); + + const workflowIds: Array<{ id: number }> = await queryRunner.query(` + SELECT id FROM ${tablePrefix}workflow_entity + `); + + await queryRunner.query( + `DELETE FROM ${tablePrefix}execution_entity + WHERE "workflowId" IS NOT NULL + ${ + workflowIds.length + ? `AND "workflowId" NOT IN (${workflowIds.map(({ id }) => id).join()})` + : '' + }`, + ); + + await queryRunner.query( + `ALTER TABLE ${tablePrefix}execution_entity + ADD CONSTRAINT "FK_${tablePrefix}execution_entity_workflowId" + FOREIGN KEY ("workflowId") REFERENCES ${tablePrefix}workflow_entity ("id") + ON DELETE CASCADE`, + ); + + logMigrationEnd(this.name); + } + + public async down(queryRunner: QueryRunner): Promise { + const tablePrefix = config.getEnv('database.tablePrefix'); + await queryRunner.query( + `ALTER TABLE ${tablePrefix}execution_entity + DROP CONSTRAINT "FK_${tablePrefix}execution_entity_workflowId"`, + ); + + await queryRunner.query( + `ALTER TABLE ${tablePrefix}execution_entity + ALTER COLUMN "workflowId" TYPE TEXT`, + ); + } +} diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 98d88aeefe..e03b72f77e 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -26,6 +26,7 @@ import { AddWorkflowVersionIdColumn1669739707126 } from './1669739707126-AddWork import { AddTriggerCountColumn1669823906995 } from './1669823906995-AddTriggerCountColumn'; import { RemoveWorkflowDataLoadedFlag1671726148421 } from './1671726148421-RemoveWorkflowDataLoadedFlag'; import { MessageEventBusDestinations1671535397530 } from './1671535397530-MessageEventBusDestinations'; +import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-DeleteExecutionsWithWorkflows'; export const postgresMigrations = [ InitialMigration1587669153312, @@ -56,4 +57,5 @@ export const postgresMigrations = [ AddTriggerCountColumn1669823906995, RemoveWorkflowDataLoadedFlag1671726148421, MessageEventBusDestinations1671535397530, + DeleteExecutionsWithWorkflows1673268682475, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1673268682475-DeleteExecutionsWithWorkflows.ts b/packages/cli/src/databases/migrations/sqlite/1673268682475-DeleteExecutionsWithWorkflows.ts new file mode 100644 index 0000000000..1e9b7b973e --- /dev/null +++ b/packages/cli/src/databases/migrations/sqlite/1673268682475-DeleteExecutionsWithWorkflows.ts @@ -0,0 +1,107 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers'; +import config from '@/config'; + +export class DeleteExecutionsWithWorkflows1673268682475 implements MigrationInterface { + name = 'DeleteExecutionsWithWorkflows1673268682475'; + public async up(queryRunner: QueryRunner): Promise { + logMigrationStart(this.name); + const tablePrefix = config.getEnv('database.tablePrefix'); + + const workflowIds: Array<{ id: number }> = await queryRunner.query(` + SELECT id FROM "${tablePrefix}workflow_entity" + `); + + await queryRunner.query( + `DELETE FROM "${tablePrefix}execution_entity" + WHERE "workflowId" IS NOT NULL + ${ + workflowIds.length + ? `AND "workflowId" NOT IN (${workflowIds.map(({ id }) => id).join()})` + : '' + }`, + ); + + await queryRunner.query('PRAGMA foreign_keys=OFF'); + + await queryRunner.query(`DROP TABLE IF EXISTS "${tablePrefix}temporary_execution_entity"`); + await queryRunner.query( + `CREATE TABLE "${tablePrefix}temporary_execution_entity" ( + "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "workflowId" int, + "finished" boolean NOT NULL, + "mode" varchar NOT NULL, + "retryOf" varchar, + "retrySuccessId" varchar, + "startedAt" datetime NOT NULL, + "stoppedAt" datetime, + "waitTill" datetime, + "workflowData" text NOT NULL, + "data" text NOT NULL, + FOREIGN KEY("workflowId") REFERENCES "${tablePrefix}workflow_entity" ("id") ON DELETE CASCADE + )`, + ); + + const columns = + '"id", "workflowId", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "waitTill", "workflowData", "data"'; + await queryRunner.query( + `INSERT INTO "${tablePrefix}temporary_execution_entity"(${columns}) SELECT ${columns} FROM "${tablePrefix}execution_entity"`, + ); + await queryRunner.query(`DROP TABLE "${tablePrefix}execution_entity"`); + await queryRunner.query( + `ALTER TABLE "${tablePrefix}temporary_execution_entity" RENAME TO "${tablePrefix}execution_entity"`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_${tablePrefix}cefb067df2402f6aed0638a6c1" ON "${tablePrefix}execution_entity" ("stoppedAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2" ON "${tablePrefix}execution_entity" ("waitTill")`, + ); + await queryRunner.query(`VACUUM;`); + + await queryRunner.query('PRAGMA foreign_keys=ON'); + + logMigrationEnd(this.name); + } + + public async down(queryRunner: QueryRunner): Promise { + const tablePrefix = config.getEnv('database.tablePrefix'); + await queryRunner.query('PRAGMA foreign_keys=OFF'); + + await queryRunner.query(`DROP TABLE IF EXISTS "${tablePrefix}temporary_execution_entity"`); + await queryRunner.query( + `CREATE TABLE "${tablePrefix}temporary_execution_entity" ( + "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "workflowId" varchar, + "finished" boolean NOT NULL, + "mode" varchar NOT NULL, + "retryOf" varchar, + "retrySuccessId" varchar, + "startedAt" datetime NOT NULL, + "stoppedAt" datetime, + "waitTill" datetime, + "workflowData" text NOT NULL, + "data" text NOT NULL + )`, + ); + + const columns = + '"id", "workflowId", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "waitTill", "workflowData", "data"'; + await queryRunner.query( + `INSERT INTO "${tablePrefix}temporary_execution_entity"(${columns}) SELECT ${columns} FROM "${tablePrefix}execution_entity"`, + ); + await queryRunner.query(`DROP TABLE "${tablePrefix}execution_entity"`); + await queryRunner.query( + `ALTER TABLE "${tablePrefix}temporary_execution_entity" RENAME TO "${tablePrefix}execution_entity"`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_${tablePrefix}cefb067df2402f6aed0638a6c1" ON "${tablePrefix}execution_entity" ("stoppedAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2" ON "${tablePrefix}execution_entity" ("waitTill")`, + ); + await queryRunner.query(`VACUUM;`); + + await queryRunner.query('PRAGMA foreign_keys=ON'); + } +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index 733eb0351b..239672e05d 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -25,6 +25,7 @@ import { AddWorkflowVersionIdColumn1669739707124 } from './1669739707124-AddWork import { AddTriggerCountColumn1669823906993 } from './1669823906993-AddTriggerCountColumn'; import { RemoveWorkflowDataLoadedFlag1671726148419 } from './1671726148419-RemoveWorkflowDataLoadedFlag'; import { MessageEventBusDestinations1671535397530 } from './1671535397530-MessageEventBusDestinations'; +import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-DeleteExecutionsWithWorkflows'; const sqliteMigrations = [ InitialMigration1588102412422, @@ -54,6 +55,7 @@ const sqliteMigrations = [ WorkflowStatistics1664196174000, RemoveWorkflowDataLoadedFlag1671726148419, MessageEventBusDestinations1671535397530, + DeleteExecutionsWithWorkflows1673268682475, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 4e1e8be823..d85e9cd10b 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -5,12 +5,11 @@ import { v4 as uuid } from 'uuid'; import { LoggerProxy } from 'n8n-workflow'; import axios from 'axios'; -import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; import * as Db from '@/Db'; import * as GenericHelpers from '@/GenericHelpers'; import * as ResponseHelper from '@/ResponseHelper'; import * as WorkflowHelpers from '@/WorkflowHelpers'; -import { IWorkflowResponse, IExecutionPushResponse } from '@/Interfaces'; +import type { IWorkflowResponse, IExecutionPushResponse } from '@/Interfaces'; import config from '@/config'; import * as TagHelpers from '@/TagHelpers'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; @@ -256,19 +255,8 @@ workflowsController.delete( ResponseHelper.send(async (req: WorkflowRequest.Delete) => { const { id: workflowId } = req.params; - await ExternalHooks().run('workflow.delete', [workflowId]); - - const shared = await Db.collections.SharedWorkflow.findOne({ - relations: ['workflow', 'role'], - where: whereClause({ - user: req.user, - entityType: 'workflow', - entityId: workflowId, - roles: ['owner'], - }), - }); - - if (!shared) { + const workflow = await WorkflowsService.delete(req.user, workflowId); + if (!workflow) { LoggerProxy.verbose('User attempted to delete a workflow without permissions', { workflowId, userId: req.user.id, @@ -278,16 +266,6 @@ workflowsController.delete( ); } - if (shared.workflow.active) { - // deactivate before deleting - await ActiveWorkflowRunner.getInstance().remove(workflowId); - } - - await Db.collections.Workflow.delete(workflowId); - - void InternalHooksManager.getInstance().onWorkflowDeleted(req.user, workflowId, false); - await ExternalHooks().run('workflow.afterDelete', [workflowId]); - return true; }), ); diff --git a/packages/cli/src/workflows/workflows.services.ts b/packages/cli/src/workflows/workflows.services.ts index 69f347578d..a798b2a51c 100644 --- a/packages/cli/src/workflows/workflows.services.ts +++ b/packages/cli/src/workflows/workflows.services.ts @@ -420,4 +420,34 @@ export class WorkflowsService { executionId, }; } + + static async delete(user: User, workflowId: string): Promise { + await ExternalHooks().run('workflow.delete', [workflowId]); + + const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ + relations: ['workflow', 'role'], + where: whereClause({ + user, + entityType: 'workflow', + entityId: workflowId, + roles: ['owner'], + }), + }); + + if (!sharedWorkflow) { + return; + } + + if (sharedWorkflow.workflow.active) { + // deactivate before deleting + await ActiveWorkflowRunner.getInstance().remove(workflowId); + } + + await Db.collections.Workflow.delete(workflowId); + + void InternalHooksManager.getInstance().onWorkflowDeleted(user, workflowId, false); + await ExternalHooks().run('workflow.afterDelete', [workflowId]); + + return sharedWorkflow.workflow; + } }