Skip to content

Commit

Permalink
refactor: Reactivate workflow locking (#4770)
Browse files Browse the repository at this point in the history
* feat: Reenable workflow locking

Co-authored-by: freyamade <[email protected]>
Co-authored-by: Csaba Tuncsik <[email protected]>
  • Loading branch information
3 people authored Dec 6, 2022
1 parent 915f144 commit 4813da5
Show file tree
Hide file tree
Showing 19 changed files with 249 additions and 118 deletions.
4 changes: 2 additions & 2 deletions packages/cli/src/ResponseHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ abstract class ResponseError extends Error {
}

export class BadRequestError extends ResponseError {
constructor(message: string) {
super(message, 400);
constructor(message: string, errorCode?: number) {
super(message, 400, errorCode);
}
}

Expand Down
33 changes: 2 additions & 31 deletions packages/cli/src/databases/entities/WorkflowEntity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import crypto from 'crypto';
import { Length } from 'class-validator';

import type {
Expand All @@ -11,9 +10,6 @@ import type {
} from 'n8n-workflow';

import {
AfterLoad,
AfterUpdate,
AfterInsert,
Column,
Entity,
Index,
Expand All @@ -29,7 +25,6 @@ import { SharedWorkflow } from './SharedWorkflow';
import { objectRetriever, sqlite } from '../utils/transformers';
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
import type { IWorkflowDb } from '@/Interfaces';
import { alphabetizeKeys } from '@/utils';

@Entity()
export class WorkflowEntity extends AbstractEntity implements IWorkflowDb {
Expand Down Expand Up @@ -90,32 +85,8 @@ export class WorkflowEntity extends AbstractEntity implements IWorkflowDb {
})
pinData: ISimplifiedPinData;

/**
* Hash of editable workflow state.
*/
hash: string;

@AfterLoad()
@AfterUpdate()
@AfterInsert()
setHash(): void {
const { name, active, nodes, connections, settings, staticData, pinData } = this;

// Workflow listing page does not request the `connections` column, so we can use this for `undefined` to avoid generating hashes for all the workflows.
if (!connections) return;

const state = JSON.stringify({
name,
active,
nodes: nodes ? nodes.map(alphabetizeKeys) : [],
connections,
settings,
staticData,
pinData,
});

this.hash = crypto.createHash('md5').update(state).digest('hex');
}
@Column({ length: 36 })
versionId: string;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
import config from '@/config';
import { v4 as uuidv4 } from 'uuid';

export class AddWorkflowVersionIdColumn1669739707125 implements MigrationInterface {
name = 'AddWorkflowVersionIdColumn1669739707125';

async up(queryRunner: QueryRunner): Promise<void> {
logMigrationStart(this.name);

const tablePrefix = config.getEnv('database.tablePrefix');

await queryRunner.query(
`ALTER TABLE ${tablePrefix}workflow_entity ADD COLUMN versionId CHAR(36)`,
);

const workflowIds: Array<{ id: number }> = await queryRunner.query(`
SELECT id
FROM ${tablePrefix}workflow_entity
`);

workflowIds.map(({ id }) => {
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
`
UPDATE ${tablePrefix}workflow_entity
SET versionId = :versionId
WHERE id = '${id}'
`,
{ versionId: uuidv4() },
{},
);

return queryRunner.query(updateQuery, updateParams);
});

logMigrationEnd(this.name);
}

async down(queryRunner: QueryRunner) {
const tablePrefix = config.getEnv('database.tablePrefix');

await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity DROP COLUMN versionId`);
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/databases/migrations/mysqldb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCr
import { CreateWorkflowsEditorRole1663755770894 } from './1663755770894-CreateWorkflowsEditorRole';
import { CreateCredentialUsageTable1665484192213 } from './1665484192213-CreateCredentialUsageTable';
import { RemoveCredentialUsageTable1665754637026 } from './1665754637026-RemoveCredentialUsageTable';
import { AddWorkflowVersionIdColumn1669739707125 } from './1669739707125-AddWorkflowVersionIdColumn';

export const mysqlMigrations = [
InitialMigration1588157391238,
Expand Down Expand Up @@ -50,4 +51,5 @@ export const mysqlMigrations = [
CreateWorkflowsEditorRole1663755770894,
CreateCredentialUsageTable1665484192213,
RemoveCredentialUsageTable1665754637026,
AddWorkflowVersionIdColumn1669739707125,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
import config from '@/config';
import { v4 as uuidv4 } from 'uuid';

export class AddWorkflowVersionIdColumn1669739707126 implements MigrationInterface {
name = 'AddWorkflowVersionIdColumn1669739707126';

async up(queryRunner: QueryRunner) {
logMigrationStart(this.name);

const tablePrefix = getTablePrefix();
await queryRunner.query(
`ALTER TABLE ${tablePrefix}workflow_entity ADD COLUMN "versionId" CHAR(36)`,
);

const workflowIds: Array<{ id: number }> = await queryRunner.query(`
SELECT id
FROM ${tablePrefix}workflow_entity
`);

workflowIds.map(({ id }) => {
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
`
UPDATE ${tablePrefix}workflow_entity
SET "versionId" = :versionId
WHERE id = '${id}'
`,
{ versionId: uuidv4() },
{},
);

return queryRunner.query(updateQuery, updateParams);
});

logMigrationEnd(this.name);
}

async down(queryRunner: QueryRunner) {
const tablePrefix = config.getEnv('database.tablePrefix');

await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity DROP COLUMN "versionId"`);
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/databases/migrations/postgresdb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCr
import { CreateWorkflowsEditorRole1663755770893 } from './1663755770893-CreateWorkflowsEditorRole';
import { CreateCredentialUsageTable1665484192212 } from './1665484192212-CreateCredentialUsageTable';
import { RemoveCredentialUsageTable1665754637025 } from './1665754637025-RemoveCredentialUsageTable';
import { AddWorkflowVersionIdColumn1669739707126 } from './1669739707126-AddWorkflowVersionIdColumn';

export const postgresMigrations = [
InitialMigration1587669153312,
Expand All @@ -46,4 +47,5 @@ export const postgresMigrations = [
CreateWorkflowsEditorRole1663755770893,
CreateCredentialUsageTable1665484192212,
RemoveCredentialUsageTable1665754637025,
AddWorkflowVersionIdColumn1669739707126,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
import config from '@/config';
import { v4 as uuidv4 } from 'uuid';

export class AddWorkflowVersionIdColumn1669739707124 implements MigrationInterface {
name = 'AddWorkflowVersionIdColumn1669739707124';

async up(queryRunner: QueryRunner) {
logMigrationStart(this.name);

const tablePrefix = config.getEnv('database.tablePrefix');

await queryRunner.query(
`ALTER TABLE \`${tablePrefix}workflow_entity\` ADD COLUMN "versionId" char(36)`,
);

const workflowIds: Array<{ id: number }> = await queryRunner.query(`
SELECT id
FROM "${tablePrefix}workflow_entity"
`);

workflowIds.map(({ id }) => {
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
`
UPDATE "${tablePrefix}workflow_entity"
SET versionId = :versionId
WHERE id = '${id}'
`,
{ versionId: uuidv4() },
{},
);

return queryRunner.query(updateQuery, updateParams);
});

logMigrationEnd(this.name);
}

async down(queryRunner: QueryRunner) {
const tablePrefix = config.getEnv('database.tablePrefix');

await queryRunner.query(
`ALTER TABLE \`${tablePrefix}workflow_entity\` DROP COLUMN "versionId"`,
);
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/databases/migrations/sqlite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { CreateCredentialsUserRole1660062385367 } from './1660062385367-CreateCr
import { CreateWorkflowsEditorRole1663755770892 } from './1663755770892-CreateWorkflowsUserRole';
import { CreateCredentialUsageTable1665484192211 } from './1665484192211-CreateCredentialUsageTable';
import { RemoveCredentialUsageTable1665754637024 } from './1665754637024-RemoveCredentialUsageTable';
import { AddWorkflowVersionIdColumn1669739707124 } from './1669739707124-AddWorkflowVersionIdColumn';

const sqliteMigrations = [
InitialMigration1588102412422,
Expand All @@ -44,6 +45,7 @@ const sqliteMigrations = [
CreateWorkflowsEditorRole1663755770892,
CreateCredentialUsageTable1665484192211,
RemoveCredentialUsageTable1665754637024,
AddWorkflowVersionIdColumn1669739707124,
];

export { sqliteMigrations };
7 changes: 5 additions & 2 deletions packages/cli/src/workflows/workflows.controller.ee.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import express from 'express';
import { v4 as uuid } from 'uuid';
import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
import * as ResponseHelper from '@/ResponseHelper';
Expand Down Expand Up @@ -112,6 +113,8 @@ EEWorkflowController.post(

Object.assign(newWorkflow, req.body);

newWorkflow.versionId = uuid();

await validateEntity(newWorkflow);

await externalHooks.run('workflow.create', [newWorkflow]);
Expand Down Expand Up @@ -213,7 +216,7 @@ EEWorkflowController.patch(
'/:id(\\d+)',
ResponseHelper.send(async (req: WorkflowRequest.Update) => {
const { id: workflowId } = req.params;
// const forceSave = req.query.forceSave === 'true'; // disabled temporarily - tests were also disabled
const forceSave = req.query.forceSave === 'true';

const updateData = new WorkflowEntity();
const { tags, ...rest } = req.body;
Expand All @@ -226,7 +229,7 @@ EEWorkflowController.patch(
safeWorkflow,
workflowId,
tags,
true,
forceSave,
);

const { id, ...remainder } = updatedWorkflow;
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/workflows/workflows.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */

import express from 'express';
import { v4 as uuid } from 'uuid';
import { LoggerProxy } from 'n8n-workflow';

import axios from 'axios';
Expand Down Expand Up @@ -52,6 +53,8 @@ workflowsController.post(

Object.assign(newWorkflow, req.body);

newWorkflow.versionId = uuid();

await validateEntity(newWorkflow);

await externalHooks.run('workflow.create', [newWorkflow]);
Expand Down
22 changes: 20 additions & 2 deletions packages/cli/src/workflows/workflows.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { validate as jsonSchemaValidate } from 'jsonschema';
import { INode, IPinData, JsonObject, jsonParse, LoggerProxy, Workflow } from 'n8n-workflow';
import { FindManyOptions, FindOneOptions, In, ObjectLiteral } from 'typeorm';
import pick from 'lodash.pick';
import { v4 as uuid } from 'uuid';
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
Expand Down Expand Up @@ -172,7 +173,7 @@ export class WorkflowsService {
}

const query: FindManyOptions<WorkflowEntity> = {
select: isSharingEnabled ? [...fields, 'nodes'] : fields,
select: isSharingEnabled ? [...fields, 'nodes', 'versionId'] : fields,
relations,
where: {
id: In(sharedWorkflowIds),
Expand Down Expand Up @@ -220,12 +221,28 @@ export class WorkflowsService {
);
}

if (!forceSave && workflow.hash !== '' && workflow.hash !== shared.workflow.hash) {
if (
!forceSave &&
workflow.versionId !== '' &&
workflow.versionId !== shared.workflow.versionId
) {
throw new ResponseHelper.BadRequestError(
'Your most recent changes may be lost, because someone else just updated this workflow. Open this workflow in a new tab to see those new updates.',
100,
);
}

// Update the workflow's version
workflow.versionId = uuid();

LoggerProxy.verbose(
`Updating versionId for workflow ${workflowId} for user ${user.id} after saving`,
{
previousVersionId: shared.workflow.versionId,
newVersionId: workflow.versionId,
},
);

// check credentials for old format
await WorkflowHelpers.replaceInvalidCredentials(workflow);

Expand Down Expand Up @@ -280,6 +297,7 @@ export class WorkflowsService {
'settings',
'staticData',
'pinData',
'versionId',
]),
);

Expand Down
Loading

0 comments on commit 4813da5

Please sign in to comment.