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:
Ben Hesseldieck 2021-10-14 00:21:00 +02:00 committed by GitHub
parent 1e34aca8bd
commit 3137de2585
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1318 additions and 251 deletions

View 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();
}
}

View file

@ -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;
}

View file

@ -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]);
}

View file

@ -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');
}

View file

@ -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',

View file

@ -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"

View file

@ -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,
};

View file

@ -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.
*

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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()

View file

@ -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);
}
});
}
}

View file

@ -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,
];

View file

@ -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);
}
});
}
}

View file

@ -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,
];

View file

@ -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);
}
});
}
}

View file

@ -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,
];

View file

@ -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];
}

View file

@ -98,6 +98,7 @@ export class Credentials extends ICredentials {
}
return {
id: this.id,
name: this.name,
type: this.type,
data: this.data,

View file

@ -738,16 +738,20 @@ export async function requestOAuth2(
credentials.oauthTokenData = newToken.data;
// Find the name of the credentials
// Find the credentials
if (!node.credentials || !node.credentials[credentialsType]) {
throw new Error(
`The node "${node.name}" does not have credentials of type "${credentialsType}"!`,
);
}
const name = node.credentials[credentialsType];
const nodeCredentials = node.credentials[credentialsType];
// Save the refreshed token
await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials);
await additionalData.credentialsHelper.updateCredentials(
nodeCredentials,
credentialsType,
credentials,
);
Logger.debug(
`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been saved to database successfully.`,
@ -955,25 +959,26 @@ export async function getCredentials(
} as ICredentialsExpressionResolveValues;
}
let name = node.credentials[type];
const nodeCredentials = node.credentials[type];
if (name.charAt(0) === '=') {
// If the credential name is an expression resolve it
const additionalKeys = getAdditionalKeys(additionalData);
name = workflow.expression.getParameterValue(
name,
runExecutionData || null,
runIndex || 0,
itemIndex || 0,
node.name,
connectionInputData || [],
mode,
additionalKeys,
) as string;
}
// TODO: solve using credentials via expression
// if (name.charAt(0) === '=') {
// // If the credential name is an expression resolve it
// const additionalKeys = getAdditionalKeys(additionalData);
// name = workflow.expression.getParameterValue(
// name,
// runExecutionData || null,
// runIndex || 0,
// itemIndex || 0,
// node.name,
// connectionInputData || [],
// mode,
// additionalKeys,
// ) as string;
// }
const decryptedDataObject = await additionalData.credentialsHelper.getDecrypted(
name,
nodeCredentials,
type,
mode,
false,

View file

@ -3,7 +3,7 @@ import { Credentials } from '../src';
describe('Credentials', () => {
describe('without nodeType set', () => {
test('should be able to set and read key data without initial data set', () => {
const credentials = new Credentials('testName', 'testType', []);
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', []);
const key = 'key1';
const password = 'password';
@ -23,7 +23,12 @@ describe('Credentials', () => {
const initialData = 4321;
const initialDataEncoded = 'U2FsdGVkX1+0baznXt+Ag/ub8A2kHLyoLxn/rR9h4XQ=';
const credentials = new Credentials('testName', 'testType', [], initialDataEncoded);
const credentials = new Credentials(
{ id: null, name: 'testName' },
'testType',
[],
initialDataEncoded,
);
const newData = 1234;
@ -46,7 +51,7 @@ describe('Credentials', () => {
},
];
const credentials = new Credentials('testName', 'testType', nodeAccess);
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', nodeAccess);
const key = 'key1';
const password = 'password';

View file

@ -5,6 +5,7 @@ import {
ICredentialsHelper,
IDataObject,
IExecuteWorkflowInfo,
INodeCredentialsDetails,
INodeExecutionData,
INodeParameters,
INodeType,
@ -22,18 +23,21 @@ import {
import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
export class CredentialsHelper extends ICredentialsHelper {
getDecrypted(name: string, type: string): Promise<ICredentialDataDecryptedObject> {
getDecrypted(
nodeCredentials: INodeCredentialsDetails,
type: string,
): Promise<ICredentialDataDecryptedObject> {
return new Promise((res) => res({}));
}
getCredentials(name: string, type: string): Promise<Credentials> {
getCredentials(nodeCredentials: INodeCredentialsDetails, type: string): Promise<Credentials> {
return new Promise((res) => {
res(new Credentials('', '', [], ''));
res(new Credentials({ id: null, name: '' }, '', [], ''));
});
}
async updateCredentials(
name: string,
nodeCredentials: INodeCredentialsDetails,
type: string,
data: ICredentialDataDecryptedObject,
): Promise<void> {}

View file

@ -249,7 +249,7 @@ export interface IActivationError {
}
export interface ICredentialsResponse extends ICredentialsEncrypted {
id?: string;
id: string;
createdAt: number | string;
updatedAt: number | string;
}

View file

@ -557,6 +557,7 @@ export default mixins(showMessage, nodeHelpers).extend({
);
const details: ICredentialsDecrypted = {
id: this.credentialId,
name: this.credentialName,
type: this.credentialTypeName!,
data: data as unknown as ICredentialDataDecryptedObject,
@ -605,6 +606,7 @@ export default mixins(showMessage, nodeHelpers).extend({
);
const credentialDetails: ICredentialsDecrypted = {
id: this.credentialId,
name: this.credentialName,
type: this.credentialTypeName!,
data: data as unknown as ICredentialDataDecryptedObject,

View file

@ -9,15 +9,15 @@
<el-col :span="10" class="parameter-name">
{{credentialTypeNames[credentialTypeDescription.name]}}:
</el-col>
<el-col :span="12" class="parameter-value" :class="getIssues(credentialTypeDescription.name).length?'has-issues':''">
<el-col v-if="!isReadOnly" :span="12" class="parameter-value" :class="getIssues(credentialTypeDescription.name).length?'has-issues':''">
<div :style="credentialInputWrapperStyle(credentialTypeDescription.name)">
<n8n-select :value="selected[credentialTypeDescription.name]" :disabled="isReadOnly" @change="(value) => credentialSelected(credentialTypeDescription.name, value)" placeholder="Select Credential" size="small">
<n8n-select :value="getSelectedId(credentialTypeDescription.name)" @change="(value) => onCredentialSelected(credentialTypeDescription.name, value)" placeholder="Select Credential" size="small">
<n8n-option
v-for="(item) in credentialOptions[credentialTypeDescription.name]"
:key="item.id"
:label="item.name"
:value="item.name">
:value="item.id">
</n8n-option>
<n8n-option
:key="NEW_CREDENTIALS_TEXT"
@ -34,10 +34,13 @@
<font-awesome-icon icon="exclamation-triangle" />
</n8n-tooltip>
</div>
</el-col>
<el-col :span="2" class="parameter-value credential-action">
<font-awesome-icon v-if="selected[credentialTypeDescription.name] && isCredentialValid(credentialTypeDescription.name)" icon="pen" @click="editCredential(credentialTypeDescription.name)" class="update-credentials clickable" title="Update Credentials" />
<el-col v-if="!isReadOnly" :span="2" class="parameter-value credential-action">
<font-awesome-icon v-if="isCredentialExisting(credentialTypeDescription.name)" icon="pen" @click="editCredential(credentialTypeDescription.name)" class="update-credentials clickable" title="Update Credentials" />
</el-col>
<el-col v-if="isReadOnly" :span="14" class="readonly-container" >
<n8n-input disabled :value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name" size="small" />
</el-col>
</el-row>
@ -49,12 +52,14 @@
<script lang="ts">
import { restApi } from '@/components/mixins/restApi';
import {
ICredentialsResponse,
INodeUi,
INodeUpdatePropertiesInformation,
} from '@/Interface';
import {
ICredentialType,
INodeCredentialDescription,
INodeCredentialsDetails,
INodeTypeDescription,
} from 'n8n-workflow';
@ -119,11 +124,17 @@ export default mixins(
}
return returnData;
},
selected(): {[type: string]: string} {
selected(): {[type: string]: INodeCredentialsDetails} {
return this.node.credentials || {};
},
},
methods: {
getSelectedId(type: string) {
if (this.isCredentialExisting(type)) {
return this.selected[type].id;
}
return undefined;
},
credentialInputWrapperStyle (credentialType: string) {
let deductWidth = 0;
const styles = {
@ -145,10 +156,10 @@ export default mixins(
this.newCredentialUnsubscribe = this.$store.subscribe((mutation, state) => {
if (mutation.type === 'credentials/upsertCredential' || mutation.type === 'credentials/enableOAuthCredential'){
this.credentialSelected(credentialType, mutation.payload.name);
this.onCredentialSelected(credentialType, mutation.payload.id);
}
if (mutation.type === 'credentials/deleteCredential') {
this.credentialSelected(credentialType, mutation.payload.name);
this.clearSelectedCredential(credentialType);
this.stopListeningForNewCredentials();
}
});
@ -160,14 +171,52 @@ export default mixins(
}
},
credentialSelected (credentialType: string, credentialName: string) {
clearSelectedCredential(credentialType: string) {
const node: INodeUi = this.node;
const credentials = {
...(node.credentials || {}),
};
delete credentials[credentialType];
const updateInformation: INodeUpdatePropertiesInformation = {
name: this.node.name,
properties: {
credentials,
},
};
this.$emit('credentialSelected', updateInformation);
},
onCredentialSelected (credentialType: string, credentialId: string | null | undefined) {
let selected = undefined;
if (credentialName === NEW_CREDENTIALS_TEXT) {
if (credentialId === NEW_CREDENTIALS_TEXT) {
this.listenForNewCredentials(credentialType);
this.$store.dispatch('ui/openNewCredential', { type: credentialType });
return;
}
else {
selected = credentialName;
const selectedCredentials = this.$store.getters['credentials/getCredentialById'](credentialId);
const oldCredentials = this.node.credentials && this.node.credentials[credentialType] ? this.node.credentials[credentialType] : {};
selected = { id: selectedCredentials.id, name: selectedCredentials.name };
// if credentials has been string or neither id matched nor name matched uniquely
if (oldCredentials.id === null || (oldCredentials.id && !this.$store.getters['credentials/getCredentialByIdAndType'](oldCredentials.id, credentialType))) {
// update all nodes in the workflow with the same old/invalid credentials
this.$store.commit('replaceInvalidWorkflowCredentials', {
credentials: selected,
invalid: oldCredentials,
type: credentialType,
});
this.updateNodesCredentialsIssues();
this.$showMessage({
title: 'Node credentials updated',
message: `Nodes that used credentials "${oldCredentials.name}" have been updated to use "${selected.name}"`,
type: 'success',
});
}
const node: INodeUi = this.node;
@ -209,18 +258,19 @@ export default mixins(
return node.issues.credentials[credentialTypeName];
},
isCredentialValid(credentialType: string): boolean {
const name = this.node.credentials[credentialType];
isCredentialExisting(credentialType: string): boolean {
if (!this.node.credentials || !this.node.credentials[credentialType] || !this.node.credentials[credentialType].id) {
return false;
}
const { id } = this.node.credentials[credentialType];
const options = this.credentialOptions[credentialType];
return options.find((option: ICredentialType) => option.name === name);
return !!options.find((option: ICredentialsResponse) => option.id === id);
},
editCredential(credentialType: string): void {
const name = this.node.credentials[credentialType];
const options = this.credentialOptions[credentialType];
const selected = options.find((option: ICredentialType) => option.name === name);
this.$store.dispatch('ui/openExisitngCredential', { id: selected.id });
const { id } = this.node.credentials[credentialType];
this.$store.dispatch('ui/openExisitngCredential', { id });
this.listenForNewCredentials(credentialType);
},
@ -283,6 +333,10 @@ export default mixins(
align-items: center;
color: var(--color-text-base);
}
.readonly-container {
padding-right: 0.5em;
}
}
</style>

View file

@ -7,11 +7,12 @@ import {
ICredentialType,
INodeCredentialDescription,
NodeHelpers,
INodeParameters,
INodeCredentialsDetails,
INodeExecutionData,
INodeIssues,
INodeIssueData,
INodeIssueObjectProperty,
INodeParameters,
INodeProperties,
INodeTypeDescription,
IRunData,
@ -196,7 +197,7 @@ export const nodeHelpers = mixins(
let userCredentials: ICredentialsResponse[] | null;
let credentialType: ICredentialType | null;
let credentialDisplayName: string;
let selectedCredentials: string;
let selectedCredentials: INodeCredentialsDetails;
for (const credentialTypeDescription of nodeType!.credentials!) {
// Check if credentials should be displayed else ignore
if (this.displayParameter(node.parameters, credentialTypeDescription, '') !== true) {
@ -218,15 +219,35 @@ export const nodeHelpers = mixins(
}
} else {
// If they are set check if the value is valid
selectedCredentials = node.credentials[credentialTypeDescription.name];
selectedCredentials = node.credentials[credentialTypeDescription.name] as INodeCredentialsDetails;
if (typeof selectedCredentials === 'string') {
selectedCredentials = {
id: null,
name: selectedCredentials,
};
}
userCredentials = this.$store.getters['credentials/getCredentialsByType'](credentialTypeDescription.name);
if (userCredentials === null) {
userCredentials = [];
}
if (userCredentials.find((credentialData) => credentialData.name === selectedCredentials) === undefined) {
foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials}" do not exist for "${credentialDisplayName}".`];
if (selectedCredentials.id) {
const idMatch = userCredentials.find((credentialData) => credentialData.id === selectedCredentials.id);
if (idMatch) {
continue;
}
}
const nameMatches = userCredentials.filter((credentialData) => credentialData.name === selectedCredentials.name);
if (nameMatches.length > 1) {
foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials.name}" exist for "${credentialDisplayName}"`, "Credentials are not clearly identified. Please select the correct credentials."];
continue;
}
if (nameMatches.length === 0) {
foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials.name}" do not exist for "${credentialDisplayName}".`, "You can create credentials with the exact name and then they get auto-selected on refresh."];
}
}
}

View file

@ -90,9 +90,15 @@ const module: Module<ICredentialsState, IRootState> = {
getCredentialById: (state: ICredentialsState) => {
return (id: string) => state.credentials[id];
},
getCredentialByIdAndType: (state: ICredentialsState) => {
return (id: string, type: string) => {
const credential = state.credentials[id];
return !credential || credential.type !== type ? undefined : credential;
};
},
getCredentialsByType: (state: ICredentialsState, getters: any) => { // tslint:disable-line:no-any
return (credentialType: string): ICredentialsResponse[] => {
return getters.allCredentials.filter((credentialData: ICredentialsResponse) => credentialData.type === credentialType);
return getters.allCredentialsByType[credentialType];
};
},
getNodesWithAccess (state: ICredentialsState, getters: any, rootState: IRootState, rootGetters: any) { // tslint:disable-line:no-any

View file

@ -375,6 +375,32 @@ export const store = new Vuex.Store({
state.workflow.name = data.newName;
},
// replace invalid credentials in workflow
replaceInvalidWorkflowCredentials(state, {credentials, invalid, type }) {
state.workflow.nodes.forEach((node) => {
if (!node.credentials || !node.credentials[type]) {
return;
}
const nodeCredentials = node.credentials[type];
if (typeof nodeCredentials === 'string' && nodeCredentials === invalid.name) {
node.credentials[type] = credentials;
return;
}
if (nodeCredentials.id === null) {
if (nodeCredentials.name === invalid.name){
node.credentials[type] = credentials;
}
return;
}
if (nodeCredentials.id === invalid.id) {
node.credentials[type] = credentials;
}
});
},
// Nodes
addNode (state, nodeData: INodeUi) {
if (!nodeData.hasOwnProperty('name')) {

View file

@ -147,9 +147,11 @@ import {
NodeHelpers,
Workflow,
IRun,
INodeCredentialsDetails,
} from 'n8n-workflow';
import {
IConnectionsUi,
ICredentialsResponse,
IExecutionResponse,
IN8nUISettings,
IWorkflowDb,
@ -327,6 +329,7 @@ export default mixins(
ctrlKeyPressed: false,
stopExecutionInProgress: false,
blankRedirect: false,
credentialsUpdated: false,
};
},
beforeDestroy () {
@ -495,8 +498,10 @@ export default mixins(
this.$store.commit('setWorkflowTagIds', tagIds || []);
await this.addNodes(data.nodes, data.connections);
if (!this.credentialsUpdated) {
this.$store.commit('setStateDirty', false);
}
this.$store.commit('setStateDirty', false);
this.zoomToFit();
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
@ -1871,6 +1876,47 @@ export default mixins(
this.deselectAllNodes();
this.nodeSelectedByName(newName);
},
matchCredentials(node: INodeUi) {
if (!node.credentials) {
return;
}
Object.entries(node.credentials).forEach(([nodeCredentialType, nodeCredentials]: [string, INodeCredentialsDetails]) => {
const credentialOptions = this.$store.getters['credentials/getCredentialsByType'](nodeCredentialType) as ICredentialsResponse[];
// Check if workflows applies old credentials style
if (typeof nodeCredentials === 'string') {
nodeCredentials = {
id: null,
name: nodeCredentials,
};
this.credentialsUpdated = true;
}
if (nodeCredentials.id) {
// Check whether the id is matching with a credential
const credentialsForId = credentialOptions.find((optionData: ICredentialsResponse) => optionData.id === nodeCredentials.id);
if (credentialsForId) {
if (credentialsForId.name !== nodeCredentials.name) {
node.credentials![nodeCredentialType].name = credentialsForId.name;
this.credentialsUpdated = true;
}
return;
}
}
// No match for id found or old credentials type used
node.credentials![nodeCredentialType] = nodeCredentials;
// check if only one option with the name would exist
const credentialsForName = credentialOptions.filter((optionData: ICredentialsResponse) => optionData.name === nodeCredentials.name);
// only one option exists for the name, take it
if (credentialsForName.length === 1) {
node.credentials![nodeCredentialType].id = credentialsForName[0].id;
this.credentialsUpdated = true;
}
});
},
async addNodes (nodes: INodeUi[], connections?: IConnections) {
if (!nodes || !nodes.length) {
return;
@ -1920,6 +1966,9 @@ export default mixins(
}
}
// check and match credentials, apply new format if old is used
this.matchCredentials(node);
foundNodeIssues = this.getNodeIssues(nodeType, node);
if (foundNodeIssues !== null) {

View file

@ -692,6 +692,7 @@ export class Wait implements INodeType {
// @ts-ignore
const mimeType = headers['content-type'] || 'application/json';
if (mimeType.includes('multipart/form-data')) {
// @ts-ignore
const form = new formidable.IncomingForm({ multiples: true });
return new Promise((resolve, reject) => {

View file

@ -407,6 +407,7 @@ export class Webhook implements INodeType {
// @ts-ignore
const mimeType = headers['content-type'] || 'application/json';
if (mimeType.includes('multipart/form-data')) {
// @ts-ignore
const form = new formidable.IncomingForm({ multiples: true });
return new Promise((resolve, reject) => {

View file

@ -52,10 +52,12 @@ export type ExecutionError = WorkflowOperationError | NodeOperationError | NodeA
// Get used to gives nodes access to credentials
export interface IGetCredentials {
get(type: string, name: string): Promise<ICredentialsEncrypted>;
get(type: string, id: string | null): Promise<ICredentialsEncrypted>;
}
export abstract class ICredentials {
id?: string;
name: string;
type: string;
@ -64,8 +66,15 @@ export abstract class ICredentials {
nodesAccess: ICredentialNodeAccess[];
constructor(name: string, type: string, nodesAccess: ICredentialNodeAccess[], data?: string) {
this.name = name;
constructor(
nodeCredentials: INodeCredentialsDetails,
type: string,
nodesAccess: ICredentialNodeAccess[],
data?: string,
) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
this.id = nodeCredentials.id || undefined;
this.name = nodeCredentials.name;
this.type = type;
this.nodesAccess = nodesAccess;
this.data = data;
@ -93,6 +102,7 @@ export interface ICredentialNodeAccess {
}
export interface ICredentialsDecrypted {
id: string | number;
name: string;
type: string;
nodesAccess: ICredentialNodeAccess[];
@ -100,6 +110,7 @@ export interface ICredentialsDecrypted {
}
export interface ICredentialsEncrypted {
id?: string | number;
name: string;
type: string;
nodesAccess: ICredentialNodeAccess[];
@ -122,10 +133,13 @@ export abstract class ICredentialsHelper {
this.encryptionKey = encryptionKey;
}
abstract getCredentials(name: string, type: string): Promise<ICredentials>;
abstract getCredentials(
nodeCredentials: INodeCredentialsDetails,
type: string,
): Promise<ICredentials>;
abstract getDecrypted(
name: string,
nodeCredentials: INodeCredentialsDetails,
type: string,
mode: WorkflowExecuteMode,
raw?: boolean,
@ -133,7 +147,7 @@ export abstract class ICredentialsHelper {
): Promise<ICredentialDataDecryptedObject>;
abstract updateCredentials(
name: string,
nodeCredentials: INodeCredentialsDetails,
type: string,
data: ICredentialDataDecryptedObject,
): Promise<void>;
@ -160,6 +174,7 @@ export interface ICredentialTypes {
// The way the credentials get saved in the database (data encrypted)
export interface ICredentialData {
id?: string;
name: string;
data: string; // Contains the access data as encrypted JSON string
nodesAccess: ICredentialNodeAccess[];
@ -356,7 +371,7 @@ export interface IExecuteFunctions {
outputIndex?: number,
): Promise<INodeExecutionData[][]>;
putExecutionToWait(waitTill: Date): Promise<void>;
sendMessageToUI(message: any): void; // tslint:disable-line:no-any
sendMessageToUI(message: any): void;
helpers: {
httpRequest(
requestOptions: IHttpRequestOptions,
@ -529,8 +544,13 @@ export interface IWebhookFunctions {
};
}
export interface INodeCredentialsDetails {
id: string | null;
name: string;
}
export interface INodeCredentials {
[key: string]: string;
[key: string]: INodeCredentialsDetails;
}
export interface INode {
@ -942,10 +962,8 @@ export interface IWorkflowBase {
}
export interface IWorkflowCredentials {
// Credential type
[key: string]: {
// Name
[key: string]: ICredentialsEncrypted;
[credentialType: string]: {
[id: string]: ICredentialsEncrypted;
};
}