From 0da398b0e46b3399426c675cc45495aeeb3a31bd Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 5 Feb 2022 22:55:43 +0100 Subject: [PATCH] :sparkles: Nodes as JSON and authentication redesign (#2401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: change FE to handle new object type * 🚸 improve UX of handling invalid credentials * 🚧 WIP * :art: fix typescript issues * 🐘 add migrations for all supported dbs * ✏️ add description to migrations * :zap: add credential update on import * :zap: resolve after merge issues * :shirt: fix lint issues * :zap: check credentials on workflow create/update * update interface * :shirt: fix ts issues * :zap: adaption to new credentials UI * :bug: intialize cache on BE for credentials check * :bug: fix undefined oldCredentials * :bug: fix deleting credential * :bug: fix check for undefined keys * :bug: fix disabling edit in execution * :art: just show credential name on execution view * ✏️ remove TODO * :zap: implement review suggestions * :zap: add cache to getCredentialsByType * ⏪ use getter instead of cache * ✏️ fix variable name typo * 🐘 include waiting nodes to migrations * :bug: fix reverting migrations command * :zap: update typeorm command * :sparkles: create db:revert command * 👕 fix lint error * :sparkles: Add optional authenticate method to credentials * :zap: Simplify code and add authentication support to MattermostApi * :shirt: Fix lint issue * :zap: Add support to own-mode * :shirt: Fix lint issue * :sparkles: Add support for predefined auth types bearer and headerAuth * :zap: Make sure that DateTime Node always returns strings * :zap: Add support for moment types to If Node * :zap: Make it possible for HTTP Request Node to use all credential types * :sparkles: Add basicAuth support * Add a new dropcontact node * :sparkles: First basic implementation of mainly JSON based nodes * :sparkles: Add fixedCollection support, added value parameter and expression support for value and property * Improvements to #2389 * :zap: Add credentials verification * :zap: Small improvement * :zap: set default time to 45 seconds * :sparkles: Add support for preSend and postReceive methods * :heavy_plus_sign: Add lodash merge and set depedency to workflow * :shirt: Fix lint issue * :zap: Improvements * :zap: Improvements * :zap: Improvements * :zap: Improvements * :zap: Improvements * :bug: Set siren and language correctly * :zap: Add support for requestDefaults * :zap: Add support for baseURL to httpRequest * :zap: Move baseURL to correct location * :sparkles: Add support for options loading * :bug: Fix error with fullAccess nodes * :sparkles: Add credential test functionality * :bug: Fix issue with OAuth autentication and lint issue * :zap: Fix build issue * :bug: Fix issue that url got always overwritten to empty * :sparkles: Add pagination support * :zap: Code fix required after merge * :zap: Remove not needed imports * :zap: Fix credential test * :sparkles: Add expression support for request properties and $self support on properties * :zap: Rename $self to $value * :shirt: Fix lint issue * :zap: Add example how to send data in path * :sparkles: Make it possible to not sent in dot notation * :sparkles: Add support for postReceive:rootProperty * :zap: Fix typo * :sparkles: Add support for postReceive:set * :zap: Some fixes * :zap: Small improvement * ;zap: Separate RoutingNode code * :zap: Simplify code and fix bug * :zap: Remove unused code * :sparkles: Make it possible to define "request" and "requestProperty" on options * :shirt: Fix lint issue * :zap: Change $credentials variables name * :sparkles: Enable expressions and access to credentials in requestDefaults * :zap: Make parameter option loading use RoutingNode.makeRoutingRequest * :sparkles: Allow requestOperations overwrite on LoadOptions * :sparkles: Make it possible to access current node parameters in loadOptions * :zap: Rename parameters variable to make future proof * :zap: Make it possible to use offset-pagination with body * :sparkles: Add support for queryAuth * :zap: Never return more items than requested * :sparkles: Make it possible to overwrite requestOperations on parameter and option level * :shirt: Fix lint issue * :sparkles: Allow simplified auth also with regular nodes * :sparkles: Add support for receiving binary data * :bug: Fix example node * :zap: Rename property "name" to "displayName" in loadOptions * :zap: Send data by default as "query" if nothing is set * :zap: Rename $self to $parent * :zap: Change to work with INodeExecutionData instead of IDataObject * :zap: Improve binaryData handling * :zap: Property design improvements * :zap: Fix property name * :rotating_light: Add some tests * :zap: Add also test for request * :zap: Improve test and fix issues * :zap: Improvements to loadOptions * :zap: Normalize loadOptions with rest of code * :zap: Add info text * :sparkles: Add support for $value in postReceive * :rotating_light: Add tests for RoutingNode.runNode * :zap: Remove TODOs and make url property optional * :zap: Fix bug and lint issue * :bug: Fix bug that not the correct property got used * :rotating_light: Add tests for CredentialsHelper.authenticate * :zap: Improve code and resolve expressions also everywhere for loadOptions and credential test requests * :sparkles: Make it possible to define multiple preSend and postReceive actions * :sparkles: Allow to define tests on credentials * :zap: Remove test data * :arrow_up: Update package-lock.json file * :zap: Remove old not longer used code Co-authored-by: Ben Hesseldieck Co-authored-by: Mutasem Co-authored-by: PaulineDropcontact Co-authored-by: ricardo --- package-lock.json | 77 +- packages/cli/commands/execute.ts | 1 + packages/cli/package.json | 3 + packages/cli/src/CredentialTypes.ts | 17 +- packages/cli/src/CredentialsHelper.ts | 424 ++++- packages/cli/src/Interfaces.ts | 6 +- packages/cli/src/LoadNodesAndCredentials.ts | 10 +- packages/cli/src/Server.ts | 123 +- packages/cli/src/WorkflowHelpers.ts | 40 +- packages/cli/src/WorkflowRunner.ts | 5 +- packages/cli/src/WorkflowRunnerProcess.ts | 29 +- packages/cli/test/CredentialsHelper.test.ts | 305 +++ packages/cli/test/Helpers.ts | 63 + packages/cli/test/placeholder.test.ts | 5 - packages/core/src/Interfaces.ts | 78 + packages/core/src/LoadNodeParameterOptions.ts | 105 +- packages/core/src/NodeExecuteFunctions.ts | 437 ++++- packages/core/test/Helpers.ts | 26 +- packages/core/tsconfig.json | 4 +- packages/editor-ui/src/Interface.ts | 7 +- packages/editor-ui/src/api/credentials.ts | 6 +- .../CredentialEdit/CredentialEdit.vue | 6 +- .../src/components/ParameterInput.vue | 17 +- .../src/components/mixins/restApi.ts | 10 +- packages/editor-ui/src/modules/credentials.ts | 4 +- packages/node-dev/tsconfig.json | 4 +- .../credentials/AsanaApi.credentials.ts | 7 + .../credentials/HttpHeaderAuth.credentials.ts | 8 + .../credentials/MattermostApi.credentials.ts | 6 + .../credentials/PipedriveApi.credentials.ts | 10 + packages/nodes-base/nodes/Asana/Asana.node.ts | 13 +- .../nodes/Asana/GenericFunctions.ts | 38 +- .../nodes/Aws/Textract/AwsTextract.node.ts | 4 +- .../BambooHr/v1/methods/credentialTest.ts | 8 +- .../nodes/Bitbucket/BitbucketTrigger.node.ts | 4 +- packages/nodes-base/nodes/Dhl/Dhl.node.ts | 4 +- .../nodes/Dropcontact/Dropcontact.node.ts | 4 +- .../ElasticSecurity/ElasticSecurity.node.ts | 4 +- .../nodes-base/nodes/Github/Github.node.ts | 4 +- .../nodes/Google/Sheet/GoogleSheets.node.ts | 4 +- .../nodes-base/nodes/Grafana/Grafana.node.ts | 6 +- packages/nodes-base/nodes/Grist/Grist.node.ts | 4 +- .../nodes/HomeAssistant/HomeAssistant.node.ts | 4 +- .../nodes-base/nodes/Jenkins/Jenkins.node.ts | 4 +- packages/nodes-base/nodes/Jira/Jira.node.ts | 4 +- .../nodes/Mattermost/v1/transport/index.ts | 11 +- .../nodes/Notion/v2/NotionV2.node.ts | 4 +- .../nodes/Pipedrive/GenericFunctions.ts | 20 +- .../nodes/Pipedrive/Pipedrive.node.ts | 10 + packages/nodes-base/nodes/Slack/Slack.node.ts | 4 +- .../nodes-base/nodes/Splunk/Splunk.node.ts | 4 +- .../nodes/Supabase/Supabase.node.ts | 4 +- .../nodes/SyncroMSP/v1/SyncroMspV1.node.ts | 4 +- .../nodes/Telegram/Telegram.node.ts | 4 +- .../nodes/Typeform/TypeformTrigger.node.ts | 4 +- .../nodes/UrlScanIo/UrlScanIo.node.ts | 4 +- .../nodes-base/nodes/Zendesk/Zendesk.node.ts | 4 +- packages/nodes-base/tsconfig.json | 5 +- packages/workflow/package.json | 3 + packages/workflow/src/Interfaces.ts | 338 +++- packages/workflow/src/RoutingNode.ts | 814 ++++++++ packages/workflow/src/Workflow.ts | 23 +- packages/workflow/src/index.ts | 1 + packages/workflow/test/Helpers.ts | 536 ++++++ packages/workflow/test/RoutingNode.test.ts | 1680 +++++++++++++++++ packages/workflow/tsconfig.json | 5 +- 66 files changed, 5074 insertions(+), 360 deletions(-) create mode 100644 packages/cli/test/CredentialsHelper.test.ts create mode 100644 packages/cli/test/Helpers.ts delete mode 100644 packages/cli/test/placeholder.test.ts create mode 100644 packages/workflow/src/RoutingNode.ts create mode 100644 packages/workflow/test/RoutingNode.test.ts diff --git a/package-lock.json b/package-lock.json index 102140fa75..610303c0cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index 51a24bea2c..07677743e4 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -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; } diff --git a/packages/cli/package.json b/packages/cli/package.json index fa357298f0..c7bac65b6b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/CredentialTypes.ts b/packages/cli/src/CredentialTypes.ts index 0d35381054..7f48050038 100644 --- a/packages/cli/src/CredentialTypes.ts +++ b/packages/cli/src/CredentialTypes.ts @@ -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 { + async init(credentialTypes: ICredentialTypeData): Promise { 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; } } diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index f16f094bb6..8dcb63d5be 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -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 => {}, - getAll: (): INodeType[] => { - // Does not get used in Workflow so no need to return it - return []; + getAll(): Array { + // @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 { + 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 { + 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!', + }; + } } diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index eedcaa182b..f92b91427a 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -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 { diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index bc1e15ce40..ae9d58be17 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -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, + }; } /** diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 3eb9e17a53..7ee1334a22 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -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 => { - const incomingData = req.body as NodeCredentialTestRequest; + async (req: express.Request, res: express.Response): Promise => { + 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) - | 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); }, ), ); diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index da8085ae35..25bac0cbc0 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -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)); } diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 0766a70e5f..ce27cfd2c9 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -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); diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 6a53b1ab4e..90bd498ba1 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -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(); diff --git a/packages/cli/test/CredentialsHelper.test.ts b/packages/cli/test/CredentialsHelper.test.ts new file mode 100644 index 0000000000..e26131f579 --- /dev/null +++ b/packages/cli/test/CredentialsHelper.test.ts @@ -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 { + 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); + }); + } + }); +}); diff --git a/packages/cli/test/Helpers.ts b/packages/cli/test/Helpers.ts new file mode 100644 index 0000000000..fb14f4007b --- /dev/null +++ b/packages/cli/test/Helpers.ts @@ -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 {} + + 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; +} diff --git a/packages/cli/test/placeholder.test.ts b/packages/cli/test/placeholder.test.ts deleted file mode 100644 index d3ea1f5dff..0000000000 --- a/packages/cli/test/placeholder.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('Placeholder', () => { - test('example', () => { - expect(1 + 1).toEqual(2); - }); -}); diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index b7e9561147..0146b9ba2a 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -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; getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise; request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise; // tslint:disable-line:no-any + requestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; requestOAuth2( this: IAllExecuteFunctions, credentialsType: string, @@ -55,6 +62,11 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase { requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, ): Promise; // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + ): Promise; }; } @@ -67,6 +79,12 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { mimeType?: string, ): Promise; request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise; // tslint:disable-line:no-any + requestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; requestOAuth2( this: IAllExecuteFunctions, credentialsType: string, @@ -78,6 +96,11 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, ): Promise; // tslint:disable-line:no-any + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + ): Promise; }; } @@ -90,6 +113,12 @@ export interface IPollFunctions extends IPollFunctionsBase { mimeType?: string, ): Promise; request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise; // tslint:disable-line:no-any + requestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; requestOAuth2( this: IAllExecuteFunctions, credentialsType: string, @@ -102,6 +131,11 @@ export interface IPollFunctions extends IPollFunctionsBase { requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, ): Promise; // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + ): Promise; }; } @@ -118,6 +152,12 @@ export interface ITriggerFunctions extends ITriggerFunctionsBase { mimeType?: string, ): Promise; request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise; // tslint:disable-line:no-any + requestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; requestOAuth2( this: IAllExecuteFunctions, credentialsType: string, @@ -130,6 +170,11 @@ export interface ITriggerFunctions extends ITriggerFunctionsBase { requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, ): Promise; // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + ): Promise; }; } @@ -152,6 +197,12 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { helpers: { httpRequest(requestOptions: IHttpRequestOptions): Promise; // tslint:disable-line:no-any request?: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise; // tslint:disable-line:no-any + requestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; requestOAuth2?: ( this: IAllExecuteFunctions, credentialsType: string, @@ -163,6 +214,11 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, ): Promise; // tslint:disable-line:no-any + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + ): Promise; }; } @@ -176,6 +232,12 @@ export interface IHookFunctions extends IHookFunctionsBase { helpers: { httpRequest(requestOptions: IHttpRequestOptions): Promise; // tslint:disable-line:no-any request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise; // tslint:disable-line:no-any + requestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; requestOAuth2( this: IAllExecuteFunctions, credentialsType: string, @@ -187,6 +249,11 @@ export interface IHookFunctions extends IHookFunctionsBase { credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, ): Promise; // tslint:disable-line:no-any + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + ): Promise; }; } @@ -199,6 +266,12 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase { mimeType?: string, ): Promise; request: (uriOrObject: string | IDataObject | any, options?: IDataObject) => Promise; // tslint:disable-line:no-any + requestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; requestOAuth2( this: IAllExecuteFunctions, credentialsType: string, @@ -211,6 +284,11 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase { requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions, ): Promise; // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + ): Promise; }; } diff --git a/packages/core/src/LoadNodeParameterOptions.ts b/packages/core/src/LoadNodeParameterOptions.ts index 3b71cb9008..ee24e85a61 100644 --- a/packages/core/src/LoadNodeParameterOptions.ts +++ b/packages/core/src/LoadNodeParameterOptions.ts @@ -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} * @memberof LoadNodeParameterOptions */ - async getOptions( + async getOptionsViaMethodName( methodName: string, additionalData: IWorkflowExecuteAdditionalData, ): Promise { @@ -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} + * @memberof LoadNodeParameterOptions + */ + async getOptionsViaRequestProperty( + loadOptions: ILoadOptions, + additionalData: IWorkflowExecuteAdditionalData, + ): Promise { + 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[]; + } } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 071f61ab5a..22b7888d46 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -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 { - // 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 { + return requestWithAuthentication.call( + this, + credentialsType, + requestOptions, + workflow, + node, + additionalData, + additionalCredentialOptions, + ); + }, async requestOAuth2( this: IAllExecuteFunctions, credentialsType: string, @@ -1488,6 +1649,22 @@ export function getExecutePollFunctions( ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, + async httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + 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 { + return requestWithAuthentication.call( + this, + credentialsType, + requestOptions, + workflow, + node, + additionalData, + additionalCredentialOptions, + ); + }, async prepareBinaryData( binaryData: Buffer, filePath?: string, @@ -1606,6 +1799,22 @@ export function getExecuteTriggerFunctions( ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, + async httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + 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 { + return requestWithAuthentication.call( + this, + credentialsType, + requestOptions, + workflow, + node, + additionalData, + additionalCredentialOptions, + ); + }, async prepareBinaryData( binaryData: Buffer, filePath?: string, @@ -1827,6 +2052,22 @@ export function getExecuteFunctions( ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, + async httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + 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 { + return requestWithAuthentication.call( + this, + credentialsType, + requestOptions, + workflow, + node, + additionalData, + additionalCredentialOptions, + ); + }, async prepareBinaryData( binaryData: Buffer, filePath?: string, @@ -2013,6 +2270,22 @@ export function getExecuteSingleFunctions( ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, + async httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + 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 { + return requestWithAuthentication.call( + this, + credentialsType, + requestOptions, + workflow, + node, + additionalData, + additionalCredentialOptions, + ); + }, request: proxyRequestToAxios, async requestOAuth2( this: IAllExecuteFunctions, @@ -2127,6 +2416,22 @@ export function getLoadOptionsFunctions( ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, + async httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + 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 { + return requestWithAuthentication.call( + this, + credentialsType, + requestOptions, + workflow, + node, + additionalData, + additionalCredentialOptions, + ); + }, request: proxyRequestToAxios, async requestOAuth2( this: IAllExecuteFunctions, @@ -2247,6 +2568,22 @@ export function getExecuteHookFunctions( ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, + async httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + 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 { + return requestWithAuthentication.call( + this, + credentialsType, + requestOptions, + workflow, + node, + additionalData, + additionalCredentialOptions, + ); + }, async prepareBinaryData( binaryData: Buffer, filePath?: string, @@ -2406,6 +2759,22 @@ export function getExecuteWebhookFunctions( ): Promise { return requestOAuth1.call(this, credentialsType, requestOptions); }, + async httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + return httpRequestWithAuthentication.call( + this, + credentialsType, + requestOptions, + workflow, + node, + additionalData, + additionalCredentialOptions, + ); + }, returnJsonArray, }, }; diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index 7335eb0d5f..6f55523372 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -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 { + return requestParams; + } + + getParentTypes(name: string): string[] { + return []; + } + + async getDecrypted( nodeCredentials: INodeCredentialsDetails, type: string, ): Promise { - return new Promise((res) => res({})); + return {}; } - getCredentials(nodeCredentials: INodeCredentialsDetails, type: string): Promise { - return new Promise((res) => { - res(new Credentials({ id: null, name: '' }, '', [], '')); - }); + async getCredentials( + nodeCredentials: INodeCredentialsDetails, + type: string, + ): Promise { + return new Credentials({ id: null, name: '' }, '', [], ''); } async updateCredentials( diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 15c303bcbe..4fba454377 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -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": [ diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 3c406dc1e2..08460e715d 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -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; getNodeTypes(onlyLatest?: boolean): Promise; getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise; - getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise; + getNodeParameterOptions(sendData: { nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName?: string, loadOptions?: ILoadOptions, currentNodeParameters: INodeParameters, credentials?: INodeCredentials }): Promise ; removeTestWebhook(workflowId: string): Promise; runWorkflow(runData: IStartRunData): Promise; createNewWorkflow(sendData: IWorkflowDataUpdate): Promise; diff --git a/packages/editor-ui/src/api/credentials.ts b/packages/editor-ui/src/api/credentials.ts index aa0f58058c..e8fd347c2d 100644 --- a/packages/editor-ui/src/api/credentials.ts +++ b/packages/editor-ui/src/api/credentials.ts @@ -4,8 +4,8 @@ import { ICredentialsDecrypted, ICredentialType, IDataObject, - NodeCredentialTestRequest, - NodeCredentialTestResult, + INodeCredentialTestRequest, + INodeCredentialTestResult, } from 'n8n-workflow'; export async function getCredentialTypes(context: IRestApiContext): Promise { @@ -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 { +export async function testCredential(context: IRestApiContext, data: INodeCredentialTestRequest): Promise { return makeRestApiRequest(context, 'POST', '/credentials-test', data as unknown as IDataObject); } diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index 6d5c3d89de..986d4bc944 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -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; diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index b15598a1e1..f3579e7cb4 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -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, () => { diff --git a/packages/editor-ui/src/components/mixins/restApi.ts b/packages/editor-ui/src/components/mixins/restApi.ts index ae889d0a3e..ad014296e1 100644 --- a/packages/editor-ui/src/components/mixins/restApi.ts +++ b/packages/editor-ui/src/components/mixins/restApi.ts @@ -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 => { - const sendData = { - nodeTypeAndVersion, - path, - methodName, - credentials, - currentNodeParameters, - }; + getNodeParameterOptions: (sendData: { nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName?: string, loadOptions?: ILoadOptions, currentNodeParameters: INodeParameters, credentials?: INodeCredentials }): Promise => { return self.restApi().makeRestApiRequest('GET', '/node-parameter-options', sendData); }, diff --git a/packages/editor-ui/src/modules/credentials.ts b/packages/editor-ui/src/modules/credentials.ts index ac20203608..efe5e4db23 100644 --- a/packages/editor-ui/src/modules/credentials.ts +++ b/packages/editor-ui/src/modules/credentials.ts @@ -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 = { oAuth1Authorize: async (context: ActionContext, data: ICredentialsResponse) => { return oAuth1CredentialAuthorize(context.rootGetters.getRestApiContext, data); }, - testCredential: async (context: ActionContext, data: ICredentialsDecrypted): Promise => { + testCredential: async (context: ActionContext, data: ICredentialsDecrypted): Promise => { return testCredential(context.rootGetters.getRestApiContext, { credentials: data }); }, getNewCredentialName: async (context: ActionContext, params: { credentialTypeName: string }) => { diff --git a/packages/node-dev/tsconfig.json b/packages/node-dev/tsconfig.json index ae576c96f1..5eb49a044e 100644 --- a/packages/node-dev/tsconfig.json +++ b/packages/node-dev/tsconfig.json @@ -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": [ diff --git a/packages/nodes-base/credentials/AsanaApi.credentials.ts b/packages/nodes-base/credentials/AsanaApi.credentials.ts index d2dffcf121..81805f4166 100644 --- a/packages/nodes-base/credentials/AsanaApi.credentials.ts +++ b/packages/nodes-base/credentials/AsanaApi.credentials.ts @@ -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; + } diff --git a/packages/nodes-base/credentials/HttpHeaderAuth.credentials.ts b/packages/nodes-base/credentials/HttpHeaderAuth.credentials.ts index fb187a9859..9ac6d4b38b 100644 --- a/packages/nodes-base/credentials/HttpHeaderAuth.credentials.ts +++ b/packages/nodes-base/credentials/HttpHeaderAuth.credentials.ts @@ -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; } diff --git a/packages/nodes-base/credentials/MattermostApi.credentials.ts b/packages/nodes-base/credentials/MattermostApi.credentials.ts index b536b546dd..19526623b4 100644 --- a/packages/nodes-base/credentials/MattermostApi.credentials.ts +++ b/packages/nodes-base/credentials/MattermostApi.credentials.ts @@ -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 { + requestOptions.headers!['Authorization'] = `Bearer ${credentials.accessToken}`; + return requestOptions; + } } diff --git a/packages/nodes-base/credentials/PipedriveApi.credentials.ts b/packages/nodes-base/credentials/PipedriveApi.credentials.ts index a639b783ab..96c3477947 100644 --- a/packages/nodes-base/credentials/PipedriveApi.credentials.ts +++ b/packages/nodes-base/credentials/PipedriveApi.credentials.ts @@ -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; } diff --git a/packages/nodes-base/nodes/Asana/Asana.node.ts b/packages/nodes-base/nodes/Asana/Asana.node.ts index cbf5021947..810d987d8f 100644 --- a/packages/nodes-base/nodes/Asana/Asana.node.ts +++ b/packages/nodes-base/nodes/Asana/Asana.node.ts @@ -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; diff --git a/packages/nodes-base/nodes/Asana/GenericFunctions.ts b/packages/nodes-base/nodes/Asana/GenericFunctions.ts index de62487dc6..a005b83a17 100644 --- a/packages/nodes-base/nodes/Asana/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Asana/GenericFunctions.ts @@ -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} */ -export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object, uri?: string | undefined): Promise { // 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 { // 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 { // tslint:disable-line:no-any +export async function asanaApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts b/packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts index ed22ef34fb..1650da1dd5 100644 --- a/packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts +++ b/packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts @@ -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 { + async awsTextractApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { try { await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject, 'sts'); } catch (error) { diff --git a/packages/nodes-base/nodes/BambooHr/v1/methods/credentialTest.ts b/packages/nodes-base/nodes/BambooHr/v1/methods/credentialTest.ts index 5e0586c52c..41950070ed 100644 --- a/packages/nodes-base/nodes/BambooHr/v1/methods/credentialTest.ts +++ b/packages/nodes-base/nodes/BambooHr/v1/methods/credentialTest.ts @@ -3,10 +3,10 @@ import { ICredentialsDecrypted, ICredentialTestFunctions, IHttpRequestOptions, - NodeCredentialTestResult, + INodeCredentialTestResult, } from 'n8n-workflow'; -export async function bambooHrApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { +export async function bambooHrApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { 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 { // tslint:disable-line:no-any @@ -43,4 +43,4 @@ async function validateCredentials(this: ICredentialTestFunctions, decryptedCred }; return await this.helpers.request(options); -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/Bitbucket/BitbucketTrigger.node.ts b/packages/nodes-base/nodes/Bitbucket/BitbucketTrigger.node.ts index 64ebef8cb6..603f6d7869 100644 --- a/packages/nodes-base/nodes/Bitbucket/BitbucketTrigger.node.ts +++ b/packages/nodes-base/nodes/Bitbucket/BitbucketTrigger.node.ts @@ -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 { + async bitbucketApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { const credentials = credential.data; const options: OptionsWithUri = { diff --git a/packages/nodes-base/nodes/Dhl/Dhl.node.ts b/packages/nodes-base/nodes/Dhl/Dhl.node.ts index 5f05ddfebc..aaaeb8e735 100644 --- a/packages/nodes-base/nodes/Dhl/Dhl.node.ts +++ b/packages/nodes-base/nodes/Dhl/Dhl.node.ts @@ -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 { + async dhlApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { try { await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject); } catch (error) { diff --git a/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts b/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts index c62bc0ade6..cd8bb4889e 100644 --- a/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts +++ b/packages/nodes-base/nodes/Dropcontact/Dropcontact.node.ts @@ -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 { + async dropcontactApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { try { await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject); } catch (error) { diff --git a/packages/nodes-base/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.ts b/packages/nodes-base/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.ts index 7257661e26..afa214fd81 100644 --- a/packages/nodes-base/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.ts +++ b/packages/nodes-base/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.ts @@ -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 { + ): Promise { const { username, password, diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index fe738e1254..d8b491910b 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -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 { + async githubApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { const credentials = credential.data; const baseUrl = credentials!.server as string || 'https://api.github.com/user'; diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts index ce2c80b5cc..eaa1782af0 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts @@ -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 { + async googleApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { try { const tokenRequest = await getAccessToken.call(this, credential.data! as unknown as IGoogleAuthCredentials); if (!tokenRequest.access_token) { diff --git a/packages/nodes-base/nodes/Grafana/Grafana.node.ts b/packages/nodes-base/nodes/Grafana/Grafana.node.ts index db6c8c210c..2173c9edcd 100644 --- a/packages/nodes-base/nodes/Grafana/Grafana.node.ts +++ b/packages/nodes-base/nodes/Grafana/Grafana.node.ts @@ -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 { + ): Promise { 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)]; } -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/Grist/Grist.node.ts b/packages/nodes-base/nodes/Grist/Grist.node.ts index 43c3ad3a3a..bacb209b96 100644 --- a/packages/nodes-base/nodes/Grist/Grist.node.ts +++ b/packages/nodes-base/nodes/Grist/Grist.node.ts @@ -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 { + ): Promise { const { apiKey, planType, diff --git a/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts b/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts index bd4341fd28..9916ef355f 100644 --- a/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts +++ b/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts @@ -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 { + async homeAssistantApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { const credentials = credential.data; const options = { method: 'GET', diff --git a/packages/nodes-base/nodes/Jenkins/Jenkins.node.ts b/packages/nodes-base/nodes/Jenkins/Jenkins.node.ts index b78a528acc..d2beca5c66 100644 --- a/packages/nodes-base/nodes/Jenkins/Jenkins.node.ts +++ b/packages/nodes-base/nodes/Jenkins/Jenkins.node.ts @@ -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 { + ): Promise { const { baseUrl, username, apiKey } = credential.data as JenkinsApiCredentials; const url = tolerateTrailingSlash(baseUrl); diff --git a/packages/nodes-base/nodes/Jira/Jira.node.ts b/packages/nodes-base/nodes/Jira/Jira.node.ts index 1256bb301e..98a51b1044 100644 --- a/packages/nodes-base/nodes/Jira/Jira.node.ts +++ b/packages/nodes-base/nodes/Jira/Jira.node.ts @@ -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 { + async jiraSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { const credentials = credential.data; const data = Buffer.from(`${credentials!.email}:${credentials!.password || credentials!.apiToken}`).toString('base64'); diff --git a/packages/nodes-base/nodes/Mattermost/v1/transport/index.ts b/packages/nodes-base/nodes/Mattermost/v1/transport/index.ts index 3ff3cb2b1b..c76440e626 100644 --- a/packages/nodes-base/nodes/Mattermost/v1/transport/index.ts +++ b/packages/nodes-base/nodes/Mattermost/v1/transport/index.ts @@ -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( diff --git a/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts index c609f7669b..0955db67e2 100644 --- a/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts +++ b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts @@ -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 { + async notionApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { try { await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject); } catch (error) { diff --git a/packages/nodes-base/nodes/Pipedrive/GenericFunctions.ts b/packages/nodes-base/nodes/Pipedrive/GenericFunctions.ts index 89b6338bbb..f9be2d2fa8 100644 --- a/packages/nodes-base/nodes/Pipedrive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Pipedrive/GenericFunctions.ts @@ -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 { diff --git a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts index d565080aa4..7a692e53db 100644 --- a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts +++ b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts @@ -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', diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index c36ff8908f..fd6cb8c861 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -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 { + async testSlackTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { const options = { method: 'POST', diff --git a/packages/nodes-base/nodes/Splunk/Splunk.node.ts b/packages/nodes-base/nodes/Splunk/Splunk.node.ts index a86e0f9980..7dc22e3db6 100644 --- a/packages/nodes-base/nodes/Splunk/Splunk.node.ts +++ b/packages/nodes-base/nodes/Splunk/Splunk.node.ts @@ -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 { + ): Promise { const { authToken, baseUrl, diff --git a/packages/nodes-base/nodes/Supabase/Supabase.node.ts b/packages/nodes-base/nodes/Supabase/Supabase.node.ts index 49f41b0f28..04a0586617 100644 --- a/packages/nodes-base/nodes/Supabase/Supabase.node.ts +++ b/packages/nodes-base/nodes/Supabase/Supabase.node.ts @@ -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 { + async supabaseApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { try { await validateCrendentials.call(this, credential.data as ICredentialDataDecryptedObject); } catch (error) { diff --git a/packages/nodes-base/nodes/SyncroMSP/v1/SyncroMspV1.node.ts b/packages/nodes-base/nodes/SyncroMSP/v1/SyncroMspV1.node.ts index 9255cde1c7..0b6b71ebb9 100644 --- a/packages/nodes-base/nodes/SyncroMSP/v1/SyncroMspV1.node.ts +++ b/packages/nodes-base/nodes/SyncroMSP/v1/SyncroMspV1.node.ts @@ -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 { + async syncroMspApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { try { await validateCredentials.call(this, credential.data as ICredentialDataDecryptedObject); } catch (error) { diff --git a/packages/nodes-base/nodes/Telegram/Telegram.node.ts b/packages/nodes-base/nodes/Telegram/Telegram.node.ts index 7921e64114..45ca8f0f44 100644 --- a/packages/nodes-base/nodes/Telegram/Telegram.node.ts +++ b/packages/nodes-base/nodes/Telegram/Telegram.node.ts @@ -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 { + async telegramBotTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { const credentials = credential.data; const options = { uri: `https://api.telegram.org/bot${credentials!.accessToken}/getMe`, diff --git a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts index 59d53b163e..6ef7889b61 100644 --- a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts +++ b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts @@ -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 { + async testTypeformTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { const credentials = credential.data; const options = { diff --git a/packages/nodes-base/nodes/UrlScanIo/UrlScanIo.node.ts b/packages/nodes-base/nodes/UrlScanIo/UrlScanIo.node.ts index f3061a181e..78076b1d1b 100644 --- a/packages/nodes-base/nodes/UrlScanIo/UrlScanIo.node.ts +++ b/packages/nodes-base/nodes/UrlScanIo/UrlScanIo.node.ts @@ -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 { + ): Promise { const { apiKey } = credentials.data as { apiKey: string }; const options: OptionsWithUri = { diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts index d166964c36..1761debb93 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -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 { + async zendeskSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { const credentials = credential.data; const subdomain = credentials!.subdomain; const email = credentials!.email; diff --git a/packages/nodes-base/tsconfig.json b/packages/nodes-base/tsconfig.json index 09d91632df..791b66eb1d 100644 --- a/packages/nodes-base/tsconfig.json +++ b/packages/nodes-base/tsconfig.json @@ -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": [ diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 39e0134b8d..d87dd9dccc 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -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" }, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index c8bf57f60d..517db7eafc 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -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; + abstract getCredentials( nodeCredentials: INodeCredentialsDetails, type: string, @@ -155,6 +182,80 @@ export abstract class ICredentialsHelper { ): Promise; } +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) + | 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; + credentialTypes?: ICredentialTypeData; + init(credentialTypes?: ICredentialTypeData): Promise; 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); +} + +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; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; [key: string]: (...args: any[]) => any; // tslint:disable-line:no-any }; } @@ -410,15 +547,32 @@ export interface IExecuteSingleFunctions { httpRequest( requestOptions: IHttpRequestOptions, ): Promise; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; [key: string]: (...args: any[]) => any; // tslint:disable-line:no-any }; } +export interface IExecutePaginationFunctions extends IExecuteSingleFunctions { + makeRoutingRequest( + this: IAllExecuteFunctions, + requestOptions: IRequestOptionsFromParameters, + ): Promise; +} export interface IExecuteWorkflowInfo { code?: IWorkflowBase; id?: string; } +export type ICredentialTestFunction = ( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, +) => Promise; + export interface ICredentialTestFunctions { helpers: { [key: string]: (...args: any[]) => any; @@ -448,6 +602,20 @@ export interface ILoadOptionsFunctions { httpRequest( requestOptions: IHttpRequestOptions, ): Promise; + // 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; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; [key: string]: ((...args: any[]) => any) | undefined; // tslint:disable-line:no-any }; } @@ -471,6 +639,12 @@ export interface IHookFunctions { httpRequest( requestOptions: IHttpRequestOptions, ): Promise; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; [key: string]: (...args: any[]) => any; // tslint:disable-line:no-any }; } @@ -493,6 +667,12 @@ export interface IPollFunctions { httpRequest( requestOptions: IHttpRequestOptions, ): Promise; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; [key: string]: (...args: any[]) => any; // tslint:disable-line:no-any }; } @@ -518,6 +698,12 @@ export interface ITriggerFunctions { httpRequest( requestOptions: IHttpRequestOptions, ): Promise; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; [key: string]: (...args: any[]) => any; // tslint:disable-line:no-any }; } @@ -549,6 +735,12 @@ export interface IWebhookFunctions { httpRequest( requestOptions: IHttpRequestOptions, ): Promise; + httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise; [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: @@ -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; + [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) + | IPostReceiveBinaryData + | IPostReceiveRootProperty + | IPostReceiveSet + | IPostReceiveSetKeyValue + | IPostReceiveSort; + +export type PreSendAction = ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +) => Promise; + +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 { + 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; diff --git a/packages/workflow/src/RoutingNode.ts b/packages/workflow/src/RoutingNode.ts new file mode 100644 index 0000000000..6a6aab984d --- /dev/null +++ b/packages/workflow/src/RoutingNode.ts @@ -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 { + 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)[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)[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 { + 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)[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 { + 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 { + 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)[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)[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)![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; + } +} diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 256b5aa2bc..db696ab9fd 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -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; diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 9913f7a3ef..9d292b2e84 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -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'; diff --git a/packages/workflow/test/Helpers.ts b/packages/workflow/test/Helpers.ts index 0d469d99dc..7c68850479 100644 --- a/packages/workflow/test/Helpers.ts +++ b/packages/workflow/test/Helpers.ts @@ -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 { + return requestParams; + } + + getParentTypes(name: string): string[] { + return []; + } + + async getDecrypted( + nodeCredentials: INodeCredentialsDetails, + type: string, + ): Promise { + return {}; + } + + async getCredentials( + nodeCredentials: INodeCredentialsDetails, + type: string, + ): Promise { + return new Credentials({ id: null, name: '' }, '', [], ''); + } + + async updateCredentials( + nodeCredentials: INodeCredentialsDetails, + type: string, + data: ICredentialDataDecryptedObject, + ): Promise {} +} + +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 { + return additionalData.executeWorkflow(workflowInfo, additionalData, inputData); + }, + getContext(type: string): IContextObject { + return NodeHelpers.getContext(runExecutionData, type, node); + }, + async getCredentials( + type: string, + itemIndex?: number, + ): Promise { + 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 { + 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 { + await additionalData.hooks?.executeHookFunctions('sendResponse', [response]); + }, + helpers: { + async httpRequest( + requestOptions: IHttpRequestOptions, + ): Promise { + return { + body: { + headers: {}, + statusCode: 200, + requestOptions, + }, + }; + }, + async requestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + return { + body: { + headers: {}, + statusCode: 200, + credentialsType, + requestOptions, + additionalCredentialOptions, + }, + }; + }, + async httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + 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 { + 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 { + return { + body: { + headers: {}, + statusCode: 200, + requestOptions, + }, + }; + }, + async requestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + return { + body: { + headers: {}, + statusCode: 200, + credentialsType, + requestOptions, + additionalCredentialOptions, + }, + }; + }, + async httpRequestWithAuthentication( + this: IAllExecuteFunctions, + credentialsType: string, + requestOptions: IHttpRequestOptions, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + 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 => {}, + sendMessageToUI: (message: string) => {}, + restApiUrl: '', + encryptionKey: 'test', + timezone: 'America/New_York', + webhookBaseUrl: 'webhook', + webhookWaitingBaseUrl: 'webhook-waiting', + webhookTestBaseUrl: 'webhook-test', + }; +} diff --git a/packages/workflow/test/RoutingNode.test.ts b/packages/workflow/test/RoutingNode.test.ts new file mode 100644 index 0000000000..33d7d3123c --- /dev/null +++ b/packages/workflow/test/RoutingNode.test.ts @@ -0,0 +1,1680 @@ +import { + INode, + INodeExecutionData, + INodeParameters, + IRequestOptionsFromParameters, + IRunExecutionData, + RoutingNode, + Workflow, + INodeProperties, + IDataObject, + IExecuteSingleFunctions, + IHttpRequestOptions, + IN8nHttpFullResponse, + ITaskDataConnections, + INodeExecuteFunctions, + IN8nRequestOperations, + INodeCredentialDescription, +} from '../src'; + +import * as Helpers from './Helpers'; + +const postReceiveFunction1 = async function ( + this: IExecuteSingleFunctions, + items: INodeExecutionData[], + response: IN8nHttpFullResponse, +): Promise { + items.forEach((item) => (item.json1 = { success: true })); + return items; +}; + +const preSendFunction1 = async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + requestOptions.headers = (requestOptions.headers || {}) as IDataObject; + requestOptions.headers.addedIn = 'preSendFunction1'; + return requestOptions; +}; + +describe('RoutingNode', () => { + describe('getRequestOptionsFromParameters', () => { + const tests: Array<{ + description: string; + input: { + nodeParameters: INodeParameters; + nodeTypeProperties: INodeProperties; + }; + output: IRequestOptionsFromParameters | undefined; + }> = [ + { + description: 'single parameter, only send defined, fixed value', + input: { + nodeParameters: {}, + nodeTypeProperties: { + displayName: 'Email', + name: 'email', + type: 'string', + routing: { + send: { + property: 'toEmail', + type: 'body', + value: 'fixedValue', + }, + }, + default: '', + }, + }, + output: { + options: { + qs: {}, + body: { + toEmail: 'fixedValue', + }, + }, + preSend: [], + postReceive: [], + requestOperations: {}, + }, + }, + { + description: 'single parameter, only send defined, using expression', + input: { + nodeParameters: { + email: 'test@test.com', + }, + nodeTypeProperties: { + displayName: 'Email', + name: 'email', + type: 'string', + routing: { + send: { + property: 'toEmail', + type: 'body', + value: '={{$value.toUpperCase()}}', + }, + }, + default: '', + }, + }, + output: { + options: { + qs: {}, + body: { + toEmail: 'TEST@TEST.COM', + }, + }, + preSend: [], + postReceive: [], + requestOperations: {}, + }, + }, + { + description: 'single parameter, send and operations defined, fixed value', + input: { + nodeParameters: {}, + nodeTypeProperties: { + displayName: 'Email', + name: 'email', + type: 'string', + routing: { + send: { + property: 'toEmail', + type: 'body', + value: 'fixedValue', + }, + operations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit', + offsetParameter: 'offset', + pageSize: 10, + rootProperty: 'data', + type: 'body', + }, + }, + }, + }, + default: '', + }, + }, + output: { + options: { + qs: {}, + body: { + toEmail: 'fixedValue', + }, + }, + preSend: [], + postReceive: [], + requestOperations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit', + offsetParameter: 'offset', + pageSize: 10, + rootProperty: 'data', + type: 'body', + }, + }, + }, + }, + }, + { + description: 'mutliple parameters, complex example with everything', + input: { + nodeParameters: { + multipleFields: { + value1: 'v1', + value2: 'v2', + value3: 'v3', + value4: 4, + lowerLevel: { + lowLevelValue1: 1, + lowLevelValue2: 'llv2', + }, + customPropertiesSingle1: { + property: { + name: 'cSName1', + value: 'cSValue1', + }, + }, + customPropertiesMulti: { + property0: [ + { + name: 'cM0Name1', + value: 'cM0Value1', + }, + { + name: 'cM0Name2', + value: 'cM0Value2', + }, + ], + property1: [ + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + ], + }, + }, + }, + nodeTypeProperties: { + displayName: 'Multiple Fields', + name: 'multipleFields', + type: 'collection', + placeholder: 'Add Field', + routing: { + request: { + method: 'GET', + url: '/destination1', + }, + operations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit1', + offsetParameter: 'offset1', + pageSize: 1, + rootProperty: 'data1', + type: 'body', + }, + }, + }, + output: { + maxResults: 10, + postReceive: [postReceiveFunction1], + }, + }, + default: {}, + options: [ + { + displayName: 'Value 1', + name: 'value1', + type: 'string', + routing: { + send: { + property: 'value1', + type: 'body', + }, + }, + default: '', + }, + { + displayName: 'Value 2', + name: 'value2', + type: 'string', + routing: { + send: { + property: 'topLevel.value2', + propertyInDotNotation: false, + type: 'body', + preSend: [preSendFunction1], + }, + }, + default: '', + }, + { + displayName: 'Value 3', + name: 'value3', + type: 'string', + routing: { + send: { + property: 'lowerLevel.value3', + type: 'body', + }, + }, + default: '', + }, + { + displayName: 'Value 4', + name: 'value4', + type: 'number', + default: 0, + routing: { + send: { + property: 'value4', + type: 'query', + }, + output: { + maxResults: '={{$value}}', + }, + operations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit100', + offsetParameter: 'offset100', + pageSize: 100, + rootProperty: 'data100', + type: 'query', + }, + }, + }, + }, + }, + // This one should not be included + { + displayName: 'Value 5', + name: 'value5', + type: 'number', + displayOptions: { + show: { + value4: [1], + }, + }, + default: 5, + routing: { + send: { + property: 'value5', + type: 'query', + }, + operations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit10', + offsetParameter: 'offset10', + pageSize: 10, + rootProperty: 'data10', + type: 'body', + }, + }, + }, + }, + }, + { + displayName: 'Lower Level', + name: 'lowerLevel', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Low Level Value1', + name: 'lowLevelValue1', + type: 'number', + default: 0, + routing: { + send: { + property: 'llvalue1', + type: 'query', + }, + }, + }, + { + displayName: 'Low Level Value2', + name: 'lowLevelValue2', + type: 'string', + default: '', + routing: { + send: { + property: 'llvalue2', + type: 'query', + preSend: [preSendFunction1], + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'data', + }, + }, + ], + }, + }, + }, + ], + }, + // Test fixed collection1: multipleValues=false + { + displayName: 'Custom Properties1 (single)', + name: 'customPropertiesSingle1', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + default: {}, + options: [ + { + name: 'property', + displayName: 'Property', + values: [ + // To set: { single-customValues: { name: 'name', value: 'value'} } + { + displayName: 'Property Name', + name: 'name', + type: 'string', + default: '', + routing: { + request: { + method: 'POST', + url: '=/{{$value}}', + }, + send: { + property: 'single-customValues.name', + }, + }, + }, + { + displayName: 'Property Value', + name: 'value', + type: 'string', + default: '', + routing: { + send: { + property: 'single-customValues.value', + }, + }, + }, + ], + }, + ], + }, + // Test fixed collection: multipleValues=true + { + displayName: 'Custom Properties (multi)', + name: 'customPropertiesMulti', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'property0', + displayName: 'Property0', + values: [ + // To set: { name0: 'value0', name1: 'value1' } + { + displayName: 'Property Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the property to set.', + }, + { + displayName: 'Property Value', + name: 'value', + type: 'string', + default: '', + routing: { + send: { + property: '=customMulti0.{{$parent.name}}', + type: 'body', + }, + }, + description: 'Value of the property to set.', + }, + ], + }, + + { + name: 'property1', + displayName: 'Property1', + values: [ + // To set: { customValues: [ { name: 'name0', value: 'value0'}, { name: 'name1', value: 'value1'} ]} + { + displayName: 'Property Name', + name: 'name', + type: 'string', + default: '', + routing: { + send: { + property: '=customMulti1[{{$index}}].name', + type: 'body', + }, + }, + }, + { + displayName: 'Property Value', + name: 'value', + type: 'string', + default: '', + routing: { + send: { + property: '=customMulti1[{{$index}}].value', + type: 'body', + }, + }, + }, + ], + }, + ], + }, + ], + }, + }, + output: { + maxResults: 4, + options: { + method: 'POST', + url: '/cSName1', + qs: { + value4: 4, + llvalue1: 1, + llvalue2: 'llv2', + 'single-customValues': { + name: 'cSName1', + value: 'cSValue1', + }, + }, + body: { + value1: 'v1', + 'topLevel.value2': 'v2', + lowerLevel: { + value3: 'v3', + }, + customMulti0: { + cM0Name1: 'cM0Value1', + cM0Name2: 'cM0Value2', + }, + customMulti1: [ + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + ], + }, + }, + preSend: [preSendFunction1, preSendFunction1], + postReceive: [ + { + actions: [postReceiveFunction1], + data: { + parameterValue: { + value1: 'v1', + value2: 'v2', + value3: 'v3', + value4: 4, + lowerLevel: { + lowLevelValue1: 1, + lowLevelValue2: 'llv2', + }, + customPropertiesSingle1: { + property: { + name: 'cSName1', + value: 'cSValue1', + }, + }, + customPropertiesMulti: { + property0: [ + { + name: 'cM0Name1', + value: 'cM0Value1', + }, + { + name: 'cM0Name2', + value: 'cM0Value2', + }, + ], + property1: [ + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + ], + }, + }, + }, + }, + { + actions: [ + { + type: 'rootProperty', + properties: { + property: 'data', + }, + }, + ], + data: { + parameterValue: 'llv2', + }, + }, + ], + requestOperations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit100', + offsetParameter: 'offset100', + pageSize: 100, + rootProperty: 'data100', + type: 'query', + }, + }, + }, + }, + }, + ]; + + const nodeTypes = Helpers.NodeTypes(); + const node: INode = { + parameters: {}, + name: 'test', + type: 'test.set', + typeVersion: 1, + position: [0, 0], + }; + + const mode = 'internal'; + const runIndex = 0; + const itemIndex = 0; + const connectionInputData: INodeExecutionData[] = []; + const runExecutionData: IRunExecutionData = { resultData: { runData: {} } }; + const additionalData = Helpers.WorkflowExecuteAdditionalData(); + const path = ''; + const nodeType = nodeTypes.getByName(node.type); + + const workflowData = { + nodes: [node], + connections: {}, + }; + + for (const testData of tests) { + test(testData.description, () => { + node.parameters = testData.input.nodeParameters; + + // @ts-ignore + nodeType.description.properties = [testData.input.nodeTypeProperties]; + + const workflow = new Workflow({ + nodes: workflowData.nodes, + connections: workflowData.connections, + active: false, + nodeTypes, + }); + + const routingNode = new RoutingNode( + workflow, + node, + connectionInputData, + runExecutionData ?? null, + additionalData, + mode, + ); + + const executeSingleFunctions = Helpers.getExecuteSingleFunctions( + workflow, + runExecutionData, + runIndex, + connectionInputData, + {}, + node, + itemIndex, + additionalData, + mode, + ); + + const result = routingNode.getRequestOptionsFromParameters( + executeSingleFunctions, + testData.input.nodeTypeProperties, + itemIndex, + runIndex, + path, + {}, + ); + + expect(result).toEqual(testData.output); + }); + } + }); + + describe('runNode', () => { + const tests: Array<{ + description: string; + input: { + nodeType: { + properties?: INodeProperties[]; + credentials?: INodeCredentialDescription[]; + requestDefaults?: IHttpRequestOptions; + requestOperations?: IN8nRequestOperations; + }; + node: { + parameters: INodeParameters; + }; + }; + output: INodeExecutionData[][] | undefined; + }> = [ + { + description: 'single parameter, only send defined, fixed value, using requestDefaults', + input: { + nodeType: { + requestDefaults: { + baseURL: 'http://127.0.0.1:5678', + url: '/test-url', + }, + properties: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + routing: { + send: { + property: 'toEmail', + type: 'body', + value: 'fixedValue', + }, + }, + default: '', + }, + ], + }, + node: { + parameters: {}, + }, + }, + output: [ + [ + { + json: { + headers: {}, + statusCode: 200, + requestOptions: { + url: '/test-url', + qs: {}, + body: { + toEmail: 'fixedValue', + }, + baseURL: 'http://127.0.0.1:5678', + returnFullResponse: true, + }, + }, + }, + ], + ], + }, + { + description: 'single parameter, only send defined, fixed value, using requestDefaults', + input: { + nodeType: { + requestDefaults: { + baseURL: 'http://127.0.0.1:5678', + url: '/test-url', + }, + properties: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + routing: { + send: { + property: 'toEmail', + type: 'body', + value: 'fixedValue', + }, + }, + default: '', + }, + ], + }, + node: { + parameters: {}, + }, + }, + output: [ + [ + { + json: { + headers: {}, + statusCode: 200, + requestOptions: { + url: '/test-url', + qs: {}, + body: { + toEmail: 'fixedValue', + }, + baseURL: 'http://127.0.0.1:5678', + returnFullResponse: true, + }, + }, + }, + ], + ], + }, + { + description: + 'single parameter, only send defined, using expression, using requestDefaults with overwrite', + input: { + node: { + parameters: { + email: 'test@test.com', + }, + }, + nodeType: { + requestDefaults: { + baseURL: 'http://127.0.0.1:5678', + url: '/test-url', + }, + properties: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + routing: { + send: { + property: 'toEmail', + type: 'body', + value: '={{$value.toUpperCase()}}', + }, + request: { + url: '/overwritten', + }, + }, + default: '', + }, + ], + }, + }, + output: [ + [ + { + json: { + headers: {}, + statusCode: 200, + requestOptions: { + url: '/overwritten', + qs: {}, + body: { + toEmail: 'TEST@TEST.COM', + }, + baseURL: 'http://127.0.0.1:5678', + returnFullResponse: true, + }, + }, + }, + ], + ], + }, + { + description: + 'single parameter, only send defined, using expression, using requestDefaults with overwrite and expressions', + input: { + node: { + parameters: { + endpoint: 'custom-overwritten', + }, + }, + nodeType: { + requestDefaults: { + baseURL: 'http://127.0.0.1:5678', + url: '/test-url', + }, + properties: [ + { + displayName: 'Endpoint', + name: 'endpoint', + type: 'string', + routing: { + send: { + property: '={{"theProperty"}}', + type: 'body', + value: '={{$value}}', + }, + request: { + url: '=/{{$value}}', + }, + }, + default: '', + }, + ], + }, + }, + output: [ + [ + { + json: { + headers: {}, + statusCode: 200, + requestOptions: { + url: '/custom-overwritten', + qs: {}, + body: { + theProperty: 'custom-overwritten', + }, + baseURL: 'http://127.0.0.1:5678', + returnFullResponse: true, + }, + }, + }, + ], + ], + }, + { + description: 'single parameter, send and operations defined, fixed value with pagination', + input: { + node: { + parameters: {}, + }, + nodeType: { + properties: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + routing: { + send: { + property: 'toEmail', + type: 'body', + value: 'fixedValue', + paginate: true, + }, + operations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit', + offsetParameter: 'offset', + pageSize: 10, + type: 'body', + }, + }, + }, + }, + default: '', + }, + ], + }, + }, + output: [ + [ + { + json: { + headers: {}, + statusCode: 200, + requestOptions: { + qs: {}, + body: { + toEmail: 'fixedValue', + limit: 10, + offset: 10, + }, + returnFullResponse: true, + }, + }, + }, + ], + ], + }, + { + description: 'mutliple parameters, complex example with everything', + input: { + node: { + parameters: { + multipleFields: { + value1: 'v1', + value2: 'v2', + value3: 'v3', + value4: 4, + lowerLevel: { + lowLevelValue1: 1, + lowLevelValue2: 'llv2', + }, + customPropertiesSingle1: { + property: { + name: 'cSName1', + value: 'cSValue1', + }, + }, + customPropertiesMulti: { + property0: [ + { + name: 'cM0Name1', + value: 'cM0Value1', + }, + { + name: 'cM0Name2', + value: 'cM0Value2', + }, + ], + property1: [ + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + ], + }, + }, + }, + }, + nodeType: { + properties: [ + { + displayName: 'Multiple Fields', + name: 'multipleFields', + type: 'collection', + placeholder: 'Add Field', + routing: { + request: { + method: 'GET', + url: '/destination1', + }, + operations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit1', + offsetParameter: 'offset1', + pageSize: 1, + rootProperty: 'data1', + type: 'body', + }, + }, + }, + output: { + maxResults: 10, + postReceive: [postReceiveFunction1], + }, + }, + default: {}, + options: [ + { + displayName: 'Value 1', + name: 'value1', + type: 'string', + routing: { + send: { + property: 'value1', + type: 'body', + }, + }, + default: '', + }, + { + displayName: 'Value 2', + name: 'value2', + type: 'string', + routing: { + send: { + property: 'topLevel.value2', + propertyInDotNotation: false, + type: 'body', + preSend: [preSendFunction1], + }, + }, + default: '', + }, + { + displayName: 'Value 3', + name: 'value3', + type: 'string', + routing: { + send: { + property: 'lowerLevel.value3', + type: 'body', + }, + }, + default: '', + }, + { + displayName: 'Value 4', + name: 'value4', + type: 'number', + default: 0, + routing: { + send: { + property: 'value4', + type: 'query', + }, + output: { + maxResults: '={{$value}}', + }, + operations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit100', + offsetParameter: 'offset100', + pageSize: 100, + rootProperty: 'data100', + type: 'query', + }, + }, + }, + }, + }, + // This one should not be included + { + displayName: 'Value 5', + name: 'value5', + type: 'number', + displayOptions: { + show: { + value4: [1], + }, + }, + default: 5, + routing: { + send: { + property: 'value5', + type: 'query', + }, + operations: { + pagination: { + type: 'offset', + properties: { + limitParameter: 'limit10', + offsetParameter: 'offset10', + pageSize: 10, + rootProperty: 'data10', + type: 'body', + }, + }, + }, + }, + }, + { + displayName: 'Lower Level', + name: 'lowerLevel', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Low Level Value1', + name: 'lowLevelValue1', + type: 'number', + default: 0, + routing: { + send: { + property: 'llvalue1', + type: 'query', + }, + }, + }, + { + displayName: 'Low Level Value2', + name: 'lowLevelValue2', + type: 'string', + default: '', + routing: { + send: { + property: 'llvalue2', + type: 'query', + preSend: [preSendFunction1], + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'requestOptions', + }, + }, + ], + }, + }, + }, + ], + }, + // Test fixed collection1: multipleValues=false + { + displayName: 'Custom Properties1 (single)', + name: 'customPropertiesSingle1', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + default: {}, + options: [ + { + name: 'property', + displayName: 'Property', + values: [ + // To set: { single-customValues: { name: 'name', value: 'value'} } + { + displayName: 'Property Name', + name: 'name', + type: 'string', + default: '', + routing: { + request: { + method: 'POST', + url: '=/{{$value}}', + }, + send: { + property: 'single-customValues.name', + }, + }, + }, + { + displayName: 'Property Value', + name: 'value', + type: 'string', + default: '', + routing: { + send: { + property: 'single-customValues.value', + }, + }, + }, + ], + }, + ], + }, + // Test fixed collection: multipleValues=true + { + displayName: 'Custom Properties (multi)', + name: 'customPropertiesMulti', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'property0', + displayName: 'Property0', + values: [ + // To set: { name0: 'value0', name1: 'value1' } + { + displayName: 'Property Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the property to set.', + }, + { + displayName: 'Property Value', + name: 'value', + type: 'string', + default: '', + routing: { + send: { + property: '=customMulti0.{{$parent.name}}', + type: 'body', + }, + }, + description: 'Value of the property to set.', + }, + ], + }, + { + name: 'property1', + displayName: 'Property1', + values: [ + // To set: { customValues: [ { name: 'name0', value: 'value0'}, { name: 'name1', value: 'value1'} ]} + { + displayName: 'Property Name', + name: 'name', + type: 'string', + default: '', + routing: { + send: { + property: '=customMulti1[{{$index}}].name', + type: 'body', + }, + }, + }, + { + displayName: 'Property Value', + name: 'value', + type: 'string', + default: '', + routing: { + send: { + property: '=customMulti1[{{$index}}].value', + type: 'body', + }, + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, + output: [ + [ + { + json: { + url: '/cSName1', + qs: { + value4: 4, + llvalue1: 1, + llvalue2: 'llv2', + 'single-customValues': { + name: 'cSName1', + value: 'cSValue1', + }, + }, + body: { + value1: 'v1', + 'topLevel.value2': 'v2', + lowerLevel: { + value3: 'v3', + }, + customMulti0: { + cM0Name1: 'cM0Value1', + cM0Name2: 'cM0Value2', + }, + customMulti1: [ + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + { + name: 'cM1Name2', + value: 'cM1Value2', + }, + ], + }, + method: 'POST', + headers: { + addedIn: 'preSendFunction1', + }, + returnFullResponse: true, + }, + }, + ], + ], + }, + { + description: 'single parameter, postReceive: set', + input: { + nodeType: { + requestDefaults: { + baseURL: 'http://127.0.0.1:5678', + url: '/test-url', + }, + properties: [ + { + displayName: 'JSON Data', + name: 'jsonData', + type: 'string', + routing: { + send: { + property: 'jsonData', + type: 'body', + }, + output: { + postReceive: [ + { + type: 'set', + properties: { + value: '={{ { "value": $value, "response": $response } }}', + }, + }, + ], + }, + }, + default: '', + }, + ], + }, + node: { + parameters: { + jsonData: { + root: [ + { + name: 'Jim', + age: 34, + }, + { + name: 'James', + age: 44, + }, + ], + }, + }, + }, + }, + output: [ + [ + { + json: { + value: { + root: [ + { + name: 'Jim', + age: 34, + }, + { + name: 'James', + age: 44, + }, + ], + }, + response: { + body: { + headers: {}, + statusCode: 200, + requestOptions: { + qs: {}, + body: { + jsonData: { + root: [ + { + name: 'Jim', + age: 34, + }, + { + name: 'James', + age: 44, + }, + ], + }, + }, + baseURL: 'http://127.0.0.1:5678', + url: '/test-url', + returnFullResponse: true, + }, + }, + }, + }, + }, + ], + ], + }, + { + description: 'single parameter, postReceive: rootProperty', + input: { + nodeType: { + requestDefaults: { + baseURL: 'http://127.0.0.1:5678', + url: '/test-url', + }, + properties: [ + { + displayName: 'JSON Data', + name: 'jsonData', + type: 'string', + routing: { + send: { + property: 'jsonData', + type: 'body', + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'requestOptions', + }, + }, + { + type: 'rootProperty', + properties: { + property: 'body.jsonData.root', + }, + }, + ], + }, + }, + default: '', + }, + ], + }, + node: { + parameters: { + jsonData: { + root: [ + { + name: 'Jim', + age: 34, + }, + { + name: 'James', + age: 44, + }, + ], + }, + }, + }, + }, + output: [ + [ + { + json: { + name: 'Jim', + age: 34, + }, + }, + { + json: { + name: 'James', + age: 44, + }, + }, + ], + ], + }, + { + description: 'single parameter, mutliple postReceive: rootProperty, setKeyValue, sort', + input: { + nodeType: { + requestDefaults: { + baseURL: 'http://127.0.0.1:5678', + url: '/test-url', + }, + properties: [ + { + displayName: 'JSON Data', + name: 'jsonData', + type: 'string', + routing: { + send: { + property: 'jsonData', + type: 'body', + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'requestOptions.body.jsonData.root', + }, + }, + { + type: 'setKeyValue', + properties: { + display1: '={{$responseItem.name}} ({{$responseItem.age}})', + display2: '={{$responseItem.name}} is {{$responseItem.age}}', + }, + }, + { + type: 'sort', + properties: { + key: 'display1', + }, + }, + ], + }, + }, + default: '', + }, + ], + }, + node: { + parameters: { + jsonData: { + root: [ + { + name: 'Jim', + age: 34, + }, + { + name: 'James', + age: 44, + }, + ], + }, + }, + }, + }, + output: [ + [ + { + json: { + display1: 'James (44)', + display2: 'James is 44', + }, + }, + { + json: { + display1: 'Jim (34)', + display2: 'Jim is 34', + }, + }, + ], + ], + }, + ]; + + const nodeTypes = Helpers.NodeTypes(); + const baseNode: INode = { + parameters: {}, + name: 'test', + type: 'test.set', + typeVersion: 1, + position: [0, 0], + }; + + const mode = 'internal'; + const runIndex = 0; + const itemIndex = 0; + const connectionInputData: INodeExecutionData[] = []; + const runExecutionData: IRunExecutionData = { resultData: { runData: {} } }; + const additionalData = Helpers.WorkflowExecuteAdditionalData(); + const nodeType = nodeTypes.getByName(baseNode.type); + + const inputData: ITaskDataConnections = { + main: [ + [ + { + json: {}, + }, + ], + ], + }; + + for (const testData of tests) { + test(testData.description, async () => { + const node: INode = { ...baseNode, ...testData.input.node }; + + const workflowData = { + nodes: [node], + connections: {}, + }; + + // @ts-ignore + nodeType.description = { ...testData.input.nodeType }; + + const workflow = new Workflow({ + nodes: workflowData.nodes, + connections: workflowData.connections, + active: false, + nodeTypes, + }); + + const routingNode = new RoutingNode( + workflow, + node, + connectionInputData, + runExecutionData ?? null, + additionalData, + mode, + ); + + // @ts-ignore + const nodeExecuteFunctions: INodeExecuteFunctions = { + getExecuteFunctions: () => { + return Helpers.getExecuteFunctions( + workflow, + runExecutionData, + runIndex, + connectionInputData, + {}, + node, + itemIndex, + additionalData, + mode, + ); + }, + getExecuteSingleFunctions: () => { + return Helpers.getExecuteSingleFunctions( + workflow, + runExecutionData, + runIndex, + connectionInputData, + {}, + node, + itemIndex, + additionalData, + mode, + ); + }, + }; + + const result = await routingNode.runNode( + inputData, + runIndex, + nodeType, + nodeExecuteFunctions, + ); + + expect(result).toEqual(testData.output); + }); + } + }); +}); diff --git a/packages/workflow/tsconfig.json b/packages/workflow/tsconfig.json index 873c637da9..5e25408639 100644 --- a/packages/workflow/tsconfig.json +++ b/packages/workflow/tsconfig.json @@ -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": [