Add tagging of workflows (#1647)

* clean up dropdown

* clean up focusoncreate

*  Ignore mistaken ID in POST /workflows

*  Fix undefined tag ID in PATCH /workflows

*  Shorten response for POST /tags

* remove scss mixins

* clean up imports

*  Implement validation with class-validator

* address ivan's comments

* implement modals

* Fix lint issues

* fix disabling shortcuts

* fix focus issues

* fix focus issues

* fix focus issues with modal

* fix linting issues

* use dispatch

* use constants for modal keys

* fix focus

* fix lint issues

* remove unused prop

* add modal root

* fix lint issues

* remove unused methods

* fix shortcut

* remove max width

*  Fix duplicate entry error for pg and MySQL

* update rename messaging

* update order of buttons

* fix firefox overflow on windows

* fix dropdown height

* 🔨 refactor tag crud controllers

* 🧹 remove unused imports

* use variable for number of items

* fix dropdown spacing

*  Restore type to fix build

*  Fix post-refactor PATCH /workflows/:id

*  Fix PATCH /workflows/:id for zero tags

*  Fix usage count becoming stringified

* address max's comments

* fix filter spacing

* fix blur bug

* address most of ivan's comments

* address tags type concern

* remove defaults

*  return tag id as string

* 🔨 add hooks to tag CUD operations

* 🏎 simplify timestamp pruning

* remove blur event

* fix onblur bug

*  Fix fs import to fix build

* address max's comments

* implement responsive tag container

* fix lint issues

* Set default dates in entities

* 👕 Fix lint in migrations

* update tag limits

* address ivan's comments

* remove rename, refactor header, implement new designs for save, remove responsive tag container

* update styling

* update styling

* implement responsive tag container

* implement header tags edit

* implement header tags edit

* fix lint issues

* implement expandable input

* minor fixes

* minor fixes

* use variable

* rename save as

* duplicate fixes

*  Implement unique workflow names

*  Create /workflows/new endpoint

* minor edit fixes

* lint fixes

* style fixes

* hook up saving name

* hook up tags

* clean up impl

* fix dirty state bug

* update limit

* update notification messages

* on click outside

* fix minor bug with count

* lint fixes

*  Add query string params to /workflows/new

* handle minor edge cases

* handle minor edge cases

* handle minor bugs; fix firefox dropdown issue

* Fix min width

* apply tags only after api success

* remove count fix

* 🚧 Adjust to new qs requirements

* clean up workflow tags impl, fix tags delete bug

* fix minor issue

* fix minor spacing issue

* disable wrap for ops

* fix viewport root; save on click in dropdown

* save button loading when saving name/tags

* implement max width on tags container

* implement cleaner create experience

* disable edit while updating

* codacy hex color

* refactor tags container

* fix clickability

* fix workflow open and count

* clean up structure

* fix up lint issues

*  Create migrations for unique workflow names

* fix button size

* increase workflow name limit for larger screen

* tslint fixes

* disable responsiveness for workflow modal

* rename event

* change min width for tags

* clean up pr

*  Adjust quotes in MySQL migration

*  Adjust quotes in Postgres migration

* address max's comments on styles

* remove success toasts

* add hover mode to name

* minor fixes

* refactor name preview

* fix name input not to jiggle

* finish up name input

* Fix up add tags

* clean up param

* clean up scss

* fix resizing name

* fix resizing name

* fix resize bug

* clean up edit spacing

* ignore on esc

* fix input bug

* focus input on clear

* build

* fix up add tags clickablity

* remove scrollbars

* move into folders

* clean up multiple patch req

* remove padding top from edit

* update tags on enter

* build

* rollout blur on enter behavior

* rollout esc behavior

* fix tags bug when duplicating tags

* move key to reload tags

* update header spacing

* build

* update hex case

* refactor workflow title

* remove unusued prop

* keep focus on error, fix bug on error

* Fix bug with name / tags toggle on error

* impl creating new workflow name

*  Refactor endpoint per new guidelines

* support naming endpoint

*  Refactor to support numeric suffixes

* 👕 Lint migrations for unique workflow names

*  Add migrations set default dates to indexes

* fix connection push bug

*  Lowercase default workflow name

*  Add prefixes to set default dates migration

*  Fix indentation on default dates migrations

*  Add temp ts-ignore for unrelated change

*  Adjust default dates migration for MySQL

Remove change to data column in credentials_entity, already covered by Omar's migration. Also, fix quotes from table prefix addition.

*  Adjust quotes in dates migration for PG

* fix safari color bug

* fix count bug

* fix scroll bugs in dropdown

* expand filter size

* apply box-sizing to main header

* update workflow names in executions to be wrapped by quotes

* fix bug where key is same in dropdown

* fix firefox bug

* move up push connection session

* 🔨 Remove mistakenly added nullable property

* 🔥 Remove unneeded index drop-create (PG)

* 🔥 Remove unneeded table copying

*  Merge dates migration with tags migration

* 🔨 Refactor endpoint and make wf name env

* dropdown colors in firefox

* update colors to use variables

* update thumb color

* change error message

* remove 100 char maximum

* fix bug with saving tags dropdowns multiple times

* update error message when no name

*  Update name missing toast message

*  Update workflow already exists message

* disable saving for executions

* fix bug causing modal to close

* make tags in workflow open clickable

* increase workflow limit to 3

* remove success notifications

* update header spacing

* escape tag names

* update tag and table colors

* remove tags from export

* build

* clean up push connection dependencies

* address ben's comments

* revert tags optional interface

* address comments

* update duplicate message

* build

* fix eol

* add one more eol

*  Update comment

* add hover style for workflow open, fix up font weight

Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
This commit is contained in:
Ben Hesseldieck
2021-05-29 20:31:21 +02:00
committed by GitHub
parent 335673d329
commit 05eec87d1d
92 changed files with 4602 additions and 1236 deletions

View File

@@ -19,7 +19,6 @@ import {
import { ChildProcess } from 'child_process';
import * as PCancelable from 'p-cancelable';
import { ObjectID } from 'typeorm';
export class ActiveExecutions {
@@ -53,13 +52,13 @@ export class ActiveExecutions {
if (executionData.workflowData.id !== undefined && WorkflowHelpers.isWorkflowIdValid(executionData.workflowData.id.toString()) === true) {
fullExecutionData.workflowId = executionData.workflowData.id.toString();
}
const execution = ResponseHelper.flattenExecutionData(fullExecutionData);
// Save the Execution in DB
const executionResult = await Db.collections.Execution!.save(execution as IExecutionFlattedDb);
const executionId = typeof executionResult.id === "object" ? executionResult.id.toString() : executionResult.id + "";
const executionId = typeof executionResult.id === "object" ? executionResult.id!.toString() : executionResult.id + "";
this.activeExecutions[executionId] = {
executionData,

View File

@@ -253,7 +253,7 @@ export class ActiveWorkflowRunner {
* @memberof ActiveWorkflowRunner
*/
async isActive(id: string): Promise<boolean> {
const workflow = await Db.collections.Workflow?.findOne({ id }) as IWorkflowDb;
const workflow = await Db.collections.Workflow?.findOne({ id: Number(id) }) as IWorkflowDb;
return workflow?.active as boolean;
}

View File

@@ -18,17 +18,14 @@ import { TlsOptions } from 'tls';
import * as config from '../config';
import {
MySQLDb,
PostgresDb,
SQLite,
} from './databases';
import { entities } from './databases/entities';
export let collections: IDatabaseCollections = {
Credentials: null,
Execution: null,
Workflow: null,
Webhook: null,
Tag: null,
};
import { postgresMigrations } from './databases/postgresdb/migrations';
@@ -41,15 +38,12 @@ export async function init(): Promise<IDatabaseCollections> {
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType;
const n8nFolder = UserSettings.getUserN8nFolderPath();
let entities;
let connectionOptions: ConnectionOptions;
const entityPrefix = config.get('database.tablePrefix');
switch (dbType) {
case 'postgresdb':
entities = PostgresDb;
const sslCa = await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca') as string;
const sslCert = await GenericHelpers.getConfigValue('database.postgresdb.ssl.cert') as string;
const sslKey = await GenericHelpers.getConfigValue('database.postgresdb.ssl.key') as string;
@@ -84,7 +78,6 @@ export async function init(): Promise<IDatabaseCollections> {
case 'mariadb':
case 'mysqldb':
entities = MySQLDb;
connectionOptions = {
type: dbType === 'mysqldb' ? 'mysql' : 'mariadb',
database: await GenericHelpers.getConfigValue('database.mysqldb.database') as string,
@@ -100,10 +93,9 @@ export async function init(): Promise<IDatabaseCollections> {
break;
case 'sqlite':
entities = SQLite;
connectionOptions = {
type: 'sqlite',
database: path.join(n8nFolder, 'database.sqlite'),
database: path.join(n8nFolder, 'database.sqlite'),
entityPrefix,
migrations: sqliteMigrations,
migrationsRun: false, // migrations for sqlite will be ran manually for now; see below
@@ -113,7 +105,7 @@ export async function init(): Promise<IDatabaseCollections> {
default:
throw new Error(`The database "${dbType}" is currently not supported!`);
}
}
Object.assign(connectionOptions, {
entities: Object.values(entities),
@@ -150,6 +142,7 @@ export async function init(): Promise<IDatabaseCollections> {
collections.Execution = getRepository(entities.ExecutionEntity);
collections.Workflow = getRepository(entities.WorkflowEntity);
collections.Webhook = getRepository(entities.WebhookEntity);
collections.Tag = getRepository(entities.TagEntity);
return collections;
}

View File

@@ -1,16 +1,14 @@
import * as config from '../config';
import * as express from 'express';
import { join as pathJoin } from 'path';
import {
readFile as fsReadFile,
} from 'fs/promises';
import { readFile as fsReadFile } from 'fs/promises';
import { readFileSync as fsReadFileSync } from 'fs';
import { IDataObject } from 'n8n-workflow';
import { IPackageVersions } from './';
let versionCache: IPackageVersions | undefined;
/**
* Returns the base URL n8n is reachable from
*
@@ -63,6 +61,27 @@ export async function getVersions(): Promise<IPackageVersions> {
return versionCache;
}
/**
* Extracts configuration schema for key
*
* @param {string} configKey
* @param {IDataObject} configSchema
* @returns {IDataObject} schema of the configKey
*/
function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDataObject {
const configKeyParts = configKey.split('.');
for (const key of configKeyParts) {
if (configSchema[key] === undefined) {
throw new Error(`Key "${key}" of ConfigKey "${configKey}" does not exist`);
} else if ((configSchema[key]! as IDataObject)._cvtProperties === undefined) {
configSchema = configSchema[key] as IDataObject;
} else {
configSchema = (configSchema[key] as IDataObject)._cvtProperties as IDataObject;
}
}
return configSchema;
}
/**
* Gets value from config with support for "_FILE" environment variables
@@ -72,22 +91,10 @@ export async function getVersions(): Promise<IPackageVersions> {
* @returns {(Promise<string | boolean | number | undefined>)}
*/
export async function getConfigValue(configKey: string): Promise<string | boolean | number | undefined> {
const configKeyParts = configKey.split('.');
// Get the environment variable
const configSchema = config.getSchema();
// @ts-ignore
let currentSchema = configSchema._cvtProperties as IDataObject;
for (const key of configKeyParts) {
if (currentSchema[key] === undefined) {
throw new Error(`Key "${key}" of ConfigKey "${configKey}" does not exist`);
} else if ((currentSchema[key]! as IDataObject)._cvtProperties === undefined) {
currentSchema = currentSchema[key] as IDataObject;
} else {
currentSchema = (currentSchema[key] as IDataObject)._cvtProperties as IDataObject;
}
}
const currentSchema = extractSchemaForKey(configKey, configSchema._cvtProperties as IDataObject);
// Check if environment variable is defined for config key
if (currentSchema.env === undefined) {
// No environment variable defined, so return value from config
@@ -114,3 +121,42 @@ export async function getConfigValue(configKey: string): Promise<string | boolea
return data;
}
/**
* Gets value from config with support for "_FILE" environment variables synchronously
*
* @export
* @param {string} configKey The key of the config data to get
* @returns {(string | boolean | number | undefined)}
*/
export function getConfigValueSync(configKey: string): string | boolean | number | undefined {
// Get the environment variable
const configSchema = config.getSchema();
// @ts-ignore
const currentSchema = extractSchemaForKey(configKey, configSchema._cvtProperties as IDataObject);
// Check if environment variable is defined for config key
if (currentSchema.env === undefined) {
// No environment variable defined, so return value from config
return config.get(configKey);
}
// Check if special file enviroment variable exists
const fileEnvironmentVariable = process.env[currentSchema.env + '_FILE'];
if (fileEnvironmentVariable === undefined) {
// Does not exist, so return value from config
return config.get(configKey);
}
let data;
try {
data = fsReadFileSync(fileEnvironmentVariable, 'utf8') as string;
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
}
throw error;
}
return data;
}

View File

@@ -20,11 +20,13 @@ import {
} from 'n8n-core';
import * as PCancelable from 'p-cancelable';
import { ObjectID, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import { ChildProcess } from 'child_process';
import { Url } from 'url';
import { Request } from 'express';
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
import { TagEntity } from './databases/entities/TagEntity';
export interface IActivationError {
time: number;
@@ -57,12 +59,13 @@ export interface ICredentialsOverwrite {
export interface IDatabaseCollections {
Credentials: Repository<ICredentialsDb> | null;
Execution: Repository<IExecutionFlattedDb> | null;
Workflow: Repository<IWorkflowDb> | null;
Workflow: Repository<WorkflowEntity> | null;
Webhook: Repository<IWebhookDb> | null;
Tag: Repository<TagEntity> | null;
}
export interface IWebhookDb {
workflowId: number | string | ObjectID;
workflowId: number | string;
webhookPath: string;
method: string;
node: string;
@@ -70,28 +73,44 @@ export interface IWebhookDb {
pathLength?: number;
}
export interface IWorkflowBase extends IWorkflowBaseWorkflow {
id?: number | string | ObjectID;
// ----------------------------------
// tags
// ----------------------------------
export interface ITagDb {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
}
export type UsageCount = {
usageCount: number
};
export type ITagWithCountDb = ITagDb & UsageCount;
// ----------------------------------
// workflows
// ----------------------------------
export interface IWorkflowBase extends IWorkflowBaseWorkflow {
id?: number | string;
}
// Almost identical to editor-ui.Interfaces.ts
export interface IWorkflowDb extends IWorkflowBase {
id: number | string | ObjectID;
id: number | string;
tags: ITagDb[];
}
export interface IWorkflowResponse extends IWorkflowBase {
id: string;
}
export interface IWorkflowShortResponse {
id: string;
name: string;
active: boolean;
createdAt: Date;
updatedAt: Date;
}
// ----------------------------------
// credentials
// ----------------------------------
export interface ICredentialsBase {
createdAt: Date;
@@ -99,7 +118,7 @@ export interface ICredentialsBase {
}
export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted {
id: number | string | ObjectID;
id: number | string;
}
export interface ICredentialsResponse extends ICredentialsDb {
@@ -107,7 +126,7 @@ export interface ICredentialsResponse extends ICredentialsDb {
}
export interface ICredentialsDecryptedDb extends ICredentialsBase, ICredentialsDecrypted {
id: number | string | ObjectID;
id: number | string;
}
export interface ICredentialsDecryptedResponse extends ICredentialsDecryptedDb {
@@ -118,14 +137,14 @@ export type DatabaseType = 'mariadb' | 'postgresdb' | 'mysqldb' | 'sqlite';
export type SaveExecutionDataType = 'all' | 'none';
export interface IExecutionBase {
id?: number | string | ObjectID;
id?: number | string;
mode: WorkflowExecuteMode;
startedAt: Date;
stoppedAt?: Date; // empty value means execution is still running
workflowId?: string; // To be able to filter executions easily //
finished: boolean;
retryOf?: number | string | ObjectID; // If it is a retry, the id of the execution it is a retry of.
retrySuccessId?: number | string | ObjectID; // If it failed and a retry did succeed. The id of the successful retry.
retryOf?: number | string; // If it is a retry, the id of the execution it is a retry of.
retrySuccessId?: number | string; // If it failed and a retry did succeed. The id of the successful retry.
}
// Data in regular format with references
@@ -155,7 +174,7 @@ export interface IExecutionFlatted extends IExecutionBase {
}
export interface IExecutionFlattedDb extends IExecutionBase {
id: number | string | ObjectID;
id: number | string;
data: string;
workflowData: IWorkflowBase;
}
@@ -398,7 +417,7 @@ export interface IWorkflowExecutionDataProcess {
executionMode: WorkflowExecuteMode;
executionData?: IRunExecutionData;
runData?: IRunData;
retryOf?: number | string | ObjectID;
retryOf?: number | string;
sessionId?: string;
startNodes?: string[];
workflowData: IWorkflowBase;

View File

@@ -10,6 +10,7 @@ import {
import {
getConnectionManager,
In,
Like,
} from 'typeorm';
import * as bodyParser from 'body-parser';
require('body-parser-xml')(bodyParser);
@@ -54,10 +55,9 @@ import {
IExternalHooksClass,
IN8nUISettings,
IPackageVersions,
IWorkflowBase,
ITagWithCountDb,
IWorkflowExecutionDataProcess,
IWorkflowResponse,
IWorkflowShortResponse,
LoadNodesAndCredentials,
NodeTypes,
Push,
@@ -67,6 +67,7 @@ import {
WebhookServer,
WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
WorkflowRunner,
} from './';
@@ -85,6 +86,7 @@ import {
INodePropertyOptions,
INodeTypeDescription,
IRunData,
IWorkflowBase,
IWorkflowCredentials,
Workflow,
WorkflowExecuteMode,
@@ -110,6 +112,11 @@ import * as Queue from '../src/Queue';
import { OptionsWithUrl } from 'request-promise-native';
import { Registry } from 'prom-client';
import * as TagHelpers from './TagHelpers';
import { TagEntity } from './databases/entities/TagEntity';
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
import { WorkflowNameRequest } from './WorkflowHelpers';
class App {
app: express.Application;
@@ -119,6 +126,7 @@ class App {
endpointWebhookTest: string;
endpointPresetCredentials: string;
externalHooks: IExternalHooksClass;
defaultWorkflowName: string;
saveDataErrorExecution: string;
saveDataSuccessExecution: string;
saveManualExecutions: boolean;
@@ -142,6 +150,9 @@ class App {
this.endpointWebhook = config.get('endpoints.webhook') as string;
this.endpointWebhookTest = config.get('endpoints.webhookTest') as string;
this.defaultWorkflowName = config.get('workflows.defaultName') as string;
this.saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
@@ -484,25 +495,30 @@ class App {
// Creates a new workflow
this.app.post(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => {
this.app.post(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<WorkflowEntity> => {
delete req.body.id; // ignore if sent by mistake
const incomingData = req.body;
const newWorkflowData = req.body as IWorkflowBase;
const newWorkflow = new WorkflowEntity();
newWorkflowData.name = newWorkflowData.name.trim();
newWorkflowData.createdAt = this.getCurrentDate();
newWorkflowData.updatedAt = this.getCurrentDate();
Object.assign(newWorkflow, incomingData);
newWorkflow.name = incomingData.name.trim();
newWorkflowData.id = undefined;
const incomingTagOrder = incomingData.tags.slice();
await this.externalHooks.run('workflow.create', [newWorkflowData]);
if (incomingData.tags.length) {
newWorkflow.tags = await Db.collections.Tag!.findByIds(incomingData.tags, { select: ['id', 'name'] });
}
// Save the workflow in DB
const result = await Db.collections.Workflow!.save(newWorkflowData);
await this.externalHooks.run('workflow.create', [newWorkflow]);
// Convert to response format in which the id is a string
(result as IWorkflowBase as IWorkflowResponse).id = result.id.toString();
return result as IWorkflowBase as IWorkflowResponse;
await WorkflowHelpers.validateWorkflow(newWorkflow);
const savedWorkflow = await Db.collections.Workflow!.save(newWorkflow).catch(WorkflowHelpers.throwDuplicateEntryError) as WorkflowEntity;
savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, incomingTagOrder);
// @ts-ignore
savedWorkflow.id = savedWorkflow.id.toString();
return savedWorkflow;
}));
@@ -535,47 +551,90 @@ class App {
// Returns workflows
this.app.get(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowShortResponse[]> => {
const findQuery = {} as FindManyOptions;
this.app.get(`/${this.restEndpoint}/workflows`, ResponseHelper.send(async (req: express.Request, res: express.Response) => {
const findQuery: FindManyOptions<WorkflowEntity> = {
select: ['id', 'name', 'active', 'createdAt', 'updatedAt'],
relations: ['tags'],
};
if (req.query.filter) {
findQuery.where = JSON.parse(req.query.filter as string);
}
// Return only the fields we need
findQuery.select = ['id', 'name', 'active', 'createdAt', 'updatedAt'];
const workflows = await Db.collections.Workflow!.find(findQuery);
const results = await Db.collections.Workflow!.find(findQuery);
workflows.forEach(workflow => {
// @ts-ignore
workflow.id = workflow.id.toString();
// @ts-ignore
workflow.tags = workflow.tags.map(({ id, name }) => ({ id: id.toString(), name }));
});
return workflows;
}));
for (const entry of results) {
(entry as unknown as IWorkflowShortResponse).id = entry.id.toString();
this.app.get(`/${this.restEndpoint}/workflows/new`, ResponseHelper.send(async (req: WorkflowNameRequest, res: express.Response): Promise<{ name: string }> => {
const nameToReturn = req.query.name && req.query.name !== ''
? req.query.name
: this.defaultWorkflowName;
const workflows = await Db.collections.Workflow!.find({
select: ['name'],
where: { name: Like(`${nameToReturn}%`) },
});
// name is unique
if (workflows.length === 0) {
return { name: nameToReturn };
}
return results as unknown as IWorkflowShortResponse[];
const maxSuffix = workflows.reduce((acc: number, { name }) => {
const parts = name.split(`${nameToReturn} `);
if (parts.length > 2) return acc;
const suffix = Number(parts[1]);
if (!isNaN(suffix) && Math.ceil(suffix) > acc) {
acc = Math.ceil(suffix);
}
return acc;
}, 0);
// name is duplicate but no numeric suffixes exist yet
if (maxSuffix === 0) {
return { name: `${nameToReturn} 2` };
}
return { name: `${nameToReturn} ${maxSuffix + 1}` };
}));
// Returns a specific workflow
this.app.get(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse | undefined> => {
const result = await Db.collections.Workflow!.findOne(req.params.id);
this.app.get(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<WorkflowEntity | undefined> => {
const workflow = await Db.collections.Workflow!.findOne(req.params.id, { relations: ['tags'] });
if (result === undefined) {
if (workflow === undefined) {
return undefined;
}
// Convert to response format in which the id is a string
(result as IWorkflowBase as IWorkflowResponse).id = result.id.toString();
return result as IWorkflowBase as IWorkflowResponse;
// @ts-ignore
workflow.id = workflow.id.toString();
// @ts-ignore
workflow.tags.forEach(tag => tag.id = tag.id.toString());
return workflow;
}));
// Updates an existing workflow
this.app.patch(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => {
this.app.patch(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<WorkflowEntity> => {
const { tags, ...updateData } = req.body;
const newWorkflowData = req.body as IWorkflowBase;
const id = req.params.id;
newWorkflowData.id = id;
updateData.id = id;
await this.externalHooks.run('workflow.update', [newWorkflowData]);
await this.externalHooks.run('workflow.update', [updateData]);
const isActive = await this.activeWorkflowRunner.isActive(id);
@@ -585,64 +644,78 @@ class App {
await this.activeWorkflowRunner.remove(id);
}
if (newWorkflowData.settings) {
if (newWorkflowData.settings.timezone === 'DEFAULT') {
if (updateData.settings) {
if (updateData.settings.timezone === 'DEFAULT') {
// Do not save the default timezone
delete newWorkflowData.settings.timezone;
delete updateData.settings.timezone;
}
if (newWorkflowData.settings.saveDataErrorExecution === 'DEFAULT') {
if (updateData.settings.saveDataErrorExecution === 'DEFAULT') {
// Do not save when default got set
delete newWorkflowData.settings.saveDataErrorExecution;
delete updateData.settings.saveDataErrorExecution;
}
if (newWorkflowData.settings.saveDataSuccessExecution === 'DEFAULT') {
if (updateData.settings.saveDataSuccessExecution === 'DEFAULT') {
// Do not save when default got set
delete newWorkflowData.settings.saveDataSuccessExecution;
delete updateData.settings.saveDataSuccessExecution;
}
if (newWorkflowData.settings.saveManualExecutions === 'DEFAULT') {
if (updateData.settings.saveManualExecutions === 'DEFAULT') {
// Do not save when default got set
delete newWorkflowData.settings.saveManualExecutions;
delete updateData.settings.saveManualExecutions;
}
if (parseInt(newWorkflowData.settings.executionTimeout as string, 10) === this.executionTimeout) {
if (parseInt(updateData.settings.executionTimeout as string, 10) === this.executionTimeout) {
// Do not save when default got set
delete newWorkflowData.settings.executionTimeout;
delete updateData.settings.executionTimeout;
}
}
newWorkflowData.updatedAt = this.getCurrentDate();
// required due to atomic update
updateData.updatedAt = this.getCurrentDate();
await Db.collections.Workflow!.update(id, newWorkflowData);
await this.externalHooks.run('workflow.afterUpdate', [newWorkflowData]);
await WorkflowHelpers.validateWorkflow(updateData);
await Db.collections.Workflow!.update(id, updateData).catch(WorkflowHelpers.throwDuplicateEntryError);
const tablePrefix = config.get('database.tablePrefix');
await TagHelpers.removeRelations(req.params.id, tablePrefix);
if (tags?.length) {
await TagHelpers.createRelations(req.params.id, tags, tablePrefix);
}
// We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry.
const responseData = await Db.collections.Workflow!.findOne(id);
const workflow = await Db.collections.Workflow!.findOne(id, { relations: ['tags'] });
if (responseData === undefined) {
if (workflow === undefined) {
throw new ResponseHelper.ResponseError(`Workflow with id "${id}" could not be found to be updated.`, undefined, 400);
}
if (responseData.active === true) {
if (tags?.length) {
workflow.tags = TagHelpers.sortByRequestOrder(workflow.tags, tags);
}
await this.externalHooks.run('workflow.afterUpdate', [workflow]);
if (workflow.active === true) {
// When the workflow is supposed to be active add it again
try {
await this.externalHooks.run('workflow.activate', [responseData]);
await this.externalHooks.run('workflow.activate', [workflow]);
await this.activeWorkflowRunner.add(id, isActive ? 'update' : 'activate');
} catch (error) {
// If workflow could not be activated set it again to inactive
newWorkflowData.active = false;
await Db.collections.Workflow!.update(id, newWorkflowData);
updateData.active = false;
// @ts-ignore
await Db.collections.Workflow!.update(id, updateData);
// Also set it in the returned data
responseData.active = false;
workflow.active = false;
// Now return the original error for UI to display
throw error;
}
}
// Convert to response format in which the id is a string
(responseData as IWorkflowBase as IWorkflowResponse).id = responseData.id.toString();
return responseData as IWorkflowBase as IWorkflowResponse;
// @ts-ignore
workflow.id = workflow.id.toString();
return workflow;
}));
@@ -665,7 +738,6 @@ class App {
return true;
}));
this.app.post(`/${this.restEndpoint}/workflows/run`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IExecutionPushResponse> => {
const workflowData = req.body.workflowData;
const runData: IRunData | undefined = req.body.runData;
@@ -713,6 +785,69 @@ class App {
};
}));
// Retrieves all tags, with or without usage count
this.app.get(`/${this.restEndpoint}/tags`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<TagEntity[] | ITagWithCountDb[]> => {
if (req.query.withUsageCount === 'true') {
const tablePrefix = config.get('database.tablePrefix');
return TagHelpers.getTagsWithCountDb(tablePrefix);
}
const tags = await Db.collections.Tag!.find({ select: ['id', 'name'] });
// @ts-ignore
tags.forEach(tag => tag.id = tag.id.toString());
return tags;
}));
// Creates a tag
this.app.post(`/${this.restEndpoint}/tags`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<TagEntity | void> => {
const newTag = new TagEntity();
newTag.name = req.body.name.trim();
await this.externalHooks.run('tag.beforeCreate', [newTag]);
await TagHelpers.validateTag(newTag);
const tag = await Db.collections.Tag!.save(newTag).catch(TagHelpers.throwDuplicateEntryError);
await this.externalHooks.run('tag.afterCreate', [tag]);
// @ts-ignore
tag.id = tag.id.toString();
return tag;
}));
// Updates a tag
this.app.patch(`/${this.restEndpoint}/tags/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<TagEntity | void> => {
const { name } = req.body;
const { id } = req.params;
const newTag = new TagEntity();
newTag.id = Number(id);
newTag.name = name.trim();
await this.externalHooks.run('tag.beforeUpdate', [newTag]);
await TagHelpers.validateTag(newTag);
const tag = await Db.collections.Tag!.save(newTag).catch(TagHelpers.throwDuplicateEntryError);
await this.externalHooks.run('tag.afterUpdate', [tag]);
// @ts-ignore
tag.id = tag.id.toString();
return tag;
}));
// Deletes a tag
this.app.delete(`/${this.restEndpoint}/tags/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
const id = Number(req.params.id);
await this.externalHooks.run('tag.beforeDelete', [id]);
await Db.collections.Tag!.delete({ id });
await this.externalHooks.run('tag.afterDelete', [id]);
return true;
}));
// Returns parameter values which normally get loaded from an external API or
// get generated dynamically
@@ -728,6 +863,7 @@ class App {
const nodeTypes = NodeTypes();
// @ts-ignore
const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, path, JSON.parse('' + req.query.currentNodeParameters), credentials!);
const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase;
@@ -892,8 +1028,6 @@ class App {
await this.externalHooks.run('credentials.create', [newCredentialsData]);
// Add special database related data
newCredentialsData.createdAt = this.getCurrentDate();
newCredentialsData.updatedAt = this.getCurrentDate();
// TODO: also add user automatically depending on who is logged in, if anybody is logged in

View File

@@ -0,0 +1,112 @@
import { getConnection } from "typeorm";
import { validate } from 'class-validator';
import {
ResponseHelper,
} from ".";
import {
TagEntity,
} from "./databases/entities/TagEntity";
import {
ITagWithCountDb,
} from "./Interfaces";
// ----------------------------------
// utils
// ----------------------------------
/**
* Sort a `TagEntity[]` by the order of the tag IDs in the incoming request.
*/
export function sortByRequestOrder(tagsDb: TagEntity[], tagIds: string[]) {
const tagMap = tagsDb.reduce((acc, tag) => {
// @ts-ignore
tag.id = tag.id.toString();
acc[tag.id] = tag;
return acc;
}, {} as { [key: string]: TagEntity });
return tagIds.map(tagId => tagMap[tagId]);
}
// ----------------------------------
// validators
// ----------------------------------
/**
* Validate a new tag based on `class-validator` constraints.
*/
export async function validateTag(newTag: TagEntity) {
const errors = await validate(newTag);
if (errors.length) {
const validationErrorMessage = Object.values(errors[0].constraints!)[0];
throw new ResponseHelper.ResponseError(validationErrorMessage, undefined, 400);
}
}
export function throwDuplicateEntryError(error: Error) {
const errorMessage = error.message.toLowerCase();
if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) {
throw new ResponseHelper.ResponseError('Tag name already exists', undefined, 400);
}
throw new ResponseHelper.ResponseError(errorMessage, undefined, 400);
}
// ----------------------------------
// queries
// ----------------------------------
/**
* Retrieve all tags and the number of workflows each tag is related to.
*/
export function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithCountDb[]> {
return getConnection()
.createQueryBuilder()
.select(`${tablePrefix}tag_entity.id`, 'id')
.addSelect(`${tablePrefix}tag_entity.name`, 'name')
.addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount')
.from(`${tablePrefix}tag_entity`, 'tag_entity')
.leftJoin(`${tablePrefix}workflows_tags`, 'workflows_tags', `${tablePrefix}workflows_tags.tagId = tag_entity.id`)
.groupBy(`${tablePrefix}tag_entity.id`)
.getRawMany()
.then(tagsWithCount => {
tagsWithCount.forEach(tag => {
tag.id = tag.id.toString();
tag.usageCount = Number(tag.usageCount);
});
return tagsWithCount;
});
}
// ----------------------------------
// mutations
// ----------------------------------
/**
* Relate a workflow to one or more tags.
*/
export function createRelations(workflowId: string, tagIds: string[], tablePrefix: string) {
return getConnection()
.createQueryBuilder()
.insert()
.into(`${tablePrefix}workflows_tags`)
.values(tagIds.map(tagId => ({ workflowId, tagId })))
.execute();
}
/**
* Remove all tags for a workflow during a tag update operation.
*/
export function removeRelations(workflowId: string, tablePrefix: string) {
return getConnection()
.createQueryBuilder()
.delete()
.from(`${tablePrefix}workflows_tags`)
.where('workflowId = :id', { id: workflowId })
.execute();
}

View File

@@ -6,6 +6,7 @@ import {
IWorkflowErrorData,
IWorkflowExecutionDataProcess,
NodeTypes,
ResponseHelper,
WorkflowCredentials,
WorkflowRunner,
} from './';
@@ -22,6 +23,8 @@ import {
Workflow,} from 'n8n-workflow';
import * as config from '../config';
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
import { validate } from 'class-validator';
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
@@ -82,7 +85,7 @@ export function isWorkflowIdValid (id: string | null | undefined | number): bool
export async function executeErrorWorkflow(workflowId: string, workflowErrorData: IWorkflowErrorData): Promise<void> {
// Wrap everything in try/catch to make sure that no errors bubble up and all get caught here
try {
const workflowData = await Db.collections.Workflow!.findOne({ id: workflowId });
const workflowData = await Db.collections.Workflow!.findOne({ id: Number(workflowId) });
if (workflowData === undefined) {
// The error workflow could not be found
@@ -357,3 +360,32 @@ export async function getStaticDataById(workflowId: string | number) {
return workflowData.staticData || {};
}
// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers?
export async function validateWorkflow(newWorkflow: WorkflowEntity) {
const errors = await validate(newWorkflow);
if (errors.length) {
const validationErrorMessage = Object.values(errors[0].constraints!)[0];
throw new ResponseHelper.ResponseError(validationErrorMessage, undefined, 400);
}
}
export function throwDuplicateEntryError(error: Error) {
const errorMessage = error.message.toLowerCase();
if (errorMessage.includes('unique') || errorMessage.includes('duplicate')) {
throw new ResponseHelper.ResponseError('There is already a workflow with this name', undefined, 400);
}
throw new ResponseHelper.ResponseError(errorMessage, undefined, 400);
}
export type WorkflowNameRequest = Express.Request & {
query: {
name?: string;
offset?: string;
}
};

View File

@@ -3,14 +3,22 @@ import {
} from 'n8n-workflow';
import {
ICredentialsDb,
} from '../../';
getTimestampSyntax,
resolveDataType
} from '../utils';
import {
ICredentialsDb,
} from '../..';
import {
BeforeUpdate,
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
@@ -33,13 +41,17 @@ export class CredentialsEntity implements ICredentialsDb {
})
type: string;
@Column('json')
@Column(resolveDataType('json'))
nodesAccess: ICredentialNodeAccess[];
@Column('timestamp')
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
createdAt: Date;
@Column('timestamp')
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() })
updatedAt: Date;
@BeforeUpdate()
setUpdateDate() {
this.updatedAt = new Date();
}
}

View File

@@ -7,14 +7,18 @@ import {
IWorkflowDb,
} from '../../';
import {
resolveDataType
} from '../utils';
import {
Column,
ColumnOptions,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class ExecutionEntity implements IExecutionFlattedDb {
@@ -36,14 +40,14 @@ export class ExecutionEntity implements IExecutionFlattedDb {
@Column({ nullable: true })
retrySuccessId: string;
@Column('datetime')
@Column(resolveDataType('datetime'))
startedAt: Date;
@Index()
@Column('datetime', { nullable: true })
@Column({ type: resolveDataType('datetime') as ColumnOptions['type'], nullable: true })
stoppedAt: Date;
@Column('json')
@Column(resolveDataType('json'))
workflowData: IWorkflowDb;
@Index()

View File

@@ -0,0 +1,37 @@
import { BeforeUpdate, Column, CreateDateColumn, Entity, Index, ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { IsDate, IsOptional, IsString, Length } from 'class-validator';
import { ITagDb } from '../../Interfaces';
import { WorkflowEntity } from './WorkflowEntity';
import { getTimestampSyntax } from '../utils';
@Entity()
export class TagEntity implements ITagDb {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 24 })
@Index({ unique: true })
@IsString({ message: 'Tag name must be of type string.' })
@Length(1, 24, { message: 'Tag name must be 1 to 24 characters long.' })
name: string;
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
@IsOptional() // ignored by validation because set at DB level
@IsDate()
createdAt: Date;
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() })
@IsOptional() // ignored by validation because set at DB level
@IsDate()
updatedAt: Date;
@ManyToMany(() => WorkflowEntity, workflow => workflow.tags)
workflows: WorkflowEntity[];
@BeforeUpdate()
setUpdateDate() {
this.updatedAt = new Date();
}
}

View File

@@ -0,0 +1,94 @@
import {
Length,
} from 'class-validator';
import {
IConnections,
IDataObject,
INode,
IWorkflowSettings,
} from 'n8n-workflow';
import {
BeforeUpdate,
Column,
ColumnOptions,
CreateDateColumn,
Entity,
Index,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import {
IWorkflowDb,
} from '../../';
import {
getTimestampSyntax,
resolveDataType
} from '../utils';
import {
TagEntity,
} from './TagEntity';
@Entity()
export class WorkflowEntity implements IWorkflowDb {
@PrimaryGeneratedColumn()
id: number;
@Index({ unique: true })
@Length(1, 128, { message: 'Workflow name must be 1 to 128 characters long.' })
@Column({ length: 128 })
name: string;
@Column()
active: boolean;
@Column(resolveDataType('json'))
nodes: INode[];
@Column(resolveDataType('json'))
connections: IConnections;
@CreateDateColumn({ precision: 3, default: () => getTimestampSyntax() })
createdAt: Date;
@UpdateDateColumn({ precision: 3, default: () => getTimestampSyntax(), onUpdate: getTimestampSyntax() })
updatedAt: Date;
@Column({
type: resolveDataType('json') as ColumnOptions['type'],
nullable: true,
})
settings?: IWorkflowSettings;
@Column({
type: resolveDataType('json') as ColumnOptions['type'],
nullable: true,
})
staticData?: IDataObject;
@ManyToMany(() => TagEntity, tag => tag.workflows)
@JoinTable({
name: "workflows_tags", // table name for the junction table of this relation
joinColumn: {
name: "workflowId",
referencedColumnName: "id",
},
inverseJoinColumn: {
name: "tagId",
referencedColumnName: "id",
},
})
tags: TagEntity[];
@BeforeUpdate()
setUpdateDate() {
this.updatedAt = new Date();
}
}

View File

@@ -0,0 +1,13 @@
import { CredentialsEntity } from './CredentialsEntity';
import { ExecutionEntity } from './ExecutionEntity';
import { WorkflowEntity } from './WorkflowEntity';
import { WebhookEntity } from './WebhookEntity';
import { TagEntity } from './TagEntity';
export const entities = {
CredentialsEntity,
ExecutionEntity,
WorkflowEntity,
WebhookEntity,
TagEntity,
};

View File

@@ -1,9 +0,0 @@
import * as PostgresDb from './postgresdb';
import * as SQLite from './sqlite';
import * as MySQLDb from './mysqldb';
export {
PostgresDb,
SQLite,
MySQLDb,
};

View File

@@ -1,44 +0,0 @@
import {
ICredentialNodeAccess,
} from 'n8n-workflow';
import {
ICredentialsDb,
} from '../../';
import {
Column,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class CredentialsEntity implements ICredentialsDb {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 128,
})
name: string;
@Column('text')
data: string;
@Index()
@Column({
length: 32,
})
type: string;
@Column('json')
nodesAccess: ICredentialNodeAccess[];
@Column('datetime')
createdAt: Date;
@Column('datetime')
updatedAt: Date;
}

View File

@@ -1,55 +0,0 @@
import {
IConnections,
IDataObject,
INode,
IWorkflowSettings,
} from 'n8n-workflow';
import {
IWorkflowDb,
} from '../../';
import {
Column,
Entity,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class WorkflowEntity implements IWorkflowDb {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 128,
})
name: string;
@Column()
active: boolean;
@Column('json')
nodes: INode[];
@Column('json')
connections: IConnections;
@Column('datetime')
createdAt: Date;
@Column('datetime')
updatedAt: Date;
@Column({
type: 'json',
nullable: true,
})
settings?: IWorkflowSettings;
@Column({
type: 'json',
nullable: true,
})
staticData?: IDataObject;
}

View File

@@ -1,4 +0,0 @@
export * from './CredentialsEntity';
export * from './ExecutionEntity';
export * from './WorkflowEntity';
export * from './WebhookEntity';

View File

@@ -0,0 +1,50 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import * as config from '../../../../config';
export class CreateTagEntity1617268711084 implements MigrationInterface {
name = 'CreateTagEntity1617268711084';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
// create tags table + relationship with workflow entity
await queryRunner.query('CREATE TABLE `' + tablePrefix + 'tag_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, UNIQUE INDEX `IDX_' + tablePrefix + '8f949d7a3a984759044054e89b` (`name`), PRIMARY KEY (`id`)) ENGINE=InnoDB');
await queryRunner.query('CREATE TABLE `' + tablePrefix + 'workflows_tags` (`workflowId` int NOT NULL, `tagId` int NOT NULL, INDEX `IDX_' + tablePrefix + '54b2f0343d6a2078fa13744386` (`workflowId`), INDEX `IDX_' + tablePrefix + '77505b341625b0b4768082e217` (`tagId`), PRIMARY KEY (`workflowId`, `tagId`)) ENGINE=InnoDB');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'workflows_tags` ADD CONSTRAINT `FK_' + tablePrefix + '54b2f0343d6a2078fa137443869` FOREIGN KEY (`workflowId`) REFERENCES `' + tablePrefix + 'workflow_entity`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'workflows_tags` ADD CONSTRAINT `FK_' + tablePrefix + '77505b341625b0b4768082e2171` FOREIGN KEY (`tagId`) REFERENCES `' + tablePrefix + 'tag_entity`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION');
// set default dates for `createdAt` and `updatedAt`
await queryRunner.query("ALTER TABLE `" + tablePrefix + "credentials_entity` CHANGE `createdAt` `createdAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "credentials_entity` CHANGE `updatedAt` `updatedAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "tag_entity` CHANGE `createdAt` `createdAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "tag_entity` CHANGE `updatedAt` `updatedAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "workflow_entity` CHANGE `createdAt` `createdAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "workflow_entity` CHANGE `updatedAt` `updatedAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)");
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
// tags
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'workflows_tags` DROP FOREIGN KEY `FK_' + tablePrefix + '77505b341625b0b4768082e2171`');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'workflows_tags` DROP FOREIGN KEY `FK_' + tablePrefix + '54b2f0343d6a2078fa137443869`');
await queryRunner.query('DROP INDEX `IDX_' + tablePrefix + '77505b341625b0b4768082e217` ON `' + tablePrefix + 'workflows_tags`');
await queryRunner.query('DROP INDEX `IDX_' + tablePrefix + '54b2f0343d6a2078fa13744386` ON `' + tablePrefix + 'workflows_tags`');
await queryRunner.query('DROP TABLE `' + tablePrefix + 'workflows_tags`');
await queryRunner.query('DROP INDEX `IDX_' + tablePrefix + '8f949d7a3a984759044054e89b` ON `' + tablePrefix + 'tag_entity`');
await queryRunner.query('DROP TABLE `' + tablePrefix + 'tag_entity`');
// `createdAt` and `updatedAt`
await queryRunner.query("ALTER TABLE `" + tablePrefix + "workflow_entity` CHANGE `updatedAt` `updatedAt` datetime(0) NOT NULL");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "workflow_entity` CHANGE `createdAt` `createdAt` datetime(0) NOT NULL");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "tag_entity` CHANGE `updatedAt` `updatedAt` datetime(0) NOT NULL");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "tag_entity` CHANGE `createdAt` `createdAt` datetime(0) NOT NULL");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "credentials_entity` CHANGE `updatedAt` `updatedAt` datetime(0) NOT NULL");
await queryRunner.query("ALTER TABLE `" + tablePrefix + "credentials_entity` CHANGE `createdAt` `createdAt` datetime(0) NOT NULL");
}
}

View File

@@ -0,0 +1,48 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import config = require("../../../../config");
export class UniqueWorkflowNames1620826335440 implements MigrationInterface {
name = 'UniqueWorkflowNames1620826335440';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
const workflowNames = await queryRunner.query(`
SELECT name
FROM ${tablePrefix}workflow_entity
`);
for (const { name } of workflowNames) {
const duplicates = await queryRunner.query(`
SELECT id, name
FROM ${tablePrefix}workflow_entity
WHERE name = '${name}'
ORDER BY createdAt ASC
`);
if (duplicates.length > 1) {
await Promise.all(duplicates.map(({ id, name }: { id: number; name: string; }, index: number) => {
if (index === 0) return Promise.resolve();
return queryRunner.query(`
UPDATE ${tablePrefix}workflow_entity
SET name = '${name} ${index + 1}'
WHERE id = '${id}'
`);
}));
}
}
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'workflow_entity` ADD UNIQUE INDEX `IDX_' + tablePrefix + '943d8f922be094eb507cb9a7f9` (`name`)');
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'workflow_entity` DROP INDEX `IDX_' + tablePrefix + '943d8f922be094eb507cb9a7f9`');
}
}

View File

@@ -5,6 +5,8 @@ import { AddWebhookId1611149998770 } from './1611149998770-AddWebhookId';
import { MakeStoppedAtNullable1607431743767 } from './1607431743767-MakeStoppedAtNullable';
import { ChangeDataSize1615306975123 } from './1615306975123-ChangeDataSize';
import { ChangeCredentialDataSize1620729500000 } from './1620729500000-ChangeCredentialDataSize';
import { CreateTagEntity1617268711084 } from './1617268711084-CreateTagEntity';
import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
export const mysqlMigrations = [
InitialMigration1588157391238,
@@ -14,4 +16,6 @@ export const mysqlMigrations = [
MakeStoppedAtNullable1607431743767,
ChangeDataSize1615306975123,
ChangeCredentialDataSize1620729500000,
CreateTagEntity1617268711084,
UniqueWorkflowNames1620826335440,
];

View File

@@ -1,52 +0,0 @@
import {
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
IExecutionFlattedDb,
IWorkflowDb,
} from '../../';
import {
Column,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class ExecutionEntity implements IExecutionFlattedDb {
@PrimaryGeneratedColumn()
id: number;
@Column('text')
data: string;
@Column()
finished: boolean;
@Column('varchar')
mode: WorkflowExecuteMode;
@Column({ nullable: true })
retryOf: string;
@Column({ nullable: true })
retrySuccessId: string;
@Column('timestamp')
startedAt: Date;
@Index()
@Column('timestamp', { nullable: true })
stoppedAt: Date;
@Column('json')
workflowData: IWorkflowDb;
@Index()
@Column({ nullable: true })
workflowId: string;
}

View File

@@ -1,33 +0,0 @@
import {
Column,
Entity,
Index,
PrimaryColumn,
} from 'typeorm';
import {
IWebhookDb,
} from '../../';
@Entity()
@Index(['webhookId', 'method', 'pathLength'])
export class WebhookEntity implements IWebhookDb {
@Column()
workflowId: number;
@PrimaryColumn()
webhookPath: string;
@PrimaryColumn()
method: string;
@Column()
node: string;
@Column({ nullable: true })
webhookId: string;
@Column({ nullable: true })
pathLength: number;
}

View File

@@ -1,55 +0,0 @@
import {
IConnections,
IDataObject,
INode,
IWorkflowSettings,
} from 'n8n-workflow';
import {
IWorkflowDb,
} from '../../';
import {
Column,
Entity,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class WorkflowEntity implements IWorkflowDb {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 128,
})
name: string;
@Column()
active: boolean;
@Column('json')
nodes: INode[];
@Column('json')
connections: IConnections;
@Column('timestamp')
createdAt: Date;
@Column('timestamp')
updatedAt: Date;
@Column({
type: 'json',
nullable: true,
})
settings?: IWorkflowSettings;
@Column({
type: 'json',
nullable: true,
})
staticData?: IDataObject;
}

View File

@@ -1,5 +0,0 @@
export * from './CredentialsEntity';
export * from './ExecutionEntity';
export * from './WorkflowEntity';
export * from './WebhookEntity';

View File

@@ -0,0 +1,76 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import * as config from '../../../../config';
export class CreateTagEntity1617270242566 implements MigrationInterface {
name = 'CreateTagEntity1617270242566';
async up(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.get('database.tablePrefix');
const tablePrefixPure = tablePrefix;
const schema = config.get('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}
// create tags table + relationship with workflow entity
await queryRunner.query(`CREATE TABLE ${tablePrefix}tag_entity ("id" SERIAL NOT NULL, "name" character varying(24) NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, CONSTRAINT "PK_${tablePrefixPure}7a50a9b74ae6855c0dcaee25052" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE UNIQUE INDEX IDX_${tablePrefixPure}812eb05f7451ca757fb98444ce ON ${tablePrefix}tag_entity ("name") `);
await queryRunner.query(`CREATE TABLE ${tablePrefix}workflows_tags ("workflowId" integer NOT NULL, "tagId" integer NOT NULL, CONSTRAINT "PK_${tablePrefixPure}a60448a90e51a114e95e2a125b3" PRIMARY KEY ("workflowId", "tagId"))`);
await queryRunner.query(`CREATE INDEX IDX_${tablePrefixPure}31140eb41f019805b40d008744 ON ${tablePrefix}workflows_tags ("workflowId") `);
await queryRunner.query(`CREATE INDEX IDX_${tablePrefixPure}5e29bfe9e22c5d6567f509d4a4 ON ${tablePrefix}workflows_tags ("tagId") `);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflows_tags ADD CONSTRAINT "FK_${tablePrefixPure}31140eb41f019805b40d0087449" FOREIGN KEY ("workflowId") REFERENCES ${tablePrefix}workflow_entity ("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflows_tags ADD CONSTRAINT "FK_${tablePrefixPure}5e29bfe9e22c5d6567f509d4a46" FOREIGN KEY ("tagId") REFERENCES ${tablePrefix}tag_entity ("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
// set default dates for `createdAt` and `updatedAt`
await queryRunner.query(`ALTER TABLE ${tablePrefix}credentials_entity ALTER COLUMN "createdAt" TYPE TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}credentials_entity ALTER COLUMN "createdAt" SET DEFAULT CURRENT_TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}credentials_entity ALTER COLUMN "updatedAt" TYPE TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}credentials_entity ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}tag_entity ALTER COLUMN "createdAt" TYPE TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}tag_entity ALTER COLUMN "createdAt" SET DEFAULT CURRENT_TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}tag_entity ALTER COLUMN "updatedAt" TYPE TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}tag_entity ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ALTER COLUMN "createdAt" TYPE TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ALTER COLUMN "createdAt" SET DEFAULT CURRENT_TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ALTER COLUMN "updatedAt" TYPE TIMESTAMP(3)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP(3)`);
}
async down(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.get('database.tablePrefix');
const tablePrefixPure = tablePrefix;
const schema = config.get('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}
// tags
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflows_tags DROP CONSTRAINT "FK_${tablePrefixPure}5e29bfe9e22c5d6567f509d4a46"`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflows_tags DROP CONSTRAINT "FK_${tablePrefixPure}31140eb41f019805b40d0087449"`);
await queryRunner.query(`DROP INDEX IDX_${tablePrefixPure}5e29bfe9e22c5d6567f509d4a4`);
await queryRunner.query(`DROP INDEX IDX_${tablePrefixPure}31140eb41f019805b40d008744`);
await queryRunner.query(`DROP TABLE ${tablePrefix}workflows_tags`);
await queryRunner.query(`DROP INDEX IDX_${tablePrefixPure}812eb05f7451ca757fb98444ce`);
await queryRunner.query(`DROP TABLE ${tablePrefix}tag_entity`);
// `createdAt` and `updatedAt`
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ALTER COLUMN "updatedAt" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ALTER COLUMN "updatedAt" TYPE TIMESTAMP(6)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ALTER COLUMN "createdAt" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}workflow_entity ALTER COLUMN "createdAt" TYPE TIMESTAMP(6)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}tag_entity ALTER COLUMN "updatedAt" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}tag_entity ALTER COLUMN "updatedAt" TYPE TIMESTAMP(6)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}tag_entity ALTER COLUMN "createdAt" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}tag_entity ALTER COLUMN "createdAt" TYPE TIMESTAMP(6)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}credentials_entity ALTER COLUMN "updatedAt" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}credentials_entity ALTER COLUMN "updatedAt" TYPE TIMESTAMP(6)`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}credentials_entity ALTER COLUMN "createdAt" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}credentials_entity ALTER COLUMN "createdAt" TYPE TIMESTAMP(6)`);
}
}

View File

@@ -0,0 +1,60 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import config = require("../../../../config");
export class UniqueWorkflowNames1620824779533 implements MigrationInterface {
name = 'UniqueWorkflowNames1620824779533';
async up(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.get('database.tablePrefix');
const tablePrefixPure = tablePrefix;
const schema = config.get('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}
const workflowNames = await queryRunner.query(`
SELECT name
FROM ${tablePrefix}workflow_entity
`);
for (const { name } of workflowNames) {
const duplicates = await queryRunner.query(`
SELECT id, name
FROM ${tablePrefix}workflow_entity
WHERE name = '${name}'
ORDER BY "createdAt" ASC
`);
if (duplicates.length > 1) {
await Promise.all(duplicates.map(({ id, name }: { id: number; name: string; }, index: number) => {
if (index === 0) return Promise.resolve();
return queryRunner.query(`
UPDATE ${tablePrefix}workflow_entity
SET name = '${name} ${index + 1}'
WHERE id = '${id}'
`);
}));
}
}
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_${tablePrefixPure}a252c527c4c89237221fe2c0ab" ON ${tablePrefix}workflow_entity ("name") `);
}
async down(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.get('database.tablePrefix');
const tablePrefixPure = tablePrefix;
const schema = config.get('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}
await queryRunner.query(`DROP INDEX "public"."IDX_${tablePrefixPure}a252c527c4c89237221fe2c0ab"`);
}
}

View File

@@ -3,6 +3,8 @@ import { WebhookModel1589476000887 } from './1589476000887-WebhookModel';
import { CreateIndexStoppedAt1594828256133 } from './1594828256133-CreateIndexStoppedAt';
import { AddWebhookId1611144599516 } from './1611144599516-AddWebhookId';
import { MakeStoppedAtNullable1607431743768 } from './1607431743768-MakeStoppedAtNullable';
import { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity';
import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames';
export const postgresMigrations = [
InitialMigration1587669153312,
@@ -10,4 +12,6 @@ export const postgresMigrations = [
CreateIndexStoppedAt1594828256133,
AddWebhookId1611144599516,
MakeStoppedAtNullable1607431743768,
CreateTagEntity1617270242566,
UniqueWorkflowNames1620824779533,
];

View File

@@ -1,44 +0,0 @@
import {
ICredentialNodeAccess,
} from 'n8n-workflow';
import {
ICredentialsDb,
} from '../../';
import {
Column,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class CredentialsEntity implements ICredentialsDb {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 128,
})
name: string;
@Column('text')
data: string;
@Index()
@Column({
length: 32,
})
type: string;
@Column('simple-json')
nodesAccess: ICredentialNodeAccess[];
@Column()
createdAt: Date;
@Column()
updatedAt: Date;
}

View File

@@ -1,52 +0,0 @@
import {
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
IExecutionFlattedDb,
IWorkflowDb,
} from '../../';
import {
Column,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class ExecutionEntity implements IExecutionFlattedDb {
@PrimaryGeneratedColumn()
id: number;
@Column('text')
data: string;
@Column()
finished: boolean;
@Column('varchar')
mode: WorkflowExecuteMode;
@Column({ nullable: true })
retryOf: string;
@Column({ nullable: true })
retrySuccessId: string;
@Column()
startedAt: Date;
@Index()
@Column({ nullable: true })
stoppedAt: Date;
@Column('simple-json')
workflowData: IWorkflowDb;
@Index()
@Column({ nullable: true })
workflowId: string;
}

View File

@@ -1,33 +0,0 @@
import {
Column,
Entity,
Index,
PrimaryColumn,
} from 'typeorm';
import {
IWebhookDb,
} from '../../Interfaces';
@Entity()
@Index(['webhookId', 'method', 'pathLength'])
export class WebhookEntity implements IWebhookDb {
@Column()
workflowId: number;
@PrimaryColumn()
webhookPath: string;
@PrimaryColumn()
method: string;
@Column()
node: string;
@Column({ nullable: true })
webhookId: string;
@Column({ nullable: true })
pathLength: number;
}

View File

@@ -1,55 +0,0 @@
import {
IConnections,
IDataObject,
INode,
IWorkflowSettings,
} from 'n8n-workflow';
import {
IWorkflowDb,
} from '../../';
import {
Column,
Entity,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class WorkflowEntity implements IWorkflowDb {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 128,
})
name: string;
@Column()
active: boolean;
@Column('simple-json')
nodes: INode[];
@Column('simple-json')
connections: IConnections;
@Column()
createdAt: Date;
@Column()
updatedAt: Date;
@Column({
type: 'simple-json',
nullable: true,
})
settings?: IWorkflowSettings;
@Column({
type: 'simple-json',
nullable: true,
})
staticData?: IDataObject;
}

View File

@@ -1,4 +0,0 @@
export * from './CredentialsEntity';
export * from './ExecutionEntity';
export * from './WorkflowEntity';
export * from './WebhookEntity';

View File

@@ -0,0 +1,69 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import * as config from '../../../../config';
export class CreateTagEntity1617213344594 implements MigrationInterface {
name = 'CreateTagEntity1617213344594';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
// create tags table + relationship with workflow entity
await queryRunner.query(`CREATE TABLE "${tablePrefix}tag_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(24) NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL)`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_${tablePrefix}8f949d7a3a984759044054e89b" ON "${tablePrefix}tag_entity" ("name") `);
await queryRunner.query(`CREATE TABLE "${tablePrefix}workflows_tags" ("workflowId" integer NOT NULL, "tagId" integer NOT NULL, CONSTRAINT "FK_54b2f0343d6a2078fa137443869" FOREIGN KEY ("workflowId") REFERENCES "${tablePrefix}workflow_entity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_77505b341625b0b4768082e2171" FOREIGN KEY ("tagId") REFERENCES "${tablePrefix}tag_entity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, PRIMARY KEY ("workflowId", "tagId"))`);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}54b2f0343d6a2078fa13744386" ON "${tablePrefix}workflows_tags" ("workflowId") `);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}77505b341625b0b4768082e217" ON "${tablePrefix}workflows_tags" ("tagId") `);
// set default dates for `createdAt` and `updatedAt`
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}07fde106c0b471d8cc80a64fc8"`);
await queryRunner.query(`CREATE TABLE "${tablePrefix}temporary_credentials_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "data" text NOT NULL, "type" varchar(32) NOT NULL, "nodesAccess" text NOT NULL, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')))`);
await queryRunner.query(`INSERT INTO "${tablePrefix}temporary_credentials_entity"("id", "name", "data", "type", "nodesAccess", "createdAt", "updatedAt") SELECT "id", "name", "data", "type", "nodesAccess", "createdAt", "updatedAt" FROM "${tablePrefix}credentials_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}credentials_entity"`);
await queryRunner.query(`ALTER TABLE "${tablePrefix}temporary_credentials_entity" RENAME TO "${tablePrefix}credentials_entity"`);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}07fde106c0b471d8cc80a64fc8" ON "${tablePrefix}credentials_entity" ("type") `);
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}8f949d7a3a984759044054e89b"`);
await queryRunner.query(`CREATE TABLE "${tablePrefix}temporary_tag_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(24) NOT NULL, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')))`);
await queryRunner.query(`INSERT INTO "${tablePrefix}temporary_tag_entity"("id", "name", "createdAt", "updatedAt") SELECT "id", "name", "createdAt", "updatedAt" FROM "${tablePrefix}tag_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}tag_entity"`);
await queryRunner.query(`ALTER TABLE "${tablePrefix}temporary_tag_entity" RENAME TO "${tablePrefix}tag_entity"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_${tablePrefix}8f949d7a3a984759044054e89b" ON "${tablePrefix}tag_entity" ("name") `);
await queryRunner.query(`CREATE TABLE "${tablePrefix}temporary_workflow_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "active" boolean NOT NULL, "nodes" text, "connections" text NOT NULL, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "settings" text, "staticData" text)`);
await queryRunner.query(`INSERT INTO "${tablePrefix}temporary_workflow_entity"("id", "name", "active", "nodes", "connections", "createdAt", "updatedAt", "settings", "staticData") SELECT "id", "name", "active", "nodes", "connections", "createdAt", "updatedAt", "settings", "staticData" FROM "${tablePrefix}workflow_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}workflow_entity"`);
await queryRunner.query(`ALTER TABLE "${tablePrefix}temporary_workflow_entity" RENAME TO "${tablePrefix}workflow_entity"`);
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
// tags
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}77505b341625b0b4768082e217"`);
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}54b2f0343d6a2078fa13744386"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}workflows_tags"`);
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}8f949d7a3a984759044054e89b"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}tag_entity"`);
// `createdAt` and `updatedAt`
await queryRunner.query(`ALTER TABLE "${tablePrefix}workflow_entity" RENAME TO "${tablePrefix}temporary_workflow_entity"`);
await queryRunner.query(`CREATE TABLE "${tablePrefix}workflow_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "active" boolean NOT NULL, "nodes" text NOT NULL, "connections" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL, "settings" text, "staticData" text)`);
await queryRunner.query(`INSERT INTO "${tablePrefix}workflow_entity"("id", "name", "active", "nodes", "connections", "createdAt", "updatedAt", "settings", "staticData") SELECT "id", "name", "active", "nodes", "connections", "createdAt", "updatedAt", "settings", "staticData" FROM "${tablePrefix}temporary_workflow_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}temporary_workflow_entity"`);
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}8f949d7a3a984759044054e89b"`);
await queryRunner.query(`ALTER TABLE "${tablePrefix}tag_entity" RENAME TO "${tablePrefix}temporary_tag_entity"`);
await queryRunner.query(`CREATE TABLE "${tablePrefix}tag_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(24) NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL)`);
await queryRunner.query(`INSERT INTO "${tablePrefix}tag_entity"("id", "name", "createdAt", "updatedAt") SELECT "id", "name", "createdAt", "updatedAt" FROM "${tablePrefix}temporary_tag_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}temporary_tag_entity"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_${tablePrefix}8f949d7a3a984759044054e89b" ON "${tablePrefix}tag_entity" ("name") `);
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}07fde106c0b471d8cc80a64fc8"`);
await queryRunner.query(`ALTER TABLE "${tablePrefix}credentials_entity" RENAME TO "temporary_credentials_entity"`);
await queryRunner.query(`CREATE TABLE "${tablePrefix}credentials_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "data" text NOT NULL, "type" varchar(32) NOT NULL, "nodesAccess" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL)`);
await queryRunner.query(`INSERT INTO "${tablePrefix}credentials_entity"("id", "name", "data", "type", "nodesAccess", "createdAt", "updatedAt") SELECT "id", "name", "data", "type", "nodesAccess", "createdAt", "updatedAt" FROM "${tablePrefix}temporary_credentials_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}temporary_credentials_entity"`);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}07fde106c0b471d8cc80a64fc8" ON "credentials_entity" ("type") `);
}
}

View File

@@ -0,0 +1,47 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import config = require("../../../../config");
export class UniqueWorkflowNames1620821879465 implements MigrationInterface {
name = 'UniqueWorkflowNames1620821879465';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
const workflowNames = await queryRunner.query(`
SELECT name
FROM "${tablePrefix}workflow_entity"
`);
for (const { name } of workflowNames) {
const duplicates = await queryRunner.query(`
SELECT id, name
FROM "${tablePrefix}workflow_entity"
WHERE name = "${name}"
ORDER BY createdAt ASC
`);
if (duplicates.length > 1) {
await Promise.all(duplicates.map(({ id, name }: { id: number; name: string; }, index: number) => {
if (index === 0) return Promise.resolve();
return queryRunner.query(`
UPDATE "${tablePrefix}workflow_entity"
SET name = "${name} ${index + 1}"
WHERE id = '${id}'
`);
}));
}
}
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_${tablePrefix}943d8f922be094eb507cb9a7f9" ON "${tablePrefix}workflow_entity" ("name") `);
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}943d8f922be094eb507cb9a7f9"`);
}
}

View File

@@ -3,6 +3,8 @@ import { WebhookModel1592445003908 } from './1592445003908-WebhookModel';
import { CreateIndexStoppedAt1594825041918 } from './1594825041918-CreateIndexStoppedAt';
import { AddWebhookId1611071044839 } from './1611071044839-AddWebhookId';
import { MakeStoppedAtNullable1607431743769 } from './1607431743769-MakeStoppedAtNullable';
import { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity';
import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames';
export const sqliteMigrations = [
InitialMigration1588102412422,
@@ -10,4 +12,6 @@ export const sqliteMigrations = [
CreateIndexStoppedAt1594825041918,
AddWebhookId1611071044839,
MakeStoppedAtNullable1607431743769,
CreateTagEntity1617213344594,
UniqueWorkflowNames1620821879465,
];

View File

@@ -0,0 +1,42 @@
import {
DatabaseType,
} from '../index';
import { getConfigValueSync } from '../../src/GenericHelpers';
/**
* Resolves the data type for the used database type
*
* @export
* @param {string} dataType
* @returns {string}
*/
export function resolveDataType(dataType: string) {
const dbType = getConfigValueSync('database.type') as DatabaseType;
const typeMap: { [key in DatabaseType]: { [key: string]: string } } = {
sqlite: {
json: 'simple-json',
},
postgresdb: {
datetime: 'timestamp',
},
mysqldb: {},
mariadb: {},
};
return typeMap[dbType][dataType] ?? dataType;
}
export function getTimestampSyntax() {
const dbType = getConfigValueSync('database.type') as DatabaseType;
const map: { [key in DatabaseType]: string } = {
sqlite: "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')",
postgresdb: "CURRENT_TIMESTAMP(3)",
mysqldb: "CURRENT_TIMESTAMP(3)",
mariadb: "CURRENT_TIMESTAMP(3)",
};
return map[dbType];
}