⚡ Change credentials structure (#2139)
* ✨ change FE to handle new object type * 🚸 improve UX of handling invalid credentials * 🚧 WIP * 🎨 fix typescript issues * 🐘 add migrations for all supported dbs * ✏️ add description to migrations * ⚡ add credential update on import * ⚡ resolve after merge issues * 👕 fix lint issues * ⚡ check credentials on workflow create/update * update interface * 👕 fix ts issues * ⚡ adaption to new credentials UI * 🐛 intialize cache on BE for credentials check * 🐛 fix undefined oldCredentials * 🐛 fix deleting credential * 🐛 fix check for undefined keys * 🐛 fix disabling edit in execution * 🎨 just show credential name on execution view * ✏️ remove TODO * ⚡ implement review suggestions * ⚡ add cache to getCredentialsByType * ⏪ use getter instead of cache * ✏️ fix variable name typo * 🐘 include waiting nodes to migrations * 🐛 fix reverting migrations command * ⚡ update typeorm command * ✨ create db:revert command * 👕 fix lint error Co-authored-by: Mutasem <mutdmour@gmail.com>
This commit is contained in:
61
packages/cli/commands/db/revert.ts
Normal file
61
packages/cli/commands/db/revert.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable no-console */
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { Connection, ConnectionOptions, createConnection } from 'typeorm';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import { getLogger } from '../../src/Logger';
|
||||
|
||||
import { Db } from '../../src';
|
||||
|
||||
export class DbRevertMigrationCommand extends Command {
|
||||
static description = 'Revert last database migration';
|
||||
|
||||
static examples = ['$ n8n db:revert'];
|
||||
|
||||
static flags = {
|
||||
help: flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
async run() {
|
||||
const logger = getLogger();
|
||||
LoggerProxy.init(logger);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow, @typescript-eslint/no-unused-vars
|
||||
const { flags } = this.parse(DbRevertMigrationCommand);
|
||||
|
||||
let connection: Connection | undefined;
|
||||
try {
|
||||
await Db.init();
|
||||
connection = Db.collections.Credentials?.manager.connection;
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`No database connection available.`);
|
||||
}
|
||||
|
||||
const connectionOptions: ConnectionOptions = Object.assign(connection.options, {
|
||||
subscribers: [],
|
||||
synchronize: false,
|
||||
migrationsRun: false,
|
||||
dropSchema: false,
|
||||
logging: ['query', 'error', 'schema'],
|
||||
});
|
||||
|
||||
// close connection in order to reconnect with updated options
|
||||
await connection.close();
|
||||
connection = await createConnection(connectionOptions);
|
||||
|
||||
await connection.undoLastMigration();
|
||||
await connection.close();
|
||||
} catch (error) {
|
||||
if (connection) await connection.close();
|
||||
|
||||
console.error('Error reverting last migration. See log messages for details.');
|
||||
logger.error(error.message);
|
||||
this.exit(1);
|
||||
}
|
||||
|
||||
this.exit();
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,8 @@ export class ExportCredentialsCommand extends Command {
|
||||
|
||||
for (let i = 0; i < credentials.length; i++) {
|
||||
const { name, type, nodesAccess, data } = credentials[i];
|
||||
const credential = new Credentials(name, type, nodesAccess, data);
|
||||
const id = credentials[i].id as string;
|
||||
const credential = new Credentials({ id, name }, type, nodesAccess, data);
|
||||
const plainData = credential.getData(encryptionKey);
|
||||
(credentials[i] as ICredentialsDecryptedDb).data = plainData;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { Command, flags } from '@oclif/command';
|
||||
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import { INode, INodeCredentialsDetails, LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as glob from 'fast-glob';
|
||||
import * as path from 'path';
|
||||
import { UserSettings } from 'n8n-core';
|
||||
import { getLogger } from '../../src/Logger';
|
||||
import { Db } from '../../src';
|
||||
import { Db, ICredentialsDb } from '../../src';
|
||||
|
||||
export class ImportWorkflowsCommand extends Command {
|
||||
static description = 'Import workflows';
|
||||
@@ -30,6 +30,32 @@ export class ImportWorkflowsCommand extends Command {
|
||||
}),
|
||||
};
|
||||
|
||||
private transformCredentials(node: INode, credentialsEntities: ICredentialsDb[]) {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const nodeCredentials: INodeCredentialsDetails = {
|
||||
id: null,
|
||||
name,
|
||||
};
|
||||
|
||||
const matchingCredentials = credentialsEntities.filter(
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
|
||||
if (matchingCredentials.length === 1) {
|
||||
nodeCredentials.id = matchingCredentials[0].id.toString();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
node.credentials[type] = nodeCredentials;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
async run() {
|
||||
const logger = getLogger();
|
||||
@@ -57,6 +83,7 @@ export class ImportWorkflowsCommand extends Command {
|
||||
|
||||
// Make sure the settings exist
|
||||
await UserSettings.prepareUserSettings();
|
||||
const credentialsEntities = (await Db.collections.Credentials?.find()) ?? [];
|
||||
let i;
|
||||
if (flags.separate) {
|
||||
const files = await glob(
|
||||
@@ -64,6 +91,12 @@ export class ImportWorkflowsCommand extends Command {
|
||||
);
|
||||
for (i = 0; i < files.length; i++) {
|
||||
const workflow = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
|
||||
if (credentialsEntities.length > 0) {
|
||||
// eslint-disable-next-line
|
||||
workflow.nodes.forEach((node: INode) => {
|
||||
this.transformCredentials(node, credentialsEntities);
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||
await Db.collections.Workflow!.save(workflow);
|
||||
}
|
||||
@@ -75,6 +108,12 @@ export class ImportWorkflowsCommand extends Command {
|
||||
}
|
||||
|
||||
for (i = 0; i < fileContents.length; i++) {
|
||||
if (credentialsEntities.length > 0) {
|
||||
// eslint-disable-next-line
|
||||
fileContents[i].nodes.forEach((node: INode) => {
|
||||
this.transformCredentials(node, credentialsEntities);
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||
await Db.collections.Workflow!.save(fileContents[i]);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import { getLogger } from '../src/Logger';
|
||||
const open = require('open');
|
||||
|
||||
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
||||
let processExistCode = 0;
|
||||
let processExitCode = 0;
|
||||
|
||||
export class Start extends Command {
|
||||
static description = 'Starts n8n. Makes Web-UI available and starts active workflows';
|
||||
@@ -92,7 +92,7 @@ export class Start extends Command {
|
||||
setTimeout(() => {
|
||||
// In case that something goes wrong with shutdown we
|
||||
// kill after max. 30 seconds no matter what
|
||||
process.exit(processExistCode);
|
||||
process.exit(processExitCode);
|
||||
}, 30000);
|
||||
|
||||
const skipWebhookDeregistration = config.get(
|
||||
@@ -133,7 +133,7 @@ export class Start extends Command {
|
||||
console.error('There was an error shutting down n8n.', error);
|
||||
}
|
||||
|
||||
process.exit(processExistCode);
|
||||
process.exit(processExitCode);
|
||||
}
|
||||
|
||||
async run() {
|
||||
@@ -160,7 +160,7 @@ export class Start extends Command {
|
||||
const startDbInitPromise = Db.init().catch((error: Error) => {
|
||||
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||
|
||||
processExistCode = 1;
|
||||
processExitCode = 1;
|
||||
// @ts-ignore
|
||||
process.emit('SIGINT');
|
||||
process.exit(1);
|
||||
@@ -355,7 +355,7 @@ export class Start extends Command {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
this.error(`There was an error: ${error.message}`);
|
||||
|
||||
processExistCode = 1;
|
||||
processExitCode = 1;
|
||||
// @ts-ignore
|
||||
process.emit('SIGINT');
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = [
|
||||
logging: true,
|
||||
entities: Object.values(entities),
|
||||
database: path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'),
|
||||
migrations: ['./src/databases/sqlite/migrations/*.ts'],
|
||||
migrations: ['./src/databases/sqlite/migrations/index.ts'],
|
||||
subscribers: ['./src/databases/sqlite/subscribers/*.ts'],
|
||||
cli: {
|
||||
entitiesDir: './src/databases/entities',
|
||||
@@ -28,7 +28,7 @@ module.exports = [
|
||||
database: 'n8n',
|
||||
schema: 'public',
|
||||
entities: Object.values(entities),
|
||||
migrations: ['./src/databases/postgresdb/migrations/*.ts'],
|
||||
migrations: ['./src/databases/postgresdb/migrations/index.ts'],
|
||||
subscribers: ['src/subscriber/**/*.ts'],
|
||||
cli: {
|
||||
entitiesDir: './src/databases/entities',
|
||||
@@ -46,7 +46,7 @@ module.exports = [
|
||||
port: '3306',
|
||||
logging: false,
|
||||
entities: Object.values(entities),
|
||||
migrations: ['./src/databases/mysqldb/migrations/*.ts'],
|
||||
migrations: ['./src/databases/mysqldb/migrations/index.ts'],
|
||||
subscribers: ['src/subscriber/**/*.ts'],
|
||||
cli: {
|
||||
entitiesDir: './src/databases/entities',
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"start:windows": "cd bin && n8n",
|
||||
"test": "jest",
|
||||
"watch": "tsc --watch",
|
||||
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
|
||||
"typeorm": "ts-node ../../node_modules/typeorm/cli.js"
|
||||
},
|
||||
"bin": {
|
||||
"n8n": "./bin/n8n"
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ICredentialsExpressionResolveValues,
|
||||
ICredentialsHelper,
|
||||
INode,
|
||||
INodeCredentialsDetails,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
INodeType,
|
||||
@@ -39,30 +40,32 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||
/**
|
||||
* Returns the credentials instance
|
||||
*
|
||||
* @param {string} name Name of the credentials to return instance of
|
||||
* @param {INodeCredentialsDetails} nodeCredentials id and name to return instance of
|
||||
* @param {string} type Type of the credentials to return instance of
|
||||
* @returns {Credentials}
|
||||
* @memberof CredentialsHelper
|
||||
*/
|
||||
async getCredentials(name: string, type: string): Promise<Credentials> {
|
||||
const credentialsDb = await Db.collections.Credentials?.find({ type });
|
||||
|
||||
if (credentialsDb === undefined || credentialsDb.length === 0) {
|
||||
throw new Error(`No credentials of type "${type}" exist.`);
|
||||
async getCredentials(
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
): Promise<Credentials> {
|
||||
if (!nodeCredentials.id) {
|
||||
throw new Error(`Credentials "${nodeCredentials.name}" for type "${type}" don't have an ID.`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const credential = credentialsDb.find((credential) => credential.name === name);
|
||||
const credentials = await Db.collections.Credentials?.findOne({ id: nodeCredentials.id, type });
|
||||
|
||||
if (credential === undefined) {
|
||||
throw new Error(`No credentials with name "${name}" exist for type "${type}".`);
|
||||
if (!credentials) {
|
||||
throw new Error(
|
||||
`Credentials with ID "${nodeCredentials.id}" don't exist for type "${type}".`,
|
||||
);
|
||||
}
|
||||
|
||||
return new Credentials(
|
||||
credential.name,
|
||||
credential.type,
|
||||
credential.nodesAccess,
|
||||
credential.data,
|
||||
{ id: credentials.id.toString(), name: credentials.name },
|
||||
credentials.type,
|
||||
credentials.nodesAccess,
|
||||
credentials.data,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,21 +104,20 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||
/**
|
||||
* Returns the decrypted credential data with applied overwrites
|
||||
*
|
||||
* @param {string} name Name of the credentials to return data of
|
||||
* @param {INodeCredentialsDetails} nodeCredentials id and name to return instance of
|
||||
* @param {string} type Type of the credentials to return data of
|
||||
* @param {boolean} [raw] Return the data as supplied without defaults or overwrites
|
||||
* @returns {ICredentialDataDecryptedObject}
|
||||
* @memberof CredentialsHelper
|
||||
*/
|
||||
async getDecrypted(
|
||||
name: string,
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
mode: WorkflowExecuteMode,
|
||||
raw?: boolean,
|
||||
expressionResolveValues?: ICredentialsExpressionResolveValues,
|
||||
): Promise<ICredentialDataDecryptedObject> {
|
||||
const credentials = await this.getCredentials(name, type);
|
||||
|
||||
const credentials = await this.getCredentials(nodeCredentials, type);
|
||||
const decryptedDataOriginal = credentials.getData(this.encryptionKey);
|
||||
|
||||
if (raw === true) {
|
||||
@@ -228,12 +230,12 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||
* @memberof CredentialsHelper
|
||||
*/
|
||||
async updateCredentials(
|
||||
name: string,
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
data: ICredentialDataDecryptedObject,
|
||||
): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
const credentials = await this.getCredentials(name, type);
|
||||
const credentials = await this.getCredentials(nodeCredentials, type);
|
||||
|
||||
if (Db.collections.Credentials === null) {
|
||||
// The first time executeWorkflow gets called the Database has
|
||||
@@ -251,7 +253,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||
|
||||
// Save the credentials in DB
|
||||
const findQuery = {
|
||||
name,
|
||||
id: credentials.id,
|
||||
type,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import * as express from 'express';
|
||||
import { join as pathJoin } from 'path';
|
||||
import { readFile as fsReadFile } from 'fs/promises';
|
||||
import { readFileSync as fsReadFileSync } from 'fs';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import * as config from '../config';
|
||||
|
||||
@@ -137,45 +136,6 @@ export async function getConfigValue(
|
||||
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');
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new Error(`The file "${fileEnvironmentVariable}" could not be found.`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique name for a workflow or credentials entity.
|
||||
*
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
INodeCredentials,
|
||||
INodeCredentialsDetails,
|
||||
INodeParameters,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
@@ -642,6 +643,9 @@ class App {
|
||||
});
|
||||
}
|
||||
|
||||
// check credentials for old format
|
||||
await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);
|
||||
|
||||
await this.externalHooks.run('workflow.create', [newWorkflow]);
|
||||
|
||||
await WorkflowHelpers.validateWorkflow(newWorkflow);
|
||||
@@ -782,6 +786,9 @@ class App {
|
||||
const { id } = req.params;
|
||||
updateData.id = id;
|
||||
|
||||
// check credentials for old format
|
||||
await WorkflowHelpers.replaceInvalidCredentials(updateData as WorkflowEntity);
|
||||
|
||||
await this.externalHooks.run('workflow.update', [updateData]);
|
||||
|
||||
const isActive = await this.activeWorkflowRunner.isActive(id);
|
||||
@@ -1293,26 +1300,9 @@ class App {
|
||||
throw new Error('Credentials have to have a name set!');
|
||||
}
|
||||
|
||||
// Check if credentials with the same name and type exist already
|
||||
const findQuery = {
|
||||
where: {
|
||||
name: incomingData.name,
|
||||
type: incomingData.type,
|
||||
},
|
||||
} as FindOneOptions;
|
||||
|
||||
const checkResult = await Db.collections.Credentials!.findOne(findQuery);
|
||||
if (checkResult !== undefined) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Credentials with the same type and name exist already.`,
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// Encrypt the data
|
||||
const credentials = new Credentials(
|
||||
incomingData.name,
|
||||
{ id: null, name: incomingData.name },
|
||||
incomingData.type,
|
||||
incomingData.nodesAccess,
|
||||
);
|
||||
@@ -1321,10 +1311,6 @@ class App {
|
||||
|
||||
await this.externalHooks.run('credentials.create', [newCredentialsData]);
|
||||
|
||||
// Add special database related data
|
||||
|
||||
// TODO: also add user automatically depending on who is logged in, if anybody is logged in
|
||||
|
||||
// Save the credentials in DB
|
||||
const result = await Db.collections.Credentials!.save(newCredentialsData);
|
||||
result.data = incomingData.data;
|
||||
@@ -1445,24 +1431,6 @@ class App {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if credentials with the same name and type exist already
|
||||
const findQuery = {
|
||||
where: {
|
||||
id: Not(id),
|
||||
name: incomingData.name,
|
||||
type: incomingData.type,
|
||||
},
|
||||
} as FindOneOptions;
|
||||
|
||||
const checkResult = await Db.collections.Credentials!.findOne(findQuery);
|
||||
if (checkResult !== undefined) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Credentials with the same type and name exist already.`,
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||
if (encryptionKey === undefined) {
|
||||
throw new Error('No encryption key got found to encrypt the credentials!');
|
||||
@@ -1479,7 +1447,7 @@ class App {
|
||||
}
|
||||
|
||||
const currentlySavedCredentials = new Credentials(
|
||||
result.name,
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
result.nodesAccess,
|
||||
result.data,
|
||||
@@ -1494,7 +1462,7 @@ class App {
|
||||
|
||||
// Encrypt the data
|
||||
const credentials = new Credentials(
|
||||
incomingData.name,
|
||||
{ id, name: incomingData.name },
|
||||
incomingData.type,
|
||||
incomingData.nodesAccess,
|
||||
);
|
||||
@@ -1563,7 +1531,7 @@ class App {
|
||||
}
|
||||
|
||||
const credentials = new Credentials(
|
||||
result.name,
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
result.nodesAccess,
|
||||
result.data,
|
||||
@@ -1707,7 +1675,7 @@ class App {
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
result.name,
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
mode,
|
||||
true,
|
||||
@@ -1766,7 +1734,11 @@ class App {
|
||||
}`;
|
||||
|
||||
// Encrypt the data
|
||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
||||
const credentials = new Credentials(
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
result.nodesAccess,
|
||||
);
|
||||
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
@@ -1823,13 +1795,13 @@ class App {
|
||||
// Decrypt the currently saved credentials
|
||||
const workflowCredentials: IWorkflowCredentials = {
|
||||
[result.type]: {
|
||||
[result.name]: result as ICredentialsEncrypted,
|
||||
[result.id.toString()]: result as ICredentialsEncrypted,
|
||||
},
|
||||
};
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
result.name,
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
mode,
|
||||
true,
|
||||
@@ -1868,7 +1840,11 @@ class App {
|
||||
|
||||
decryptedDataOriginal.oauthTokenData = oauthTokenJson;
|
||||
|
||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
||||
const credentials = new Credentials(
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
result.nodesAccess,
|
||||
);
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
// Add special database related data
|
||||
@@ -1913,7 +1889,7 @@ class App {
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
result.name,
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
mode,
|
||||
true,
|
||||
@@ -1950,7 +1926,11 @@ class App {
|
||||
const oAuthObj = new clientOAuth2(oAuthOptions);
|
||||
|
||||
// Encrypt the data
|
||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
||||
const credentials = new Credentials(
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
result.nodesAccess,
|
||||
);
|
||||
decryptedDataOriginal.csrfSecret = csrfSecret;
|
||||
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
@@ -2039,14 +2019,14 @@ class App {
|
||||
// Decrypt the currently saved credentials
|
||||
const workflowCredentials: IWorkflowCredentials = {
|
||||
[result.type]: {
|
||||
[result.name]: result as ICredentialsEncrypted,
|
||||
[result.id.toString()]: result as ICredentialsEncrypted,
|
||||
},
|
||||
};
|
||||
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
result.name,
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
mode,
|
||||
true,
|
||||
@@ -2128,7 +2108,11 @@ class App {
|
||||
|
||||
_.unset(decryptedDataOriginal, 'csrfSecret');
|
||||
|
||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
||||
const credentials = new Credentials(
|
||||
result as INodeCredentialsDetails,
|
||||
result.type,
|
||||
result.nodesAccess,
|
||||
);
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
// Add special database related data
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
|
||||
|
||||
let node;
|
||||
let type;
|
||||
let name;
|
||||
let nodeCredentials;
|
||||
let foundCredentials;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (node of nodes) {
|
||||
@@ -21,19 +21,30 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (type of Object.keys(node.credentials)) {
|
||||
if (!returnCredentials.hasOwnProperty(type)) {
|
||||
if (!returnCredentials[type]) {
|
||||
returnCredentials[type] = {};
|
||||
}
|
||||
name = node.credentials[type];
|
||||
nodeCredentials = node.credentials[type];
|
||||
|
||||
if (!returnCredentials[type].hasOwnProperty(name)) {
|
||||
if (!nodeCredentials.id) {
|
||||
throw new Error(
|
||||
`Credentials with name "${nodeCredentials.name}" for type "${type}" miss an ID.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!returnCredentials[type][nodeCredentials.id]) {
|
||||
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-non-null-assertion
|
||||
foundCredentials = await Db.collections.Credentials!.find({ name, type });
|
||||
if (!foundCredentials.length) {
|
||||
throw new Error(`Could not find credentials for type "${type}" with name "${name}".`);
|
||||
foundCredentials = await Db.collections.Credentials!.findOne({
|
||||
id: nodeCredentials.id,
|
||||
type,
|
||||
});
|
||||
if (!foundCredentials) {
|
||||
throw new Error(
|
||||
`Could not find credentials for type "${type}" with ID "${nodeCredentials.id}".`,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
returnCredentials[type][name] = foundCredentials[0];
|
||||
returnCredentials[type][nodeCredentials.id] = foundCredentials;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
IDataObject,
|
||||
IExecuteData,
|
||||
INode,
|
||||
INodeCredentialsDetails,
|
||||
IRun,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
@@ -385,6 +386,113 @@ export async function getStaticDataById(workflowId: string | number) {
|
||||
return workflowData.staticData || {};
|
||||
}
|
||||
|
||||
// Checking if credentials of old format are in use and run a DB check if they might exist uniquely
|
||||
export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promise<WorkflowEntity> {
|
||||
const { nodes } = workflow;
|
||||
if (!nodes) return workflow;
|
||||
|
||||
// caching
|
||||
const credentialsByName: Record<string, Record<string, INodeCredentialsDetails>> = {};
|
||||
const credentialsById: Record<string, Record<string, INodeCredentialsDetails>> = {};
|
||||
|
||||
// for loop to run DB fetches sequential and use cache to keep pressure off DB
|
||||
// trade-off: longer response time for less DB queries
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const node of nodes) {
|
||||
if (!node.credentials || node.disabled) {
|
||||
continue;
|
||||
}
|
||||
// extract credentials types
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [nodeCredentialType, nodeCredentials] of allNodeCredentials) {
|
||||
// Check if Node applies old credentials style
|
||||
if (typeof nodeCredentials === 'string' || nodeCredentials.id === null) {
|
||||
const name = typeof nodeCredentials === 'string' ? nodeCredentials : nodeCredentials.name;
|
||||
// init cache for type
|
||||
if (!credentialsByName[nodeCredentialType]) {
|
||||
credentialsByName[nodeCredentialType] = {};
|
||||
}
|
||||
if (credentialsByName[nodeCredentialType][name] === undefined) {
|
||||
const credentials = await Db.collections.Credentials?.find({
|
||||
name,
|
||||
type: nodeCredentialType,
|
||||
});
|
||||
// if credential name-type combination is unique, use it
|
||||
if (credentials?.length === 1) {
|
||||
credentialsByName[nodeCredentialType][name] = {
|
||||
id: credentials[0].id.toString(),
|
||||
name: credentials[0].name,
|
||||
};
|
||||
node.credentials[nodeCredentialType] = credentialsByName[nodeCredentialType][name];
|
||||
continue;
|
||||
}
|
||||
|
||||
// nothing found - add invalid credentials to cache to prevent further DB checks
|
||||
credentialsByName[nodeCredentialType][name] = {
|
||||
id: null,
|
||||
name,
|
||||
};
|
||||
} else {
|
||||
// get credentials from cache
|
||||
node.credentials[nodeCredentialType] = credentialsByName[nodeCredentialType][name];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Node has credentials with an ID
|
||||
|
||||
// init cache for type
|
||||
if (!credentialsById[nodeCredentialType]) {
|
||||
credentialsById[nodeCredentialType] = {};
|
||||
}
|
||||
|
||||
// check if credentials for ID-type are not yet cached
|
||||
if (credentialsById[nodeCredentialType][nodeCredentials.id] === undefined) {
|
||||
// check first if ID-type combination exists
|
||||
const credentials = await Db.collections.Credentials?.findOne({
|
||||
id: nodeCredentials.id,
|
||||
type: nodeCredentialType,
|
||||
});
|
||||
if (credentials) {
|
||||
credentialsById[nodeCredentialType][nodeCredentials.id] = {
|
||||
id: credentials.id.toString(),
|
||||
name: credentials.name,
|
||||
};
|
||||
node.credentials[nodeCredentialType] =
|
||||
credentialsById[nodeCredentialType][nodeCredentials.id];
|
||||
continue;
|
||||
}
|
||||
// no credentials found for ID, check if some exist for name
|
||||
const credsByName = await Db.collections.Credentials?.find({
|
||||
name: nodeCredentials.name,
|
||||
type: nodeCredentialType,
|
||||
});
|
||||
// if credential name-type combination is unique, take it
|
||||
if (credsByName?.length === 1) {
|
||||
// add found credential to cache
|
||||
credentialsById[nodeCredentialType][credsByName[0].id] = {
|
||||
id: credsByName[0].id.toString(),
|
||||
name: credsByName[0].name,
|
||||
};
|
||||
node.credentials[nodeCredentialType] =
|
||||
credentialsById[nodeCredentialType][credsByName[0].id];
|
||||
continue;
|
||||
}
|
||||
|
||||
// nothing found - add invalid credentials to cache to prevent further DB checks
|
||||
credentialsById[nodeCredentialType][nodeCredentials.id] = nodeCredentials;
|
||||
continue;
|
||||
}
|
||||
|
||||
// get credentials from cache
|
||||
node.credentials[nodeCredentialType] =
|
||||
credentialsById[nodeCredentialType][nodeCredentials.id];
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
return workflow;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate `validateWorkflow` and `throwDuplicateEntryError` with TagHelpers?
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
|
||||
@@ -11,9 +11,40 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { getTimestampSyntax, resolveDataType } from '../utils';
|
||||
|
||||
import { ICredentialsDb } from '../..';
|
||||
import config = require('../../../config');
|
||||
import { DatabaseType, ICredentialsDb } from '../..';
|
||||
|
||||
function resolveDataType(dataType: string) {
|
||||
const dbType = config.get('database.type') as DatabaseType;
|
||||
|
||||
const typeMap: { [key in DatabaseType]: { [key: string]: string } } = {
|
||||
sqlite: {
|
||||
json: 'simple-json',
|
||||
},
|
||||
postgresdb: {
|
||||
datetime: 'timestamptz',
|
||||
},
|
||||
mysqldb: {},
|
||||
mariadb: {},
|
||||
};
|
||||
|
||||
return typeMap[dbType][dataType] ?? dataType;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
function getTimestampSyntax() {
|
||||
const dbType = config.get('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];
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class CredentialsEntity implements ICredentialsDb {
|
||||
|
||||
@@ -2,9 +2,25 @@
|
||||
import { WorkflowExecuteMode } from 'n8n-workflow';
|
||||
|
||||
import { Column, ColumnOptions, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { IExecutionFlattedDb, IWorkflowDb } from '../..';
|
||||
import config = require('../../../config');
|
||||
import { DatabaseType, IExecutionFlattedDb, IWorkflowDb } from '../..';
|
||||
|
||||
import { resolveDataType } from '../utils';
|
||||
function resolveDataType(dataType: string) {
|
||||
const dbType = config.get('database.type') as DatabaseType;
|
||||
|
||||
const typeMap: { [key in DatabaseType]: { [key: string]: string } } = {
|
||||
sqlite: {
|
||||
json: 'simple-json',
|
||||
},
|
||||
postgresdb: {
|
||||
datetime: 'timestamptz',
|
||||
},
|
||||
mysqldb: {},
|
||||
mariadb: {},
|
||||
};
|
||||
|
||||
return typeMap[dbType][dataType] ?? dataType;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class ExecutionEntity implements IExecutionFlattedDb {
|
||||
|
||||
@@ -12,9 +12,24 @@ import {
|
||||
} from 'typeorm';
|
||||
import { IsDate, IsOptional, IsString, Length } from 'class-validator';
|
||||
|
||||
import config = require('../../../config');
|
||||
import { DatabaseType } from '../../index';
|
||||
import { ITagDb } from '../../Interfaces';
|
||||
import { WorkflowEntity } from './WorkflowEntity';
|
||||
import { getTimestampSyntax } from '../utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
function getTimestampSyntax() {
|
||||
const dbType = config.get('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];
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class TagEntity implements ITagDb {
|
||||
|
||||
@@ -17,12 +17,41 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { IWorkflowDb } from '../..';
|
||||
|
||||
import { getTimestampSyntax, resolveDataType } from '../utils';
|
||||
|
||||
import config = require('../../../config');
|
||||
import { DatabaseType, IWorkflowDb } from '../..';
|
||||
import { TagEntity } from './TagEntity';
|
||||
|
||||
function resolveDataType(dataType: string) {
|
||||
const dbType = config.get('database.type') as DatabaseType;
|
||||
|
||||
const typeMap: { [key in DatabaseType]: { [key: string]: string } } = {
|
||||
sqlite: {
|
||||
json: 'simple-json',
|
||||
},
|
||||
postgresdb: {
|
||||
datetime: 'timestamptz',
|
||||
},
|
||||
mysqldb: {},
|
||||
mariadb: {},
|
||||
};
|
||||
|
||||
return typeMap[dbType][dataType] ?? dataType;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
function getTimestampSyntax() {
|
||||
const dbType = config.get('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];
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class WorkflowEntity implements IWorkflowDb {
|
||||
@PrimaryGeneratedColumn()
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
|
||||
// replacing the credentials in workflows and execution
|
||||
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||
|
||||
export class UpdateWorkflowCredentials1630451444017 implements MigrationInterface {
|
||||
name = 'UpdateWorkflowCredentials1630451444017';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
SELECT id, workflowData
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE waitTill IS NOT NULL AND finished = 0
|
||||
`);
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, workflowData
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE waitTill IS NULL AND finished = 0 AND mode != 'retry'
|
||||
ORDER BY startedAt DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
SELECT id, workflowData
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE waitTill IS NOT NULL AND finished = 0
|
||||
`);
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, workflowData
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE waitTill IS NULL AND finished = 0 AND mode != 'retry'
|
||||
ORDER BY startedAt DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { CreateTagEntity1617268711084 } from './1617268711084-CreateTagEntity';
|
||||
import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
|
||||
import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation';
|
||||
import { AddWaitColumnId1626183952959 } from './1626183952959-AddWaitColumn';
|
||||
import { UpdateWorkflowCredentials1630451444017 } from './1630451444017-UpdateWorkflowCredentials';
|
||||
|
||||
export const mysqlMigrations = [
|
||||
InitialMigration1588157391238,
|
||||
@@ -22,4 +23,5 @@ export const mysqlMigrations = [
|
||||
UniqueWorkflowNames1620826335440,
|
||||
CertifyCorrectCollation1623936588000,
|
||||
AddWaitColumnId1626183952959,
|
||||
UpdateWorkflowCredentials1630451444017,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
|
||||
// replacing the credentials in workflows and execution
|
||||
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||
|
||||
export class UpdateWorkflowCredentials1630419189837 implements MigrationInterface {
|
||||
name = 'UpdateWorkflowCredentials1630419189837';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
let tablePrefix = config.get('database.tablePrefix');
|
||||
const schema = config.get('database.postgresdb.schema');
|
||||
if (schema) {
|
||||
tablePrefix = schema + '.' + tablePrefix;
|
||||
}
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE "waitTill" IS NOT NULL AND finished = FALSE
|
||||
`);
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE "waitTill" IS NULL AND finished = FALSE AND mode != 'retry'
|
||||
ORDER BY "startedAt" DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
let tablePrefix = config.get('database.tablePrefix');
|
||||
const schema = config.get('database.postgresdb.schema');
|
||||
if (schema) {
|
||||
tablePrefix = schema + '.' + tablePrefix;
|
||||
}
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE "waitTill" IS NOT NULL AND finished = FALSE
|
||||
`);
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE "waitTill" IS NULL AND finished = FALSE AND mode != 'retry'
|
||||
ORDER BY "startedAt" DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { MakeStoppedAtNullable1607431743768 } from './1607431743768-MakeStoppedA
|
||||
import { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity';
|
||||
import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames';
|
||||
import { AddwaitTill1626176912946 } from './1626176912946-AddwaitTill';
|
||||
import { UpdateWorkflowCredentials1630419189837 } from './1630419189837-UpdateWorkflowCredentials';
|
||||
|
||||
export const postgresMigrations = [
|
||||
InitialMigration1587669153312,
|
||||
@@ -16,4 +17,5 @@ export const postgresMigrations = [
|
||||
CreateTagEntity1617270242566,
|
||||
UniqueWorkflowNames1620824779533,
|
||||
AddwaitTill1626176912946,
|
||||
UpdateWorkflowCredentials1630419189837,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
|
||||
// replacing the credentials in workflows and execution
|
||||
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||
|
||||
export class UpdateWorkflowCredentials1630330987096 implements MigrationInterface {
|
||||
name = 'UpdateWorkflowCredentials1630330987096';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM "${tablePrefix}credentials_entity"
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
SELECT id, nodes
|
||||
FROM "${tablePrefix}workflow_entity"
|
||||
`);
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}workflow_entity"
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
FROM "${tablePrefix}execution_entity"
|
||||
WHERE "waitTill" IS NOT NULL AND finished = 0
|
||||
`);
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
FROM "${tablePrefix}execution_entity"
|
||||
WHERE "waitTill" IS NULL AND finished = 0 AND mode != 'retry'
|
||||
ORDER BY "startedAt" DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM "${tablePrefix}credentials_entity"
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
SELECT id, nodes
|
||||
FROM "${tablePrefix}workflow_entity"
|
||||
`);
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}workflow_entity"
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
FROM "${tablePrefix}execution_entity"
|
||||
WHERE "waitTill" IS NOT NULL AND finished = 0
|
||||
`);
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
FROM "${tablePrefix}execution_entity"
|
||||
WHERE "waitTill" IS NULL AND finished = 0 AND mode != 'retry'
|
||||
ORDER BY "startedAt" DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { MakeStoppedAtNullable1607431743769 } from './1607431743769-MakeStoppedA
|
||||
import { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity';
|
||||
import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames';
|
||||
import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn';
|
||||
import { UpdateWorkflowCredentials1630330987096 } from './1630330987096-UpdateWorkflowCredentials';
|
||||
|
||||
export const sqliteMigrations = [
|
||||
InitialMigration1588102412422,
|
||||
@@ -16,4 +17,5 @@ export const sqliteMigrations = [
|
||||
CreateTagEntity1617213344594,
|
||||
UniqueWorkflowNames1620821879465,
|
||||
AddWaitColumn1621707690587,
|
||||
UpdateWorkflowCredentials1630330987096,
|
||||
];
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { DatabaseType } from '../index';
|
||||
import { getConfigValueSync } from '../GenericHelpers';
|
||||
|
||||
/**
|
||||
* Resolves the data type for the used database type
|
||||
*
|
||||
* @export
|
||||
* @param {string} dataType
|
||||
* @returns {string}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
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: 'timestamptz',
|
||||
},
|
||||
mysqldb: {},
|
||||
mariadb: {},
|
||||
};
|
||||
|
||||
return typeMap[dbType][dataType] ?? dataType;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
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];
|
||||
}
|
||||
Reference in New Issue
Block a user