🔀 Merge branch 'external-hooks'

This commit is contained in:
Jan Oberhauser 2020-06-10 13:02:45 +02:00
commit 1451f94814
9 changed files with 159 additions and 3 deletions

View file

@ -11,6 +11,7 @@ import {
ActiveExecutions, ActiveExecutions,
CredentialsOverwrites, CredentialsOverwrites,
Db, Db,
ExternalHooks,
GenericHelpers, GenericHelpers,
IWorkflowBase, IWorkflowBase,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
@ -108,6 +109,10 @@ export class Execute extends Command {
const credentialsOverwrites = CredentialsOverwrites(); const credentialsOverwrites = CredentialsOverwrites();
await credentialsOverwrites.init(); await credentialsOverwrites.init();
// Load all external hooks
const externalHooks = ExternalHooks();
await externalHooks.init();
// Add the found types to an instance other parts of the application can use // Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
await nodeTypes.init(loadNodesAndCredentials.nodeTypes); await nodeTypes.init(loadNodesAndCredentials.nodeTypes);

View file

@ -5,7 +5,6 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { Command, flags } from '@oclif/command'; import { Command, flags } from '@oclif/command';
const open = require('open'); const open = require('open');
// import { dirname } from 'path';
import * as config from '../config'; import * as config from '../config';
import { import {
@ -13,6 +12,7 @@ import {
CredentialTypes, CredentialTypes,
CredentialsOverwrites, CredentialsOverwrites,
Db, Db,
ExternalHooks,
GenericHelpers, GenericHelpers,
LoadNodesAndCredentials, LoadNodesAndCredentials,
NodeTypes, NodeTypes,
@ -113,6 +113,10 @@ export class Start extends Command {
const credentialsOverwrites = CredentialsOverwrites(); const credentialsOverwrites = CredentialsOverwrites();
await credentialsOverwrites.init(); await credentialsOverwrites.init();
// Load all external hooks
const externalHooks = ExternalHooks();
await externalHooks.init();
// Add the found types to an instance other parts of the application can use // Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
await nodeTypes.init(loadNodesAndCredentials.nodeTypes); await nodeTypes.init(loadNodesAndCredentials.nodeTypes);

View file

@ -271,6 +271,13 @@ const config = convict({
}, },
}, },
externalHookFiles: {
doc: 'Files containing external hooks. Multiple files can be separated by colon (":")',
format: String,
default: '',
env: 'EXTERNAL_HOOK_FILES'
},
nodes: { nodes: {
exclude: { exclude: {
doc: 'Nodes not to load', doc: 'Nodes not to load',

View file

@ -0,0 +1,87 @@
import {
Db,
IExternalHooksFunctions,
IExternalHooksClass,
} from './';
import * as config from '../config';
class ExternalHooksClass implements IExternalHooksClass {
externalHooks: {
[key: string]: Array<() => {}>
} = {};
initDidRun = false;
async init(): Promise<void> {
console.log('ExternalHooks.init');
if (this.initDidRun === true) {
return;
}
const externalHookFiles = config.get('externalHookFiles').split(':');
console.log('externalHookFiles');
console.log(externalHookFiles);
// Load all the provided hook-files
for (let hookFilePath of externalHookFiles) {
hookFilePath = hookFilePath.trim();
if (hookFilePath !== '') {
console.log(' --- load: ' + 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
console.log('RUN NOW: ' + hookName);
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;
}

View file

@ -197,6 +197,30 @@ export interface IExecutingWorkflowData {
workflowExecution?: PCancelable<IRun>; 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 { export interface IN8nConfig {
database: IN8nConfigDatabase; database: IN8nConfigDatabase;
endpoints: IN8nConfigEndpoints; endpoints: IN8nConfigEndpoints;

View file

@ -27,6 +27,7 @@ import {
CredentialsHelper, CredentialsHelper,
CredentialTypes, CredentialTypes,
Db, Db,
ExternalHooks,
IActivationError, IActivationError,
ICustomRequest, ICustomRequest,
ICredentialsDb, ICredentialsDb,
@ -41,6 +42,7 @@ import {
IExecutionsListResponse, IExecutionsListResponse,
IExecutionsStopData, IExecutionsStopData,
IExecutionsSummary, IExecutionsSummary,
IExternalHooksClass,
IN8nUISettings, IN8nUISettings,
IPackageVersions, IPackageVersions,
IWorkflowBase, IWorkflowBase,
@ -103,6 +105,7 @@ class App {
testWebhooks: TestWebhooks.TestWebhooks; testWebhooks: TestWebhooks.TestWebhooks;
endpointWebhook: string; endpointWebhook: string;
endpointWebhookTest: string; endpointWebhookTest: string;
externalHooks: IExternalHooksClass;
saveDataErrorExecution: string; saveDataErrorExecution: string;
saveDataSuccessExecution: string; saveDataSuccessExecution: string;
saveManualExecutions: boolean; saveManualExecutions: boolean;
@ -134,6 +137,8 @@ class App {
this.protocol = config.get('protocol'); this.protocol = config.get('protocol');
this.sslKey = config.get('ssl_key'); this.sslKey = config.get('ssl_key');
this.sslCert = config.get('ssl_cert'); this.sslCert = config.get('ssl_cert');
this.externalHooks = ExternalHooks();
} }
@ -351,7 +356,7 @@ class App {
// Creates a new workflow // Creates a new workflow
this.app.post('/rest/workflows', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => { 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.name = newWorkflowData.name.trim();
newWorkflowData.createdAt = this.getCurrentDate(); newWorkflowData.createdAt = this.getCurrentDate();
@ -359,6 +364,8 @@ class App {
newWorkflowData.id = undefined; newWorkflowData.id = undefined;
await this.externalHooks.run('workflow.create', [newWorkflowData]);
// Save the workflow in DB // Save the workflow in DB
const result = await Db.collections.Workflow!.save(newWorkflowData); const result = await Db.collections.Workflow!.save(newWorkflowData);
@ -434,9 +441,11 @@ class App {
// Updates an existing workflow // Updates an existing workflow
this.app.patch('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IWorkflowResponse> => { 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; const id = req.params.id;
await this.externalHooks.run('workflow.update', [newWorkflowData]);
if (this.activeWorkflowRunner.isActive(id)) { if (this.activeWorkflowRunner.isActive(id)) {
// When workflow gets saved always remove it as the triggers could have been // When workflow gets saved always remove it as the triggers could have been
// changed and so the changes would not take effect // changed and so the changes would not take effect
@ -478,6 +487,8 @@ class App {
if (responseData.active === true) { if (responseData.active === true) {
// When the workflow is supposed to be active add it again // When the workflow is supposed to be active add it again
try { try {
await this.externalHooks.run('workflow.activate', [responseData]);
await this.activeWorkflowRunner.add(id); await this.activeWorkflowRunner.add(id);
} catch (error) { } catch (error) {
// If workflow could not be activated set it again to inactive // If workflow could not be activated set it again to inactive
@ -502,6 +513,8 @@ class App {
this.app.delete('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => { this.app.delete('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
const id = req.params.id; const id = req.params.id;
await this.externalHooks.run('workflow.delete', [id]);
if (this.activeWorkflowRunner.isActive(id)) { if (this.activeWorkflowRunner.isActive(id)) {
// Before deleting a workflow deactivate it // Before deleting a workflow deactivate it
await this.activeWorkflowRunner.remove(id); await this.activeWorkflowRunner.remove(id);
@ -663,6 +676,8 @@ class App {
this.app.delete('/rest/credentials/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => { this.app.delete('/rest/credentials/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
const id = req.params.id; const id = req.params.id;
await this.externalHooks.run('credentials.delete', [id]);
await Db.collections.Credentials!.delete({ id }); await Db.collections.Credentials!.delete({ id });
return true; return true;
@ -708,6 +723,8 @@ class App {
credentials.setData(incomingData.data, encryptionKey); credentials.setData(incomingData.data, encryptionKey);
const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; const newCredentialsData = credentials.getDataToSave() as ICredentialsDb;
await this.externalHooks.run('credentials.create', [newCredentialsData]);
// Add special database related data // Add special database related data
newCredentialsData.createdAt = this.getCurrentDate(); newCredentialsData.createdAt = this.getCurrentDate();
newCredentialsData.updatedAt = this.getCurrentDate(); newCredentialsData.updatedAt = this.getCurrentDate();
@ -783,6 +800,8 @@ class App {
// Add special database related data // Add special database related data
newCredentialsData.updatedAt = this.getCurrentDate(); newCredentialsData.updatedAt = this.getCurrentDate();
await this.externalHooks.run('credentials.update', [newCredentialsData]);
// Update the credentials in DB // Update the credentials in DB
await Db.collections.Credentials!.update(id, newCredentialsData); await Db.collections.Credentials!.update(id, newCredentialsData);

View file

@ -1,6 +1,7 @@
import { import {
CredentialsHelper, CredentialsHelper,
Db, Db,
ExternalHooks,
IExecutionDb, IExecutionDb,
IExecutionFlattedDb, IExecutionFlattedDb,
IPushDataExecutionFinished, IPushDataExecutionFinished,
@ -303,6 +304,10 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
workflowData = workflowInfo.code; workflowData = workflowInfo.code;
} }
const externalHooks = ExternalHooks();
await externalHooks.init();
await externalHooks.run('workflow.execute', [workflowData, mode]);
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
const workflowName = workflowData ? workflowData.name : undefined; const workflowName = workflowData ? workflowData.name : undefined;

View file

@ -2,6 +2,7 @@ import {
ActiveExecutions, ActiveExecutions,
CredentialsOverwrites, CredentialsOverwrites,
CredentialTypes, CredentialTypes,
ExternalHooks,
ICredentialsOverwrite, ICredentialsOverwrite,
ICredentialsTypeData, ICredentialsTypeData,
IProcessMessageDataHook, IProcessMessageDataHook,
@ -100,6 +101,9 @@ export class WorkflowRunner {
* @memberof WorkflowRunner * @memberof WorkflowRunner
*/ */
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> { 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; const executionsProcess = config.get('executions.process') as string;
if (executionsProcess === 'main') { if (executionsProcess === 'main') {
return this.runMainProcess(data, loadStaticData); return this.runMainProcess(data, loadStaticData);

View file

@ -1,6 +1,7 @@
export * from './CredentialsHelper'; export * from './CredentialsHelper';
export * from './CredentialTypes'; export * from './CredentialTypes';
export * from './CredentialsOverwrites'; export * from './CredentialsOverwrites';
export * from './ExternalHooks';
export * from './Interfaces'; export * from './Interfaces';
export * from './LoadNodesAndCredentials'; export * from './LoadNodesAndCredentials';
export * from './NodeTypes'; export * from './NodeTypes';