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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 5074 additions and 360 deletions

77
package-lock.json generated
View file

@ -4,12 +4,12 @@
"lockfileVersion": 1,
"dependencies": {
"@ampproject/remapping": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.0.2.tgz",
"integrity": "sha512-sE8Gx+qSDMLoJvb3QarJJlDQK7SSY4rK3hxp4XsiANeFOmjU46ZI7Y9adAQRJrmbz8zbtZkp3mJTT+rGxtF0XA==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.0.3.tgz",
"integrity": "sha512-DmIAguV77yFP0MGVFWknCMgSLAtsLR3VlRTteR6xgMpIfYtwaZuMvjGv5YlpiqN7S/5q87DHyuIx8oa15kiyag==",
"requires": {
"@jridgewell/trace-mapping": "^0.2.2",
"sourcemap-codec": "1.4.8"
"@jridgewell/sourcemap-codec": "^1.4.9",
"@jridgewell/trace-mapping": "^0.2.7"
}
},
"@azure/abort-controller": {
@ -1935,9 +1935,9 @@
}
},
"@fontsource/open-sans": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.5.3.tgz",
"integrity": "sha512-zabYpvz2XkZ4Vp1EN2/k0r5X9kQgwjdj1+kJ6B0T/oN4h9yqJqr9VKxa+JspRxClxDEo23K5GqfuIEH1+WyFOw=="
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.5.4.tgz",
"integrity": "sha512-iaEuU7l3VGA/bqWW9UsBD2bgFwCwDFwKlmOUft4Jps3pD3Zc9POMNYV0+mNyKbA4OIcIice32l+BMif8vY6pdg=="
},
"@fortawesome/fontawesome-common-types": {
"version": "0.2.36",
@ -4049,13 +4049,18 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz",
"integrity": "sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg=="
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.10.tgz",
"integrity": "sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg=="
},
"@jridgewell/trace-mapping": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.2.6.tgz",
"integrity": "sha512-rVJf5dSMEBxnDEwtAT5x8+p6tZ+xU6Ocm+cR1MYL2gMsRi4MMzVf9Pvq6JaxIsEeKAyYmo2U+yPQN4QfdTfFnA==",
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.2.7.tgz",
"integrity": "sha512-ZKfRhw6eK2vvdWqpU7DQq49+BZESqh5rmkYpNhuzkz01tapssl2sNNy6uMUIgrTtUWQDijomWJzJRCoevVrfgw==",
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"sourcemap-codec": "1.4.8"
"@jridgewell/sourcemap-codec": "^1.4.9"
}
},
"@kafkajs/confluent-schema-registry": {
@ -12292,9 +12297,9 @@
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w=="
},
"@types/cheerio": {
"version": "0.22.30",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.30.tgz",
"integrity": "sha512-t7ZVArWZlq3dFa9Yt33qFBQIK4CQd1Q3UJp0V+UhP6vgLWLM6Qug7vZuRSGXg45zXeB1Fm5X2vmBkEX58LV2Tw==",
"version": "0.22.31",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.31.tgz",
"integrity": "sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==",
"requires": {
"@types/node": "*"
}
@ -12602,6 +12607,14 @@
"@types/lodash": "*"
}
},
"@types/lodash.merge": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.6.tgz",
"integrity": "sha512-IB90krzMf7YpfgP3u/EvZEdXVvm4e3gJbUvh5ieuI+o+XqiNEt6fCzqNRaiLlPVScLI59RxIGZMQ3+Ko/DJ8vQ==",
"requires": {
"@types/lodash": "*"
}
},
"@types/lodash.set": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.6.tgz",
@ -15858,9 +15871,9 @@
"integrity": "sha512-uUbetCWczQHbsKyX1C99XpQHBM8SWfovvaZhPIj23/1uV7SQf0WeRZbiLpw0JZm+LHTChfNgrLfDJOVoU2kU+A=="
},
"aws-sdk": {
"version": "2.1068.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1068.0.tgz",
"integrity": "sha512-lD8JaEVSDueoTdhwxYinkZKuCzsqCE1L6+NZhO1AVdwgtK62pzjU20VHX2K39+Y2XebeAO2QzD+32m0ROHAeZg==",
"version": "2.1069.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1069.0.tgz",
"integrity": "sha512-AF7/5JotrVd8g/D3WWHgQto+IryB1V7iudIYm+H+qxmkGOU3xvL63ChhEoLTY/CxuK/diayg0oWILEsXUn3dfw==",
"requires": {
"buffer": "4.9.2",
"events": "1.1.1",
@ -21284,9 +21297,9 @@
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
},
"electron-to-chromium": {
"version": "1.4.64",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.64.tgz",
"integrity": "sha512-8mec/99xgLUZCIZZq3wt61Tpxg55jnOSpxGYapE/1Ma9MpFEYYaz4QNYm0CM1rrnCo7i3FRHhbaWjeCLsveGjQ=="
"version": "1.4.65",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.65.tgz",
"integrity": "sha512-0/d8Skk8sW3FxXP0Dd6MnBlrwx7Qo9cqQec3BlIAlvKnrmS3pHsIbaroEi+nd0kZkGpQ6apMEre7xndzjlEnLw=="
},
"element-resize-detector": {
"version": "1.2.4",
@ -27805,9 +27818,9 @@
}
},
"istanbul-reports": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.3.tgz",
"integrity": "sha512-x9LtDVtfm/t1GFiLl3NffC7hz+I1ragvgX1P/Lg1NlIagifZDKUkuuaAxH/qpwj2IuEfD8G2Bs/UKp+sZ/pKkg==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz",
"integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==",
"requires": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
@ -40357,9 +40370,9 @@
},
"dependencies": {
"ajv": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz",
"integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==",
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz",
"integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@ -43624,13 +43637,13 @@
}
},
"winston-transport": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.2.tgz",
"integrity": "sha512-9jmhltAr5ygt5usgUTQbEiw/7RYXpyUbEAFRCSicIacpUzPkrnQsQZSPGEI12aLK9Jth4zNcYJx3Cvznwrl8pw==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
"requires": {
"logform": "^2.3.2",
"readable-stream": "^3.4.0",
"triple-beam": "^1.2.0"
"readable-stream": "^3.6.0",
"triple-beam": "^1.3.0"
}
},
"with": {

View file

@ -102,6 +102,7 @@ export class Execute extends Command {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
workflowId = workflowData.id ? workflowData.id.toString() : PLACEHOLDER_EMPTY_WORKFLOW_ID;
}

View file

@ -66,11 +66,13 @@
"@types/jest": "^26.0.13",
"@types/localtunnel": "^1.9.0",
"@types/lodash.get": "^4.4.6",
"@types/lodash.merge": "^4.6.6",
"@types/node": "14.17.27",
"@types/open": "^6.1.0",
"@types/parseurl": "^1.3.1",
"@types/request-promise-native": "~1.0.15",
"@types/validator": "^13.7.0",
"axios": "^0.21.1",
"concurrently": "^5.1.0",
"jest": "^26.4.2",
"nodemon": "^2.0.2",
@ -110,6 +112,7 @@
"jwks-rsa": "~1.12.1",
"localtunnel": "^2.0.0",
"lodash.get": "^4.4.2",
"lodash.merge": "^4.6.2",
"mysql2": "~2.3.0",
"n8n-core": "~0.103.0",
"n8n-editor-ui": "~0.128.0",

View file

@ -1,21 +1,22 @@
import { ICredentialType, ICredentialTypes as ICredentialTypesInterface } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import { ICredentialsTypeData } from '.';
import {
ICredentialType,
ICredentialTypeData,
ICredentialTypes as ICredentialTypesInterface,
} from 'n8n-workflow';
class CredentialTypesClass implements ICredentialTypesInterface {
credentialTypes: ICredentialsTypeData = {};
credentialTypes: ICredentialTypeData = {};
async init(credentialTypes: ICredentialsTypeData): Promise<void> {
async init(credentialTypes: ICredentialTypeData): Promise<void> {
this.credentialTypes = credentialTypes;
}
getAll(): ICredentialType[] {
return Object.values(this.credentialTypes);
return Object.values(this.credentialTypes).map((data) => data.type);
}
getByName(credentialType: string): ICredentialType {
return this.credentialTypes[credentialType];
return this.credentialTypes[credentialType].type;
}
}

View file

@ -1,42 +1,217 @@
import { Credentials } from 'n8n-core';
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
import { NodeVersionedType } from 'n8n-nodes-base';
import {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialsExpressionResolveValues,
ICredentialsHelper,
ICredentialTestFunction,
ICredentialTestRequestData,
IHttpRequestOptions,
INode,
INodeCredentialsDetails,
INodeCredentialTestResult,
INodeExecutionData,
INodeParameters,
INodeProperties,
INodeType,
INodeTypeData,
INodeTypes,
INodeVersionedType,
IRequestOptionsSimplified,
IRunExecutionData,
IWorkflowDataProxyAdditionalKeys,
NodeHelpers,
RoutingNode,
Workflow,
WorkflowExecuteMode,
ITaskDataConnections,
} from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import { CredentialsOverwrites, CredentialTypes, Db, ICredentialsDb } from '.';
import {
CredentialsOverwrites,
CredentialTypes,
Db,
ICredentialsDb,
NodeTypes,
WorkflowExecuteAdditionalData,
} from '.';
const mockNodeTypes: INodeTypes = {
nodeTypes: {},
nodeTypes: {} as INodeTypeData,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
init: async (nodeTypes?: INodeTypeData): Promise<void> => {},
getAll: (): INodeType[] => {
// Does not get used in Workflow so no need to return it
return [];
getAll(): Array<INodeType | INodeVersionedType> {
// @ts-ignore
return Object.values(this.nodeTypes).map((data) => data.type);
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getByName: (nodeType: string): INodeType | undefined => {
return undefined;
getByName(nodeType: string): INodeType | INodeVersionedType | undefined {
if (this.nodeTypes[nodeType] === undefined) {
return undefined;
}
return this.nodeTypes[nodeType].type;
},
getByNameAndVersion: (): INodeType | undefined => {
return undefined;
getByNameAndVersion(nodeType: string, version?: number): INodeType | undefined {
if (this.nodeTypes[nodeType] === undefined) {
return undefined;
}
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
},
};
export class CredentialsHelper extends ICredentialsHelper {
private credentialTypes = CredentialTypes();
/**
* Add the required authentication information to the request
*/
async authenticate(
credentials: ICredentialDataDecryptedObject,
typeName: string,
incomingRequestOptions: IHttpRequestOptions | IRequestOptionsSimplified,
workflow: Workflow,
node: INode,
): Promise<IHttpRequestOptions> {
const requestOptions = incomingRequestOptions;
const credentialType = this.credentialTypes.getByName(typeName);
if (credentialType.authenticate) {
if (typeof credentialType.authenticate === 'function') {
// Special authentication function is defined
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
return credentialType.authenticate(credentials, requestOptions as IHttpRequestOptions);
}
if (typeof credentialType.authenticate === 'object') {
// Predefined authentication method
const { authenticate } = credentialType;
if (requestOptions.headers === undefined) {
requestOptions.headers = {};
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (authenticate.type === 'bearer') {
const tokenPropertyName: string =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
authenticate.properties.tokenPropertyName ?? 'accessToken';
requestOptions.headers.Authorization = `Bearer ${
credentials[tokenPropertyName] as string
}`;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} else if (authenticate.type === 'basicAuth') {
const userPropertyName: string =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
authenticate.properties.userPropertyName ?? 'user';
const passwordPropertyName: string =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
authenticate.properties.passwordPropertyName ?? 'password';
requestOptions.auth = {
username: credentials[userPropertyName] as string,
password: credentials[passwordPropertyName] as string,
};
} else if (authenticate.type === 'headerAuth') {
const key = this.resolveValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
authenticate.properties.name,
{ $credentials: credentials },
workflow,
node,
);
const value = this.resolveValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
authenticate.properties.value,
{ $credentials: credentials },
workflow,
node,
);
requestOptions.headers[key] = value;
} else if (authenticate.type === 'queryAuth') {
const key = this.resolveValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
authenticate.properties.key,
{ $credentials: credentials },
workflow,
node,
);
const value = this.resolveValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
authenticate.properties.value,
{ $credentials: credentials },
workflow,
node,
);
if (!requestOptions.qs) {
requestOptions.qs = {};
}
requestOptions.qs[key] = value;
}
}
}
return requestOptions as IHttpRequestOptions;
}
/**
* Resolves the given value in case it is an expression
*/
resolveValue(
parameterValue: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
workflow: Workflow,
node: INode,
): string {
if (parameterValue.charAt(0) !== '=') {
return parameterValue;
}
const returnValue = workflow.expression.getSimpleParameterValue(
node,
parameterValue,
'internal',
additionalKeys,
'',
);
if (!returnValue) {
return '';
}
return returnValue.toString();
}
/**
* Returns all parent types of the given credential type
*/
getParentTypes(typeName: string): string[] {
const credentialType = this.credentialTypes.getByName(typeName);
if (credentialType === undefined || credentialType.extends === undefined) {
return [];
}
let types: string[] = [];
credentialType.extends.forEach((type: string) => {
types = [...types, typeName, ...this.getParentTypes(type)];
});
return types;
}
/**
* Returns the credentials instance
*
@ -77,8 +252,7 @@ export class CredentialsHelper extends ICredentialsHelper {
* @memberof CredentialsHelper
*/
getCredentialsProperties(type: string): INodeProperties[] {
const credentialTypes = CredentialTypes();
const credentialTypeData = credentialTypes.getByName(type);
const credentialTypeData = this.credentialTypes.getByName(type);
if (credentialTypeData === undefined) {
throw new Error(`The credentials of type "${type}" are not known.`);
@ -89,7 +263,6 @@ export class CredentialsHelper extends ICredentialsHelper {
}
const combineProperties = [] as INodeProperties[];
// eslint-disable-next-line no-restricted-syntax
for (const credentialsTypeName of credentialTypeData.extends) {
const mergeCredentialProperties = this.getCredentialsProperties(credentialsTypeName);
NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
@ -260,4 +433,229 @@ export class CredentialsHelper extends ICredentialsHelper {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await Db.collections.Credentials!.update(findQuery, newCredentialsData);
}
getCredentialTestFunction(
credentialType: string,
nodeToTestWith?: string,
): ICredentialTestFunction | ICredentialTestRequestData | undefined {
const nodeTypes = NodeTypes();
const allNodes = nodeTypes.getAll();
// Check all the nodes one by one if they have a test function defined
for (let i = 0; i < allNodes.length; i++) {
const node = allNodes[i];
if (nodeToTestWith && node.description.name !== nodeToTestWith) {
// eslint-disable-next-line no-continue
continue;
}
// Always set to an array even if node is not versioned to not having
// to duplicate the logic
const allNodeTypes: INodeType[] = [];
if (node instanceof NodeVersionedType) {
// Node is versioned
allNodeTypes.push(...Object.values((node as INodeVersionedType).nodeVersions));
} else {
// Node is not versioned
allNodeTypes.push(node as INodeType);
}
// Check each of the node versions for credential tests
for (const nodeType of allNodeTypes) {
// Check each of teh credentials
for (const credential of nodeType.description.credentials ?? []) {
if (credential.name === credentialType && !!credential.testedBy) {
if (typeof credential.testedBy === 'string') {
// Test is defined as string which links to a functoin
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return (node as unknown as INodeType).methods?.credentialTest![credential.testedBy];
}
// Test is defined as JSON with a defintion for the request to make
return {
nodeType,
testRequest: credential.testedBy,
};
}
}
}
}
// Check if test is defined on credentials
const type = this.credentialTypes.getByName(credentialType);
if (type.test) {
return {
testRequest: type.test,
};
}
return undefined;
}
async testCredentials(
credentialType: string,
credentialsDecrypted: ICredentialsDecrypted,
nodeToTestWith?: string,
): Promise<INodeCredentialTestResult> {
const credentialTestFunction = this.getCredentialTestFunction(credentialType, nodeToTestWith);
if (credentialTestFunction === undefined) {
return Promise.resolve({
status: 'Error',
message: 'No testing function found for this credential.',
});
}
if (typeof credentialTestFunction === 'function') {
// The credentials get tested via a function that is defined on the node
const credentialTestFunctions = NodeExecuteFunctions.getCredentialTestFunctions();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
return credentialTestFunction.call(credentialTestFunctions, credentialsDecrypted);
}
// Credentials get tested via request instructions
// TODO: Temp worfklows get created at multiple locations (for example also LoadNodeParameterOptions),
// check if some of them are identical enough that it can be combined
let nodeType: INodeType;
if (credentialTestFunction.nodeType) {
nodeType = credentialTestFunction.nodeType;
} else {
const nodeTypes = NodeTypes();
nodeType = nodeTypes.getByName('n8n-nodes-base.noOp') as INodeType;
}
const node: INode = {
parameters: {},
name: 'Temp-Node',
type: nodeType.description.name,
typeVersion: nodeType.description.version,
position: [0, 0],
};
const workflowData = {
nodes: [node],
connections: {},
};
const nodeTypeCopy: INodeType = {
description: {
...nodeType.description,
credentials: [
{
name: credentialType,
required: true,
},
],
properties: [
{
displayName: 'Temp',
name: 'temp',
type: 'string',
routing: {
request: credentialTestFunction.testRequest.request,
},
default: '',
},
],
},
};
const nodeTypes: INodeTypes = {
...mockNodeTypes,
nodeTypes: {
[nodeTypeCopy.description.name]: {
sourcePath: '',
type: nodeTypeCopy,
},
},
};
const workflow = new Workflow({
nodes: workflowData.nodes,
connections: workflowData.connections,
active: false,
nodeTypes,
});
const mode = 'internal';
const runIndex = 0;
const inputData: ITaskDataConnections = {
main: [[{ json: {} }]],
};
const connectionInputData: INodeExecutionData[] = [];
const runExecutionData: IRunExecutionData = {
resultData: {
runData: {},
},
};
const additionalData = await WorkflowExecuteAdditionalData.getBase(node.parameters);
const routingNode = new RoutingNode(
workflow,
node,
connectionInputData,
runExecutionData ?? null,
additionalData,
mode,
);
try {
await routingNode.runNode(
inputData,
runIndex,
nodeTypeCopy,
NodeExecuteFunctions,
credentialsDecrypted,
);
} catch (error) {
// Do not fail any requests to allow custom error messages and
// make logic easier
if (error.cause.response) {
const errorResponseData = {
statusCode: error.cause.response.status,
statusMessage: error.cause.response.statusText,
};
if (credentialTestFunction.testRequest.rules) {
// Special testing rules are defined so check all in order
for (const rule of credentialTestFunction.testRequest.rules) {
if (rule.type === 'responseCode') {
if (errorResponseData.statusCode === rule.properties.value) {
return {
status: 'Error',
message: rule.properties.message,
};
}
}
}
}
if (errorResponseData.statusCode < 199 || errorResponseData.statusCode > 299) {
// All requests with response codes that are not 2xx are treated by default as failed
return {
status: 'Error',
message:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
errorResponseData.statusMessage ||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Received HTTP status code: ${errorResponseData.statusCode}`,
};
}
}
return {
status: 'Error',
message: error.message.toString(),
};
}
return {
status: 'OK',
message: 'Connection successful!',
};
}
}

View file

@ -5,7 +5,6 @@ import {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialsEncrypted,
ICredentialType,
IDataObject,
IDeferredPromise,
IExecuteResponsePromiseData,
@ -57,7 +56,10 @@ export interface ICustomRequest extends Request {
}
export interface ICredentialsTypeData {
[key: string]: ICredentialType;
[key: string]: {
className: string;
sourcePath: string;
};
}
export interface ICredentialsOverwrite {

View file

@ -12,6 +12,7 @@ import { CUSTOM_EXTENSION_ENV, UserSettings } from 'n8n-core';
import {
CodexData,
ICredentialType,
ICredentialTypeData,
ILogger,
INodeType,
INodeTypeData,
@ -35,9 +36,7 @@ const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
class LoadNodesAndCredentialsClass {
nodeTypes: INodeTypeData = {};
credentialTypes: {
[key: string]: ICredentialType;
} = {};
credentialTypes: ICredentialTypeData = {};
excludeNodes: string[] | undefined = undefined;
@ -170,7 +169,10 @@ class LoadNodesAndCredentialsClass {
}
}
this.credentialTypes[tempCredential.name] = tempCredential;
this.credentialTypes[tempCredential.name] = {
type: tempCredential,
sourcePath: filePath,
};
}
/**

View file

@ -28,7 +28,7 @@
/* eslint-disable no-await-in-loop */
import * as express from 'express';
import { readFileSync, existsSync } from 'fs';
import { readFileSync } from 'fs';
import { readFile } from 'fs/promises';
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
import { FindManyOptions, getConnectionManager, In, IsNull, LessThanOrEqual, Not } from 'typeorm';
@ -52,37 +52,30 @@ import {
BinaryDataManager,
Credentials,
IBinaryDataConfig,
ICredentialTestFunctions,
LoadNodeParameterOptions,
NodeExecuteFunctions,
UserSettings,
} from 'n8n-core';
import {
ICredentialsDecrypted,
ICredentialType,
IDataObject,
INodeCredentials,
INodeCredentialsDetails,
INodeCredentialTestRequest,
INodeCredentialTestResult,
INodeParameters,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
INodeTypeNameVersion,
INodeVersionedType,
ITelemetrySettings,
IWorkflowBase,
LoggerProxy,
NodeCredentialTestRequest,
NodeCredentialTestResult,
NodeHelpers,
Workflow,
ICredentialsEncrypted,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { NodeVersionedType } from 'n8n-nodes-base';
import * as basicAuth from 'basic-auth';
import * as compression from 'compression';
import * as jwt from 'jsonwebtoken';
@ -1122,7 +1115,6 @@ class App {
if (req.query.credentials !== undefined) {
credentials = JSON.parse(req.query.credentials as string);
}
const methodName = req.query.methodName as string;
const nodeTypes = NodeTypes();
@ -1137,7 +1129,20 @@ class App {
const additionalData = await WorkflowExecuteAdditionalData.getBase(currentNodeParameters);
return loadDataInstance.getOptions(methodName, additionalData);
if (req.query.methodName) {
return loadDataInstance.getOptionsViaMethodName(
req.query.methodName as string,
additionalData,
);
}
if (req.query.loadOptions) {
return loadDataInstance.getOptionsViaRequestProperty(
JSON.parse(req.query.loadOptions as string),
additionalData,
);
}
return [];
},
),
);
@ -1433,87 +1438,25 @@ class App {
this.app.post(
`/${this.restEndpoint}/credentials-test`,
ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<NodeCredentialTestResult> => {
const incomingData = req.body as NodeCredentialTestRequest;
async (req: express.Request, res: express.Response): Promise<INodeCredentialTestResult> => {
const incomingData = req.body as INodeCredentialTestRequest;
const encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) {
return {
status: 'Error',
message: 'No encryption key got found to decrypt the credentials!',
};
}
const credentialsHelper = new CredentialsHelper(encryptionKey);
const credentialType = incomingData.credentials.type;
// Find nodes that can test this credential.
const nodeTypes = NodeTypes();
const allNodes = nodeTypes.getAll();
let foundTestFunction:
| ((
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
) => Promise<NodeCredentialTestResult>)
| undefined;
const nodeThatCanTestThisCredential = allNodes.find((node) => {
if (
incomingData.nodeToTestWith &&
node.description.name !== incomingData.nodeToTestWith
) {
return false;
}
if (node instanceof NodeVersionedType) {
const versionNames = Object.keys((node as INodeVersionedType).nodeVersions);
for (const versionName of versionNames) {
const nodeType = (node as INodeVersionedType).nodeVersions[
versionName as unknown as number
];
// eslint-disable-next-line @typescript-eslint/no-loop-func
const credentialTestable = nodeType.description.credentials?.find((credential) => {
const testFunctionSearch =
credential.name === credentialType && !!credential.testedBy;
if (testFunctionSearch) {
foundTestFunction = (nodeType as unknown as INodeType).methods!.credentialTest![
credential.testedBy!
];
}
return testFunctionSearch;
});
if (credentialTestable) {
return true;
}
}
return false;
}
const credentialTestable = (node as INodeType).description.credentials?.find(
(credential) => {
const testFunctionSearch =
credential.name === credentialType && !!credential.testedBy;
if (testFunctionSearch) {
foundTestFunction = (node as INodeType).methods!.credentialTest![
credential.testedBy!
];
}
return testFunctionSearch;
},
);
return !!credentialTestable;
});
if (!nodeThatCanTestThisCredential) {
return Promise.resolve({
status: 'Error',
message: 'There are no nodes that can test this credential.',
});
}
if (foundTestFunction === undefined) {
return Promise.resolve({
status: 'Error',
message: 'No testing function found for this credential.',
});
}
const credentialTestFunctions = NodeExecuteFunctions.getCredentialTestFunctions();
const output = await foundTestFunction.call(
credentialTestFunctions,
return credentialsHelper.testCredentials(
credentialType,
incomingData.credentials,
incomingData.nodeToTestWith,
);
return Promise.resolve(output);
},
),
);

View file

@ -16,8 +16,6 @@ import {
IRun,
IRunExecutionData,
ITaskData,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IWorkflowCredentials,
LoggerProxy as Logger,
Workflow,
} from 'n8n-workflow';
@ -32,8 +30,6 @@ import {
IWorkflowExecutionDataProcess,
NodeTypes,
ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowRunner,
} from '.';
@ -211,6 +207,32 @@ export function getAllNodeTypeData(): ITransferNodeTypes {
return returnData;
}
/**
* Returns all the defined CredentialTypes
*
* @export
* @returns {ICredentialsTypeData}
*/
export function getAllCredentalsTypeData(): ICredentialsTypeData {
const credentialTypes = CredentialTypes();
// Get the data of all the credential types that they
// can be loaded again in the subprocess
const returnData: ICredentialsTypeData = {};
for (const credentialTypeName of Object.keys(credentialTypes.credentialTypes)) {
if (credentialTypes.credentialTypes[credentialTypeName] === undefined) {
throw new Error(`The CredentialType "${credentialTypeName}" could not be found!`);
}
returnData[credentialTypeName] = {
className: credentialTypes.credentialTypes[credentialTypeName].type.constructor.name,
sourcePath: credentialTypes.credentialTypes[credentialTypeName].sourcePath,
};
}
return returnData;
}
/**
* Returns the data of the node types that are needed
* to execute the given nodes
@ -256,7 +278,10 @@ export function getCredentialsDataWithParents(type: string): ICredentialsTypeDat
const credentialType = credentialTypes.getByName(type);
const credentialTypeData: ICredentialsTypeData = {};
credentialTypeData[type] = credentialType;
credentialTypeData[type] = {
className: credentialTypes.credentialTypes[type].type.constructor.name,
sourcePath: credentialTypes.credentialTypes[type].sourcePath,
};
if (credentialType === undefined || credentialType.extends === undefined) {
return credentialTypeData;
@ -267,7 +292,10 @@ export function getCredentialsDataWithParents(type: string): ICredentialsTypeDat
continue;
}
credentialTypeData[typeName] = credentialTypes.getByName(typeName);
credentialTypeData[typeName] = {
className: credentialTypes.credentialTypes[typeName].type.constructor.name,
sourcePath: credentialTypes.credentialTypes[typeName].sourcePath,
};
Object.assign(credentialTypeData, getCredentialsDataWithParents(typeName));
}

View file

@ -592,7 +592,7 @@ export class WorkflowRunner {
// be needed and so have to load all of them in the workflowRunnerProcess
let loadAllNodeTypes = false;
for (const node of data.workflowData.nodes) {
if (node.type === 'n8n-nodes-base.executeWorkflow') {
if (node.type === 'n8n-nodes-base.executeWorkflow' && node.disabled !== true) {
loadAllNodeTypes = true;
break;
}
@ -604,8 +604,7 @@ export class WorkflowRunner {
if (loadAllNodeTypes) {
// Supply all nodeTypes and credentialTypes
nodeTypeData = WorkflowHelpers.getAllNodeTypeData();
const credentialTypes = CredentialTypes();
credentialTypeData = credentialTypes.credentialTypes;
credentialTypeData = WorkflowHelpers.getAllCredentalsTypeData();
} else {
// Supply only nodeTypes, credentialTypes and overwrites that the workflow needs
nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes);

View file

@ -15,6 +15,8 @@ import {
import {
ExecutionError,
ICredentialType,
ICredentialTypeData,
IDataObject,
IExecuteResponsePromiseData,
IExecuteWorkflowInfo,
@ -94,10 +96,12 @@ export class WorkflowRunnerProcess {
let className: string;
let tempNode: INodeType;
let tempCredential: ICredentialType;
let filePath: string;
this.startedAt = new Date();
// Load the required nodes
const nodeTypesData: INodeTypeData = {};
// eslint-disable-next-line no-restricted-syntax
for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) {
@ -131,9 +135,32 @@ export class WorkflowRunnerProcess {
const nodeTypes = NodeTypes();
await nodeTypes.init(nodeTypesData);
// Load the required credentials
const credentialsTypeData: ICredentialTypeData = {};
// eslint-disable-next-line no-restricted-syntax
for (const credentialTypeName of Object.keys(this.data.credentialsTypeData)) {
className = this.data.credentialsTypeData[credentialTypeName].className;
filePath = this.data.credentialsTypeData[credentialTypeName].sourcePath;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
const tempModule = require(filePath);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
tempCredential = new tempModule[className]() as ICredentialType;
} catch (error) {
throw new Error(`Error loading credential "${credentialTypeName}" from: "${filePath}"`);
}
credentialsTypeData[credentialTypeName] = {
type: tempCredential,
sourcePath: filePath,
};
}
// Init credential types the workflow uses (is needed to apply default values to credentials)
const credentialTypes = CredentialTypes();
await credentialTypes.init(inputData.credentialsTypeData);
await credentialTypes.init(credentialsTypeData);
// Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites();

View file

@ -0,0 +1,305 @@
import { CredentialsHelper, CredentialTypes } from '../src';
import * as Helpers from './Helpers';
import {
IAuthenticateBasicAuth,
IAuthenticateBearer,
IAuthenticateHeaderAuth,
IAuthenticateQueryAuth,
ICredentialDataDecryptedObject,
ICredentialType,
ICredentialTypeData,
IHttpRequestOptions,
INode,
INodeProperties,
Workflow,
} from 'n8n-workflow';
const TEST_ENCRYPTION_KEY = 'test';
describe('CredentialsHelper', () => {
describe('authenticate', () => {
const tests: Array<{
description: string;
input: {
credentials: ICredentialDataDecryptedObject;
credentialType: ICredentialType;
};
output: IHttpRequestOptions;
}> = [
{
description: 'built-in basicAuth, default property names',
input: {
credentials: {
user: 'user1',
password: 'password1',
},
credentialType: new (class TestApi implements ICredentialType {
name = 'testApi';
displayName = 'Test API';
properties: INodeProperties[] = [
{
displayName: 'User',
name: 'user',
type: 'string',
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string',
default: '',
},
];
authenticate = {
type: 'basicAuth',
properties: {},
} as IAuthenticateBasicAuth;
})(),
},
output: {
url: '',
headers: {},
auth: { username: 'user1', password: 'password1' },
qs: {},
},
},
{
description: 'built-in basicAuth, custom property names',
input: {
credentials: {
customUser: 'user2',
customPassword: 'password2',
},
credentialType: new (class TestApi implements ICredentialType {
name = 'testApi';
displayName = 'Test API';
properties: INodeProperties[] = [
{
displayName: 'User',
name: 'user',
type: 'string',
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string',
default: '',
},
];
authenticate = {
type: 'basicAuth',
properties: {
userPropertyName: 'customUser',
passwordPropertyName: 'customPassword',
},
} as IAuthenticateBasicAuth;
})(),
},
output: {
url: '',
headers: {},
auth: { username: 'user2', password: 'password2' },
qs: {},
},
},
{
description: 'built-in headerAuth',
input: {
credentials: {
accessToken: 'test',
},
credentialType: new (class TestApi implements ICredentialType {
name = 'testApi';
displayName = 'Test API';
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string',
default: '',
},
];
authenticate = {
type: 'headerAuth',
properties: {
name: 'Authorization',
value: '=Bearer {{$credentials.accessToken}}',
},
} as IAuthenticateHeaderAuth;
})(),
},
output: { url: '', headers: { Authorization: 'Bearer test' }, qs: {} },
},
{
description: 'built-in bearer, default property name',
input: {
credentials: {
accessToken: 'test',
},
credentialType: new (class TestApi implements ICredentialType {
name = 'testApi';
displayName = 'Test API';
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string',
default: '',
},
];
authenticate = {
type: 'bearer',
properties: {},
} as IAuthenticateBearer;
})(),
},
output: { url: '', headers: { Authorization: 'Bearer test' }, qs: {} },
},
{
description: 'built-in bearer, custom property name',
input: {
credentials: {
myToken: 'test',
},
credentialType: new (class TestApi implements ICredentialType {
name = 'testApi';
displayName = 'Test API';
properties: INodeProperties[] = [
{
displayName: 'My Token',
name: 'myToken',
type: 'string',
default: '',
},
];
authenticate = {
type: 'bearer',
properties: {
tokenPropertyName: 'myToken',
},
} as IAuthenticateBearer;
})(),
},
output: { url: '', headers: { Authorization: 'Bearer test' }, qs: {} },
},
{
description: 'built-in queryAuth',
input: {
credentials: {
accessToken: 'test',
},
credentialType: new (class TestApi implements ICredentialType {
name = 'testApi';
displayName = 'Test API';
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string',
default: '',
},
];
authenticate = {
type: 'queryAuth',
properties: {
key: 'accessToken',
value: '={{$credentials.accessToken}}',
},
} as IAuthenticateQueryAuth;
})(),
},
output: { url: '', headers: {}, qs: { accessToken: 'test' } },
},
{
description: 'custom authentication',
input: {
credentials: {
accessToken: 'test',
user: 'testUser',
},
credentialType: new (class TestApi implements ICredentialType {
name = 'testApi';
displayName = 'Test API';
properties: INodeProperties[] = [
{
displayName: 'My Token',
name: 'myToken',
type: 'string',
default: '',
},
];
async authenticate(
credentials: ICredentialDataDecryptedObject,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
requestOptions.headers!['Authorization'] = `Bearer ${credentials.accessToken}`;
requestOptions.qs!['user'] = credentials.user;
return requestOptions;
}
})(),
},
output: {
url: '',
headers: { Authorization: 'Bearer test' },
qs: { user: 'testUser' },
},
},
];
const node: INode = {
parameters: {},
name: 'test',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
};
const incomingRequestOptions = {
url: '',
headers: {},
qs: {},
};
const nodeTypes = Helpers.NodeTypes();
const workflow = new Workflow({
nodes: [node],
connections: {},
active: false,
nodeTypes,
});
for (const testData of tests) {
test(testData.description, async () => {
const credentialTypes: ICredentialTypeData = {
[testData.input.credentialType.name]: {
type: testData.input.credentialType,
sourcePath: '',
},
};
await CredentialTypes().init(credentialTypes);
const credentialsHelper = new CredentialsHelper(TEST_ENCRYPTION_KEY);
const result = await credentialsHelper.authenticate(
testData.input.credentials,
testData.input.credentialType.name,
JSON.parse(JSON.stringify(incomingRequestOptions)),
workflow,
node,
);
expect(result).toEqual(testData.output);
});
}
});
});

View file

@ -0,0 +1,63 @@
import { INodeType, INodeTypeData, INodeTypes, NodeHelpers } from 'n8n-workflow';
class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypeData = {
'test.set': {
sourcePath: '',
type: {
description: {
displayName: 'Set',
name: 'set',
group: ['input'],
version: 1,
description: 'Sets a value',
defaults: {
name: 'Set',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Value1',
name: 'value1',
type: 'string',
default: 'default-value1',
},
{
displayName: 'Value2',
name: 'value2',
type: 'string',
default: 'default-value2',
},
],
},
},
},
};
async init(nodeTypes: INodeTypeData): Promise<void> {}
getAll(): INodeType[] {
console.log('1234');
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedNodeType(data.type));
}
getByName(nodeType: string): INodeType {
return this.getByNameAndVersion(nodeType);
}
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
}
}
let nodeTypesInstance: NodeTypesClass | undefined;
export function NodeTypes(): NodeTypesClass {
if (nodeTypesInstance === undefined) {
nodeTypesInstance = new NodeTypesClass();
}
return nodeTypesInstance;
}

View file

@ -1,5 +0,0 @@
describe('Placeholder', () => {
test('example', () => {
expect(1 + 1).toEqual(2);
});
});

View file

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
IAdditionalCredentialOptions,
IAllExecuteFunctions,
IBinaryData,
ICredentialTestFunctions as ICredentialTestFunctionsBase,
@ -43,6 +44,12 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase {
): Promise<IBinaryData>;
getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise<Buffer>;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any>;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -55,6 +62,11 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase {
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
): Promise<any>;
};
}
@ -67,6 +79,12 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
mimeType?: string,
): Promise<IBinaryData>;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any>;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -78,6 +96,11 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
): Promise<any>;
};
}
@ -90,6 +113,12 @@ export interface IPollFunctions extends IPollFunctionsBase {
mimeType?: string,
): Promise<IBinaryData>;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any>;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -102,6 +131,11 @@ export interface IPollFunctions extends IPollFunctionsBase {
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
): Promise<any>;
};
}
@ -118,6 +152,12 @@ export interface ITriggerFunctions extends ITriggerFunctionsBase {
mimeType?: string,
): Promise<IBinaryData>;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any>;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -130,6 +170,11 @@ export interface ITriggerFunctions extends ITriggerFunctionsBase {
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
): Promise<any>;
};
}
@ -152,6 +197,12 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
helpers: {
httpRequest(requestOptions: IHttpRequestOptions): Promise<any>; // tslint:disable-line:no-any
request?: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any>;
requestOAuth2?: (
this: IAllExecuteFunctions,
credentialsType: string,
@ -163,6 +214,11 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
): Promise<any>;
};
}
@ -176,6 +232,12 @@ export interface IHookFunctions extends IHookFunctionsBase {
helpers: {
httpRequest(requestOptions: IHttpRequestOptions): Promise<any>; // tslint:disable-line:no-any
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any>;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -187,6 +249,11 @@ export interface IHookFunctions extends IHookFunctionsBase {
credentialsType: string,
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
): Promise<any>;
};
}
@ -199,6 +266,12 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase {
mimeType?: string,
): Promise<IBinaryData>;
request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise<any>; // tslint:disable-line:no-any
requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any>;
requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -211,6 +284,11 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase {
requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions,
): Promise<any>; // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
): Promise<any>;
};
}

View file

@ -1,17 +1,26 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
ILoadOptions,
INode,
INodeCredentials,
INodeExecutionData,
INodeParameters,
INodeProperties,
INodePropertyOptions,
INodeType,
INodeTypeNameVersion,
INodeTypes,
IRunExecutionData,
ITaskDataConnections,
IWorkflowExecuteAdditionalData,
RoutingNode,
Workflow,
} from 'n8n-workflow';
@ -22,6 +31,8 @@ const TEMP_NODE_NAME = 'Temp-Node';
const TEMP_WORKFLOW_NAME = 'Temp-Workflow';
export class LoadNodeParameterOptions {
currentNodeParameters: INodeParameters;
path: string;
workflow: Workflow;
@ -37,6 +48,7 @@ export class LoadNodeParameterOptions {
nodeTypeNameAndVersion.name,
nodeTypeNameAndVersion.version,
);
this.currentNodeParameters = currentNodeParameters;
this.path = path;
if (nodeType === undefined) {
throw new Error(
@ -87,14 +99,14 @@ export class LoadNodeParameterOptions {
}
/**
* Returns the available options
* Returns the available options via a predefined method
*
* @param {string} methodName The name of the method of which to get the data from
* @param {IWorkflowExecuteAdditionalData} additionalData
* @returns {Promise<INodePropertyOptions[]>}
* @memberof LoadNodeParameterOptions
*/
async getOptions(
async getOptionsViaMethodName(
methodName: string,
additionalData: IWorkflowExecuteAdditionalData,
): Promise<INodePropertyOptions[]> {
@ -122,4 +134,93 @@ export class LoadNodeParameterOptions {
return nodeType.methods.loadOptions[methodName].call(thisArgs);
}
/**
* Returns the available options via a load request informatoin
*
* @param {ILoadOptions} loadOptions The load options which also contain the request information
* @param {IWorkflowExecuteAdditionalData} additionalData
* @returns {Promise<INodePropertyOptions[]>}
* @memberof LoadNodeParameterOptions
*/
async getOptionsViaRequestProperty(
loadOptions: ILoadOptions,
additionalData: IWorkflowExecuteAdditionalData,
): Promise<INodePropertyOptions[]> {
const node = this.workflow.getNode(TEMP_NODE_NAME);
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(node!.type, node?.typeVersion);
if (
nodeType === undefined ||
!nodeType.description.requestDefaults ||
!nodeType.description.requestDefaults.baseURL
) {
// This in in here for now for security reasons.
// Background: As the full data for the request to make does get send, and the auth data
// will then be applied, would it be possible to retrieve that data like that. By at least
// requiring a baseURL to be defined can at least not a random server be called.
// In the future this code has to get improved that it does not use the request information from
// the request rather resolves it via the parameter-path and nodeType data.
throw new Error(
`The node-type "${
node!.type
}" does not exist or does not have "requestDefaults.baseURL" defined!`,
);
}
const mode = 'internal';
const runIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
const runExecutionData: IRunExecutionData = { resultData: { runData: {} } };
const routingNode = new RoutingNode(
this.workflow,
node!,
connectionInputData,
runExecutionData ?? null,
additionalData,
mode,
);
// Create copy of node-type with the single property we want to get the data off
const tempNode: INodeType = {
...nodeType,
...{
description: {
...nodeType.description,
properties: [
{
displayName: '',
type: 'string',
name: '',
default: '',
routing: loadOptions.routing,
} as INodeProperties,
],
},
},
};
const inputData: ITaskDataConnections = {
main: [[{ json: {} }]],
};
const optionsData = await routingNode.runNode(
inputData,
runIndex,
tempNode,
NodeExecuteFunctions,
);
if (optionsData?.length === 0) {
return [];
}
if (!Array.isArray(optionsData)) {
throw new Error('The returned data is not an array!');
}
return optionsData[0].map((item) => item.json) as unknown as INodePropertyOptions[];
}
}

View file

@ -15,6 +15,7 @@
/* eslint-disable no-param-reassign */
import {
GenericValue,
IAdditionalCredentialOptions,
IAllExecuteFunctions,
IBinaryData,
IContextObject,
@ -29,6 +30,8 @@ import {
IN8nHttpFullResponse,
IN8nHttpResponse,
INode,
INodeCredentialDescription,
INodeCredentialsDetails,
INodeExecutionData,
INodeParameters,
INodeType,
@ -44,6 +47,7 @@ import {
IWorkflowDataProxyData,
IWorkflowExecuteAdditionalData,
IWorkflowMetadata,
NodeApiError,
NodeHelpers,
NodeOperationError,
NodeParameterValue,
@ -676,6 +680,10 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
axiosRequest.params = n8nRequest.qs;
if (n8nRequest.baseURL !== undefined) {
axiosRequest.baseURL = n8nRequest.baseURL;
}
if (n8nRequest.disableFollowRedirect === true) {
axiosRequest.maxRedirects = 0;
}
@ -733,12 +741,11 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
}
async function httpRequest(
requestParams: IHttpRequestOptions,
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpFullResponse | IN8nHttpResponse> {
// tslint:disable-line:no-any
const axiosRequest = convertN8nRequestToAxios(requestParams);
const axiosRequest = convertN8nRequestToAxios(requestOptions);
const result = await axios(axiosRequest);
if (requestParams.returnFullResponse) {
if (requestOptions.returnFullResponse) {
return {
body: result.data,
headers: result.headers,
@ -853,10 +860,11 @@ export async function prepareBinaryData(
export async function requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions | IHttpRequestOptions,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
oAuth2Options?: IOAuth2Options,
isN8nRequest = false,
) {
const credentials = (await this.getCredentials(
credentialsType,
@ -952,7 +960,9 @@ export async function requestOAuth2(
// Make the request again with the new token
const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject);
if (isN8nRequest) {
return this.helpers.httpRequest(newRequestOptions);
}
return this.helpers.request!(newRequestOptions);
}
@ -972,7 +982,12 @@ export async function requestOAuth2(
export async function requestOAuth1(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUrl | OptionsWithUri | requestPromise.RequestPromiseOptions,
requestOptions:
| OptionsWithUrl
| OptionsWithUri
| requestPromise.RequestPromiseOptions
| IHttpRequestOptions,
isN8nRequest = false,
) {
const credentials = (await this.getCredentials(
credentialsType,
@ -1020,12 +1035,71 @@ export async function requestOAuth1(
// @ts-ignore
requestOptions.headers = oauth.toHeader(oauth.authorize(requestOptions, token));
if (isN8nRequest) {
return this.helpers.httpRequest(requestOptions as IHttpRequestOptions);
}
return this.helpers.request!(requestOptions).catch(async (error: IResponseError) => {
// Unknown error so simply throw it
throw error;
});
}
export async function httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
additionalCredentialOptions?: IAdditionalCredentialOptions,
) {
try {
const parentTypes = additionalData.credentialsHelper.getParentTypes(credentialsType);
if (parentTypes.includes('oAuth1Api')) {
return await requestOAuth1.call(this, credentialsType, requestOptions, true);
}
if (parentTypes.includes('oAuth2Api')) {
return await requestOAuth2.call(
this,
credentialsType,
requestOptions,
node,
additionalData,
additionalCredentialOptions?.oauth2,
true,
);
}
let credentialsDecrypted: ICredentialDataDecryptedObject | undefined;
if (additionalCredentialOptions?.credentialsDecrypted) {
credentialsDecrypted = additionalCredentialOptions.credentialsDecrypted.data;
} else {
credentialsDecrypted = await this.getCredentials(credentialsType);
}
if (credentialsDecrypted === undefined) {
throw new NodeOperationError(
node,
`Node "${node.name}" does not have any credentials of type "${credentialsType}" set!`,
);
}
requestOptions = await additionalData.credentialsHelper.authenticate(
credentialsDecrypted,
credentialsType,
requestOptions,
workflow,
node,
);
return await httpRequest(requestOptions);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
/**
* Takes generic input data and brings it into the json format n8n uses.
*
@ -1047,6 +1121,62 @@ export function returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExe
return returnData;
}
// TODO: Move up later
export async function requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
additionalCredentialOptions?: IAdditionalCredentialOptions,
) {
try {
const parentTypes = additionalData.credentialsHelper.getParentTypes(credentialsType);
if (parentTypes.includes('oAuth1Api')) {
return await requestOAuth1.call(this, credentialsType, requestOptions, false);
}
if (parentTypes.includes('oAuth2Api')) {
return await requestOAuth2.call(
this,
credentialsType,
requestOptions,
node,
additionalData,
additionalCredentialOptions?.oauth2,
false,
);
}
let credentialsDecrypted: ICredentialDataDecryptedObject | undefined;
if (additionalCredentialOptions?.credentialsDecrypted) {
credentialsDecrypted = additionalCredentialOptions.credentialsDecrypted.data;
} else {
credentialsDecrypted = await this.getCredentials(credentialsType);
}
if (credentialsDecrypted === undefined) {
throw new NodeOperationError(
node,
`Node "${node.name}" does not have any credentials of type "${credentialsType}" set!`,
);
}
requestOptions = await additionalData.credentialsHelper.authenticate(
credentialsDecrypted,
credentialsType,
requestOptions as IHttpRequestOptions,
workflow,
node,
);
return await proxyRequestToAxios(requestOptions as IDataObject);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
/**
* Returns the additional keys for Expressions and Function-Nodes
*
@ -1094,39 +1224,46 @@ export async function getCredentials(
);
}
if (nodeType.description.credentials === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials defined!`,
);
}
// Hardcode for now for security reasons that only a single node can access
// all credentials
const fullAccess = ['n8n-nodes-base.httpRequest'].includes(node.type);
const nodeCredentialDescription = nodeType.description.credentials.find(
(credentialTypeDescription) => credentialTypeDescription.name === type,
);
if (nodeCredentialDescription === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials of type "${type}" defined!`,
);
}
let nodeCredentialDescription: INodeCredentialDescription | undefined;
if (!fullAccess) {
if (nodeType.description.credentials === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials defined!`,
);
}
if (
!NodeHelpers.displayParameter(
additionalData.currentNodeParameters || node.parameters,
nodeCredentialDescription,
node.parameters,
)
) {
// Credentials should not be displayed so return undefined even if they would be defined
return undefined;
nodeCredentialDescription = nodeType.description.credentials.find(
(credentialTypeDescription) => credentialTypeDescription.name === type,
);
if (nodeCredentialDescription === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials of type "${type}" defined!`,
);
}
if (
!NodeHelpers.displayParameter(
additionalData.currentNodeParameters || node.parameters,
nodeCredentialDescription,
node.parameters,
)
) {
// Credentials should not be displayed so return undefined even if they would be defined
return undefined;
}
}
// Check if node has any credentials defined
if (!node.credentials || !node.credentials[type]) {
if (!fullAccess && (!node.credentials || !node.credentials[type])) {
// If none are defined check if the credentials are required or not
if (nodeCredentialDescription.required === true) {
if (nodeCredentialDescription?.required === true) {
// Credentials are required so error
if (!node.credentials) {
throw new NodeOperationError(node, 'Node does not have any credentials set!');
@ -1140,6 +1277,12 @@ export async function getCredentials(
}
}
if (fullAccess && (!node.credentials || !node.credentials[type])) {
// Make sure that fullAccess nodes still behave like before that if they
// request access to credentials that are currently not set it returns undefined
return undefined;
}
let expressionResolveValues: ICredentialsExpressionResolveValues | undefined;
if (connectionInputData && runExecutionData && runIndex !== undefined) {
expressionResolveValues = {
@ -1152,7 +1295,9 @@ export async function getCredentials(
} as ICredentialsExpressionResolveValues;
}
const nodeCredentials = node.credentials[type];
const nodeCredentials = node.credentials
? node.credentials[type]
: ({} as INodeCredentialsDetails);
// TODO: solve using credentials via expression
// if (name.charAt(0) === '=') {
@ -1466,6 +1611,22 @@ export function getExecutePollFunctions(
);
},
request: proxyRequestToAxios,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@ -1488,6 +1649,22 @@ export function getExecutePollFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
returnJsonArray,
},
};
@ -1570,6 +1747,22 @@ export function getExecuteTriggerFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async prepareBinaryData(
binaryData: Buffer,
filePath?: string,
@ -1606,6 +1799,22 @@ export function getExecuteTriggerFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
returnJsonArray,
},
};
@ -1784,6 +1993,22 @@ export function getExecuteFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async prepareBinaryData(
binaryData: Buffer,
filePath?: string,
@ -1827,6 +2052,22 @@ export function getExecuteFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
returnJsonArray,
},
};
@ -1977,6 +2218,22 @@ export function getExecuteSingleFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async prepareBinaryData(
binaryData: Buffer,
filePath?: string,
@ -2013,6 +2270,22 @@ export function getExecuteSingleFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
},
};
})(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex);
@ -2104,6 +2377,22 @@ export function getLoadOptionsFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
@ -2127,6 +2416,22 @@ export function getLoadOptionsFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
},
};
return that;
@ -2224,6 +2529,22 @@ export function getExecuteHookFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
@ -2247,6 +2568,22 @@ export function getExecuteHookFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
},
};
return that;
@ -2370,6 +2707,22 @@ export function getExecuteWebhookFunctions(
prepareOutputData: NodeHelpers.prepareOutputData,
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async prepareBinaryData(
binaryData: Buffer,
filePath?: string,
@ -2406,6 +2759,22 @@ export function getExecuteWebhookFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
returnJsonArray,
},
};

View file

@ -6,6 +6,7 @@ import {
IDataObject,
IDeferredPromise,
IExecuteWorkflowInfo,
IHttpRequestOptions,
INodeCredentialsDetails,
INodeExecutionData,
INodeParameters,
@ -24,17 +25,30 @@ import {
import { Credentials, IExecuteFunctions } from '../src';
export class CredentialsHelper extends ICredentialsHelper {
getDecrypted(
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 new Promise((res) => res({}));
return {};
}
getCredentials(nodeCredentials: INodeCredentialsDetails, type: string): Promise<Credentials> {
return new Promise((res) => {
res(new Credentials({ id: null, name: '' }, '', [], ''));
});
async getCredentials(
nodeCredentials: INodeCredentialsDetails,
type: string,
): Promise<Credentials> {
return new Credentials({ id: null, name: '' }, '', [], '');
}
async updateCredentials(

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"lib": [
"es2017"
"es2019"
],
"types": [
"node",
@ -16,7 +16,7 @@
"preserveConstEnums": true,
"declaration": true,
"outDir": "./dist/",
"target": "es2017",
"target": "es2019",
"sourceMap": true
},
"include": [

View file

@ -1,12 +1,12 @@
import {
GenericValue,
IConnections,
ICredentialsDecrypted,
ICredentialsEncrypted,
ICredentialType,
IDataObject,
GenericValue,
IWorkflowSettings as IWorkflowSettingsWorkflow,
ILoadOptions,
INode,
INodeCredentials,
INodeIssues,
@ -19,6 +19,7 @@ import {
IRunData,
ITaskData,
ITelemetrySettings,
IWorkflowSettings as IWorkflowSettingsWorkflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
@ -167,7 +168,7 @@ export interface IRestApi {
getNodeTranslationHeaders(): Promise<INodeTranslationHeaders>;
getNodeTypes(onlyLatest?: boolean): Promise<INodeTypeDescription[]>;
getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise<INodeTypeDescription[]>;
getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
getNodeParameterOptions(sendData: { nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName?: string, loadOptions?: ILoadOptions, currentNodeParameters: INodeParameters, credentials?: INodeCredentials }): Promise<INodePropertyOptions[]> ;
removeTestWebhook(workflowId: string): Promise<boolean>;
runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
createNewWorkflow(sendData: IWorkflowDataUpdate): Promise<IWorkflowDb>;

View file

@ -4,8 +4,8 @@ import {
ICredentialsDecrypted,
ICredentialType,
IDataObject,
NodeCredentialTestRequest,
NodeCredentialTestResult,
INodeCredentialTestRequest,
INodeCredentialTestResult,
} from 'n8n-workflow';
export async function getCredentialTypes(context: IRestApiContext): Promise<ICredentialType[]> {
@ -48,6 +48,6 @@ export async function oAuth2CredentialAuthorize(context: IRestApiContext, data:
return makeRestApiRequest(context, 'GET', `/oauth2-credential/auth`, data as unknown as IDataObject);
}
export async function testCredential(context: IRestApiContext, data: NodeCredentialTestRequest): Promise<NodeCredentialTestResult> {
export async function testCredential(context: IRestApiContext, data: INodeCredentialTestRequest): Promise<INodeCredentialTestResult> {
return makeRestApiRequest(context, 'POST', '/credentials-test', data as unknown as IDataObject);
}

View file

@ -108,10 +108,10 @@ import {
ICredentialNodeAccess,
ICredentialsDecrypted,
ICredentialType,
INodeCredentialTestResult,
INodeParameters,
INodeProperties,
INodeTypeDescription,
NodeCredentialTestResult,
NodeHelpers,
} from 'n8n-workflow';
import CredentialIcon from '../CredentialIcon.vue';
@ -279,7 +279,7 @@ export default mixins(showMessage, nodeHelpers).extend({
return false;
});
return !!nodesThatCanTest.length;
return !!nodesThatCanTest.length || (!!this.credentialType && !!this.credentialType.test);
},
nodesWithAccess(): INodeTypeDescription[] {
if (this.credentialTypeName) {
@ -566,7 +566,7 @@ export default mixins(showMessage, nodeHelpers).extend({
},
async testCredential(credentialDetails: ICredentialsDecrypted) {
const result: NodeCredentialTestResult = await this.$store.dispatch('credentials/testCredential', credentialDetails);
const result: INodeCredentialTestResult = await this.$store.dispatch('credentials/testCredential', credentialDetails);
if (result.status === 'Error') {
this.authError = result.message;
this.testedSuccessfully = false;

View file

@ -200,6 +200,8 @@ import {
import {
NodeHelpers,
NodeParameterValue,
IHttpRequestOptions,
ILoadOptions,
INodeParameters,
INodePropertyOptions,
Workflow,
@ -517,7 +519,7 @@ export default mixins(
return this.getArgument('editor') as string;
},
parameterOptions (): INodePropertyOptions[] {
if (this.remoteMethod === undefined) {
if (this.hasRemoteMethod === false) {
// Options are already given
return this.parameter.options;
}
@ -560,8 +562,8 @@ export default mixins(
return styles;
},
remoteMethod (): string | undefined {
return this.getArgument('loadOptionsMethod') as string | undefined;
hasRemoteMethod (): boolean {
return !!this.getArgument('loadOptionsMethod') || !!this.getArgument('loadOptions');
},
shortPath (): string {
const shortPath = this.path.split('.');
@ -590,7 +592,7 @@ export default mixins(
},
async loadRemoteParameterOptions () {
if (this.node === null || this.remoteMethod === undefined || this.remoteParameterOptionsLoading) {
if (this.node === null || this.hasRemoteMethod === false || this.remoteParameterOptionsLoading) {
return;
}
this.remoteParameterOptionsLoadingIssues = null;
@ -602,7 +604,10 @@ export default mixins(
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters) as INodeParameters;
try {
const options = await this.restApi().getNodeParameterOptions({name: this.node.type, version: this.node.typeVersion}, this.path, this.remoteMethod, resolvedNodeParameters, this.node.credentials);
const loadOptionsMethod = this.getArgument('loadOptionsMethod') as string | undefined;
const loadOptions = this.getArgument('loadOptions') as ILoadOptions | undefined;
const options = await this.restApi().getNodeParameterOptions({ nodeTypeAndVersion: { name: this.node.type, version: this.node.typeVersion}, path: this.path, methodName: loadOptionsMethod, loadOptions, currentNodeParameters: resolvedNodeParameters, credentials: this.node.credentials });
this.remoteParameterOptions.push.apply(this.remoteParameterOptions, options);
} catch (error) {
this.remoteParameterOptionsLoadingIssues = error.message;
@ -771,7 +776,7 @@ export default mixins(
}
}
if (this.remoteMethod !== undefined && this.node !== null) {
if (this.hasRemoteMethod === true && this.node !== null) {
// Make sure to load the parameter options
// directly and whenever the credentials change
this.$watch(() => this.node!.credentials, () => {

View file

@ -20,6 +20,7 @@ import {
} from '@/Interface';
import {
IDataObject,
ILoadOptions,
INodeCredentials,
INodeParameters,
INodePropertyOptions,
@ -97,14 +98,7 @@ export const restApi = Vue.extend({
},
// Returns all the parameter options from the server
getNodeParameterOptions: (nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
const sendData = {
nodeTypeAndVersion,
path,
methodName,
credentials,
currentNodeParameters,
};
getNodeParameterOptions: (sendData: { nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName?: string, loadOptions?: ILoadOptions, currentNodeParameters: INodeParameters, credentials?: INodeCredentials }): Promise<INodePropertyOptions[]> => {
return self.restApi().makeRestApiRequest('GET', '/node-parameter-options', sendData);
},

View file

@ -21,7 +21,7 @@ import {
import {
ICredentialType,
ICredentialsDecrypted,
NodeCredentialTestResult,
INodeCredentialTestResult,
INodeTypeDescription,
} from 'n8n-workflow';
import { getAppNameFromCredType } from '@/components/helpers';
@ -158,7 +158,7 @@ const module: Module<ICredentialsState, IRootState> = {
oAuth1Authorize: async (context: ActionContext<ICredentialsState, IRootState>, data: ICredentialsResponse) => {
return oAuth1CredentialAuthorize(context.rootGetters.getRestApiContext, data);
},
testCredential: async (context: ActionContext<ICredentialsState, IRootState>, data: ICredentialsDecrypted): Promise<NodeCredentialTestResult> => {
testCredential: async (context: ActionContext<ICredentialsState, IRootState>, data: ICredentialsDecrypted): Promise<INodeCredentialTestResult> => {
return testCredential(context.rootGetters.getRestApiContext, { credentials: data });
},
getNewCredentialName: async (context: ActionContext<ICredentialsState, IRootState>, params: { credentialTypeName: string }) => {

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"lib": [
"es2017"
"es2019"
],
"types": [
"node"
@ -13,7 +13,7 @@
"preserveConstEnums": true,
"declaration": true,
"outDir": "./dist/",
"target": "es2017",
"target": "es2019",
"sourceMap": true
},
"include": [

View file

@ -1,4 +1,5 @@
import {
IAuthenticateBearer,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
@ -15,4 +16,10 @@ export class AsanaApi implements ICredentialType {
default: '',
},
];
authenticate = {
type: 'bearer',
properties: {},
} as IAuthenticateBearer;
}

View file

@ -1,4 +1,5 @@
import {
IAuthenticateHeaderAuth,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
@ -24,4 +25,11 @@ export class HttpHeaderAuth implements ICredentialType {
default: '',
},
];
authenticate = {
type: 'headerAuth',
properties: {
name: '={{credentials.name}}',
value: '={{credentials.value}}',
},
} as IAuthenticateHeaderAuth;
}

View file

@ -1,5 +1,7 @@
import {
ICredentialDataDecryptedObject,
ICredentialType,
IHttpRequestOptions,
INodeProperties,
} from 'n8n-workflow';
@ -22,4 +24,8 @@ export class MattermostApi implements ICredentialType {
default: '',
},
];
async authenticate(credentials: ICredentialDataDecryptedObject, requestOptions: IHttpRequestOptions): Promise<IHttpRequestOptions> {
requestOptions.headers!['Authorization'] = `Bearer ${credentials.accessToken}`;
return requestOptions;
}
}

View file

@ -1,4 +1,6 @@
import {
IAuthenticateQueryAuth,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
@ -16,4 +18,12 @@ export class PipedriveApi implements ICredentialType {
default: '',
},
];
authenticate = {
type: 'queryAuth',
properties: {
key: 'api_token',
value: '={{$credentials.apiToken}}',
},
} as IAuthenticateQueryAuth;
}

View file

@ -4,6 +4,7 @@ import {
import {
IDataObject,
IHttpRequestMethods,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
@ -51,6 +52,12 @@ export class Asana implements INodeType {
],
},
},
testedBy: {
request: {
method: 'GET',
url: '/users/me',
},
},
},
{
name: 'asanaOAuth2Api',
@ -64,6 +71,10 @@ export class Asana implements INodeType {
},
},
],
requestDefaults: {
baseURL: 'https://app.asana.com/api/1.0',
url: '',
},
properties: [
{
displayName: 'Authentication',
@ -1834,7 +1845,7 @@ export class Asana implements INodeType {
const operation = this.getNodeParameter('operation', 0) as string;
let endpoint = '';
let requestMethod = '';
let requestMethod: IHttpRequestMethods = 'GET';
let body: IDataObject;
let qs: IDataObject;

View file

@ -4,15 +4,11 @@ import {
ILoadOptionsFunctions,
} from 'n8n-core';
import {
OptionsWithUri,
} from 'request';
import {
IDataObject,
IHttpRequestMethods,
IHttpRequestOptions,
INodePropertyOptions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
@ -28,39 +24,23 @@ import {
* @param {object} body
* @returns {Promise<any>}
*/
export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object, uri?: string | undefined): Promise<any> { // tslint:disable-line:no-any
const authenticationMethod = this.getNodeParameter('authentication', 0);
export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, endpoint: string, body: object, query?: IDataObject, uri?: string | undefined): Promise<any> { // tslint:disable-line:no-any
const authenticationMethod = this.getNodeParameter('authentication', 0) as string;
const options: OptionsWithUri = {
const options: IHttpRequestOptions = {
headers: {},
method,
body: { data: body },
qs: query,
uri: uri || `https://app.asana.com/api/1.0${endpoint}`,
url: uri || `https://app.asana.com/api/1.0${endpoint}`,
json: true,
};
try {
if (authenticationMethod === 'accessToken') {
const credentials = await this.getCredentials('asanaApi');
if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
options.headers!['Authorization'] = `Bearer ${credentials.accessToken}`;
return await this.helpers.request!(options);
} else {
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'asanaOAuth2Api', options);
}
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
const credentialType = authenticationMethod === 'accessToken' ? 'asanaApi' : 'asanaOAuth2Api';
return this.helpers.requestWithAuthentication.call(this, credentialType, options);
}
export async function asanaApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
export async function asanaApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];

View file

@ -8,10 +8,10 @@ import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -91,7 +91,7 @@ export class AwsTextract implements INodeType {
methods = {
credentialTest: {
async awsTextractApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async awsTextractApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
try {
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject, 'sts');
} catch (error) {

View file

@ -3,10 +3,10 @@ import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IHttpRequestOptions,
NodeCredentialTestResult,
INodeCredentialTestResult,
} from 'n8n-workflow';
export async function bambooHrApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
export async function bambooHrApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
try {
await validateCredentials.call(this, credential.data as ICredentialDataDecryptedObject);
} catch (error) {
@ -19,7 +19,7 @@ export async function bambooHrApiCredentialTest(this: ICredentialTestFunctions,
return {
status: 'OK',
message: 'Connection successful!',
} as NodeCredentialTestResult;
} as INodeCredentialTestResult;
}
async function validateCredentials(this: ICredentialTestFunctions, decryptedCredentials: ICredentialDataDecryptedObject): Promise<any> { // tslint:disable-line:no-any
@ -43,4 +43,4 @@ async function validateCredentials(this: ICredentialTestFunctions, decryptedCred
};
return await this.helpers.request(options);
}
}

View file

@ -10,11 +10,11 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -153,7 +153,7 @@ export class BitbucketTrigger implements INodeType {
methods = {
credentialTest: {
async bitbucketApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async bitbucketApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const credentials = credential.data;
const options: OptionsWithUri = {

View file

@ -7,10 +7,10 @@ import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -101,7 +101,7 @@ export class Dhl implements INodeType {
methods = {
credentialTest: {
async dhlApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async dhlApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
try {
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject);
} catch (error) {

View file

@ -7,11 +7,11 @@ import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -268,7 +268,7 @@ export class Dropcontact implements INodeType {
methods = {
credentialTest: {
async dropcontactApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async dropcontactApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
try {
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject);
} catch (error) {

View file

@ -7,11 +7,11 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -121,7 +121,7 @@ export class ElasticSecurity implements INodeType {
async elasticSecurityApiTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<NodeCredentialTestResult> {
): Promise<INodeCredentialTestResult> {
const {
username,
password,

View file

@ -8,10 +8,10 @@ import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -1738,7 +1738,7 @@ export class Github implements INodeType {
methods = {
credentialTest: {
async githubApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async githubApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const credentials = credential.data;
const baseUrl = credentials!.server as string || 'https://api.github.com/user';

View file

@ -8,11 +8,11 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -1017,7 +1017,7 @@ export class GoogleSheets implements INodeType {
},
},
credentialTest: {
async googleApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async googleApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
try {
const tokenRequest = await getAccessToken.call(this, credential.data! as unknown as IGoogleAuthCredentials);
if (!tokenRequest.access_token) {

View file

@ -7,12 +7,12 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -134,7 +134,7 @@ export class Grafana implements INodeType {
async grafanaApiTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<NodeCredentialTestResult> {
): Promise<INodeCredentialTestResult> {
const { apiKey, baseUrl: rawBaseUrl } = credential.data as GrafanaCredentials;
const baseUrl = tolerateTrailingSlash(rawBaseUrl);
@ -573,4 +573,4 @@ export class Grafana implements INodeType {
return [this.helpers.returnJsonArray(returnData)];
}
}
}

View file

@ -7,10 +7,10 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -80,7 +80,7 @@ export class Grist implements INodeType {
async gristApiTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<NodeCredentialTestResult> {
): Promise<INodeCredentialTestResult> {
const {
apiKey,
planType,

View file

@ -7,11 +7,11 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -142,7 +142,7 @@ export class HomeAssistant implements INodeType {
methods = {
credentialTest: {
async homeAssistantApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async homeAssistantApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const credentials = credential.data;
const options = {
method: 'GET',

View file

@ -7,12 +7,12 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -438,7 +438,7 @@ export class Jenkins implements INodeType {
async jenkinApiCredentialTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<NodeCredentialTestResult> {
): Promise<INodeCredentialTestResult> {
const { baseUrl, username, apiKey } = credential.data as JenkinsApiCredentials;
const url = tolerateTrailingSlash(baseUrl);

View file

@ -13,11 +13,11 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -154,7 +154,7 @@ export class Jira implements INodeType {
methods = {
credentialTest: {
async jiraSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async jiraSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const credentials = credential.data;
const data = Buffer.from(`${credentials!.email}:${credentials!.password || credentials!.apiToken}`).toString('base64');

View file

@ -7,8 +7,8 @@ import {
import {
GenericValue,
IDataObject,
IHttpRequestMethods,
IHttpRequestOptions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
@ -17,7 +17,7 @@ import {
*/
export async function apiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD',
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject | GenericValue | GenericValue[] = {},
query: IDataObject = {},
@ -34,16 +34,11 @@ export async function apiRequest(
qs: query,
url: `${credentials.baseUrl}/api/v4/${endpoint}`,
headers: {
authorization: `Bearer ${credentials.accessToken}`,
'content-type': 'application/json; charset=utf-8',
},
};
try {
return await this.helpers.httpRequest(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
return this.helpers.httpRequestWithAuthentication.call(this, 'mattermostApi', options);
}
export async function apiRequestAllItems(

View file

@ -8,13 +8,13 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
NodeApiError,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -194,7 +194,7 @@ export class NotionV2 implements INodeType {
},
},
credentialTest: {
async notionApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async notionApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
try {
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject);
} catch (error) {

View file

@ -1,11 +1,11 @@
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodePropertyOptions,
NodeApiError,
NodeOperationError,
@ -67,23 +67,9 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
query = {};
}
let responseData;
try {
if (authenticationMethod === 'basicAuth' || authenticationMethod === 'apiToken' || authenticationMethod === 'none') {
const credentials = await this.getCredentials('pipedriveApi');
if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
query.api_token = credentials.apiToken;
//@ts-ignore
responseData = await this.helpers.request(options);
} else {
responseData = await this.helpers.requestOAuth2!.call(this, 'pipedriveOAuth2Api', options);
}
const credentialType = authenticationMethod === 'apiToken' ? 'pipedriveApi' : 'pipedriveOAuth2Api';
const responseData = await this.helpers.requestWithAuthentication.call(this, credentialType, options);
if (downloadFile === true) {
return {

View file

@ -73,6 +73,12 @@ export class Pipedrive implements INodeType {
],
},
},
testedBy: {
request: {
method: 'GET',
url: '/users/me',
},
},
},
{
name: 'pipedriveOAuth2Api',
@ -86,6 +92,10 @@ export class Pipedrive implements INodeType {
},
},
],
requestDefaults: {
baseURL: 'https://api.pipedrive.com/v1',
url: '',
},
properties: [
{
displayName: 'Authentication',

View file

@ -5,11 +5,11 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -288,7 +288,7 @@ export class Slack implements INodeType {
},
},
credentialTest: {
async testSlackTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async testSlackTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const options = {
method: 'POST',

View file

@ -7,10 +7,10 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -124,7 +124,7 @@ export class Splunk implements INodeType {
async splunkApiTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<NodeCredentialTestResult> {
): Promise<INodeCredentialTestResult> {
const {
authToken,
baseUrl,

View file

@ -8,11 +8,11 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -104,7 +104,7 @@ export class Supabase implements INodeType {
},
},
credentialTest: {
async supabaseApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async supabaseApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
try {
await validateCrendentials.call(this, credential.data as ICredentialDataDecryptedObject);
} catch (error) {

View file

@ -6,10 +6,10 @@ import {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialTestFunctions,
INodeCredentialTestResult,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
NodeCredentialTestResult,
} from 'n8n-workflow';
import { versionDescription } from './actions/versionDescription';
@ -31,7 +31,7 @@ export class SyncroMspV1 implements INodeType {
methods = {
loadOptions,
credentialTest: {
async syncroMspApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async syncroMspApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
try {
await validateCredentials.call(this, credential.data as ICredentialDataDecryptedObject);
} catch (error) {

View file

@ -7,10 +7,10 @@ import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -1815,7 +1815,7 @@ export class Telegram implements INodeType {
methods = {
credentialTest: {
async telegramBotTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async telegramBotTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const credentials = credential.data;
const options = {
uri: `https://api.telegram.org/bot${credentials!.accessToken}/getMe`,

View file

@ -7,12 +7,12 @@ import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
JsonObject,
NodeApiError,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -122,7 +122,7 @@ export class TypeformTrigger implements INodeType {
getForms,
},
credentialTest: {
async testTypeformTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async testTypeformTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const credentials = credential.data;
const options = {

View file

@ -6,10 +6,10 @@ import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -73,7 +73,7 @@ export class UrlScanIo implements INodeType {
async urlScanIoApiTest(
this: ICredentialTestFunctions,
credentials: ICredentialsDecrypted,
): Promise<NodeCredentialTestResult> {
): Promise<INodeCredentialTestResult> {
const { apiKey } = credentials.data as { apiKey: string };
const options: OptionsWithUri = {

View file

@ -11,12 +11,12 @@ import {
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -154,7 +154,7 @@ export class Zendesk implements INodeType {
methods = {
credentialTest: {
async zendeskSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
async zendeskSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<INodeCredentialTestResult> {
const credentials = credential.data;
const subdomain = credentials!.subdomain;
const email = credentials!.email;

View file

@ -2,8 +2,7 @@
"compilerOptions": {
"lib": [
"dom",
"es2017",
"es2019.array"
"es2019"
],
"types": [
"node",
@ -16,7 +15,7 @@
"resolveJsonModule": true,
"declaration": true,
"outDir": "./dist/",
"target": "es2017",
"target": "es2019",
"sourceMap": true
},
"include": [

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": [