Merge branch 'master' into static-stateless-webhooks
This commit is contained in:
@@ -3,16 +3,31 @@ import {
|
||||
ICredentialTypes as ICredentialTypesInterface,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
CredentialsOverwrites,
|
||||
ICredentialsTypeData,
|
||||
} from './';
|
||||
|
||||
class CredentialTypesClass implements ICredentialTypesInterface {
|
||||
|
||||
credentialTypes: {
|
||||
[key: string]: ICredentialType
|
||||
} = {};
|
||||
credentialTypes: ICredentialsTypeData = {};
|
||||
|
||||
|
||||
async init(credentialTypes: { [key: string]: ICredentialType }): Promise<void> {
|
||||
async init(credentialTypes: ICredentialsTypeData): Promise<void> {
|
||||
this.credentialTypes = credentialTypes;
|
||||
|
||||
// Load the credentials overwrites if any exist
|
||||
const credentialsOverwrites = CredentialsOverwrites().getAll();
|
||||
|
||||
for (const credentialType of Object.keys(credentialsOverwrites)) {
|
||||
if (credentialTypes[credentialType] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add which properties got overwritten that the Editor-UI knows
|
||||
// which properties it should hide
|
||||
credentialTypes[credentialType].__overwrittenProperties = Object.keys(credentialsOverwrites[credentialType]);
|
||||
}
|
||||
}
|
||||
|
||||
getAll(): ICredentialType[] {
|
||||
|
||||
159
packages/cli/src/CredentialsHelper.ts
Normal file
159
packages/cli/src/CredentialsHelper.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
Credentials,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialsHelper,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
CredentialsOverwrites,
|
||||
CredentialTypes,
|
||||
Db,
|
||||
ICredentialsDb,
|
||||
} from './';
|
||||
|
||||
|
||||
export class CredentialsHelper extends ICredentialsHelper {
|
||||
|
||||
/**
|
||||
* Returns the credentials instance
|
||||
*
|
||||
* @param {string} name Name of the credentials to return instance of
|
||||
* @param {string} type Type of the credentials to return instance of
|
||||
* @returns {Credentials}
|
||||
* @memberof CredentialsHelper
|
||||
*/
|
||||
getCredentials(name: string, type: string): Credentials {
|
||||
if (!this.workflowCredentials[type]) {
|
||||
throw new Error(`No credentials of type "${type}" exist.`);
|
||||
}
|
||||
if (!this.workflowCredentials[type][name]) {
|
||||
throw new Error(`No credentials with name "${name}" exist for type "${type}".`);
|
||||
}
|
||||
const credentialData = this.workflowCredentials[type][name];
|
||||
|
||||
return new Credentials(credentialData.name, credentialData.type, credentialData.nodesAccess, credentialData.data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns all the properties of the credentials with the given name
|
||||
*
|
||||
* @param {string} type The name of the type to return credentials off
|
||||
* @returns {INodeProperties[]}
|
||||
* @memberof CredentialsHelper
|
||||
*/
|
||||
getCredentialsProperties(type: string): INodeProperties[] {
|
||||
const credentialTypes = CredentialTypes();
|
||||
const credentialTypeData = credentialTypes.getByName(type);
|
||||
|
||||
if (credentialTypeData === undefined) {
|
||||
throw new Error(`The credentials of type "${type}" are not known.`);
|
||||
}
|
||||
|
||||
if (credentialTypeData.extends === undefined) {
|
||||
return credentialTypeData.properties;
|
||||
}
|
||||
|
||||
const combineProperties = [] as INodeProperties[];
|
||||
for (const credentialsTypeName of credentialTypeData.extends) {
|
||||
const mergeCredentialProperties = this.getCredentialsProperties(credentialsTypeName);
|
||||
NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
|
||||
}
|
||||
|
||||
// The properties defined on the parent credentials take presidence
|
||||
NodeHelpers.mergeNodeProperties(combineProperties, credentialTypeData.properties);
|
||||
|
||||
return combineProperties;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the decrypted credential data with applied overwrites
|
||||
*
|
||||
* @param {string} name Name of the credentials to return data 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
|
||||
*/
|
||||
getDecrypted(name: string, type: string, raw?: boolean): ICredentialDataDecryptedObject {
|
||||
const credentials = this.getCredentials(name, type);
|
||||
|
||||
const decryptedDataOriginal = credentials.getData(this.encryptionKey);
|
||||
|
||||
if (raw === true) {
|
||||
return decryptedDataOriginal;
|
||||
}
|
||||
|
||||
return this.applyDefaultsAndOverwrites(decryptedDataOriginal, type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies credential default data and overwrites
|
||||
*
|
||||
* @param {ICredentialDataDecryptedObject} decryptedDataOriginal The credential data to overwrite data on
|
||||
* @param {string} type Type of the credentials to overwrite data of
|
||||
* @returns {ICredentialDataDecryptedObject}
|
||||
* @memberof CredentialsHelper
|
||||
*/
|
||||
applyDefaultsAndOverwrites(decryptedDataOriginal: ICredentialDataDecryptedObject, type: string): ICredentialDataDecryptedObject {
|
||||
const credentialsProperties = this.getCredentialsProperties(type);
|
||||
|
||||
// Add the default credential values
|
||||
const decryptedData = NodeHelpers.getNodeParameters(credentialsProperties, decryptedDataOriginal as INodeParameters, true, false) as ICredentialDataDecryptedObject;
|
||||
|
||||
if (decryptedDataOriginal.oauthTokenData !== undefined) {
|
||||
// The OAuth data gets removed as it is not defined specifically as a parameter
|
||||
// on the credentials so add it back in case it was set
|
||||
decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData;
|
||||
}
|
||||
|
||||
// Load and apply the credentials overwrites if any exist
|
||||
const credentialsOverwrites = CredentialsOverwrites();
|
||||
return credentialsOverwrites.applyOverwrite(type, decryptedData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates credentials in the database
|
||||
*
|
||||
* @param {string} name Name of the credentials to set data of
|
||||
* @param {string} type Type of the credentials to set data of
|
||||
* @param {ICredentialDataDecryptedObject} data The data to set
|
||||
* @returns {Promise<void>}
|
||||
* @memberof CredentialsHelper
|
||||
*/
|
||||
async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise<void> {
|
||||
const credentials = await this.getCredentials(name, type);
|
||||
|
||||
if (Db.collections!.Credentials === null) {
|
||||
// The first time executeWorkflow gets called the Database has
|
||||
// to get initialized first
|
||||
await Db.init();
|
||||
}
|
||||
|
||||
credentials.setData(data, this.encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as ICredentialsDb;
|
||||
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = new Date();
|
||||
|
||||
// TODO: also add user automatically depending on who is logged in, if anybody is logged in
|
||||
|
||||
// Save the credentials in DB
|
||||
const findQuery = {
|
||||
name,
|
||||
type,
|
||||
};
|
||||
|
||||
await Db.collections.Credentials!.update(findQuery, newCredentialsData);
|
||||
}
|
||||
|
||||
}
|
||||
63
packages/cli/src/CredentialsOverwrites.ts
Normal file
63
packages/cli/src/CredentialsOverwrites.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
ICredentialDataDecryptedObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
ICredentialsOverwrite,
|
||||
GenericHelpers,
|
||||
} from './';
|
||||
|
||||
|
||||
class CredentialsOverwritesClass {
|
||||
|
||||
private overwriteData: ICredentialsOverwrite = {};
|
||||
|
||||
async init(overwriteData?: ICredentialsOverwrite) {
|
||||
if (overwriteData !== undefined) {
|
||||
// If data is already given it can directly be set instead of
|
||||
// loaded from environment
|
||||
this.overwriteData = overwriteData;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await GenericHelpers.getConfigValue('credentials.overwrite') as string;
|
||||
|
||||
try {
|
||||
this.overwriteData = JSON.parse(data);
|
||||
} catch (error) {
|
||||
throw new Error(`The credentials-overwrite is not valid JSON.`);
|
||||
}
|
||||
}
|
||||
|
||||
applyOverwrite(type: string, data: ICredentialDataDecryptedObject) {
|
||||
const overwrites = this.get(type);
|
||||
|
||||
if (overwrites === undefined) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const returnData = JSON.parse(JSON.stringify(data));
|
||||
Object.assign(returnData, overwrites);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
get(type: string): ICredentialDataDecryptedObject | undefined {
|
||||
return this.overwriteData[type];
|
||||
}
|
||||
|
||||
getAll(): ICredentialsOverwrite {
|
||||
return this.overwriteData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined;
|
||||
|
||||
export function CredentialsOverwrites(): CredentialsOverwritesClass {
|
||||
if (credentialsOverwritesInstance === undefined) {
|
||||
credentialsOverwritesInstance = new CredentialsOverwritesClass();
|
||||
}
|
||||
|
||||
return credentialsOverwritesInstance;
|
||||
}
|
||||
79
packages/cli/src/ExternalHooks.ts
Normal file
79
packages/cli/src/ExternalHooks.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Db,
|
||||
IExternalHooksFunctions,
|
||||
IExternalHooksClass,
|
||||
} from './';
|
||||
|
||||
import * as config from '../config';
|
||||
|
||||
|
||||
class ExternalHooksClass implements IExternalHooksClass {
|
||||
|
||||
externalHooks: {
|
||||
[key: string]: Array<() => {}>
|
||||
} = {};
|
||||
initDidRun = false;
|
||||
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.initDidRun === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const externalHookFiles = config.get('externalHookFiles').split(':');
|
||||
|
||||
// Load all the provided hook-files
|
||||
for (let hookFilePath of externalHookFiles) {
|
||||
hookFilePath = hookFilePath.trim();
|
||||
if (hookFilePath !== '') {
|
||||
try {
|
||||
const hookFile = require(hookFilePath);
|
||||
|
||||
for (const resource of Object.keys(hookFile)) {
|
||||
for (const operation of Object.keys(hookFile[resource])) {
|
||||
// Save all the hook functions directly under their string
|
||||
// format in an array
|
||||
const hookString = `${resource}.${operation}`;
|
||||
if (this.externalHooks[hookString] === undefined) {
|
||||
this.externalHooks[hookString] = [];
|
||||
}
|
||||
|
||||
this.externalHooks[hookString].push.apply(this.externalHooks[hookString], hookFile[resource][operation]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Problem loading external hook file "${hookFilePath}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.initDidRun = true;
|
||||
}
|
||||
|
||||
async run(hookName: string, hookParameters?: any[]): Promise<void> { // tslint:disable-line:no-any
|
||||
const externalHookFunctions: IExternalHooksFunctions = {
|
||||
dbCollections: Db.collections,
|
||||
};
|
||||
|
||||
if (this.externalHooks[hookName] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(const externalHookFunction of this.externalHooks[hookName]) {
|
||||
await externalHookFunction.apply(externalHookFunctions, hookParameters);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
let externalHooksInstance: ExternalHooksClass | undefined;
|
||||
|
||||
export function ExternalHooks(): ExternalHooksClass {
|
||||
if (externalHooksInstance === undefined) {
|
||||
externalHooksInstance = new ExternalHooksClass();
|
||||
}
|
||||
|
||||
return externalHooksInstance;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialsDecrypted,
|
||||
ICredentialsEncrypted,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
IExecutionError,
|
||||
IRun,
|
||||
@@ -35,6 +37,13 @@ export interface ICustomRequest extends Request {
|
||||
parsedUrl: Url | undefined;
|
||||
}
|
||||
|
||||
export interface ICredentialsTypeData {
|
||||
[key: string]: ICredentialType;
|
||||
}
|
||||
|
||||
export interface ICredentialsOverwrite {
|
||||
[key: string]: ICredentialDataDecryptedObject;
|
||||
}
|
||||
|
||||
export interface IDatabaseCollections {
|
||||
Credentials: Repository<ICredentialsDb> | null;
|
||||
@@ -78,7 +87,7 @@ export interface ICredentialsBase {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted{
|
||||
export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted {
|
||||
id: number | string | ObjectID;
|
||||
}
|
||||
|
||||
@@ -195,6 +204,30 @@ export interface IExecutingWorkflowData {
|
||||
workflowExecution?: PCancelable<IRun>;
|
||||
}
|
||||
|
||||
export interface IExternalHooks {
|
||||
credentials?: {
|
||||
create?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise<void>; }>
|
||||
delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise<void>; }>
|
||||
update?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise<void>; }>
|
||||
};
|
||||
workflow?: {
|
||||
activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }>
|
||||
create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise<void>; }>
|
||||
delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise<void>; }>
|
||||
execute?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode): Promise<void>; }>
|
||||
update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise<void>; }>
|
||||
};
|
||||
}
|
||||
|
||||
export interface IExternalHooksFunctions {
|
||||
dbCollections: IDatabaseCollections;
|
||||
}
|
||||
|
||||
export interface IExternalHooksClass {
|
||||
init(): Promise<void>;
|
||||
run(hookName: string, hookParameters?: any[]): Promise<void>; // tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
export interface IN8nConfig {
|
||||
database: IN8nConfigDatabase;
|
||||
endpoints: IN8nConfigEndpoints;
|
||||
@@ -353,7 +386,10 @@ export interface IWorkflowExecutionDataProcess {
|
||||
workflowData: IWorkflowBase;
|
||||
}
|
||||
|
||||
|
||||
export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExecutionDataProcess {
|
||||
credentialsOverwrite: ICredentialsOverwrite;
|
||||
credentialsTypeData: ICredentialsTypeData;
|
||||
executionId: string;
|
||||
nodeTypeData: ITransferNodeTypes;
|
||||
}
|
||||
|
||||
@@ -97,24 +97,28 @@ class LoadNodesAndCredentialsClass {
|
||||
* @memberof LoadNodesAndCredentialsClass
|
||||
*/
|
||||
async getN8nNodePackages(): Promise<string[]> {
|
||||
const packages: string[] = [];
|
||||
for (const file of await fsReaddirAsync(this.nodeModulesPath)) {
|
||||
if (file.indexOf('n8n-nodes-') !== 0) {
|
||||
continue;
|
||||
const getN8nNodePackagesRecursive = async (relativePath: string): Promise<string[]> => {
|
||||
const results: string[] = [];
|
||||
const nodeModulesPath = `${this.nodeModulesPath}/${relativePath}`;
|
||||
for (const file of await fsReaddirAsync(nodeModulesPath)) {
|
||||
const isN8nNodesPackage = file.indexOf('n8n-nodes-') === 0;
|
||||
const isNpmScopedPackage = file.indexOf('@') === 0;
|
||||
if (!isN8nNodesPackage && !isNpmScopedPackage) {
|
||||
continue;
|
||||
}
|
||||
if (!(await fsStatAsync(nodeModulesPath)).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
if (isN8nNodesPackage) { results.push(`${relativePath}${file}`); }
|
||||
if (isNpmScopedPackage) {
|
||||
results.push(...await getN8nNodePackagesRecursive(`${relativePath}${file}/`));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is really a folder
|
||||
if (!(await fsStatAsync(path.join(this.nodeModulesPath, file))).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
packages.push(file);
|
||||
}
|
||||
|
||||
return packages;
|
||||
return results;
|
||||
};
|
||||
return getN8nNodePackagesRecursive('');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads credentials from a file
|
||||
*
|
||||
@@ -137,7 +141,7 @@ class LoadNodesAndCredentialsClass {
|
||||
}
|
||||
}
|
||||
|
||||
this.credentialTypes[credentialName] = tempCredential;
|
||||
this.credentialTypes[tempCredential.name] = tempCredential;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import {
|
||||
dirname as pathDirname,
|
||||
join as pathJoin,
|
||||
resolve as pathResolve,
|
||||
} from 'path';
|
||||
import {
|
||||
getConnectionManager,
|
||||
@@ -12,13 +13,21 @@ import {
|
||||
import * as bodyParser from 'body-parser';
|
||||
require('body-parser-xml')(bodyParser);
|
||||
import * as history from 'connect-history-api-fallback';
|
||||
import * as requestPromise from 'request-promise-native';
|
||||
import * as _ from 'lodash';
|
||||
import * as clientOAuth2 from 'client-oauth2';
|
||||
import * as clientOAuth1 from 'oauth-1.0a';
|
||||
import { RequestOptions } from 'oauth-1.0a';
|
||||
import * as csrf from 'csrf';
|
||||
import * as requestPromise from 'request-promise-native';
|
||||
import { createHmac } from 'crypto';
|
||||
|
||||
import {
|
||||
ActiveExecutions,
|
||||
ActiveWorkflowRunner,
|
||||
CredentialsHelper,
|
||||
CredentialTypes,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
IActivationError,
|
||||
ICustomRequest,
|
||||
ICredentialsDb,
|
||||
@@ -33,6 +42,7 @@ import {
|
||||
IExecutionsListResponse,
|
||||
IExecutionsStopData,
|
||||
IExecutionsSummary,
|
||||
IExternalHooksClass,
|
||||
IN8nUISettings,
|
||||
IPackageVersions,
|
||||
IWorkflowBase,
|
||||
@@ -57,6 +67,7 @@ import {
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
ICredentialsEncrypted,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
INodeCredentials,
|
||||
@@ -64,6 +75,7 @@ import {
|
||||
INodeParameters,
|
||||
INodePropertyOptions,
|
||||
IRunData,
|
||||
IWorkflowCredentials,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
@@ -83,7 +95,8 @@ import * as jwks from 'jwks-rsa';
|
||||
// @ts-ignore
|
||||
import * as timezones from 'google-timezones-json';
|
||||
import * as parseUrl from 'parseurl';
|
||||
|
||||
import * as querystring from 'querystring';
|
||||
import { OptionsWithUrl } from 'request-promise-native';
|
||||
|
||||
class App {
|
||||
|
||||
@@ -92,6 +105,7 @@ class App {
|
||||
testWebhooks: TestWebhooks.TestWebhooks;
|
||||
endpointWebhook: string;
|
||||
endpointWebhookTest: string;
|
||||
externalHooks: IExternalHooksClass;
|
||||
saveDataErrorExecution: string;
|
||||
saveDataSuccessExecution: string;
|
||||
saveManualExecutions: boolean;
|
||||
@@ -123,6 +137,8 @@ class App {
|
||||
this.protocol = config.get('protocol');
|
||||
this.sslKey = config.get('ssl_key');
|
||||
this.sslCert = config.get('ssl_cert');
|
||||
|
||||
this.externalHooks = ExternalHooks();
|
||||
}
|
||||
|
||||
|
||||
@@ -340,7 +356,7 @@ class App {
|
||||
// Creates a new workflow
|
||||
this.app.post('/rest/workflows', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => {
|
||||
|
||||
const newWorkflowData = req.body;
|
||||
const newWorkflowData = req.body as IWorkflowBase;
|
||||
|
||||
newWorkflowData.name = newWorkflowData.name.trim();
|
||||
newWorkflowData.createdAt = this.getCurrentDate();
|
||||
@@ -348,6 +364,8 @@ class App {
|
||||
|
||||
newWorkflowData.id = undefined;
|
||||
|
||||
await this.externalHooks.run('workflow.create', [newWorkflowData]);
|
||||
|
||||
// Save the workflow in DB
|
||||
const result = await Db.collections.Workflow!.save(newWorkflowData);
|
||||
|
||||
@@ -423,9 +441,11 @@ class App {
|
||||
// Updates an existing workflow
|
||||
this.app.patch('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => {
|
||||
|
||||
const newWorkflowData = req.body;
|
||||
const newWorkflowData = req.body as IWorkflowBase;
|
||||
const id = req.params.id;
|
||||
|
||||
await this.externalHooks.run('workflow.update', [newWorkflowData]);
|
||||
|
||||
const isActive = await this.activeWorkflowRunner.isActive(id);
|
||||
|
||||
if (isActive) {
|
||||
@@ -469,6 +489,8 @@ class App {
|
||||
if (responseData.active === true) {
|
||||
// When the workflow is supposed to be active add it again
|
||||
try {
|
||||
await this.externalHooks.run('workflow.activate', [responseData]);
|
||||
|
||||
await this.activeWorkflowRunner.add(id);
|
||||
} catch (error) {
|
||||
// If workflow could not be activated set it again to inactive
|
||||
@@ -493,6 +515,8 @@ class App {
|
||||
this.app.delete('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
|
||||
const id = req.params.id;
|
||||
|
||||
await this.externalHooks.run('workflow.delete', [id]);
|
||||
|
||||
const isActive = await this.activeWorkflowRunner.isActive(id);
|
||||
|
||||
if (isActive) {
|
||||
@@ -567,7 +591,7 @@ class App {
|
||||
|
||||
const nodeTypes = NodeTypes();
|
||||
|
||||
const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, credentials);
|
||||
const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, JSON.parse('' + req.query.currentNodeParameters), credentials!);
|
||||
|
||||
const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase;
|
||||
const workflowCredentials = await WorkflowCredentials(workflowData.nodes);
|
||||
@@ -601,8 +625,8 @@ class App {
|
||||
|
||||
|
||||
// Returns the node icon
|
||||
this.app.get('/rest/node-icon/:nodeType', async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
const nodeTypeName = req.params.nodeType;
|
||||
this.app.get(['/rest/node-icon/:nodeType', '/rest/node-icon/:scope/:nodeType'], async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
const nodeTypeName = `${req.params.scope ? `${req.params.scope}/` : ''}${req.params.nodeType}`;
|
||||
|
||||
const nodeTypes = NodeTypes();
|
||||
const nodeType = nodeTypes.getByName(nodeTypeName);
|
||||
@@ -658,6 +682,8 @@ class App {
|
||||
this.app.delete('/rest/credentials/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
|
||||
const id = req.params.id;
|
||||
|
||||
await this.externalHooks.run('credentials.delete', [id]);
|
||||
|
||||
await Db.collections.Credentials!.delete({ id });
|
||||
|
||||
return true;
|
||||
@@ -667,6 +693,10 @@ class App {
|
||||
this.app.post('/rest/credentials', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<ICredentialsResponse> => {
|
||||
const incomingData = req.body;
|
||||
|
||||
if (!incomingData.name || incomingData.name.length < 3) {
|
||||
throw new ResponseHelper.ResponseError(`Credentials name must be at least 3 characters long.`, undefined, 400);
|
||||
}
|
||||
|
||||
// Add the added date for node access permissions
|
||||
for (const nodeAccess of incomingData.nodesAccess) {
|
||||
nodeAccess.date = this.getCurrentDate();
|
||||
@@ -699,6 +729,8 @@ class App {
|
||||
credentials.setData(incomingData.data, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as ICredentialsDb;
|
||||
|
||||
await this.externalHooks.run('credentials.create', [newCredentialsData]);
|
||||
|
||||
// Add special database related data
|
||||
newCredentialsData.createdAt = this.getCurrentDate();
|
||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
||||
@@ -707,6 +739,7 @@ class App {
|
||||
|
||||
// Save the credentials in DB
|
||||
const result = await Db.collections.Credentials!.save(newCredentialsData);
|
||||
result.data = incomingData.data;
|
||||
|
||||
// Convert to response format in which the id is a string
|
||||
(result as unknown as ICredentialsResponse).id = result.id.toString();
|
||||
@@ -750,6 +783,21 @@ class App {
|
||||
throw new Error('No encryption key got found to encrypt the credentials!');
|
||||
}
|
||||
|
||||
// Load the currently saved credentials to be able to persist some of the data if
|
||||
const result = await Db.collections.Credentials!.findOne(id);
|
||||
if (result === undefined) {
|
||||
throw new ResponseHelper.ResponseError(`Credentials with the id "${id}" do not exist.`, undefined, 400);
|
||||
}
|
||||
|
||||
const currentlySavedCredentials = new Credentials(result.name, result.type, result.nodesAccess, result.data);
|
||||
const decryptedData = currentlySavedCredentials.getData(encryptionKey!);
|
||||
|
||||
// Do not overwrite the oauth data else data like the access or refresh token would get lost
|
||||
// everytime anybody changes anything on the credentials even if it is just the name.
|
||||
if (decryptedData.oauthTokenData) {
|
||||
incomingData.data.oauthTokenData = decryptedData.oauthTokenData;
|
||||
}
|
||||
|
||||
// Encrypt the data
|
||||
const credentials = new Credentials(incomingData.name, incomingData.type, incomingData.nodesAccess);
|
||||
credentials.setData(incomingData.data, encryptionKey);
|
||||
@@ -758,6 +806,8 @@ class App {
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
||||
|
||||
await this.externalHooks.run('credentials.update', [newCredentialsData]);
|
||||
|
||||
// Update the credentials in DB
|
||||
await Db.collections.Credentials!.update(id, newCredentialsData);
|
||||
|
||||
@@ -869,6 +919,331 @@ class App {
|
||||
return returnData;
|
||||
}));
|
||||
|
||||
// ----------------------------------------
|
||||
// OAuth1-Credential/Auth
|
||||
// ----------------------------------------
|
||||
|
||||
// Authorize OAuth Data
|
||||
this.app.get('/rest/oauth1-credential/auth', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
|
||||
if (req.query.id === undefined) {
|
||||
throw new Error('Required credential id is missing!');
|
||||
}
|
||||
|
||||
const result = await Db.collections.Credentials!.findOne(req.query.id as string);
|
||||
if (result === undefined) {
|
||||
res.status(404).send('The credential is not known.');
|
||||
return '';
|
||||
}
|
||||
|
||||
let encryptionKey = undefined;
|
||||
encryptionKey = await UserSettings.getEncryptionKey();
|
||||
if (encryptionKey === undefined) {
|
||||
throw new Error('No encryption key got found to decrypt the credentials!');
|
||||
}
|
||||
|
||||
// Decrypt the currently saved credentials
|
||||
const workflowCredentials: IWorkflowCredentials = {
|
||||
[result.type as string]: {
|
||||
[result.name as string]: result as ICredentialsEncrypted,
|
||||
},
|
||||
};
|
||||
const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey);
|
||||
const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true);
|
||||
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type);
|
||||
|
||||
const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string;
|
||||
|
||||
const oauth = new clientOAuth1({
|
||||
consumer: {
|
||||
key: _.get(oauthCredentials, 'consumerKey') as string,
|
||||
secret: _.get(oauthCredentials, 'consumerSecret') as string,
|
||||
},
|
||||
signature_method: signatureMethod,
|
||||
hash_function(base, key) {
|
||||
const algorithm = (signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256';
|
||||
return createHmac(algorithm, key)
|
||||
.update(base)
|
||||
.digest('base64');
|
||||
},
|
||||
});
|
||||
|
||||
const callback = `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth1-credential/callback?cid=${req.query.id}`;
|
||||
|
||||
const options: RequestOptions = {
|
||||
method: 'POST',
|
||||
url: (_.get(oauthCredentials, 'requestTokenUrl') as string),
|
||||
data: {
|
||||
oauth_callback: callback,
|
||||
},
|
||||
};
|
||||
|
||||
const data = oauth.toHeader(oauth.authorize(options as RequestOptions));
|
||||
|
||||
//@ts-ignore
|
||||
options.headers = data;
|
||||
|
||||
const response = await requestPromise(options);
|
||||
|
||||
// Response comes as x-www-form-urlencoded string so convert it to JSON
|
||||
|
||||
const responseJson = querystring.parse(response);
|
||||
|
||||
const returnUri = `${_.get(oauthCredentials, 'authUrl')}?oauth_token=${responseJson.oauth_token}`;
|
||||
|
||||
// Encrypt the data
|
||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
||||
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
||||
|
||||
// Update the credentials in DB
|
||||
await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData);
|
||||
|
||||
return returnUri;
|
||||
}));
|
||||
|
||||
// Verify and store app code. Generate access tokens and store for respective credential.
|
||||
this.app.get('/rest/oauth1-credential/callback', async (req: express.Request, res: express.Response) => {
|
||||
const { oauth_verifier, oauth_token, cid } = req.query;
|
||||
|
||||
if (oauth_verifier === undefined || oauth_token === undefined) {
|
||||
throw new Error('Insufficient parameters for OAuth1 callback');
|
||||
}
|
||||
|
||||
const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any
|
||||
if (result === undefined) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('The credential is not known.', undefined, 404);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
let encryptionKey = undefined;
|
||||
encryptionKey = await UserSettings.getEncryptionKey();
|
||||
if (encryptionKey === undefined) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
// Decrypt the currently saved credentials
|
||||
const workflowCredentials: IWorkflowCredentials = {
|
||||
[result.type as string]: {
|
||||
[result.name as string]: result as ICredentialsEncrypted,
|
||||
},
|
||||
};
|
||||
const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey);
|
||||
const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true);
|
||||
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type);
|
||||
|
||||
const options: OptionsWithUrl = {
|
||||
method: 'POST',
|
||||
url: _.get(oauthCredentials, 'accessTokenUrl') as string,
|
||||
qs: {
|
||||
oauth_token,
|
||||
oauth_verifier,
|
||||
}
|
||||
};
|
||||
|
||||
let oauthToken;
|
||||
|
||||
try {
|
||||
oauthToken = await requestPromise(options);
|
||||
} catch (error) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
// Response comes as x-www-form-urlencoded string so convert it to JSON
|
||||
|
||||
const oauthTokenJson = querystring.parse(oauthToken);
|
||||
|
||||
decryptedDataOriginal.oauthTokenData = oauthTokenJson;
|
||||
|
||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
||||
// Save the credentials in DB
|
||||
await Db.collections.Credentials!.update(cid as any, newCredentialsData); // tslint:disable-line:no-any
|
||||
|
||||
res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
|
||||
});
|
||||
|
||||
|
||||
// ----------------------------------------
|
||||
// OAuth2-Credential/Auth
|
||||
// ----------------------------------------
|
||||
|
||||
|
||||
// Authorize OAuth Data
|
||||
this.app.get('/rest/oauth2-credential/auth', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
|
||||
if (req.query.id === undefined) {
|
||||
throw new Error('Required credential id is missing!');
|
||||
}
|
||||
|
||||
const result = await Db.collections.Credentials!.findOne(req.query.id as string);
|
||||
if (result === undefined) {
|
||||
res.status(404).send('The credential is not known.');
|
||||
return '';
|
||||
}
|
||||
|
||||
let encryptionKey = undefined;
|
||||
encryptionKey = await UserSettings.getEncryptionKey();
|
||||
if (encryptionKey === undefined) {
|
||||
throw new Error('No encryption key got found to decrypt the credentials!');
|
||||
}
|
||||
|
||||
// Decrypt the currently saved credentials
|
||||
const workflowCredentials: IWorkflowCredentials = {
|
||||
[result.type as string]: {
|
||||
[result.name as string]: result as ICredentialsEncrypted,
|
||||
},
|
||||
};
|
||||
const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey);
|
||||
const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true);
|
||||
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type);
|
||||
|
||||
const token = new csrf();
|
||||
// Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR
|
||||
const csrfSecret = token.secretSync();
|
||||
const state = {
|
||||
token: token.create(csrfSecret),
|
||||
cid: req.query.id
|
||||
};
|
||||
const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string;
|
||||
|
||||
const oAuthObj = new clientOAuth2({
|
||||
clientId: _.get(oauthCredentials, 'clientId') as string,
|
||||
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string,
|
||||
accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
|
||||
authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
|
||||
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth2-credential/callback`,
|
||||
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','),
|
||||
state: stateEncodedStr,
|
||||
});
|
||||
|
||||
// Encrypt the data
|
||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
||||
decryptedDataOriginal.csrfSecret = csrfSecret;
|
||||
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
||||
|
||||
// Update the credentials in DB
|
||||
await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData);
|
||||
|
||||
const authQueryParameters = _.get(oauthCredentials, 'authQueryParameters', '') as string;
|
||||
let returnUri = oAuthObj.code.getUri();
|
||||
|
||||
if (authQueryParameters) {
|
||||
returnUri += '&' + authQueryParameters;
|
||||
}
|
||||
|
||||
return returnUri;
|
||||
}));
|
||||
|
||||
// ----------------------------------------
|
||||
// OAuth2-Credential/Callback
|
||||
// ----------------------------------------
|
||||
|
||||
// Verify and store app code. Generate access tokens and store for respective credential.
|
||||
this.app.get('/rest/oauth2-credential/callback', async (req: express.Request, res: express.Response) => {
|
||||
const {code, state: stateEncoded } = req.query;
|
||||
|
||||
if (code === undefined || stateEncoded === undefined) {
|
||||
throw new Error('Insufficient parameters for OAuth2 callback');
|
||||
}
|
||||
|
||||
let state;
|
||||
try {
|
||||
state = JSON.parse(Buffer.from(stateEncoded as string, 'base64').toString());
|
||||
} catch (error) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('Invalid state format returned', undefined, 503);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
const result = await Db.collections.Credentials!.findOne(state.cid);
|
||||
if (result === undefined) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('The credential is not known.', undefined, 404);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
let encryptionKey = undefined;
|
||||
encryptionKey = await UserSettings.getEncryptionKey();
|
||||
if (encryptionKey === undefined) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
// Decrypt the currently saved credentials
|
||||
const workflowCredentials: IWorkflowCredentials = {
|
||||
[result.type as string]: {
|
||||
[result.name as string]: result as ICredentialsEncrypted,
|
||||
},
|
||||
};
|
||||
const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey);
|
||||
const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true);
|
||||
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type);
|
||||
|
||||
const token = new csrf();
|
||||
if (decryptedDataOriginal.csrfSecret === undefined || !token.verify(decryptedDataOriginal.csrfSecret as string, state.token)) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('The OAuth2 callback state is invalid!', undefined, 404);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
let options = {};
|
||||
|
||||
if (_.get(oauthCredentials, 'authentication', 'header') as string === 'body') {
|
||||
options = {
|
||||
body: {
|
||||
client_id: _.get(oauthCredentials, 'clientId') as string,
|
||||
client_secret: _.get(oauthCredentials, 'clientSecret', '') as string,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const oAuthObj = new clientOAuth2({
|
||||
clientId: _.get(oauthCredentials, 'clientId') as string,
|
||||
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string,
|
||||
accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
|
||||
authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
|
||||
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth2-credential/callback`,
|
||||
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',')
|
||||
});
|
||||
|
||||
const oauthToken = await oAuthObj.code.getToken(req.originalUrl, options);
|
||||
|
||||
if (oauthToken === undefined) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
if (decryptedDataOriginal.oauthTokenData) {
|
||||
// Only overwrite supplied data as some providers do for example just return the
|
||||
// refresh_token on the very first request and not on subsequent ones.
|
||||
Object.assign(decryptedDataOriginal.oauthTokenData, oauthToken.data);
|
||||
} else {
|
||||
// No data exists so simply set
|
||||
decryptedDataOriginal.oauthTokenData = oauthToken.data;
|
||||
}
|
||||
|
||||
_.unset(decryptedDataOriginal, 'csrfSecret');
|
||||
|
||||
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
||||
// Save the credentials in DB
|
||||
await Db.collections.Credentials!.update(state.cid, newCredentialsData);
|
||||
|
||||
res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
|
||||
});
|
||||
|
||||
|
||||
// ----------------------------------------
|
||||
@@ -1299,6 +1674,7 @@ class App {
|
||||
|
||||
export async function start(): Promise<void> {
|
||||
const PORT = config.get('port');
|
||||
const ADDRESS = config.get('listen_address');
|
||||
|
||||
const app = new App();
|
||||
|
||||
@@ -1317,9 +1693,9 @@ export async function start(): Promise<void> {
|
||||
server = http.createServer(app.app);
|
||||
}
|
||||
|
||||
server.listen(PORT, async () => {
|
||||
server.listen(PORT, ADDRESS, async () => {
|
||||
const versions = await GenericHelpers.getVersions();
|
||||
console.log(`n8n ready on port ${PORT}`);
|
||||
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
|
||||
console.log(`Version: ${versions.cli}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,6 +176,9 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
|
||||
};
|
||||
}
|
||||
|
||||
// Save static data if it changed
|
||||
await WorkflowHelpers.saveStaticData(workflow);
|
||||
|
||||
if (webhookData.webhookDescription['responseHeaders'] !== undefined) {
|
||||
const responseHeaders = workflow.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], undefined) as {
|
||||
entries?: Array<{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
CredentialsHelper,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
IExecutionDb,
|
||||
IExecutionFlattedDb,
|
||||
IPushDataExecutionFinished,
|
||||
@@ -302,6 +304,10 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
|
||||
workflowData = workflowInfo.code;
|
||||
}
|
||||
|
||||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.init();
|
||||
await externalHooks.run('workflow.execute', [workflowData, mode]);
|
||||
|
||||
const nodeTypes = NodeTypes();
|
||||
|
||||
const workflowName = workflowData ? workflowData.name : undefined;
|
||||
@@ -404,6 +410,7 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara
|
||||
|
||||
return {
|
||||
credentials,
|
||||
credentialsHelper: new CredentialsHelper(credentials, encryptionKey),
|
||||
encryptionKey,
|
||||
executeWorkflow,
|
||||
restApiUrl: urlBaseWebhook + config.get('endpoints.rest') as string,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
CredentialTypes,
|
||||
Db,
|
||||
ICredentialsTypeData,
|
||||
ITransferNodeTypes,
|
||||
IWorkflowExecutionDataProcess,
|
||||
IWorkflowErrorData,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
IRun,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
IWorkflowCredentials,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
@@ -217,6 +220,63 @@ export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the credentials data of the given type and its parent types
|
||||
* it extends
|
||||
*
|
||||
* @export
|
||||
* @param {string} type The credential type to return data off
|
||||
* @returns {ICredentialsTypeData}
|
||||
*/
|
||||
export function getCredentialsDataWithParents(type: string): ICredentialsTypeData {
|
||||
const credentialTypes = CredentialTypes();
|
||||
const credentialType = credentialTypes.getByName(type);
|
||||
|
||||
const credentialTypeData: ICredentialsTypeData = {};
|
||||
credentialTypeData[type] = credentialType;
|
||||
|
||||
if (credentialType === undefined || credentialType.extends === undefined) {
|
||||
return credentialTypeData;
|
||||
}
|
||||
|
||||
for (const typeName of credentialType.extends) {
|
||||
if (credentialTypeData[typeName] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
credentialTypeData[typeName] = credentialTypes.getByName(typeName);
|
||||
Object.assign(credentialTypeData, getCredentialsDataWithParents(typeName));
|
||||
}
|
||||
|
||||
return credentialTypeData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns all the credentialTypes which are needed to resolve
|
||||
* the given workflow credentials
|
||||
*
|
||||
* @export
|
||||
* @param {IWorkflowCredentials} credentials The credentials which have to be able to be resolved
|
||||
* @returns {ICredentialsTypeData}
|
||||
*/
|
||||
export function getCredentialsData(credentials: IWorkflowCredentials): ICredentialsTypeData {
|
||||
const credentialTypeData: ICredentialsTypeData = {};
|
||||
|
||||
for (const credentialType of Object.keys(credentials)) {
|
||||
if (credentialTypeData[credentialType] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object.assign(credentialTypeData, getCredentialsDataWithParents(credentialType));
|
||||
}
|
||||
|
||||
return credentialTypeData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the names of the NodeTypes which are are needed
|
||||
* to execute the gives nodes
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import {
|
||||
ActiveExecutions,
|
||||
CredentialsOverwrites,
|
||||
CredentialTypes,
|
||||
ExternalHooks,
|
||||
ICredentialsOverwrite,
|
||||
ICredentialsTypeData,
|
||||
IProcessMessageDataHook,
|
||||
ITransferNodeTypes,
|
||||
IWorkflowExecutionDataProcess,
|
||||
@@ -31,12 +36,14 @@ import { fork } from 'child_process';
|
||||
|
||||
export class WorkflowRunner {
|
||||
activeExecutions: ActiveExecutions.ActiveExecutions;
|
||||
credentialsOverwrites: ICredentialsOverwrite;
|
||||
push: Push.Push;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.push = Push.getInstance();
|
||||
this.activeExecutions = ActiveExecutions.getInstance();
|
||||
this.credentialsOverwrites = CredentialsOverwrites().getAll();
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +101,9 @@ export class WorkflowRunner {
|
||||
* @memberof WorkflowRunner
|
||||
*/
|
||||
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
|
||||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]);
|
||||
|
||||
const executionsProcess = config.get('executions.process') as string;
|
||||
if (executionsProcess === 'main') {
|
||||
return this.runMainProcess(data, loadStaticData);
|
||||
@@ -173,8 +183,8 @@ export class WorkflowRunner {
|
||||
const executionId = this.activeExecutions.add(data, subprocess);
|
||||
|
||||
// Check if workflow contains a "executeWorkflow" Node as in this
|
||||
// case we can not know which nodeTypes will be needed and so have
|
||||
// to load all of them in the workflowRunnerProcess
|
||||
// case we can not know which nodeTypes and credentialTypes will
|
||||
// be needed and so have to load all of them in the workflowRunnerProcess
|
||||
let loadAllNodeTypes = false;
|
||||
for (const node of data.workflowData.nodes) {
|
||||
if (node.type === 'n8n-nodes-base.executeWorkflow') {
|
||||
@@ -184,16 +194,24 @@ export class WorkflowRunner {
|
||||
}
|
||||
|
||||
let nodeTypeData: ITransferNodeTypes;
|
||||
let credentialTypeData: ICredentialsTypeData;
|
||||
|
||||
if (loadAllNodeTypes === true) {
|
||||
// Supply all nodeTypes
|
||||
// Supply all nodeTypes and credentialTypes
|
||||
nodeTypeData = WorkflowHelpers.getAllNodeTypeData();
|
||||
const credentialTypes = CredentialTypes();
|
||||
credentialTypeData = credentialTypes.credentialTypes;
|
||||
} else {
|
||||
// Supply only nodeTypes which the workflow needs
|
||||
// Supply only nodeTypes and credentialTypes which the workflow needs
|
||||
nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes);
|
||||
credentialTypeData = WorkflowHelpers.getCredentialsData(data.credentials);
|
||||
}
|
||||
|
||||
|
||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
|
||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
|
||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = this.credentialsOverwrites;
|
||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsTypeData = credentialTypeData; // TODO: Still needs correct value
|
||||
|
||||
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
import {
|
||||
CredentialsOverwrites,
|
||||
CredentialTypes,
|
||||
IWorkflowExecutionDataProcessWithExecution,
|
||||
NodeTypes,
|
||||
WorkflowExecuteAdditionalData,
|
||||
@@ -58,6 +60,14 @@ export class WorkflowRunnerProcess {
|
||||
const nodeTypes = NodeTypes();
|
||||
await nodeTypes.init(nodeTypesData);
|
||||
|
||||
// Init credential types the workflow uses (is needed to apply default values to credentials)
|
||||
const credentialTypes = CredentialTypes();
|
||||
await credentialTypes.init(inputData.credentialsTypeData);
|
||||
|
||||
// Load the credentials overwrites if any exist
|
||||
const credentialsOverwrites = CredentialsOverwrites();
|
||||
await credentialsOverwrites.init();
|
||||
|
||||
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings});
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials);
|
||||
additionalData.hooks = this.getProcessForwardHooks();
|
||||
|
||||
@@ -8,8 +8,8 @@ export class InitialMigration1588157391238 implements MigrationInterface {
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
await queryRunner.query('CREATE TABLE IF NOT EXISTS `' + tablePrefix + 'credentials_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, `data` text NOT NULL, `type` varchar(32) NOT NULL, `nodesAccess` json NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, INDEX `IDX_07fde106c0b471d8cc80a64fc8` (`type`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined);
|
||||
await queryRunner.query('CREATE TABLE IF NOT EXISTS `' + tablePrefix + 'execution_entity` (`id` int NOT NULL AUTO_INCREMENT, `data` text NOT NULL, `finished` tinyint NOT NULL, `mode` varchar(255) NOT NULL, `retryOf` varchar(255) NULL, `retrySuccessId` varchar(255) NULL, `startedAt` datetime NOT NULL, `stoppedAt` datetime NOT NULL, `workflowData` json NOT NULL, `workflowId` varchar(255) NULL, INDEX `IDX_c4d999a5e90784e8caccf5589d` (`workflowId`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined);
|
||||
await queryRunner.query('CREATE TABLE IF NOT EXISTS `' + tablePrefix + 'credentials_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, `data` text NOT NULL, `type` varchar(32) NOT NULL, `nodesAccess` json NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, INDEX `IDX_' + tablePrefix + '07fde106c0b471d8cc80a64fc8` (`type`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined);
|
||||
await queryRunner.query('CREATE TABLE IF NOT EXISTS `' + tablePrefix + 'execution_entity` (`id` int NOT NULL AUTO_INCREMENT, `data` text NOT NULL, `finished` tinyint NOT NULL, `mode` varchar(255) NOT NULL, `retryOf` varchar(255) NULL, `retrySuccessId` varchar(255) NULL, `startedAt` datetime NOT NULL, `stoppedAt` datetime NOT NULL, `workflowData` json NOT NULL, `workflowId` varchar(255) NULL, INDEX `IDX_' + tablePrefix + 'c4d999a5e90784e8caccf5589d` (`workflowId`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined);
|
||||
await queryRunner.query('CREATE TABLE IF NOT EXISTS`' + tablePrefix + 'workflow_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, `active` tinyint NOT NULL, `nodes` json NOT NULL, `connections` json NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, `settings` json NULL, `staticData` json NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ export class InitialMigration1588157391238 implements MigrationInterface {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
await queryRunner.query('DROP TABLE `' + tablePrefix + 'workflow_entity`', undefined);
|
||||
await queryRunner.query('DROP INDEX `IDX_c4d999a5e90784e8caccf5589d` ON `' + tablePrefix + 'execution_entity`', undefined);
|
||||
await queryRunner.query('DROP INDEX `IDX_' + tablePrefix + 'c4d999a5e90784e8caccf5589d` ON `' + tablePrefix + 'execution_entity`', undefined);
|
||||
await queryRunner.query('DROP TABLE `' + tablePrefix + 'execution_entity`', undefined);
|
||||
await queryRunner.query('DROP INDEX `IDX_07fde106c0b471d8cc80a64fc8` ON `' + tablePrefix + 'credentials_entity`', undefined);
|
||||
await queryRunner.query('DROP INDEX `IDX_' + tablePrefix + '07fde106c0b471d8cc80a64fc8` ON `' + tablePrefix + 'credentials_entity`', undefined);
|
||||
await queryRunner.query('DROP TABLE `' + tablePrefix + 'credentials_entity`', undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,29 +8,31 @@ export class InitialMigration1587669153312 implements MigrationInterface {
|
||||
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
let tablePrefix = config.get('database.tablePrefix');
|
||||
const tablePrefixIndex = tablePrefix;
|
||||
const schema = config.get('database.postgresdb.schema');
|
||||
if (schema) {
|
||||
tablePrefix = schema + '.' + tablePrefix;
|
||||
}
|
||||
|
||||
await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}credentials_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "data" text NOT NULL, "type" character varying(32) NOT NULL, "nodesAccess" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, CONSTRAINT PK_814c3d3c36e8a27fa8edb761b0e PRIMARY KEY ("id"))`, undefined);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_07fde106c0b471d8cc80a64fc8 ON ${tablePrefix}credentials_entity (type) `, undefined);
|
||||
await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}execution_entity ("id" SERIAL NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" character varying NOT NULL, "retryOf" character varying, "retrySuccessId" character varying, "startedAt" TIMESTAMP NOT NULL, "stoppedAt" TIMESTAMP NOT NULL, "workflowData" json NOT NULL, "workflowId" character varying, CONSTRAINT PK_e3e63bbf986767844bbe1166d4e PRIMARY KEY ("id"))`, undefined);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_c4d999a5e90784e8caccf5589d ON ${tablePrefix}execution_entity ("workflowId") `, undefined);
|
||||
await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}workflow_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "active" boolean NOT NULL, "nodes" json NOT NULL, "connections" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, "settings" json, "staticData" json, CONSTRAINT PK_eded7d72664448da7745d551207 PRIMARY KEY ("id"))`, undefined);
|
||||
await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}credentials_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "data" text NOT NULL, "type" character varying(32) NOT NULL, "nodesAccess" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, CONSTRAINT PK_${tablePrefixIndex}814c3d3c36e8a27fa8edb761b0e PRIMARY KEY ("id"))`, undefined);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_${tablePrefixIndex}07fde106c0b471d8cc80a64fc8 ON ${tablePrefix}credentials_entity (type) `, undefined);
|
||||
await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}execution_entity ("id" SERIAL NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" character varying NOT NULL, "retryOf" character varying, "retrySuccessId" character varying, "startedAt" TIMESTAMP NOT NULL, "stoppedAt" TIMESTAMP NOT NULL, "workflowData" json NOT NULL, "workflowId" character varying, CONSTRAINT PK_${tablePrefixIndex}e3e63bbf986767844bbe1166d4e PRIMARY KEY ("id"))`, undefined);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_${tablePrefixIndex}c4d999a5e90784e8caccf5589d ON ${tablePrefix}execution_entity ("workflowId") `, undefined);
|
||||
await queryRunner.query(`CREATE TABLE IF NOT EXISTS ${tablePrefix}workflow_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "active" boolean NOT NULL, "nodes" json NOT NULL, "connections" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, "settings" json, "staticData" json, CONSTRAINT PK_${tablePrefixIndex}eded7d72664448da7745d551207 PRIMARY KEY ("id"))`, undefined);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner): Promise<void> {
|
||||
let tablePrefix = config.get('database.tablePrefix');
|
||||
const tablePrefixIndex = tablePrefix;
|
||||
const schema = config.get('database.postgresdb.schema');
|
||||
if (schema) {
|
||||
tablePrefix = schema + '.' + tablePrefix;
|
||||
}
|
||||
|
||||
await queryRunner.query(`DROP TABLE ${tablePrefix}workflow_entity`, undefined);
|
||||
await queryRunner.query(`DROP INDEX IDX_c4d999a5e90784e8caccf5589d`, undefined);
|
||||
await queryRunner.query(`DROP INDEX IDX_${tablePrefixIndex}c4d999a5e90784e8caccf5589d`, undefined);
|
||||
await queryRunner.query(`DROP TABLE ${tablePrefix}execution_entity`, undefined);
|
||||
await queryRunner.query(`DROP INDEX IDX_07fde106c0b471d8cc80a64fc8`, undefined);
|
||||
await queryRunner.query(`DROP INDEX IDX_${tablePrefixIndex}07fde106c0b471d8cc80a64fc8`, undefined);
|
||||
await queryRunner.query(`DROP TABLE ${tablePrefix}credentials_entity`, undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ export class InitialMigration1588102412422 implements MigrationInterface {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "${tablePrefix}credentials_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "data" text NOT NULL, "type" varchar(32) NOT NULL, "nodesAccess" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL)`, undefined);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_07fde106c0b471d8cc80a64fc8" ON "${tablePrefix}credentials_entity" ("type") `, undefined);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}07fde106c0b471d8cc80a64fc8" ON "${tablePrefix}credentials_entity" ("type") `, undefined);
|
||||
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "${tablePrefix}execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" varchar NOT NULL, "retryOf" varchar, "retrySuccessId" varchar, "startedAt" datetime NOT NULL, "stoppedAt" datetime NOT NULL, "workflowData" text NOT NULL, "workflowId" varchar)`, undefined);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_c4d999a5e90784e8caccf5589d" ON "${tablePrefix}execution_entity" ("workflowId") `, undefined);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_${tablePrefix}c4d999a5e90784e8caccf5589d" ON "${tablePrefix}execution_entity" ("workflowId") `, undefined);
|
||||
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "${tablePrefix}workflow_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "active" boolean NOT NULL, "nodes" text NOT NULL, "connections" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL, "settings" text, "staticData" text)`, undefined);
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ export class InitialMigration1588102412422 implements MigrationInterface {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(`DROP TABLE "${tablePrefix}workflow_entity"`, undefined);
|
||||
await queryRunner.query(`DROP INDEX "IDX_c4d999a5e90784e8caccf5589d"`, undefined);
|
||||
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}c4d999a5e90784e8caccf5589d"`, undefined);
|
||||
await queryRunner.query(`DROP TABLE "${tablePrefix}execution_entity"`, undefined);
|
||||
await queryRunner.query(`DROP INDEX "IDX_07fde106c0b471d8cc80a64fc8"`, undefined);
|
||||
await queryRunner.query(`DROP INDEX "IDX_${tablePrefix}07fde106c0b471d8cc80a64fc8"`, undefined);
|
||||
await queryRunner.query(`DROP TABLE "${tablePrefix}credentials_entity"`, undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export * from './CredentialsHelper';
|
||||
export * from './CredentialTypes';
|
||||
export * from './CredentialsOverwrites';
|
||||
export * from './ExternalHooks';
|
||||
export * from './Interfaces';
|
||||
export * from './LoadNodesAndCredentials';
|
||||
export * from './NodeTypes';
|
||||
|
||||
Reference in New Issue
Block a user