Nodes as JSON and authentication redesign (#2401)

*  change FE to handle new object type

* 🚸 improve UX of handling invalid credentials

* 🚧 WIP

* 🎨 fix typescript issues

* 🐘 add migrations for all supported dbs

* ✏️ add description to migrations

*  add credential update on import

*  resolve after merge issues

* 👕 fix lint issues

*  check credentials on workflow create/update

* update interface

* 👕 fix ts issues

*  adaption to new credentials UI

* 🐛 intialize cache on BE for credentials check

* 🐛 fix undefined oldCredentials

* 🐛 fix deleting credential

* 🐛 fix check for undefined keys

* 🐛 fix disabling edit in execution

* 🎨 just show credential name on execution view

* ✏️  remove TODO

*  implement review suggestions

*  add cache to getCredentialsByType

*  use getter instead of cache

* ✏️ fix variable name typo

* 🐘 include waiting nodes to migrations

* 🐛 fix reverting migrations command

*  update typeorm command

*  create db:revert command

* 👕 fix lint error

*  Add optional authenticate method to credentials

*  Simplify code and add authentication support to MattermostApi

* 👕 Fix lint issue

*  Add support to own-mode

* 👕 Fix lint issue

*  Add support for predefined auth types bearer and headerAuth

*  Make sure that DateTime Node always returns strings

*  Add support for moment types to If Node

*  Make it possible for HTTP Request Node to use all credential types

*  Add basicAuth support

* Add a new dropcontact node

*  First basic implementation of mainly JSON based nodes

*  Add fixedCollection support, added value parameter and
expression support for value and property

* Improvements to #2389

*  Add credentials verification

*  Small improvement

*  set default time to 45 seconds

*  Add support for preSend and postReceive methods

*  Add lodash merge and set depedency to workflow

* 👕 Fix lint issue

*  Improvements

*  Improvements

*  Improvements

*  Improvements

*  Improvements

* 🐛 Set siren and language correctly

*  Add support for requestDefaults

*  Add support for baseURL to httpRequest

*  Move baseURL to correct location

*  Add support for options loading

* 🐛 Fix error with fullAccess nodes

*  Add credential test functionality

* 🐛 Fix issue with OAuth autentication and lint issue

*  Fix build issue

* 🐛 Fix issue that url got always overwritten to empty

*  Add pagination support

*  Code fix required after merge

*  Remove not needed imports

*  Fix credential test

*  Add expression support for request properties and $self
support on properties

*  Rename $self to $value

* 👕 Fix lint issue

*  Add example how to send data in path

*  Make it possible to not sent in dot notation

*  Add support for postReceive:rootProperty

*  Fix typo

*  Add support for postReceive:set

*  Some fixes

*  Small improvement

* ;zap: Separate RoutingNode code

*  Simplify code and fix bug

*  Remove unused code

*  Make it possible to define "request" and "requestProperty" on
options

* 👕 Fix lint issue

*  Change $credentials variables name

*  Enable expressions and access to credentials in requestDefaults

*  Make parameter option loading use RoutingNode.makeRoutingRequest

*  Allow requestOperations overwrite on LoadOptions

*  Make it possible to access current node parameters in loadOptions

*  Rename parameters variable to make future proof

*  Make it possible to use offset-pagination with body

*  Add support for queryAuth

*  Never return more items than requested

*  Make it possible to overwrite requestOperations on parameter
and option level

* 👕 Fix lint issue

*  Allow simplified auth also with regular nodes

*  Add support for receiving binary data

* 🐛 Fix example node

*  Rename property "name" to "displayName" in loadOptions

*  Send data by default as "query" if nothing is set

*  Rename $self to $parent

*  Change to work with INodeExecutionData instead of IDataObject

*  Improve binaryData handling

*  Property design improvements

*  Fix property name

* 🚨 Add some tests

*  Add also test for request

*  Improve test and fix issues

*  Improvements to loadOptions

*  Normalize loadOptions with rest of code

*  Add info text

*  Add support for $value in postReceive

* 🚨 Add tests for RoutingNode.runNode

*  Remove TODOs and make url property optional

*  Fix bug and lint issue

* 🐛 Fix bug that not the correct property got used

* 🚨 Add tests for CredentialsHelper.authenticate

*  Improve code and resolve expressions also everywhere for
loadOptions and credential test requests

*  Make it possible to define multiple preSend and postReceive
actions

*  Allow to define tests on credentials

*  Remove test data

* ⬆️ Update package-lock.json file

*  Remove old not longer used code

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: PaulineDropcontact <pauline@dropcontact.io>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
This commit is contained in:
Jan Oberhauser
2022-02-05 22:55:43 +01:00
committed by GitHub
parent f23098e38b
commit 0da398b0e4
66 changed files with 5074 additions and 360 deletions

View File

@@ -30,6 +30,7 @@
"@types/express": "^4.17.6",
"@types/jest": "^26.0.13",
"@types/lodash.get": "^4.4.6",
"@types/lodash.merge": "^4.6.6",
"@types/node": "14.17.27",
"@types/xml2js": "^0.4.3",
"@typescript-eslint/eslint-plugin": "^4.29.0",
@@ -48,6 +49,8 @@
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2",
"lodash.set": "^4.3.2",
"riot-tmpl": "^3.0.8",
"xml2js": "^0.4.23"
},

View File

@@ -12,8 +12,14 @@ import { WorkflowHooks } from './WorkflowHooks';
import { WorkflowOperationError } from './WorkflowErrors';
import { NodeApiError, NodeOperationError } from './NodeErrors';
export interface IAdditionalCredentialOptions {
oauth2?: IOAuth2Options;
credentialsDecrypted?: ICredentialsDecrypted;
}
export type IAllExecuteFunctions =
| IExecuteFunctions
| IExecutePaginationFunctions
| IExecuteSingleFunctions
| IHookFunctions
| ILoadOptionsFunctions
@@ -128,6 +134,17 @@ export interface ICredentialsExpressionResolveValues {
workflow: Workflow;
}
// Simplified options of request library
export interface IRequestOptionsSimplified {
auth?: {
username: string;
password: string;
};
body: IDataObject;
headers: IDataObject;
qs: IDataObject;
}
export abstract class ICredentialsHelper {
encryptionKey: string;
@@ -135,6 +152,16 @@ export abstract class ICredentialsHelper {
this.encryptionKey = encryptionKey;
}
abstract getParentTypes(name: string): string[];
abstract authenticate(
credentials: ICredentialDataDecryptedObject,
typeName: string,
requestOptions: IHttpRequestOptions | IRequestOptionsSimplified,
workflow: Workflow,
node: INode,
): Promise<IHttpRequestOptions>;
abstract getCredentials(
nodeCredentials: INodeCredentialsDetails,
type: string,
@@ -155,6 +182,80 @@ export abstract class ICredentialsHelper {
): Promise<void>;
}
export interface IAuthenticateBase {
type: string;
properties: {
[key: string]: string;
};
}
export interface IAuthenticateBasicAuth extends IAuthenticateBase {
type: 'basicAuth';
properties: {
userPropertyName?: string;
passwordPropertyName?: string;
};
}
export interface IAuthenticateBearer extends IAuthenticateBase {
type: 'bearer';
properties: {
tokenPropertyName?: string;
};
}
export interface IAuthenticateHeaderAuth extends IAuthenticateBase {
type: 'headerAuth';
properties: {
name: string;
value: string;
};
}
export interface IAuthenticateQueryAuth extends IAuthenticateBase {
type: 'queryAuth';
properties: {
key: string;
value: string;
};
}
export type IAuthenticate =
| ((
credentials: ICredentialDataDecryptedObject,
requestOptions: IHttpRequestOptions,
) => Promise<IHttpRequestOptions>)
| IAuthenticateBasicAuth
| IAuthenticateBearer
| IAuthenticateHeaderAuth
| IAuthenticateQueryAuth;
export interface IAuthenticateRuleBase {
type: string;
properties: {
[key: string]: string | number;
};
errorMessage?: string;
}
export interface IAuthenticateRuleResponseCode extends IAuthenticateRuleBase {
type: 'responseCode';
properties: {
value: number;
message: string;
};
}
export interface ICredentialTestRequest {
request: IHttpRequestOptions;
rules?: IAuthenticateRuleResponseCode[];
}
export interface ICredentialTestRequestData {
nodeType?: INodeType;
testRequest: ICredentialTestRequest;
}
export interface ICredentialType {
name: string;
displayName: string;
@@ -163,13 +264,13 @@ export interface ICredentialType {
properties: INodeProperties[];
documentationUrl?: string;
__overwrittenProperties?: string[];
authenticate?: IAuthenticate;
test?: ICredentialTestRequest;
}
export interface ICredentialTypes {
credentialTypes?: {
[key: string]: ICredentialType;
};
init(credentialTypes?: { [key: string]: ICredentialType }): Promise<void>;
credentialTypes?: ICredentialTypeData;
init(credentialTypes?: ICredentialTypeData): Promise<void>;
getAll(): ICredentialType[];
getByName(credentialType: string): ICredentialType;
}
@@ -301,10 +402,13 @@ export interface IExecuteContextData {
[key: string]: IContextObject;
}
export type IHttpRequestMethods = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT';
export interface IHttpRequestOptions {
url: string;
baseURL?: string;
headers?: IDataObject;
method?: 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT';
method?: IHttpRequestMethods;
body?: FormData | GenericValue | GenericValue[] | Buffer | URLSearchParams;
qs?: IDataObject;
arrayFormat?: 'indices' | 'brackets' | 'repeat' | 'comma';
@@ -338,6 +442,33 @@ export interface IN8nHttpFullResponse {
statusMessage?: string;
}
export interface IN8nRequestOperations {
pagination?:
| IN8nRequestOperationPaginationOffset
| ((
this: IExecutePaginationFunctions,
requestOptions: IRequestOptionsFromParameters,
) => Promise<INodeExecutionData[]>);
}
export interface IN8nRequestOperationPaginationBase {
type: string;
properties: {
[key: string]: string | number;
};
}
export interface IN8nRequestOperationPaginationOffset extends IN8nRequestOperationPaginationBase {
type: 'offset';
properties: {
limitParameter: string;
offsetParameter: string;
pageSize: number;
rootProperty?: string; // Optional Path to option array
type: 'body' | 'query';
};
}
export interface IExecuteFunctions {
continueOnFail(): boolean;
evaluateExpression(
@@ -382,6 +513,12 @@ export interface IExecuteFunctions {
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@@ -410,15 +547,32 @@ export interface IExecuteSingleFunctions {
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
export interface IExecutePaginationFunctions extends IExecuteSingleFunctions {
makeRoutingRequest(
this: IAllExecuteFunctions,
requestOptions: IRequestOptionsFromParameters,
): Promise<INodeExecutionData[]>;
}
export interface IExecuteWorkflowInfo {
code?: IWorkflowBase;
id?: string;
}
export type ICredentialTestFunction = (
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
) => Promise<INodeCredentialTestResult>;
export interface ICredentialTestFunctions {
helpers: {
[key: string]: (...args: any[]) => any;
@@ -448,6 +602,20 @@ export interface ILoadOptionsFunctions {
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
// TODO: Remove from here. Add it only now to LoadOptions as many nodes do import
// from n8n-workflow instead of n8n-core
requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: any, // tslint:disable-line:no-any
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any>;
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: ((...args: any[]) => any) | undefined; // tslint:disable-line:no-any
};
}
@@ -471,6 +639,12 @@ export interface IHookFunctions {
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@@ -493,6 +667,12 @@ export interface IPollFunctions {
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@@ -518,6 +698,12 @@ export interface ITriggerFunctions {
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@@ -549,6 +735,12 @@ export interface IWebhookFunctions {
httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<IN8nHttpResponse | IN8nHttpFullResponse>;
[key: string]: (...args: any[]) => any; // tslint:disable-line:no-any
};
}
@@ -636,12 +828,21 @@ export type CodeAutocompleteTypes = 'function' | 'functionItem';
export type EditorTypes = 'code' | 'json';
export interface ILoadOptions {
routing?: {
operations?: IN8nRequestOperations;
output?: INodeRequestOutput;
request?: IHttpRequestOptionsFromParameters;
};
}
export interface INodePropertyTypeOptions {
alwaysOpenEditWindow?: boolean; // Supported by: string
codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string
editor?: EditorTypes; // Supported by: string
loadOptionsDependsOn?: string[]; // Supported by: options
loadOptionsMethod?: string; // Supported by: options
loadOptions?: ILoadOptions; // Supported by: options
maxValue?: number; // Supported by: number
minValue?: number; // Supported by: number
multipleValues?: boolean; // Supported by: <All>
@@ -651,7 +852,7 @@ export interface INodePropertyTypeOptions {
rows?: number; // Supported by: string
showAlpha?: boolean; // Supported by: color
sortable?: boolean; // Supported when "multipleValues" set to true
[key: string]: boolean | number | string | EditorTypes | undefined | string[];
[key: string]: any;
}
export interface IDisplayOptions {
@@ -677,11 +878,13 @@ export interface INodeProperties {
isNodeSetting?: boolean;
noDataExpression?: boolean;
required?: boolean;
routing?: INodePropertyRouting;
}
export interface INodePropertyOptions {
name: string;
value: string | number | boolean;
description?: string;
routing?: INodePropertyRouting;
}
export interface INodePropertyCollection {
@@ -723,10 +926,7 @@ export interface INodeType {
};
credentialTest?: {
// Contains a group of functins that test credentials.
[functionName: string]: (
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
) => Promise<NodeCredentialTestResult>;
[functionName: string]: ICredentialTestFunction;
};
};
webhookMethods?: {
@@ -742,12 +942,12 @@ export interface INodeVersionedType {
description: INodeTypeBaseDescription;
getNodeType: (version?: number) => INodeType;
}
export interface NodeCredentialTestResult {
export interface INodeCredentialTestResult {
status: 'OK' | 'Error';
message: string;
}
export interface NodeCredentialTestRequest {
export interface INodeCredentialTestRequest {
nodeToTestWith?: string; // node name i.e. slack
credentials: ICredentialsDecrypted;
}
@@ -765,7 +965,7 @@ export interface INodeCredentialDescription {
name: string;
required?: boolean;
displayOptions?: IDisplayOptions;
testedBy?: string; // Name of a function inside `loadOptions.credentialTest`
testedBy?: ICredentialTestRequest | string; // Name of a function inside `loadOptions.credentialTest`
}
export type INodeIssueTypes = 'credentials' | 'execution' | 'parameters' | 'typeUnknown';
@@ -804,6 +1004,105 @@ export interface INodeTypeBaseDescription {
codex?: CodexData;
}
export interface INodePropertyRouting {
operations?: IN8nRequestOperations; // Should be changed, does not sound right
output?: INodeRequestOutput;
request?: IHttpRequestOptionsFromParameters;
send?: INodeRequestSend;
}
export type PostReceiveAction =
| ((
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
response: IN8nHttpFullResponse,
) => Promise<INodeExecutionData[]>)
| IPostReceiveBinaryData
| IPostReceiveRootProperty
| IPostReceiveSet
| IPostReceiveSetKeyValue
| IPostReceiveSort;
export type PreSendAction = (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
) => Promise<IHttpRequestOptions>;
export interface INodeRequestOutput {
maxResults?: number | string;
postReceive?: PostReceiveAction[];
}
export interface INodeRequestSend {
preSend?: PreSendAction[];
paginate?: boolean | string; // Where should this life?
property?: string; // Maybe: propertyName, destinationProperty?
propertyInDotNotation?: boolean; // Enabled by default
type?: 'body' | 'query';
value?: string;
}
export interface IPostReceiveBase {
type: string;
properties: {
[key: string]: string | number | IDataObject;
};
errorMessage?: string;
}
export interface IPostReceiveBinaryData extends IPostReceiveBase {
type: 'binaryData';
properties: {
destinationProperty: string;
};
}
export interface IPostReceiveRootProperty extends IPostReceiveBase {
type: 'rootProperty';
properties: {
property: string;
};
}
export interface IPostReceiveSet extends IPostReceiveBase {
type: 'set';
properties: {
value: string;
};
}
export interface IPostReceiveSetKeyValue extends IPostReceiveBase {
type: 'setKeyValue';
properties: {
[key: string]: string | number;
};
}
export interface IPostReceiveSort extends IPostReceiveBase {
type: 'sort';
properties: {
key: string;
};
}
export interface IHttpRequestOptionsFromParameters extends Partial<IHttpRequestOptions> {
url?: string;
}
export interface IRequestOptionsFromParameters {
maxResults?: number | string;
options: IHttpRequestOptionsFromParameters;
paginate?: boolean | string;
preSend: PreSendAction[];
postReceive: Array<{
data: {
parameterValue: string | IDataObject | undefined;
};
actions: PostReceiveAction[];
}>;
requestOperations?: IN8nRequestOperations;
}
export interface INodeTypeDescription extends INodeTypeBaseDescription {
version: number;
defaults: INodeParameters;
@@ -817,6 +1116,8 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
credentials?: INodeCredentialDescription[];
maxNodes?: number; // How many nodes of that type can be created in a workflow
polling?: boolean;
requestDefaults?: IHttpRequestOptionsFromParameters;
requestOperations?: IN8nRequestOperations;
hooks?: {
[key: string]: INodeHookDescription[] | undefined;
activate?: INodeHookDescription[];
@@ -869,9 +1170,7 @@ export interface IWorkflowDataProxyData {
$workflow: any;
}
export interface IWorkflowDataProxyAdditionalKeys {
[key: string]: string | number | undefined;
}
export type IWorkflowDataProxyAdditionalKeys = IDataObject;
export interface IWorkflowMetadata {
id?: number | string;
@@ -898,6 +1197,13 @@ export interface INodeTypes {
getByNameAndVersion(nodeType: string, version?: number): INodeType | undefined;
}
export interface ICredentialTypeData {
[key: string]: {
type: ICredentialType;
sourcePath: string;
};
}
export interface INodeTypeData {
[key: string]: {
type: INodeType | INodeVersionedType;

View File

@@ -0,0 +1,814 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable import/no-cycle */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable no-param-reassign */
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import get from 'lodash.get';
import merge from 'lodash.merge';
import set from 'lodash.set';
import {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
IHttpRequestOptions,
IN8nHttpFullResponse,
INode,
INodeExecuteFunctions,
INodeExecutionData,
INodeParameters,
INodePropertyOptions,
INodeType,
IRequestOptionsFromParameters,
IRunExecutionData,
ITaskDataConnections,
IWorkflowDataProxyAdditionalKeys,
IWorkflowExecuteAdditionalData,
NodeHelpers,
NodeParameterValue,
Workflow,
WorkflowExecuteMode,
} from '.';
import {
IDataObject,
IExecuteSingleFunctions,
IN8nRequestOperations,
INodeProperties,
INodePropertyCollection,
PostReceiveAction,
} from './Interfaces';
export class RoutingNode {
additionalData: IWorkflowExecuteAdditionalData;
connectionInputData: INodeExecutionData[];
node: INode;
mode: WorkflowExecuteMode;
runExecutionData: IRunExecutionData;
workflow: Workflow;
constructor(
workflow: Workflow,
node: INode,
connectionInputData: INodeExecutionData[],
runExecutionData: IRunExecutionData,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
) {
this.additionalData = additionalData;
this.connectionInputData = connectionInputData;
this.runExecutionData = runExecutionData;
this.mode = mode;
this.node = node;
this.workflow = workflow;
}
async runNode(
inputData: ITaskDataConnections,
runIndex: number,
nodeType: INodeType,
nodeExecuteFunctions: INodeExecuteFunctions,
credentialsDecrypted?: ICredentialsDecrypted,
): Promise<INodeExecutionData[][] | null | undefined> {
const items = inputData.main[0] as INodeExecutionData[];
const returnData: INodeExecutionData[] = [];
let responseData;
let credentialType: string | undefined;
if (nodeType.description.credentials?.length) {
credentialType = nodeType.description.credentials[0].name;
}
const executeFunctions = nodeExecuteFunctions.getExecuteFunctions(
this.workflow,
this.runExecutionData,
runIndex,
this.connectionInputData,
inputData,
this.node,
this.additionalData,
this.mode,
);
let credentials: ICredentialDataDecryptedObject | undefined;
if (credentialsDecrypted) {
credentials = credentialsDecrypted.data;
} else if (credentialType) {
credentials = (await executeFunctions.getCredentials(credentialType)) || {};
}
// TODO: Think about how batching could be handled for REST APIs which support it
for (let i = 0; i < items.length; i++) {
try {
const thisArgs = nodeExecuteFunctions.getExecuteSingleFunctions(
this.workflow,
this.runExecutionData,
runIndex,
this.connectionInputData,
inputData,
this.node,
i,
this.additionalData,
this.mode,
);
const requestData: IRequestOptionsFromParameters = {
options: {
qs: {},
body: {},
},
preSend: [],
postReceive: [],
requestOperations: {},
};
if (nodeType.description.requestOperations) {
requestData.requestOperations = { ...nodeType.description.requestOperations };
}
if (nodeType.description.requestDefaults) {
for (const key of Object.keys(nodeType.description.requestDefaults)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let value = (nodeType.description.requestDefaults as Record<string, any>)[key];
// If the value is an expression resolve it
value = this.getParameterValue(
value,
i,
runIndex,
{ $credentials: credentials },
true,
) as string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(requestData.options as Record<string, any>)[key] = value;
}
}
for (const property of nodeType.description.properties) {
let value = get(this.node.parameters, property.name, []) as string | NodeParameterValue;
// If the value is an expression resolve it
value = this.getParameterValue(
value,
i,
runIndex,
{ $credentials: credentials },
true,
) as string | NodeParameterValue;
const tempOptions = this.getRequestOptionsFromParameters(
thisArgs,
property,
i,
runIndex,
'',
{ $credentials: credentials, $value: value },
);
this.mergeOptions(requestData, tempOptions);
}
// TODO: Change to handle some requests in parallel (should be configurable)
responseData = await this.makeRoutingRequest(
requestData,
thisArgs,
i,
runIndex,
credentialType,
requestData.requestOperations,
credentialsDecrypted,
);
if (requestData.maxResults) {
// Remove not needed items in case APIs return to many
responseData.splice(requestData.maxResults as number);
}
returnData.push(...responseData);
} catch (error) {
if (get(this.node, 'continueOnFail', false)) {
returnData.push({ json: {}, error: error.message });
continue;
}
throw error;
}
}
return [returnData];
}
mergeOptions(
destinationOptions: IRequestOptionsFromParameters,
sourceOptions?: IRequestOptionsFromParameters,
): void {
if (sourceOptions) {
destinationOptions.paginate = destinationOptions.paginate ?? sourceOptions.paginate;
destinationOptions.maxResults = sourceOptions.maxResults
? sourceOptions.maxResults
: destinationOptions.maxResults;
merge(destinationOptions.options, sourceOptions.options);
destinationOptions.preSend.push(...sourceOptions.preSend);
destinationOptions.postReceive.push(...sourceOptions.postReceive);
if (sourceOptions.requestOperations) {
destinationOptions.requestOperations = Object.assign(
destinationOptions.requestOperations,
sourceOptions.requestOperations,
);
}
}
}
async runPostReceiveAction(
executeSingleFunctions: IExecuteSingleFunctions,
action: PostReceiveAction,
inputData: INodeExecutionData[],
responseData: IN8nHttpFullResponse,
parameterValue: string | IDataObject | undefined,
itemIndex: number,
runIndex: number,
): Promise<INodeExecutionData[]> {
if (typeof action === 'function') {
return action.call(executeSingleFunctions, inputData, responseData);
}
if (action.type === 'rootProperty') {
try {
return inputData.flatMap((item) => {
// let itemContent = item.json[action.properties.property];
let itemContent = get(item.json, action.properties.property);
if (!Array.isArray(itemContent)) {
itemContent = [itemContent];
}
return (itemContent as IDataObject[]).map((json) => {
return {
json,
};
});
});
} catch (e) {
throw new Error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`The rootProperty "${action.properties.property}" could not be found on item.`,
);
}
}
if (action.type === 'set') {
const { value } = action.properties;
// If the value is an expression resolve it
return [
{
json: this.getParameterValue(
value,
itemIndex,
runIndex,
{ $response: responseData, $value: parameterValue },
false,
) as IDataObject,
},
];
}
if (action.type === 'sort') {
// Sort the returned options
const sortKey = action.properties.key;
inputData.sort((a, b) => {
const aSortValue = a.json[sortKey]
? (a.json[sortKey]?.toString().toLowerCase() as string)
: '';
const bSortValue = b.json[sortKey]
? (b.json[sortKey]?.toString().toLowerCase() as string)
: '';
if (aSortValue < bSortValue) {
return -1;
}
if (aSortValue > bSortValue) {
return 1;
}
return 0;
});
return inputData;
}
if (action.type === 'setKeyValue') {
const returnData: INodeExecutionData[] = [];
// eslint-disable-next-line @typescript-eslint/no-loop-func
inputData.forEach((item) => {
const returnItem: IDataObject = {};
for (const key of Object.keys(action.properties)) {
let propertyValue = (
action.properties as Record<
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>
)[key];
// If the value is an expression resolve it
propertyValue = this.getParameterValue(
propertyValue,
itemIndex,
runIndex,
{
$response: responseData,
$responseItem: item.json,
$value: parameterValue,
},
true,
) as string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(returnItem as Record<string, any>)[key] = propertyValue;
}
returnData.push({ json: returnItem });
});
return returnData;
}
if (action.type === 'binaryData') {
responseData.body = Buffer.from(responseData.body as string);
let { destinationProperty } = action.properties;
destinationProperty = this.getParameterValue(
destinationProperty,
itemIndex,
runIndex,
{ $response: responseData, $value: parameterValue },
false,
) as string;
const binaryData = await executeSingleFunctions.helpers.prepareBinaryData(responseData.body);
return inputData.map((item) => {
if (typeof item.json === 'string') {
// By default is probably the binary data as string set, in this case remove it
item.json = {};
}
item.binary = {
[destinationProperty]: binaryData,
};
return item;
});
}
return [];
}
async rawRoutingRequest(
executeSingleFunctions: IExecuteSingleFunctions,
requestData: IRequestOptionsFromParameters,
itemIndex: number,
runIndex: number,
credentialType?: string,
credentialsDecrypted?: ICredentialsDecrypted,
): Promise<INodeExecutionData[]> {
let responseData: IN8nHttpFullResponse;
requestData.options.returnFullResponse = true;
if (credentialType) {
responseData = (await executeSingleFunctions.helpers.httpRequestWithAuthentication.call(
executeSingleFunctions,
credentialType,
requestData.options as IHttpRequestOptions,
{ credentialsDecrypted },
)) as IN8nHttpFullResponse;
} else {
responseData = (await executeSingleFunctions.helpers.httpRequest(
requestData.options as IHttpRequestOptions,
)) as IN8nHttpFullResponse;
}
let returnData: INodeExecutionData[] = [
{
json: responseData.body as IDataObject,
},
];
if (requestData.postReceive.length) {
// If postReceive functionality got defined execute all of them
for (const postReceiveMethod of requestData.postReceive) {
for (const action of postReceiveMethod.actions) {
returnData = await this.runPostReceiveAction(
executeSingleFunctions,
action,
returnData,
responseData,
postReceiveMethod.data.parameterValue,
itemIndex,
runIndex,
);
}
}
} else {
// No postReceive functionality got defined so simply add data as it is
// eslint-disable-next-line no-lonely-if
if (Array.isArray(responseData.body)) {
returnData = responseData.body.map((json) => {
return {
json,
} as INodeExecutionData;
});
} else {
returnData[0].json = responseData.body as IDataObject;
}
}
return returnData;
}
async makeRoutingRequest(
requestData: IRequestOptionsFromParameters,
executeSingleFunctions: IExecuteSingleFunctions,
itemIndex: number,
runIndex: number,
credentialType?: string,
requestOperations?: IN8nRequestOperations,
credentialsDecrypted?: ICredentialsDecrypted,
): Promise<INodeExecutionData[]> {
let responseData: INodeExecutionData[];
for (const preSendMethod of requestData.preSend) {
requestData.options = await preSendMethod.call(
executeSingleFunctions,
requestData.options as IHttpRequestOptions,
);
}
const executePaginationFunctions = {
...executeSingleFunctions,
makeRoutingRequest: async (requestOptions: IRequestOptionsFromParameters) => {
return this.rawRoutingRequest(
executeSingleFunctions,
requestOptions,
itemIndex,
runIndex,
credentialType,
credentialsDecrypted,
);
},
};
if (requestData.paginate && requestOperations?.pagination) {
// Has pagination
if (typeof requestOperations.pagination === 'function') {
// Pagination via function
responseData = await requestOperations.pagination.call(
executePaginationFunctions,
requestData,
);
} else {
// Pagination via JSON properties
const { properties } = requestOperations.pagination;
responseData = [];
if (!requestData.options.qs) {
requestData.options.qs = {};
}
// Different predefined pagination types
if (requestOperations.pagination.type === 'offset') {
const optionsType = properties.type === 'body' ? 'body' : 'qs';
if (properties.type === 'body' && !requestData.options.body) {
requestData.options.body = {};
}
(requestData.options[optionsType] as IDataObject)[properties.limitParameter] =
properties.pageSize;
(requestData.options[optionsType] as IDataObject)[properties.offsetParameter] = 0;
let tempResponseData: INodeExecutionData[];
do {
if (requestData?.maxResults) {
// Only request as many results as needed
const resultsMissing = (requestData?.maxResults as number) - responseData.length;
if (resultsMissing < 1) {
break;
}
(requestData.options[optionsType] as IDataObject)[properties.limitParameter] =
Math.min(properties.pageSize, resultsMissing);
}
tempResponseData = await this.rawRoutingRequest(
executeSingleFunctions,
requestData,
itemIndex,
runIndex,
credentialType,
credentialsDecrypted,
);
(requestData.options[optionsType] as IDataObject)[properties.offsetParameter] =
((requestData.options[optionsType] as IDataObject)[
properties.offsetParameter
] as number) + properties.pageSize;
if (properties.rootProperty) {
const tempResponseValue = get(tempResponseData[0].json, properties.rootProperty) as
| IDataObject[]
| undefined;
if (tempResponseValue === undefined) {
throw new Error(
`The rootProperty "${properties.rootProperty}" could not be found on item.`,
);
}
tempResponseData = tempResponseValue.map((item) => {
return {
json: item,
};
});
}
responseData.push(...tempResponseData);
} while (tempResponseData.length && tempResponseData.length === properties.pageSize);
}
}
} else {
// No pagination
responseData = await this.rawRoutingRequest(
executeSingleFunctions,
requestData,
itemIndex,
runIndex,
credentialType,
credentialsDecrypted,
);
}
return responseData;
}
getParameterValue(
parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
itemIndex: number,
runIndex: number,
additionalKeys?: IWorkflowDataProxyAdditionalKeys,
returnObjectAsString = false,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | string {
if (typeof parameterValue === 'string' && parameterValue.charAt(0) === '=') {
return this.workflow.expression.getParameterValue(
parameterValue,
this.runExecutionData ?? null,
runIndex,
itemIndex,
this.node.name,
this.connectionInputData,
this.mode,
additionalKeys ?? {},
returnObjectAsString,
);
}
return parameterValue;
}
getRequestOptionsFromParameters(
executeSingleFunctions: IExecuteSingleFunctions,
nodeProperties: INodeProperties | INodePropertyOptions,
itemIndex: number,
runIndex: number,
path: string,
additionalKeys?: IWorkflowDataProxyAdditionalKeys,
): IRequestOptionsFromParameters | undefined {
const returnData: IRequestOptionsFromParameters = {
options: {
qs: {},
body: {},
},
preSend: [],
postReceive: [],
requestOperations: {},
};
let basePath = path ? `${path}.` : '';
if (!NodeHelpers.displayParameter(this.node.parameters, nodeProperties, this.node.parameters)) {
return undefined;
}
if (nodeProperties.routing) {
let parameterValue: string | undefined;
if (basePath + nodeProperties.name && 'type' in nodeProperties) {
parameterValue = executeSingleFunctions.getNodeParameter(
basePath + nodeProperties.name,
) as string;
}
if (nodeProperties.routing.operations) {
returnData.requestOperations = { ...nodeProperties.routing.operations };
}
if (nodeProperties.routing.request) {
for (const key of Object.keys(nodeProperties.routing.request)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let propertyValue = (nodeProperties.routing.request as Record<string, any>)[key];
// If the value is an expression resolve it
propertyValue = this.getParameterValue(
propertyValue,
itemIndex,
runIndex,
{ ...additionalKeys, $value: parameterValue },
true,
) as string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(returnData.options as Record<string, any>)[key] = propertyValue;
}
}
if (nodeProperties.routing.send) {
let propertyName = nodeProperties.routing.send.property;
if (propertyName !== undefined) {
// If the propertyName is an expression resolve it
propertyName = this.getParameterValue(
propertyName,
itemIndex,
runIndex,
additionalKeys,
true,
) as string;
let value = parameterValue;
if (nodeProperties.routing.send.value) {
const valueString = nodeProperties.routing.send.value;
// Special value got set
// If the valueString is an expression resolve it
value = this.getParameterValue(
valueString,
itemIndex,
runIndex,
{ ...additionalKeys, $value: value },
true,
) as string;
}
if (nodeProperties.routing.send.type === 'body') {
// Send in "body"
// eslint-disable-next-line no-lonely-if
if (nodeProperties.routing.send.propertyInDotNotation === false) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(returnData.options.body as Record<string, any>)![propertyName] = value;
} else {
set(returnData.options.body as object, propertyName, value);
}
} else {
// Send in "query"
// eslint-disable-next-line no-lonely-if
if (nodeProperties.routing.send.propertyInDotNotation === false) {
returnData.options.qs![propertyName] = value;
} else {
set(returnData.options.qs as object, propertyName, value);
}
}
}
if (nodeProperties.routing.send.paginate !== undefined) {
let paginateValue = nodeProperties.routing.send.paginate;
if (typeof paginateValue === 'string' && paginateValue.charAt(0) === '=') {
// If the propertyName is an expression resolve it
paginateValue = this.getParameterValue(
paginateValue,
itemIndex,
runIndex,
{ ...additionalKeys, $value: parameterValue },
true,
) as string;
}
returnData.paginate = !!paginateValue;
}
if (nodeProperties.routing.send.preSend) {
returnData.preSend.push(...nodeProperties.routing.send.preSend);
}
}
if (nodeProperties.routing.output) {
if (nodeProperties.routing.output.maxResults !== undefined) {
let maxResultsValue = nodeProperties.routing.output.maxResults;
if (typeof maxResultsValue === 'string' && maxResultsValue.charAt(0) === '=') {
// If the propertyName is an expression resolve it
maxResultsValue = this.getParameterValue(
maxResultsValue,
itemIndex,
runIndex,
{ ...additionalKeys, $value: parameterValue },
true,
) as string;
}
returnData.maxResults = maxResultsValue;
}
if (nodeProperties.routing.output.postReceive) {
returnData.postReceive.push({
data: {
parameterValue,
},
actions: nodeProperties.routing.output.postReceive,
});
}
}
}
// Check if there are any child properties
if (!Object.prototype.hasOwnProperty.call(nodeProperties, 'options')) {
// There are none so nothing else to check
return returnData;
}
// Everything after this point can only be of type INodeProperties
nodeProperties = nodeProperties as INodeProperties;
// Check the child parameters
let value;
if (nodeProperties.type === 'options') {
const optionValue = NodeHelpers.getParameterValueByPath(
this.node.parameters,
nodeProperties.name,
basePath.slice(0, -1),
);
// Find the selected option
const selectedOption = (nodeProperties.options as INodePropertyOptions[]).filter(
(option) => option.value === optionValue,
);
if (selectedOption.length) {
// Check only if option is set and if of type INodeProperties
const tempOptions = this.getRequestOptionsFromParameters(
executeSingleFunctions,
selectedOption[0],
itemIndex,
runIndex,
`${basePath}${nodeProperties.name}`,
{ $value: optionValue },
);
this.mergeOptions(returnData, tempOptions);
}
} else if (nodeProperties.type === 'collection') {
value = NodeHelpers.getParameterValueByPath(
this.node.parameters,
nodeProperties.name,
basePath.slice(0, -1),
);
for (const propertyOption of nodeProperties.options as INodeProperties[]) {
if (
Object.keys(value as IDataObject).includes(propertyOption.name) &&
propertyOption.type !== undefined
) {
// Check only if option is set and if of type INodeProperties
const tempOptions = this.getRequestOptionsFromParameters(
executeSingleFunctions,
propertyOption,
itemIndex,
runIndex,
`${basePath}${nodeProperties.name}`,
);
this.mergeOptions(returnData, tempOptions);
}
}
} else if (nodeProperties.type === 'fixedCollection') {
basePath = `${basePath}${nodeProperties.name}.`;
for (const propertyOptions of nodeProperties.options as INodePropertyCollection[]) {
// Check if the option got set and if not skip it
value = NodeHelpers.getParameterValueByPath(
this.node.parameters,
propertyOptions.name,
basePath.slice(0, -1),
);
if (value === undefined) {
continue;
}
// Make sure that it is always an array to be able to use the same code for multi and single
if (!Array.isArray(value)) {
value = [value];
}
const loopBasePath = `${basePath}${propertyOptions.name}`;
for (let i = 0; i < (value as INodeParameters[]).length; i++) {
for (const option of propertyOptions.values) {
const tempOptions = this.getRequestOptionsFromParameters(
executeSingleFunctions,
option,
itemIndex,
runIndex,
nodeProperties.typeOptions?.multipleValues ? `${loopBasePath}[${i}]` : loopBasePath,
{ ...(additionalKeys || {}), $index: i, $parent: value[i] },
);
this.mergeOptions(returnData, tempOptions);
}
}
}
}
return returnData;
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
@@ -12,7 +13,7 @@
/* eslint-disable no-continue */
/* eslint-disable no-restricted-syntax */
/* eslint-disable import/no-cycle */
// eslint-disable-next-line import/no-cycle
import {
Expression,
IConnections,
@@ -39,6 +40,7 @@ import {
NodeHelpers,
NodeParameterValue,
ObservableObject,
RoutingNode,
WebhookSetupMethodNames,
WorkflowActivateMode,
WorkflowExecuteMode,
@@ -1078,7 +1080,11 @@ export class Workflow {
}
let connectionInputData: INodeExecutionData[] = [];
if (nodeType.execute || nodeType.executeSingle) {
if (
nodeType.execute ||
nodeType.executeSingle ||
(!nodeType.poll && !nodeType.trigger && !nodeType.webhook)
) {
// Only stop if first input is empty for execute & executeSingle runs. For all others run anyways
// because then it is a trigger node. As they only pass data through and so the input-data
// becomes output-data it has to be possible.
@@ -1217,6 +1223,19 @@ export class Workflow {
} else if (nodeType.webhook) {
// For webhook nodes always simply pass the data through
return inputData.main as INodeExecutionData[][];
} else {
// For nodes which have routing information on properties
const routingNode = new RoutingNode(
this,
node,
connectionInputData,
runExecutionData ?? null,
additionalData,
mode,
);
return routingNode.runNode(inputData, runIndex, nodeType, nodeExecuteFunctions);
}
return null;

View File

@@ -8,6 +8,7 @@ export * from './Interfaces';
export * from './Expression';
export * from './NodeErrors';
export * as TelemetryHelpers from './TelemetryHelpers';
export * from './RoutingNode';
export * from './Workflow';
export * from './WorkflowDataProxy';
export * from './WorkflowErrors';

View File

@@ -1,14 +1,526 @@
import get from 'lodash.get';
import {
CredentialInformation,
IAdditionalCredentialOptions,
IAllExecuteFunctions,
IContextObject,
ICredentialDataDecryptedObject,
ICredentials,
ICredentialsEncrypted,
ICredentialsHelper,
IDataObject,
IExecuteFunctions,
IExecuteResponsePromiseData,
IExecuteSingleFunctions,
IExecuteWorkflowInfo,
IHttpRequestOptions,
IN8nHttpFullResponse,
IN8nHttpResponse,
INode,
INodeCredentialsDetails,
INodeExecutionData,
INodeParameters,
INodeType,
INodeTypeData,
INodeTypes,
IRunExecutionData,
ITaskDataConnections,
IWorkflowBase,
IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData,
IWorkflowExecuteAdditionalData,
NodeHelpers,
NodeParameterValue,
Workflow,
WorkflowDataProxy,
WorkflowExecuteMode,
WorkflowHooks,
} from '../src';
export interface INodeTypesObject {
[key: string]: INodeType;
}
export class Credentials extends ICredentials {
hasNodeAccess(nodeType: string): boolean {
return true;
}
setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void {
this.data = JSON.stringify(data);
}
setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void {
let fullData;
try {
fullData = this.getData(encryptionKey);
} catch (e) {
fullData = {};
}
fullData[key] = data;
return this.setData(fullData, encryptionKey);
}
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
if (this.data === undefined) {
throw new Error('No data is set so nothing can be returned.');
}
return JSON.parse(this.data);
}
getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation {
const fullData = this.getData(encryptionKey, nodeType);
if (fullData === null) {
throw new Error(`No data was set.`);
}
// eslint-disable-next-line no-prototype-builtins
if (!fullData.hasOwnProperty(key)) {
throw new Error(`No data for key "${key}" exists.`);
}
return fullData[key];
}
getDataToSave(): ICredentialsEncrypted {
if (this.data === undefined) {
throw new Error(`No credentials were set to save.`);
}
return {
id: this.id,
name: this.name,
type: this.type,
data: this.data,
nodesAccess: this.nodesAccess,
};
}
}
export class CredentialsHelper extends ICredentialsHelper {
async authenticate(
credentials: ICredentialDataDecryptedObject,
typeName: string,
requestParams: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
return requestParams;
}
getParentTypes(name: string): string[] {
return [];
}
async getDecrypted(
nodeCredentials: INodeCredentialsDetails,
type: string,
): Promise<ICredentialDataDecryptedObject> {
return {};
}
async getCredentials(
nodeCredentials: INodeCredentialsDetails,
type: string,
): Promise<ICredentials> {
return new Credentials({ id: null, name: '' }, '', [], '');
}
async updateCredentials(
nodeCredentials: INodeCredentialsDetails,
type: string,
data: ICredentialDataDecryptedObject,
): Promise<void> {}
}
export function getNodeParameter(
workflow: Workflow,
runExecutionData: IRunExecutionData | null,
runIndex: number,
connectionInputData: INodeExecutionData[],
node: INode,
parameterName: string,
itemIndex: number,
mode: WorkflowExecuteMode,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
fallbackValue?: any,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object {
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new Error(`Node type "${node.type}" is not known so can not return paramter value!`);
}
const value = get(node.parameters, parameterName, fallbackValue);
if (value === undefined) {
throw new Error(`Could not get parameter "${parameterName}"!`);
}
let returnData;
try {
returnData = workflow.expression.getParameterValue(
value,
runExecutionData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
additionalKeys,
);
} catch (e) {
e.message += ` [Error in parameter: "${parameterName}"]`;
throw e;
}
return returnData;
}
export function getExecuteFunctions(
workflow: Workflow,
runExecutionData: IRunExecutionData,
runIndex: number,
connectionInputData: INodeExecutionData[],
inputData: ITaskDataConnections,
node: INode,
itemIndex: number,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
): IExecuteFunctions {
return ((workflow, runExecutionData, connectionInputData, inputData, node) => {
return {
continueOnFail: () => {
return false;
},
evaluateExpression: (expression: string, itemIndex: number) => {
return expression;
},
async executeWorkflow(
workflowInfo: IExecuteWorkflowInfo,
inputData?: INodeExecutionData[],
): Promise<any> {
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
},
getContext(type: string): IContextObject {
return NodeHelpers.getContext(runExecutionData, type, node);
},
async getCredentials(
type: string,
itemIndex?: number,
): Promise<ICredentialDataDecryptedObject | undefined> {
return {
apiKey: '12345',
};
},
getExecutionId: (): string => {
return additionalData.executionId!;
},
getInputData: (inputIndex = 0, inputName = 'main') => {
if (!inputData.hasOwnProperty(inputName)) {
// Return empty array because else it would throw error when nothing is connected to input
return [];
}
if (inputData[inputName].length < inputIndex) {
throw new Error(`Could not get input index "${inputIndex}" of input "${inputName}"!`);
}
if (inputData[inputName][inputIndex] === null) {
// return [];
throw new Error(`Value "${inputIndex}" of input "${inputName}" did not get set!`);
}
return inputData[inputName][inputIndex] as INodeExecutionData[];
},
getNodeParameter: (
parameterName: string,
itemIndex: number,
fallbackValue?: any,
):
| NodeParameterValue
| INodeParameters
| NodeParameterValue[]
| INodeParameters[]
| object => {
return getNodeParameter(
workflow,
runExecutionData,
runIndex,
connectionInputData,
node,
parameterName,
itemIndex,
mode,
{},
fallbackValue,
);
},
getMode: (): WorkflowExecuteMode => {
return mode;
},
getNode: () => {
return JSON.parse(JSON.stringify(node));
},
getRestApiUrl: (): string => {
return additionalData.restApiUrl;
},
getTimezone: (): string => {
return additionalData.timezone;
},
getWorkflow: () => {
return {
id: workflow.id,
name: workflow.name,
active: workflow.active,
};
},
getWorkflowDataProxy: (itemIndex: number): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(
workflow,
runExecutionData,
runIndex,
itemIndex,
node.name,
connectionInputData,
{},
mode,
{},
);
return dataProxy.getDataProxy();
},
getWorkflowStaticData(type: string): IDataObject {
return workflow.getStaticData(type, node);
},
prepareOutputData: NodeHelpers.prepareOutputData,
async putExecutionToWait(waitTill: Date): Promise<void> {
runExecutionData.waitTill = waitTill;
},
sendMessageToUI(...args: any[]): void {
if (mode !== 'manual') {
return;
}
try {
if (additionalData.sendMessageToUI) {
additionalData.sendMessageToUI(node.name, args);
}
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.error(`There was a problem sending messsage to UI: ${error.message}`);
}
},
async sendResponse(response: IExecuteResponsePromiseData): Promise<void> {
await additionalData.hooks?.executeHookFunctions('sendResponse', [response]);
},
helpers: {
async httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpFullResponse | IN8nHttpResponse> {
return {
body: {
headers: {},
statusCode: 200,
requestOptions,
},
};
},
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return {
body: {
headers: {},
statusCode: 200,
credentialsType,
requestOptions,
additionalCredentialOptions,
},
};
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return {
body: {
headers: {},
statusCode: 200,
credentialsType,
requestOptions,
additionalCredentialOptions,
},
};
},
},
};
})(workflow, runExecutionData, connectionInputData, inputData, node);
}
export function getExecuteSingleFunctions(
workflow: Workflow,
runExecutionData: IRunExecutionData,
runIndex: number,
connectionInputData: INodeExecutionData[],
inputData: ITaskDataConnections,
node: INode,
itemIndex: number,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
): IExecuteSingleFunctions {
return ((workflow, runExecutionData, connectionInputData, inputData, node, itemIndex) => {
return {
continueOnFail: () => {
return false;
},
evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => {
return expression;
},
getContext(type: string): IContextObject {
return NodeHelpers.getContext(runExecutionData, type, node);
},
async getCredentials(type: string): Promise<ICredentialDataDecryptedObject | undefined> {
return {
apiKey: '12345',
};
},
getInputData: (inputIndex = 0, inputName = 'main') => {
if (!inputData.hasOwnProperty(inputName)) {
// Return empty array because else it would throw error when nothing is connected to input
return { json: {} };
}
if (inputData[inputName].length < inputIndex) {
throw new Error(`Could not get input index "${inputIndex}" of input "${inputName}"!`);
}
const allItems = inputData[inputName][inputIndex];
if (allItems === null) {
// return [];
throw new Error(`Value "${inputIndex}" of input "${inputName}" did not get set!`);
}
if (allItems[itemIndex] === null) {
// return [];
throw new Error(
`Value "${inputIndex}" of input "${inputName}" with itemIndex "${itemIndex}" did not get set!`,
);
}
return allItems[itemIndex];
},
getMode: (): WorkflowExecuteMode => {
return mode;
},
getNode: () => {
return JSON.parse(JSON.stringify(node));
},
getRestApiUrl: (): string => {
return additionalData.restApiUrl;
},
getTimezone: (): string => {
return additionalData.timezone;
},
getNodeParameter: (
parameterName: string,
fallbackValue?: any,
):
| NodeParameterValue
| INodeParameters
| NodeParameterValue[]
| INodeParameters[]
| object => {
return getNodeParameter(
workflow,
runExecutionData,
runIndex,
connectionInputData,
node,
parameterName,
itemIndex,
mode,
{},
fallbackValue,
);
},
getWorkflow: () => {
return {
id: workflow.id,
name: workflow.name,
active: workflow.active,
};
},
getWorkflowDataProxy: (): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(
workflow,
runExecutionData,
runIndex,
itemIndex,
node.name,
connectionInputData,
{},
mode,
{},
);
return dataProxy.getDataProxy();
},
getWorkflowStaticData(type: string): IDataObject {
return workflow.getStaticData(type, node);
},
helpers: {
async httpRequest(
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpFullResponse | IN8nHttpResponse> {
return {
body: {
headers: {},
statusCode: 200,
requestOptions,
},
};
},
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return {
body: {
headers: {},
statusCode: 200,
credentialsType,
requestOptions,
additionalCredentialOptions,
},
};
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return {
body: {
headers: {},
statusCode: 200,
credentialsType,
requestOptions,
additionalCredentialOptions,
},
};
},
},
};
})(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex);
}
class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypeData = {
'test.set': {
@@ -120,3 +632,27 @@ export function NodeTypes(): NodeTypesClass {
return nodeTypesInstance;
}
export function WorkflowExecuteAdditionalData(): IWorkflowExecuteAdditionalData {
const workflowData: IWorkflowBase = {
name: '',
createdAt: new Date(),
updatedAt: new Date(),
active: true,
nodes: [],
connections: {},
};
return {
credentialsHelper: new CredentialsHelper(''),
hooks: new WorkflowHooks({}, 'trigger', '1', workflowData),
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
sendMessageToUI: (message: string) => {},
restApiUrl: '',
encryptionKey: 'test',
timezone: 'America/New_York',
webhookBaseUrl: 'webhook',
webhookWaitingBaseUrl: 'webhook-waiting',
webhookTestBaseUrl: 'webhook-test',
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
"lib": [
"dom",
"es2017"
"es2019"
],
"types": [
"node",
@@ -11,13 +11,14 @@
"module": "commonjs",
"removeComments": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"noImplicitReturns": true,
"strict": true,
"noUnusedLocals": true,
"preserveConstEnums": true,
"declaration": true,
"outDir": "./dist/",
"target": "es2017",
"target": "es2019",
"sourceMap": true
},
"include": [