🔀 Merge master

This commit is contained in:
Iván Ovejero 2021-11-09 10:00:30 +01:00
commit 61bb8de352
39 changed files with 1252 additions and 89 deletions

View file

@ -12,7 +12,7 @@ import * as PCancelable from 'p-cancelable';
import { Command, flags } from '@oclif/command';
import { UserSettings, WorkflowExecute } from 'n8n-core';
import { INodeTypes, IRun, Workflow, LoggerProxy } from 'n8n-workflow';
import { IExecuteResponsePromiseData, INodeTypes, IRun, Workflow, LoggerProxy } from 'n8n-workflow';
import { FindOneOptions } from 'typeorm';
@ -25,11 +25,13 @@ import {
GenericHelpers,
IBullJobData,
IBullJobResponse,
IBullWebhookResponse,
IExecutionFlattedDb,
InternalHooksManager,
LoadNodesAndCredentials,
NodeTypes,
ResponseHelper,
WebhookHelpers,
WorkflowExecuteAdditionalData,
} from '../src';
@ -172,6 +174,16 @@ export class Worker extends Command {
currentExecutionDb.workflowData,
{ retryOf: currentExecutionDb.retryOf as string },
);
additionalData.hooks.hookFunctions.sendResponse = [
async (response: IExecuteResponsePromiseData): Promise<void> => {
await job.progress({
executionId: job.data.executionId as string,
response: WebhookHelpers.encodeWebhookResponse(response),
} as IBullWebhookResponse);
},
];
additionalData.executionId = jobData.executionId;
let workflowExecute: WorkflowExecute;

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.147.1",
"version": "0.148.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -110,10 +110,10 @@
"localtunnel": "^2.0.0",
"lodash.get": "^4.4.2",
"mysql2": "~2.3.0",
"n8n-core": "~0.91.0",
"n8n-editor-ui": "~0.114.0",
"n8n-nodes-base": "~0.144.1",
"n8n-workflow": "~0.74.0",
"n8n-core": "~0.92.0",
"n8n-editor-ui": "~0.115.0",
"n8n-nodes-base": "~0.145.0",
"n8n-workflow": "~0.75.0",
"oauth-1.0a": "^2.2.6",
"open": "^7.0.0",
"pg": "^8.3.0",

View file

@ -5,9 +5,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { IRun } from 'n8n-workflow';
import { createDeferredPromise } from 'n8n-core';
import {
createDeferredPromise,
IDeferredPromise,
IExecuteResponsePromiseData,
IRun,
} from 'n8n-workflow';
import { ChildProcess } from 'child_process';
// eslint-disable-next-line import/no-extraneous-dependencies
@ -116,6 +119,28 @@ export class ActiveExecutions {
this.activeExecutions[executionId].workflowExecution = workflowExecution;
}
attachResponsePromise(
executionId: string,
responsePromise: IDeferredPromise<IExecuteResponsePromiseData>,
): void {
if (this.activeExecutions[executionId] === undefined) {
throw new Error(
`No active execution with id "${executionId}" got found to attach to workflowExecution to!`,
);
}
this.activeExecutions[executionId].responsePromise = responsePromise;
}
resolveResponsePromise(executionId: string, response: IExecuteResponsePromiseData): void {
if (this.activeExecutions[executionId] === undefined) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.activeExecutions[executionId].responsePromise?.resolve(response);
}
/**
* Remove an active execution
*
@ -193,6 +218,7 @@ export class ActiveExecutions {
this.activeExecutions[executionId].postExecutePromises.push(waitPromise);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return waitPromise.promise();
}

View file

@ -12,7 +12,9 @@
import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
import {
IDeferredPromise,
IExecuteData,
IExecuteResponsePromiseData,
IGetExecutePollFunctions,
IGetExecuteTriggerFunctions,
INode,
@ -40,8 +42,6 @@ import {
NodeTypes,
ResponseHelper,
WebhookHelpers,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
WorkflowRunner,
@ -550,6 +550,7 @@ export class ActiveWorkflowRunner {
data: INodeExecutionData[][],
additionalData: IWorkflowExecuteAdditionalDataWorkflow,
mode: WorkflowExecuteMode,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
) {
const nodeExecutionStack: IExecuteData[] = [
{
@ -580,7 +581,7 @@ export class ActiveWorkflowRunner {
};
const workflowRunner = new WorkflowRunner();
return workflowRunner.run(runData, true);
return workflowRunner.run(runData, true, undefined, undefined, responsePromise);
}
/**
@ -641,13 +642,16 @@ export class ActiveWorkflowRunner {
mode,
activation,
);
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
returnFunctions.emit = (
data: INodeExecutionData[][],
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
): void => {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Logger.debug(`Received trigger for workflow "${workflow.name}"`);
WorkflowHelpers.saveStaticData(workflow);
// eslint-disable-next-line id-denylist
this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) =>
console.error(err),
this.runWorkflow(workflowData, node, data, additionalData, mode, responsePromise).catch(
(error) => console.error(error),
);
};
return returnFunctions;

View file

@ -7,19 +7,19 @@ import {
ICredentialsEncrypted,
ICredentialType,
IDataObject,
IDeferredPromise,
IExecuteResponsePromiseData,
IRun,
IRunData,
IRunExecutionData,
ITaskData,
ITelemetrySettings,
IWorkflowBase as IWorkflowBaseWorkflow,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IWorkflowCredentials,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { IDeferredPromise, WorkflowExecute } from 'n8n-core';
import { WorkflowExecute } from 'n8n-core';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as PCancelable from 'p-cancelable';
@ -47,6 +47,11 @@ export interface IBullJobResponse {
success: boolean;
}
export interface IBullWebhookResponse {
executionId: string;
response: IExecuteResponsePromiseData;
}
export interface ICustomRequest extends Request {
parsedUrl: Url | undefined;
}
@ -237,6 +242,7 @@ export interface IExecutingWorkflowData {
process?: ChildProcess;
startedAt: Date;
postExecutePromises: Array<IDeferredPromise<IRun | undefined>>;
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>;
workflowExecution?: PCancelable<IRun>;
}
@ -491,6 +497,7 @@ export interface IPushDataConsoleMessage {
export interface IResponseCallbackData {
data?: IDataObject | IDataObject[];
headers?: object;
noWebhookResponse?: boolean;
responseCode?: number;
}

View file

@ -40,6 +40,9 @@ class NodeTypesClass implements INodeTypes {
}
getByNameAndVersion(nodeType: string, version?: number): INodeType {
if (this.nodeTypes[nodeType] === undefined) {
throw new Error(`The node-type "${nodeType}" is not known!`);
}
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
}
}

View file

@ -1,12 +1,21 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import * as Bull from 'bull';
import * as config from '../config';
// eslint-disable-next-line import/no-cycle
import { IBullJobData } from './Interfaces';
import { IBullJobData, IBullWebhookResponse } from './Interfaces';
// eslint-disable-next-line import/no-cycle
import * as ActiveExecutions from './ActiveExecutions';
// eslint-disable-next-line import/no-cycle
import * as WebhookHelpers from './WebhookHelpers';
export class Queue {
private activeExecutions: ActiveExecutions.ActiveExecutions;
private jobQueue: Bull.Queue;
constructor() {
this.activeExecutions = ActiveExecutions.getInstance();
const prefix = config.get('queue.bull.prefix') as string;
const redisOptions = config.get('queue.bull.redis') as object;
// Disabling ready check is necessary as it allows worker to
@ -16,6 +25,14 @@ export class Queue {
// More here: https://github.com/OptimalBits/bull/issues/890
// @ts-ignore
this.jobQueue = new Bull('jobs', { prefix, redis: redisOptions, enableReadyCheck: false });
this.jobQueue.on('global:progress', (jobId, progress: IBullWebhookResponse) => {
this.activeExecutions.resolveResponsePromise(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
progress.executionId,
WebhookHelpers.decodeWebhookResponse(progress.response),
);
});
}
async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> {

View file

@ -72,11 +72,16 @@ export function sendSuccessResponse(
data: any,
raw?: boolean,
responseCode?: number,
responseHeader?: object,
) {
if (responseCode !== undefined) {
res.status(responseCode);
}
if (responseHeader) {
res.header(responseHeader);
}
if (raw === true) {
if (typeof data === 'string') {
res.send(data);

View file

@ -680,6 +680,7 @@ class App {
// @ts-ignore
savedWorkflow.id = savedWorkflow.id.toString();
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowCreated(newWorkflow as IWorkflowBase);
return savedWorkflow;
},
@ -2669,7 +2670,13 @@ class App {
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
@ -2720,7 +2727,13 @@ class App {
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
@ -2746,7 +2759,13 @@ class App {
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-shadow */
@ -18,9 +19,13 @@ import { get } from 'lodash';
import { BINARY_ENCODING, NodeExecuteFunctions } from 'n8n-core';
import {
createDeferredPromise,
IBinaryKeyData,
IDataObject,
IDeferredPromise,
IExecuteData,
IExecuteResponsePromiseData,
IN8nHttpFullResponse,
INode,
IRunExecutionData,
IWebhookData,
@ -34,20 +39,20 @@ import {
} from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
ActiveExecutions,
GenericHelpers,
IExecutionDb,
IResponseCallbackData,
IWorkflowDb,
IWorkflowExecutionDataProcess,
ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
WorkflowRunner,
} from '.';
// eslint-disable-next-line import/no-cycle
import * as ActiveExecutions from './ActiveExecutions';
const activeExecutions = ActiveExecutions.getInstance();
/**
@ -91,6 +96,35 @@ export function getWorkflowWebhooks(
return returnData;
}
export function decodeWebhookResponse(
response: IExecuteResponsePromiseData,
): IExecuteResponsePromiseData {
if (
typeof response === 'object' &&
typeof response.body === 'object' &&
(response.body as IDataObject)['__@N8nEncodedBuffer@__']
) {
response.body = Buffer.from(
(response.body as IDataObject)['__@N8nEncodedBuffer@__'] as string,
BINARY_ENCODING,
);
}
return response;
}
export function encodeWebhookResponse(
response: IExecuteResponsePromiseData,
): IExecuteResponsePromiseData {
if (typeof response === 'object' && Buffer.isBuffer(response.body)) {
response.body = {
'__@N8nEncodedBuffer@__': response.body.toString(BINARY_ENCODING),
};
}
return response;
}
/**
* Returns all the webhooks which should be created for the give workflow
*
@ -169,7 +203,7 @@ export async function executeWebhook(
200,
) as number;
if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
if (!['onReceived', 'lastNode', 'responseNode'].includes(responseMode as string)) {
// If the mode is not known we error. Is probably best like that instead of using
// the default that people know as early as possible (probably already testing phase)
// that something does not resolve properly.
@ -356,9 +390,52 @@ export async function executeWebhook(
workflowData,
};
let responsePromise: IDeferredPromise<IN8nHttpFullResponse> | undefined;
if (responseMode === 'responseNode') {
responsePromise = await createDeferredPromise<IN8nHttpFullResponse>();
responsePromise
.promise()
.then((response: IN8nHttpFullResponse) => {
if (didSendResponse) {
return;
}
if (Buffer.isBuffer(response.body)) {
res.header(response.headers);
res.end(response.body);
responseCallback(null, {
noWebhookResponse: true,
});
} else {
// TODO: This probably needs some more changes depending on the options on the
// Webhook Response node
responseCallback(null, {
data: response.body as IDataObject,
headers: response.headers,
responseCode: response.statusCode,
});
}
didSendResponse = true;
})
.catch(async (error) => {
Logger.error(
`Error with Webhook-Response for execution "${executionId}": "${error.message}"`,
{ executionId, workflowId: workflow.id },
);
});
}
// Start now to run the workflow
const workflowRunner = new WorkflowRunner();
executionId = await workflowRunner.run(runData, true, !didSendResponse, executionId);
executionId = await workflowRunner.run(
runData,
true,
!didSendResponse,
executionId,
responsePromise,
);
Logger.verbose(
`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`,
@ -398,6 +475,20 @@ export async function executeWebhook(
return data;
}
if (responseMode === 'responseNode') {
if (!didSendResponse) {
// Return an error if no Webhook-Response node did send any data
responseCallback(null, {
data: {
message: 'Workflow executed sucessfully.',
},
responseCode,
});
didSendResponse = true;
}
return undefined;
}
if (returnData === undefined) {
if (!didSendResponse) {
responseCallback(null, {

View file

@ -64,7 +64,13 @@ export function registerProductionWebhooks() {
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
@ -115,7 +121,13 @@ export function registerProductionWebhooks() {
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
@ -141,7 +153,13 @@ export function registerProductionWebhooks() {
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
@ -173,7 +191,13 @@ export function registerProductionWebhooks() {
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
@ -199,7 +223,13 @@ export function registerProductionWebhooks() {
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
@ -225,7 +255,13 @@ export function registerProductionWebhooks() {
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
}

View file

@ -15,6 +15,8 @@ import { IProcessMessage, WorkflowExecute } from 'n8n-core';
import {
ExecutionError,
IDeferredPromise,
IExecuteResponsePromiseData,
IRun,
LoggerProxy as Logger,
Workflow,
@ -41,9 +43,7 @@ import {
IBullJobResponse,
ICredentialsOverwrite,
ICredentialsTypeData,
IExecutionDb,
IExecutionFlattedDb,
IExecutionResponse,
IProcessMessageDataHook,
ITransferNodeTypes,
IWorkflowExecutionDataProcess,
@ -51,6 +51,7 @@ import {
NodeTypes,
Push,
ResponseHelper,
WebhookHelpers,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
} from '.';
@ -146,6 +147,7 @@ export class WorkflowRunner {
loadStaticData?: boolean,
realtime?: boolean,
executionId?: string,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
): Promise<string> {
const executionsProcess = config.get('executions.process') as string;
const executionsMode = config.get('executions.mode') as string;
@ -153,11 +155,17 @@ export class WorkflowRunner {
if (executionsMode === 'queue' && data.executionMode !== 'manual') {
// Do not run "manual" executions in bull because sending events to the
// frontend would not be possible
executionId = await this.runBull(data, loadStaticData, realtime, executionId);
executionId = await this.runBull(
data,
loadStaticData,
realtime,
executionId,
responsePromise,
);
} else if (executionsProcess === 'main') {
executionId = await this.runMainProcess(data, loadStaticData, executionId);
executionId = await this.runMainProcess(data, loadStaticData, executionId, responsePromise);
} else {
executionId = await this.runSubprocess(data, loadStaticData, executionId);
executionId = await this.runSubprocess(data, loadStaticData, executionId, responsePromise);
}
const postExecutePromise = this.activeExecutions.getPostExecutePromise(executionId);
@ -200,6 +208,7 @@ export class WorkflowRunner {
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
restartExecutionId?: string,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
): Promise<string> {
if (loadStaticData === true && data.workflowData.id) {
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(
@ -256,6 +265,15 @@ export class WorkflowRunner {
executionId,
true,
);
additionalData.hooks.hookFunctions.sendResponse = [
async (response: IExecuteResponsePromiseData): Promise<void> => {
if (responsePromise) {
responsePromise.resolve(response);
}
},
];
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({
sessionId: data.sessionId,
});
@ -341,11 +359,15 @@ export class WorkflowRunner {
loadStaticData?: boolean,
realtime?: boolean,
restartExecutionId?: string,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
): Promise<string> {
// TODO: If "loadStaticData" is set to true it has to load data new on worker
// Register the active execution
const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId);
if (responsePromise) {
this.activeExecutions.attachResponsePromise(executionId, responsePromise);
}
const jobData: IBullJobData = {
executionId,
@ -545,6 +567,7 @@ export class WorkflowRunner {
data: IWorkflowExecutionDataProcess,
loadStaticData?: boolean,
restartExecutionId?: string,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
): Promise<string> {
let startedAt = new Date();
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
@ -653,6 +676,10 @@ export class WorkflowRunner {
} else if (message.type === 'end') {
clearTimeout(executionTimeout);
this.activeExecutions.remove(executionId, message.data.runData);
} else if (message.type === 'sendResponse') {
if (responsePromise) {
responsePromise.resolve(WebhookHelpers.decodeWebhookResponse(message.data.response));
}
} else if (message.type === 'sendMessageToUI') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })(

View file

@ -10,6 +10,7 @@ import { IProcessMessage, UserSettings, WorkflowExecute } from 'n8n-core';
import {
ExecutionError,
IDataObject,
IExecuteResponsePromiseData,
IExecuteWorkflowInfo,
ILogger,
INodeExecutionData,
@ -33,6 +34,7 @@ import {
IWorkflowExecuteProcess,
IWorkflowExecutionDataProcessWithExecution,
NodeTypes,
WebhookHelpers,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
} from '.';
@ -200,6 +202,15 @@ export class WorkflowRunnerProcess {
workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000,
);
additionalData.hooks = this.getProcessForwardHooks();
additionalData.hooks.hookFunctions.sendResponse = [
async (response: IExecuteResponsePromiseData): Promise<void> => {
await sendToParentProcess('sendResponse', {
response: WebhookHelpers.encodeWebhookResponse(response),
});
},
];
additionalData.executionId = inputData.executionId;
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.91.0",
"version": "0.92.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -50,7 +50,7 @@
"form-data": "^4.0.0",
"lodash.get": "^4.4.2",
"mime-types": "^2.1.27",
"n8n-workflow": "~0.74.0",
"n8n-workflow": "~0.75.0",
"oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0",
"qs": "^6.10.1",

View file

@ -22,6 +22,7 @@ import {
ICredentialsExpressionResolveValues,
IDataObject,
IExecuteFunctions,
IExecuteResponsePromiseData,
IExecuteSingleFunctions,
IExecuteWorkflowInfo,
IHttpRequestOptions,
@ -1635,6 +1636,9 @@ export function getExecuteFunctions(
Logger.warn(`There was a problem sending messsage to UI: ${error.message}`);
}
},
async sendResponse(response: IExecuteResponsePromiseData): Promise<void> {
await additionalData.hooks?.executeHookFunctions('sendResponse', [response]);
},
helpers: {
httpRequest,
prepareBinaryData,

View file

@ -12,7 +12,6 @@ export * from './ActiveWorkflows';
export * from './ActiveWebhooks';
export * from './Constants';
export * from './Credentials';
export * from './DeferredPromise';
export * from './Interfaces';
export * from './LoadNodeParameterOptions';
export * from './NodeExecuteFunctions';

View file

@ -4,6 +4,7 @@ import {
ICredentialDataDecryptedObject,
ICredentialsHelper,
IDataObject,
IDeferredPromise,
IExecuteWorkflowInfo,
INodeCredentialsDetails,
INodeExecutionData,
@ -20,7 +21,7 @@ import {
WorkflowHooks,
} from 'n8n-workflow';
import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
import { Credentials, IExecuteFunctions } from '../src';
export class CredentialsHelper extends ICredentialsHelper {
getDecrypted(

View file

@ -1,6 +1,14 @@
import { IConnections, ILogger, INode, IRun, LoggerProxy, Workflow } from 'n8n-workflow';
import {
createDeferredPromise,
IConnections,
ILogger,
INode,
IRun,
LoggerProxy,
Workflow,
} from 'n8n-workflow';
import { createDeferredPromise, WorkflowExecute } from '../src';
import { WorkflowExecute } from '../src';
import * as Helpers from './Helpers';

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.114.0",
"version": "0.115.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -72,7 +72,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.74.0",
"n8n-workflow": "~0.75.0",
"sass": "^1.26.5",
"normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1",

View file

@ -98,7 +98,7 @@ const module: Module<ICredentialsState, IRootState> = {
},
getCredentialsByType: (state: ICredentialsState, getters: any) => { // tslint:disable-line:no-any
return (credentialType: string): ICredentialsResponse[] => {
return getters.allCredentialsByType[credentialType];
return getters.allCredentialsByType[credentialType] || [];
};
},
getNodesWithAccess (state: ICredentialsState, getters: any, rootState: IRootState, rootGetters: any) { // tslint:disable-line:no-any

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.31.0",
"version": "0.32.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -60,8 +60,8 @@
"change-case": "^4.1.1",
"copyfiles": "^2.1.1",
"inquirer": "^7.0.1",
"n8n-core": "~0.91.0",
"n8n-workflow": "~0.74.0",
"n8n-core": "~0.92.0",
"n8n-workflow": "~0.75.0",
"oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0",
"request": "^2.88.2",

View file

@ -0,0 +1,18 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class DropcontactApi implements ICredentialType {
name = 'dropcontactApi';
displayName = 'Dropcontact API';
documentationUrl = 'dropcontact';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.awsTextract",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Utility"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/aws"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.awsTextract/"
}
]
}
}

View file

@ -0,0 +1,370 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
dropcontactApiRequest,
validateCrendetials,
} from './GenericFunction';
export class Dropcontact implements INodeType {
description: INodeTypeDescription = {
displayName: 'Dropcontact',
name: 'dropcontact',
icon: 'file:dropcontact.svg',
group: ['transform'],
version: 1,
description: 'Find B2B emails and enrich contacts',
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
defaults: {
name: 'Dropcontact',
color: '#0ABA9F',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'dropcontactApi',
required: true,
testedBy: 'dropcontactApiCredentialTest',
},
],
properties: [
{
displayName: 'Resource',
noDataExpression: true,
name: 'resource',
type: 'options',
options: [
{
name: 'Contact',
value: 'contact',
},
],
default: 'contact',
required: true,
},
{
displayName: 'Operation',
noDataExpression: true,
name: 'operation',
type: 'options',
options: [
{
name: 'Enrich',
value: 'enrich',
description: 'Find B2B emails and enrich your contact from his name and his website',
},
{
name: 'Fetch Request',
value: 'fetchRequest',
},
],
default: 'enrich',
required: true,
},
{
displayName: 'Request ID',
name: 'requestId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'fetchRequest',
],
},
},
default: '',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'enrich',
],
},
},
default: '',
},
{
displayName: 'Simplify Output (Faster)',
name: 'simplify',
type: 'boolean',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'enrich',
],
},
},
default: false,
description: 'When off, waits for the contact data before completing. Waiting time can be adjusted with Extend Wait Time option. When on, returns a request_id that can be used later in the Fetch Request operation.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'enrich',
],
},
},
options: [
{
displayName: 'Company SIREN Number',
name: 'num_siren',
type: 'string',
default: '',
},
{
displayName: 'Company SIRET Code',
name: 'siret',
type: 'string',
default: '',
},
{
displayName: 'Company Name',
name: 'company',
type: 'string',
default: '',
},
{
displayName: 'Country',
name: 'country',
type: 'string',
default: '',
},
{
displayName: 'First Name',
name: 'first_name',
type: 'string',
default: '',
},
{
displayName: 'Full Name',
name: 'full_name',
type: 'string',
default: '',
},
{
displayName: 'Last Name',
name: 'last_name',
type: 'string',
default: '',
},
{
displayName: 'LinkedIn Profile',
name: 'linkedin',
type: 'string',
default: '',
},
{
displayName: 'Phone Number',
name: 'phone',
type: 'string',
default: '',
},
{
displayName: 'Website',
name: 'website',
type: 'string',
default: '',
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'enrich',
],
},
},
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Data Fetch Wait Time',
name: 'waitTime',
type: 'number',
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
'/simplify': [
false,
],
},
},
default: 45,
description: 'When not simplifying the response, data will be fetched in two steps. This parameter controls how long to wait (in seconds) before trying the second step',
},
{
displayName: 'French Company Enrich',
name: 'siren',
type: 'boolean',
default: false,
description: `Whether you want the <a href="https://en.wikipedia.org/wiki/SIREN_code" target="_blank">SIREN number</a>, NAF code, TVA number, company address and informations about the company leader.</br>
Only applies to french companies`,
},
{
displayName: 'Language',
name: 'language',
type: 'options',
options: [
{
name: 'English',
value: 'en',
},
{
name: 'French',
value: 'fr',
},
],
default: 'en',
description: 'Whether the response is in English or French',
},
],
},
],
};
methods = {
credentialTest: {
async dropcontactApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
try {
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject);
} catch (error) {
return {
status: 'Error',
message: 'The API Key included in the request is invalid',
};
}
return {
status: 'OK',
message: 'Connection successful!',
};
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const entryData = this.getInputData();
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
// tslint:disable-next-line: no-any
let responseData: any;
const returnData: IDataObject[] = [];
if (resource === 'contact') {
if (operation === 'enrich') {
const options = this.getNodeParameter('options', 0) as IDataObject;
const data = [];
const simplify = this.getNodeParameter('simplify', 0) as boolean;
const siren = options.siren === true ? true : false;
const language = options.language ? options.language : 'en';
for (let i = 0; i < entryData.length; i++) {
const email = this.getNodeParameter('email', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {};
if (email !== '') {
body.email = email;
}
Object.assign(body, additionalFields);
data.push(body);
}
responseData = await dropcontactApiRequest.call(this, 'POST', '/batch', { data, siren, language }, {}) as { request_id: string, error: string, success: boolean };
if (!responseData.success) {
if (this.continueOnFail()) {
returnData.push({ error: responseData.reason || 'invalid request' });
} else {
throw new NodeApiError(this.getNode(), { error: responseData.reason || 'invalid request' });
}
}
if (simplify === false) {
const waitTime = this.getNodeParameter('options.waitTime', 0, 45) as number;
// tslint:disable-next-line: no-any
const delay = (ms: any) => new Promise(res => setTimeout(res, ms * 1000));
await delay(waitTime);
responseData = await dropcontactApiRequest.call(this, 'GET', `/batch/${responseData.request_id}`, {}, {});
if (!responseData.success) {
if (this.continueOnFail()) {
responseData.push({ error: responseData.reason });
} else {
throw new NodeApiError(this.getNode(), {
error: responseData.reason,
description: 'Hint: Increase the Wait Time to avoid this error',
});
}
} else {
returnData.push(...responseData.data);
}
} else {
returnData.push(responseData);
}
}
if (operation === 'fetchRequest') {
for (let i = 0; i < entryData.length; i++) {
const requestId = this.getNodeParameter('requestId', i) as string;
responseData = await dropcontactApiRequest.call(this, 'GET', `/batch/${requestId}`, {}, {}) as { request_id: string, error: string, success: boolean };
if (!responseData.success) {
if (this.continueOnFail()) {
responseData.push({ error: responseData.reason || 'invalid request' });
} else {
throw new NodeApiError(this.getNode(), { error: responseData.reason || 'invalid request' });
}
}
returnData.push(...responseData.data);
}
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,82 @@
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
NodeApiError,
} from 'n8n-workflow';
import {
OptionsWithUri,
} from 'request';
/**
* Make an authenticated API request to Bubble.
*/
export async function dropcontactApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: string,
endpoint: string,
body: IDataObject,
qs: IDataObject,
) {
const { apiKey } = await this.getCredentials('dropcontactApi') as {
apiKey: string,
};
const options: OptionsWithUri = {
headers: {
'user-agent': 'n8n',
'X-Access-Token': apiKey,
},
method,
uri: `https://api.dropcontact.io${endpoint}`,
qs,
body,
json: true,
};
if (!Object.keys(body).length) {
delete options.body;
}
if (!Object.keys(qs).length) {
delete options.qs;
}
try {
return await this.helpers.request!(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
export async function validateCrendetials(this: ICredentialTestFunctions, decryptedCredentials: ICredentialDataDecryptedObject): Promise<any> { // tslint:disable-line:no-any
const credentials = decryptedCredentials;
const { apiKey } = credentials as {
apiKey: string,
};
const options: OptionsWithUri = {
headers: {
'user-agent': 'n8n',
'X-Access-Token': apiKey,
},
method: 'POST',
body: {
data: [{ email: '' }],
},
uri: `https://api.dropcontact.io/batch`,
json: true,
};
return this.helpers.request!(options);
}

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.5692 16.219C28.1469 15.5371 26.5535 15.1552 24.8709 15.1552C18.8615 15.1552 13.9899 20.0268 13.9899 26.0362C13.9899 32.0456 18.8615 36.9172 24.8709 36.9172C28.9621 36.9172 32.526 34.6592 34.3843 31.3215L31.7614 29.8643C30.4154 32.2818 27.8341 33.9172 24.8709 33.9172C20.5183 33.9172 16.9899 30.3888 16.9899 26.0362C16.9899 21.6836 20.5183 18.1552 24.8709 18.1552C26.0896 18.1552 27.2437 18.4318 28.2738 18.9257L29.5692 16.219ZM34.7779 1.98444V27.7461H31.669V0.899363C29.5459 0.313171 27.3095 0 25 0C11.1929 0 0 11.1929 0 25C0 38.8071 11.1929 50 25 50C38.8071 50 50 38.8071 50 25C50 14.6626 43.7259 5.7907 34.7779 1.98444Z" fill="#0ABA9F"/>
</svg>

After

Width:  |  Height:  |  Size: 773 B

View file

@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.googleDriveTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Data & Storage"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/google"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleDriveTrigger/"
}
]
}
}

View file

@ -0,0 +1,24 @@
{
"node": "n8n-nodes-base.localFileTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Core Nodes"
],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.localFileTrigger/"
}
]
},
"alias": [
"Watch",
"Monitor"
],
"subcategories": {
"Core Nodes":[
"Files"
]
}
}

View file

@ -0,0 +1,21 @@
{
"node": "n8n-nodes-base.microsoftDynamicsCrm",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Marketing & Content",
"Sales"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/microsoft"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.microsoftDynamicsCrm/"
}
]
}
}

View file

@ -0,0 +1,278 @@
import {
BINARY_ENCODING,
} from 'n8n-core';
import {
IDataObject,
IExecuteFunctions,
IN8nHttpFullResponse,
IN8nHttpResponse,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
export class RespondToWebhook implements INodeType {
description: INodeTypeDescription = {
displayName: 'Respond to Webhook',
icon: 'file:webhook.svg',
name: 'respondToWebhook',
group: ['transform'],
version: 1,
description: 'Returns data for Webhook',
defaults: {
name: 'Respond to Webhook',
color: '#885577',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
],
properties: [
{
displayName: 'Respond With',
name: 'respondWith',
type: 'options',
options: [
{
name: 'First Incoming Item',
value: 'firstIncomingItem',
},
{
name: 'Text',
value: 'text',
},
{
name: 'JSON',
value: 'json',
},
{
name: 'Binary',
value: 'binary',
},
{
name: 'No Data',
value: 'noData',
},
],
default: 'firstIncomingItem',
description: 'The data that should be returned',
},
{
displayName: 'When using expressions, note that this node will only run for the first item in the input data.',
name: 'webhookNotice',
type: 'notice',
displayOptions: {
show: {
respondWith: [
'json',
'text',
],
},
},
default: '',
},
{
displayName: 'Response Body',
name: 'responseBody',
type: 'json',
displayOptions: {
show: {
respondWith: [
'json',
],
},
},
default: '',
placeholder: '{ "key": "value" }',
description: 'The HTTP Response JSON data',
},
{
displayName: 'Response Body',
name: 'responseBody',
type: 'string',
displayOptions: {
show: {
respondWith: [
'text',
],
},
},
default: '',
placeholder: 'e.g. Workflow started',
description: 'The HTTP Response text data',
},
{
displayName: 'Response Data Source',
name: 'responseDataSource',
type: 'options',
displayOptions: {
show: {
respondWith: [
'binary',
],
},
},
options: [
{
name: 'Choose Automatically From Input',
value: 'automatically',
description: 'Use if input data will contain a single piece of binary data',
},
{
name: 'Specify Myself',
value: 'set',
description: 'Enter the name of the input field the binary data will be in',
},
],
default: 'automatically',
},
{
displayName: 'Input Field Name',
name: 'inputFieldName',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
respondWith: [
'binary',
],
responseDataSource: [
'set',
],
},
},
description: 'The name of the node input field with the binary data',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Response Code',
name: 'responseCode',
type: 'number',
typeOptions: {
minValue: 100,
maxValue: 599,
},
default: 200,
description: 'The HTTP Response code to return. Defaults to 200.',
},
{
displayName: 'Response Headers',
name: 'responseHeaders',
placeholder: 'Add Response Header',
description: 'Add headers to the webhook response',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'entries',
displayName: 'Entries',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the header',
},
],
},
],
},
],
},
],
};
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const respondWith = this.getNodeParameter('respondWith', 0) as string;
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
const headers = {} as IDataObject;
if (options.responseHeaders) {
for (const header of (options.responseHeaders as IDataObject).entries as IDataObject[]) {
if (typeof header.name !== 'string') {
header.name = header.name?.toString();
}
headers[header.name?.toLowerCase() as string] = header.value?.toString();
}
}
let responseBody: IN8nHttpResponse;
if (respondWith === 'json') {
const responseBodyParameter = this.getNodeParameter('responseBody', 0) as string;
if (responseBodyParameter) {
responseBody = JSON.parse(responseBodyParameter);
}
} else if (respondWith === 'firstIncomingItem') {
responseBody = items[0].json;
} else if (respondWith === 'text') {
responseBody = this.getNodeParameter('responseBody', 0) as string;
} else if (respondWith === 'binary') {
const item = this.getInputData()[0];
if (item.binary === undefined) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on the first item!');
}
let responseBinaryPropertyName: string;
const responseDataSource = this.getNodeParameter('responseDataSource', 0) as string;
if (responseDataSource === 'set') {
responseBinaryPropertyName = this.getNodeParameter('inputFieldName', 0) as string;
} else {
const binaryKeys = Object.keys(item.binary);
if (binaryKeys.length === 0) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on the first item!');
}
responseBinaryPropertyName = binaryKeys[0];
}
const binaryData = item.binary[responseBinaryPropertyName];
if (binaryData === undefined) {
throw new NodeOperationError(this.getNode(), `No binary data property "${responseBinaryPropertyName}" does not exists on item!`);
}
if (headers['content-type']) {
headers['content-type'] = binaryData.mimeType;
}
responseBody = Buffer.from(binaryData.data, BINARY_ENCODING);
} else if (respondWith !== 'noData') {
throw new NodeOperationError(this.getNode(), `The Response Data option "${respondWith}" is not supported!`);
}
const response: IN8nHttpFullResponse = {
body: responseBody,
headers,
statusCode: options.responseCode as number || 200,
};
this.sendResponse(response);
return this.prepareOutputData(items);
}
}

View file

@ -283,7 +283,7 @@ export class Wait implements INodeType {
description: 'The HTTP Response code to return',
},
{
displayName: 'Respond When',
displayName: 'Respond',
name: 'responseMode',
type: 'options',
displayOptions: {
@ -295,14 +295,19 @@ export class Wait implements INodeType {
},
options: [
{
name: 'Webhook received',
name: 'Immediately',
value: 'onReceived',
description: 'Returns directly with defined Response Code',
description: 'As soon as this node executes',
},
{
name: 'Last node finishes',
name: 'When last node finishes',
value: 'lastNode',
description: 'Returns data of the last executed node',
description: 'Returns data of the last-executed node',
},
{
name: 'Using \'Respond to Webhook\' node',
value: 'responseNode',
description: 'Response defined in that node',
},
],
default: 'onReceived',

View file

@ -9,7 +9,6 @@ import {
INodeType,
INodeTypeDescription,
IWebhookResponseData,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
@ -143,10 +142,54 @@ export class Webhook implements INodeType {
required: true,
description: 'The path to listen to.',
},
{
displayName: 'Respond',
name: 'responseMode',
type: 'options',
options: [
{
name: 'Immediately',
value: 'onReceived',
description: 'As soon as this node executes',
},
{
name: 'When last node finishes',
value: 'lastNode',
description: 'Returns data of the last-executed node',
},
{
name: 'Using \'Respond to Webhook\' node',
value: 'responseNode',
description: 'Response defined in that node',
},
],
default: 'onReceived',
description: 'When and how to respond to the webhook.',
},
{
displayName: 'Insert a \'Respond to Webhook\' node to control when and how you respond. <a href="https://docs.n8n.io/nodes/n8n-nodes-base.respondToWebhook" target="_blank">More details</a>',
name: 'webhookNotice',
type: 'notice',
displayOptions: {
show: {
responseMode: [
'responseNode',
],
},
},
default: '',
},
{
displayName: 'Response Code',
name: 'responseCode',
type: 'number',
displayOptions: {
hide: {
responseMode: [
'responseNode',
],
},
},
typeOptions: {
minValue: 100,
maxValue: 599,
@ -154,25 +197,6 @@ export class Webhook implements INodeType {
default: 200,
description: 'The HTTP Response code to return',
},
{
displayName: 'Respond When',
name: 'responseMode',
type: 'options',
options: [
{
name: 'Webhook received',
value: 'onReceived',
description: 'Returns directly with defined Response Code',
},
{
name: 'Last node finishes',
value: 'lastNode',
description: 'Returns data of the last executed node',
},
],
default: 'onReceived',
description: 'When and how to respond to the webhook.',
},
{
displayName: 'Response Data',
name: 'responseData',

View file

@ -130,7 +130,7 @@ export const userFields = [
},
{
displayName: 'External ID',
name: 'externalId',
name: 'external_id',
type: 'string',
default: '',
description: 'A unique identifier from another system',
@ -387,7 +387,7 @@ export const userFields = [
},
{
displayName: 'External ID',
name: 'externalId',
name: 'external_id',
type: 'string',
default: '',
description: 'A unique identifier from another system',

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "0.144.1",
"version": "0.145.0",
"description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -78,6 +78,7 @@
"dist/credentials/DriftOAuth2Api.credentials.js",
"dist/credentials/DropboxApi.credentials.js",
"dist/credentials/DropboxOAuth2Api.credentials.js",
"dist/credentials/DropcontactApi.credentials.js",
"dist/credentials/EgoiApi.credentials.js",
"dist/credentials/ElasticsearchApi.credentials.js",
"dist/credentials/ElasticSecurityApi.credentials.js",
@ -379,6 +380,7 @@
"dist/nodes/Disqus/Disqus.node.js",
"dist/nodes/Drift/Drift.node.js",
"dist/nodes/Dropbox/Dropbox.node.js",
"dist/nodes/Dropcontact/Dropcontact.node.js",
"dist/nodes/EditImage.node.js",
"dist/nodes/Egoi/Egoi.node.js",
"dist/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.js",
@ -552,6 +554,7 @@
"dist/nodes/Reddit/Reddit.node.js",
"dist/nodes/Redis/Redis.node.js",
"dist/nodes/RenameKeys.node.js",
"dist/nodes/RespondToWebhook.node.js",
"dist/nodes/Rocketchat/Rocketchat.node.js",
"dist/nodes/RssFeedRead.node.js",
"dist/nodes/Rundeck/Rundeck.node.js",
@ -668,7 +671,7 @@
"@types/xml2js": "^0.4.3",
"gulp": "^4.0.0",
"jest": "^26.4.2",
"n8n-workflow": "~0.74.0",
"n8n-workflow": "~0.75.0",
"nodelinter": "^0.1.9",
"ts-jest": "^26.3.0",
"tslint": "^6.1.2",
@ -708,7 +711,7 @@
"mqtt": "4.2.6",
"mssql": "^6.2.0",
"mysql2": "~2.3.0",
"n8n-core": "~0.91.0",
"n8n-core": "~0.92.0",
"node-ssh": "^12.0.0",
"nodemailer": "^6.5.0",
"pdf-parse": "^1.1.1",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "0.74.0",
"version": "0.75.0",
"description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",

View file

@ -6,6 +6,7 @@
import * as express from 'express';
import * as FormData from 'form-data';
import { URLSearchParams } from 'url';
import { IDeferredPromise } from './DeferredPromise';
import { Workflow } from './Workflow';
import { WorkflowHooks } from './WorkflowHooks';
import { WorkflowOperationError } from './WorkflowErrors';
@ -208,6 +209,9 @@ export interface IDataObject {
[key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[];
}
// export type IExecuteResponsePromiseData = IDataObject;
export type IExecuteResponsePromiseData = IDataObject | IN8nHttpFullResponse;
export interface INodeTypeNameVersion {
name: string;
version: number;
@ -324,13 +328,13 @@ export interface IHttpRequestOptions {
json?: boolean;
}
export type IN8nHttpResponse = IDataObject | Buffer | GenericValue | GenericValue[];
export type IN8nHttpResponse = IDataObject | Buffer | GenericValue | GenericValue[] | null;
export interface IN8nHttpFullResponse {
body: IN8nHttpResponse;
headers: IDataObject;
statusCode: number;
statusMessage: string;
statusMessage?: string;
}
export interface IExecuteFunctions {
@ -371,7 +375,8 @@ export interface IExecuteFunctions {
outputIndex?: number,
): Promise<INodeExecutionData[][]>;
putExecutionToWait(waitTill: Date): Promise<void>;
sendMessageToUI(message: any): void;
sendMessageToUI(message: any): void; // tslint:disable-line:no-any
sendResponse(response: IExecuteResponsePromiseData): void; // tslint:disable-line:no-any
helpers: {
httpRequest(
requestOptions: IHttpRequestOptions,
@ -492,7 +497,10 @@ export interface IPollFunctions {
}
export interface ITriggerFunctions {
emit(data: INodeExecutionData[][]): void;
emit(
data: INodeExecutionData[][],
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
): void;
getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined>;
getMode(): WorkflowExecuteMode;
getActivationMode(): WorkflowActivateMode;
@ -975,6 +983,7 @@ export interface IWorkflowExecuteHooks {
nodeExecuteBefore?: Array<(nodeName: string) => Promise<void>>;
workflowExecuteAfter?: Array<(data: IRun, newStaticData: IDataObject) => Promise<void>>;
workflowExecuteBefore?: Array<(workflow: Workflow, data: IRunExecutionData) => Promise<void>>;
sendResponse?: Array<(response: IExecuteResponsePromiseData) => Promise<void>>;
}
export interface IWorkflowExecuteAdditionalData {

View file

@ -16,6 +16,8 @@
import {
Expression,
IConnections,
IDeferredPromise,
IExecuteResponsePromiseData,
IGetExecuteTriggerFunctions,
INode,
INodeExecuteFunctions,
@ -946,10 +948,23 @@ export class Workflow {
// Add the manual trigger response which resolves when the first time data got emitted
triggerResponse!.manualTriggerResponse = new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-shadow
triggerFunctions.emit = ((resolve) => (data: INodeExecutionData[][]) => {
resolve(data);
})(resolve);
triggerFunctions.emit = (
(resolveEmit) =>
(
data: INodeExecutionData[][],
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
) => {
additionalData.hooks!.hookFunctions.sendResponse = [
async (response: IExecuteResponsePromiseData): Promise<void> => {
if (responsePromise) {
responsePromise.resolve(response);
}
},
];
resolveEmit(data);
}
)(resolve);
});
return triggerResponse;

View file

@ -3,6 +3,7 @@ import * as LoggerProxy from './LoggerProxy';
import * as NodeHelpers from './NodeHelpers';
import * as ObservableObject from './ObservableObject';
export * from './DeferredPromise';
export * from './Interfaces';
export * from './Expression';
export * from './NodeErrors';