From ca2960102dd6bce0cbad693f8bab8842623ca696 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Tue, 31 Jan 2023 10:58:12 +0100 Subject: [PATCH 1/8] feat(core): Fix populating of node custom api call options --- packages/cli/src/api/nodeTypes.api.ts | 62 +----------------- packages/core/src/DirectoryLoader.ts | 65 +++++++++++++++++-- .../Node/NodeCreator/TriggerHelperPanel.vue | 19 +----- packages/editor-ui/src/stores/nodeCreator.ts | 7 +- 4 files changed, 70 insertions(+), 83 deletions(-) diff --git a/packages/cli/src/api/nodeTypes.api.ts b/packages/cli/src/api/nodeTypes.api.ts index 1e0eb7c6227b4..6c55d195661be 100644 --- a/packages/cli/src/api/nodeTypes.api.ts +++ b/packages/cli/src/api/nodeTypes.api.ts @@ -2,69 +2,13 @@ import express from 'express'; import { readFile } from 'fs/promises'; import get from 'lodash.get'; -import type { ICredentialType, INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow'; +import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow'; -import { CredentialTypes } from '@/CredentialTypes'; import config from '@/config'; import { NodeTypes } from '@/NodeTypes'; import * as ResponseHelper from '@/ResponseHelper'; import { getNodeTranslationPath } from '@/TranslationHelpers'; -function isOAuth(credType: ICredentialType) { - return ( - Array.isArray(credType.extends) && - credType.extends.some((parentType) => - ['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType), - ) - ); -} - -/** - * Whether any of the node's credential types may be used to - * make a request from a node other than itself. - */ -function supportsProxyAuth(description: INodeTypeDescription) { - if (!description.credentials) return false; - - const credentialTypes = CredentialTypes(); - - return description.credentials.some(({ name }) => { - const credType = credentialTypes.getByName(name); - - if (credType.authenticate !== undefined) return true; - - return isOAuth(credType); - }); -} - -const CUSTOM_API_CALL_NAME = 'Custom API Call'; -const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__'; - -/** - * Inject a `Custom API Call` option into `resource` and `operation` - * parameters in a node that supports proxy auth. - */ -function injectCustomApiCallOption(description: INodeTypeDescription) { - if (!supportsProxyAuth(description)) return description; - - description.properties.forEach((p) => { - if ( - ['resource', 'operation'].includes(p.name) && - Array.isArray(p.options) && - p.options[p.options.length - 1].name !== CUSTOM_API_CALL_NAME - ) { - p.options.push({ - name: CUSTOM_API_CALL_NAME, - value: CUSTOM_API_CALL_KEY, - }); - } - - return p; - }); - - return description; -} - export const nodeTypesController = express.Router(); // Returns node information based on node names and versions @@ -78,7 +22,7 @@ nodeTypesController.post( if (defaultLocale === 'en') { return nodeInfos.reduce((acc, { name, version }) => { const { description } = NodeTypes().getByNameAndVersion(name, version); - acc.push(injectCustomApiCallOption(description)); + acc.push(description); return acc; }, []); } @@ -103,7 +47,7 @@ nodeTypesController.post( // ignore - no translation exists at path } - nodeTypes.push(injectCustomApiCallOption(description)); + nodeTypes.push(description); } const nodeTypes: INodeTypeDescription[] = []; diff --git a/packages/core/src/DirectoryLoader.ts b/packages/core/src/DirectoryLoader.ts index 0682cfec7c9a9..ff2bdda6cb1b8 100644 --- a/packages/core/src/DirectoryLoader.ts +++ b/packages/core/src/DirectoryLoader.ts @@ -19,6 +19,9 @@ import { CUSTOM_NODES_CATEGORY } from './Constants'; import type { n8n } from './Interfaces'; import { loadClassInIsolation } from './ClassLoader'; +const CUSTOM_API_CALL_NAME = 'Custom API Call'; +const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__'; + function toJSON(this: ICredentialType) { return { ...this, @@ -54,6 +57,58 @@ export abstract class DirectoryLoader { return path.resolve(this.directory, file); } + protected isOAuth(credType: ICredentialType) { + return ( + Array.isArray(credType.extends) && + credType.extends.some((parentType) => + ['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType), + ) + ); + } + + /** + * Whether any of the node's credential types may be used to + * make a request from a node other than itself. + */ + protected supportsProxyAuth(description: INodeTypeDescription) { + if (!description.credentials) return false; + + // const credentialTypes = CredentialTypes(); + + return description.credentials.some(({ name }) => { + const credType = this.credentialTypes[name].type; + + if (credType.authenticate !== undefined) return true; + + return this.isOAuth(credType); + }); + } + + /** + * Inject a `Custom API Call` option into `resource` and `operation` + * parameters in a node that supports proxy auth. + */ + protected injectCustomApiCallOption(description: INodeTypeDescription) { + if (!this.supportsProxyAuth(description)) return description; + + description.properties.forEach((p) => { + if ( + ['resource', 'operation'].includes(p.name) && + Array.isArray(p.options) && + p.options[p.options.length - 1].name !== CUSTOM_API_CALL_NAME + ) { + p.options.push({ + name: CUSTOM_API_CALL_NAME, + value: CUSTOM_API_CALL_KEY, + }); + } + + return p; + }); + + return description; + } + protected loadNodeFromFile(packageName: string, nodeName: string, filePath: string) { let tempNode: INodeType | IVersionedNodeType; let nodeVersion = 1; @@ -84,6 +139,7 @@ export abstract class DirectoryLoader { if ('nodeVersions' in tempNode) { for (const versionNode of Object.values(tempNode.nodeVersions)) { + this.injectCustomApiCallOption(versionNode.description); this.fixIconPath(versionNode.description, filePath); } @@ -99,10 +155,11 @@ export abstract class DirectoryLoader { } } else { // Short renaming to avoid type issues - const tmpNode = tempNode; - nodeVersion = Array.isArray(tmpNode.description.version) - ? tmpNode.description.version.slice(-1)[0] - : tmpNode.description.version; + + this.injectCustomApiCallOption(tempNode.description); + nodeVersion = Array.isArray(tempNode.description.version) + ? tempNode.description.version.slice(-1)[0] + : tempNode.description.version; } this.known.nodes[fullNodeName] = { diff --git a/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue index ad6d2dfaa7dbd..26612564dea42 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue @@ -233,7 +233,7 @@ const telemetry = instance?.proxy.$telemetry; const { categorizedItems: allNodes, isTriggerNode } = useNodeTypesStore(); const containsAPIAction = computed( () => - state.latestNodeData?.properties.some((p) => + activeNodeActions.value?.properties.some((p) => p.options?.find((o) => o.name === CUSTOM_API_CALL_NAME), ) === true, ); @@ -338,27 +338,10 @@ function getCustomAPICallHintLocale(key: string) { interpolate: { nodeNameTitle }, }); } -// The nodes.json doesn't contain API CALL option so we need to fetch the node detail -// to determine if need to render the API CALL hint -async function fetchNodeDetails() { - if (!state.activeNodeActions) return; - - const { getNodesInformation } = useNodeTypesStore(); - const { version, name } = state.activeNodeActions; - const payload = { - name, - version: Array.isArray(version) ? version?.slice(-1)[0] : version, - } as INodeTypeNameVersion; - - const nodesInfo = await getNodesInformation([payload], false); - - state.latestNodeData = nodesInfo[0]; -} function setActiveActionsNodeType(nodeType: INodeTypeDescription | null) { state.activeNodeActions = nodeType; setShowTabs(false); - fetchNodeDetails(); if (nodeType) trackActionsView(); } diff --git a/packages/editor-ui/src/stores/nodeCreator.ts b/packages/editor-ui/src/stores/nodeCreator.ts index 5604b01ef28d9..edc0c00645757 100644 --- a/packages/editor-ui/src/stores/nodeCreator.ts +++ b/packages/editor-ui/src/stores/nodeCreator.ts @@ -56,9 +56,12 @@ const customNodeActionsParsers: { }, }; -function filterSinglePlaceholderAction(actions: INodeActionTypeDescription[]) { +function filterActions(actions: INodeActionTypeDescription[]) { return actions.filter( (action: INodeActionTypeDescription, _: number, arr: INodeActionTypeDescription[]) => { + const isApiCall = action.actionKey === CUSTOM_API_CALL_KEY; + if(isApiCall) return false; + const isPlaceholderTriggerAction = action.actionKey === PLACEHOLDER_RECOMMENDED_ACTION_KEY; return !isPlaceholderTriggerAction || (isPlaceholderTriggerAction && arr.length > 1); }, @@ -339,7 +342,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, { const filteredNodes = Object.values(mergedNodes).map((node) => ({ ...node, - actions: filterSinglePlaceholderAction(node.actions || []), + actions: filterActions(node.actions || []), })); return filteredNodes; From e3ed882f0653504a948f65071b6c19290040bb97 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Tue, 31 Jan 2023 10:59:09 +0100 Subject: [PATCH 2/8] lint fixes --- .../src/components/Node/NodeCreator/TriggerHelperPanel.vue | 2 +- packages/editor-ui/src/stores/nodeCreator.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue index 26612564dea42..3ce89f63d167c 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue @@ -233,7 +233,7 @@ const telemetry = instance?.proxy.$telemetry; const { categorizedItems: allNodes, isTriggerNode } = useNodeTypesStore(); const containsAPIAction = computed( () => - activeNodeActions.value?.properties.some((p) => + activeNodeActions.value?.properties.some((p) => p.options?.find((o) => o.name === CUSTOM_API_CALL_NAME), ) === true, ); diff --git a/packages/editor-ui/src/stores/nodeCreator.ts b/packages/editor-ui/src/stores/nodeCreator.ts index edc0c00645757..f7485247337fb 100644 --- a/packages/editor-ui/src/stores/nodeCreator.ts +++ b/packages/editor-ui/src/stores/nodeCreator.ts @@ -60,7 +60,7 @@ function filterActions(actions: INodeActionTypeDescription[]) { return actions.filter( (action: INodeActionTypeDescription, _: number, arr: INodeActionTypeDescription[]) => { const isApiCall = action.actionKey === CUSTOM_API_CALL_KEY; - if(isApiCall) return false; + if (isApiCall) return false; const isPlaceholderTriggerAction = action.actionKey === PLACEHOLDER_RECOMMENDED_ACTION_KEY; return !isPlaceholderTriggerAction || (isPlaceholderTriggerAction && arr.length > 1); From 125bf41c9403140e2b61a089f17a6647ec5d5796 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Wed, 1 Feb 2023 14:03:18 +0100 Subject: [PATCH 3/8] Adress PR comments --- packages/core/src/Constants.ts | 2 ++ packages/core/src/DirectoryLoader.ts | 31 ++++++++-------------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/packages/core/src/Constants.ts b/packages/core/src/Constants.ts index 6d2fe99321336..45c1741dd020e 100644 --- a/packages/core/src/Constants.ts +++ b/packages/core/src/Constants.ts @@ -11,6 +11,8 @@ export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__'; export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__'; export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN'; export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z'; +export const CUSTOM_API_CALL_NAME = 'Custom API Call'; +export const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__'; export const RESPONSE_ERROR_MESSAGES = { NO_ENCRYPTION_KEY: 'Encryption key is missing or was not set', diff --git a/packages/core/src/DirectoryLoader.ts b/packages/core/src/DirectoryLoader.ts index ff2bdda6cb1b8..892484572f583 100644 --- a/packages/core/src/DirectoryLoader.ts +++ b/packages/core/src/DirectoryLoader.ts @@ -15,13 +15,10 @@ import type { IVersionedNodeType, KnownNodesAndCredentials, } from 'n8n-workflow'; -import { CUSTOM_NODES_CATEGORY } from './Constants'; +import { CUSTOM_NODES_CATEGORY, CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_NAME } from './Constants'; import type { n8n } from './Interfaces'; import { loadClassInIsolation } from './ClassLoader'; -const CUSTOM_API_CALL_NAME = 'Custom API Call'; -const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__'; - function toJSON(this: ICredentialType) { return { ...this, @@ -57,15 +54,6 @@ export abstract class DirectoryLoader { return path.resolve(this.directory, file); } - protected isOAuth(credType: ICredentialType) { - return ( - Array.isArray(credType.extends) && - credType.extends.some((parentType) => - ['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType), - ) - ); - } - /** * Whether any of the node's credential types may be used to * make a request from a node other than itself. @@ -73,14 +61,17 @@ export abstract class DirectoryLoader { protected supportsProxyAuth(description: INodeTypeDescription) { if (!description.credentials) return false; - // const credentialTypes = CredentialTypes(); - return description.credentials.some(({ name }) => { const credType = this.credentialTypes[name].type; if (credType.authenticate !== undefined) return true; - return this.isOAuth(credType); + return ( + Array.isArray(credType.extends) && + credType.extends.some((parentType) => + ['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType), + ) + ); }); } @@ -88,8 +79,8 @@ export abstract class DirectoryLoader { * Inject a `Custom API Call` option into `resource` and `operation` * parameters in a node that supports proxy auth. */ - protected injectCustomApiCallOption(description: INodeTypeDescription) { - if (!this.supportsProxyAuth(description)) return description; + private injectCustomApiCallOption(description: INodeTypeDescription) { + if (!this.supportsProxyAuth(description)) return; description.properties.forEach((p) => { if ( @@ -102,11 +93,7 @@ export abstract class DirectoryLoader { value: CUSTOM_API_CALL_KEY, }); } - - return p; }); - - return description; } protected loadNodeFromFile(packageName: string, nodeName: string, filePath: string) { From 1b71a45b7659b605eff2b9fa32fe548e7d5a9aaf Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Wed, 1 Feb 2023 14:46:03 +0100 Subject: [PATCH 4/8] Add e2e test and only inject custom API options for latest version --- cypress/e2e/5-ndv.cy.ts | 12 ++++++++++++ cypress/pages/ndv.ts | 1 + packages/core/src/DirectoryLoader.ts | 1 - packages/editor-ui/src/components/NodeSettings.vue | 6 +++++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 9fead48e51a4c..f535ab363870f 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -87,4 +87,16 @@ describe('NDV', () => { cy.get('[class*=hasIssues]').should('have.length', 1); }); }); + + it('should show http node hint if node has custom api option', () => { + workflowPage.actions.addNodeToCanvas('Manual Trigger'); + workflowPage.actions.addNodeToCanvas('Slack', true, true); + ndv.getters.httpRequestNotice().should('not.exist'); + ndv.getters.parameterInput('operation').click(); + ndv.getters.parameterInput('operation').contains('Custom API').click(); + ndv.getters.httpRequestNotice().should('be.visible'); + ndv.getters.parameterInput('operation').click(); + ndv.getters.parameterInput('operation').contains('Post').click(); + ndv.getters.httpRequestNotice().should('not.exist'); + }); }); diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index ba38b940652e8..3761db326f3b9 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -27,6 +27,7 @@ export class NDV extends BasePage { parameterInput: (parameterName: string) => cy.getByTestId(`parameter-input-${parameterName}`), nodeNameContainer: () => cy.getByTestId('node-title-container'), nodeRenameInput: () => cy.getByTestId('node-rename-input'), + httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'), }; actions = { diff --git a/packages/core/src/DirectoryLoader.ts b/packages/core/src/DirectoryLoader.ts index 892484572f583..bb94363c34365 100644 --- a/packages/core/src/DirectoryLoader.ts +++ b/packages/core/src/DirectoryLoader.ts @@ -126,7 +126,6 @@ export abstract class DirectoryLoader { if ('nodeVersions' in tempNode) { for (const versionNode of Object.values(tempNode.nodeVersions)) { - this.injectCustomApiCallOption(versionNode.description); this.fixIconPath(versionNode.description, filePath); } diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index e533e20f8c050..b1673a6f13217 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -116,7 +116,11 @@ -
+
{ + const credType = this.types.credentials.find((t) => t.name === name); + if (!credType) return false; + if (credType.authenticate !== undefined) return true; + + return ( + Array.isArray(credType.extends) && + credType.extends.some((parentType) => + ['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType), + ) + ); + }); + } + + /** + * Inject a `Custom API Call` option into `resource` and `operation` + * parameters in a latest-version node that supports proxy auth. + */ + private injectCustomApiCallOptions() { + this.types.nodes.forEach((node: INodeTypeDescription) => { + const isLatestVersion = + node.defaultVersion === undefined || node.defaultVersion === node.version; + + if (isLatestVersion) { + if (!this.supportsProxyAuth(node)) return; + + node.properties.forEach((p) => { + if ( + ['resource', 'operation'].includes(p.name) && + Array.isArray(p.options) && + p.options[p.options.length - 1].name !== CUSTOM_API_CALL_NAME + ) { + p.options.push({ + name: CUSTOM_API_CALL_NAME, + value: CUSTOM_API_CALL_KEY, + }); + } + }); + } + }); + } + private unloadNodes(installedNodes: InstalledNodes[]): void { installedNodes.forEach((installedNode) => { delete this.loaded.nodes[installedNode.type]; diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 4169b7c69a0c3..9084784aa339e 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -12,6 +12,8 @@ export const inProduction = NODE_ENV === 'production'; export const inDevelopment = !NODE_ENV || NODE_ENV === 'development'; export const inTest = NODE_ENV === 'test'; export const inE2ETests = E2E_TESTS === 'true'; +export const CUSTOM_API_CALL_NAME = 'Custom API Call'; +export const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__'; export const CLI_DIR = resolve(__dirname, '..'); export const TEMPLATES_DIR = join(CLI_DIR, 'templates'); diff --git a/packages/core/src/Constants.ts b/packages/core/src/Constants.ts index 45c1741dd020e..6d2fe99321336 100644 --- a/packages/core/src/Constants.ts +++ b/packages/core/src/Constants.ts @@ -11,8 +11,6 @@ export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__'; export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__'; export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN'; export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z'; -export const CUSTOM_API_CALL_NAME = 'Custom API Call'; -export const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__'; export const RESPONSE_ERROR_MESSAGES = { NO_ENCRYPTION_KEY: 'Encryption key is missing or was not set', diff --git a/packages/core/src/DirectoryLoader.ts b/packages/core/src/DirectoryLoader.ts index 0af2f534f230d..85528dcc07c62 100644 --- a/packages/core/src/DirectoryLoader.ts +++ b/packages/core/src/DirectoryLoader.ts @@ -15,7 +15,7 @@ import type { IVersionedNodeType, KnownNodesAndCredentials, } from 'n8n-workflow'; -import { CUSTOM_NODES_CATEGORY, CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_NAME } from './Constants'; +import { CUSTOM_NODES_CATEGORY } from './Constants'; import type { n8n } from './Interfaces'; import { loadClassInIsolation } from './ClassLoader'; @@ -54,48 +54,6 @@ export abstract class DirectoryLoader { return path.resolve(this.directory, file); } - /** - * Whether any of the node's credential types may be used to - * make a request from a node other than itself. - */ - private supportsProxyAuth(description: INodeTypeDescription) { - if (!description.credentials) return false; - - return description.credentials.some(({ name }) => { - const credType = this.credentialTypes[name].type; - - if (credType.authenticate !== undefined) return true; - - return ( - Array.isArray(credType.extends) && - credType.extends.some((parentType) => - ['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType), - ) - ); - }); - } - - /** - * Inject a `Custom API Call` option into `resource` and `operation` - * parameters in a node that supports proxy auth. - */ - private injectCustomApiCallOption(description: INodeTypeDescription) { - if (!this.supportsProxyAuth(description)) return; - - description.properties.forEach((p) => { - if ( - ['resource', 'operation'].includes(p.name) && - Array.isArray(p.options) && - p.options[p.options.length - 1].name !== CUSTOM_API_CALL_NAME - ) { - p.options.push({ - name: CUSTOM_API_CALL_NAME, - value: CUSTOM_API_CALL_KEY, - }); - } - }); - } - protected loadNodeFromFile(packageName: string, nodeName: string, filePath: string) { let tempNode: INodeType | IVersionedNodeType; let nodeVersion = 1; @@ -131,7 +89,6 @@ export abstract class DirectoryLoader { const currentVersionNode = tempNode.nodeVersions[tempNode.currentVersion]; this.addCodex({ node: currentVersionNode, filePath, isCustom: packageName === 'CUSTOM' }); - this.injectCustomApiCallOption(currentVersionNode.description); nodeVersion = tempNode.currentVersion; if (currentVersionNode.hasOwnProperty('executeSingle')) { @@ -143,7 +100,6 @@ export abstract class DirectoryLoader { } else { // Short renaming to avoid type issues - this.injectCustomApiCallOption(tempNode.description); nodeVersion = Array.isArray(tempNode.description.version) ? tempNode.description.version.slice(-1)[0] : tempNode.description.version; From 823530f72516fd19e8b278453390eb7b74ab90fa Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Fri, 3 Feb 2023 12:09:16 +0100 Subject: [PATCH 7/8] Load nodes and credentials fixtures from a single place --- cypress/e2e/2-credentials.cy.ts | 21 --------------------- cypress/e2e/4-node-creator.cy.ts | 16 +--------------- cypress/e2e/5-ndv.cy.ts | 17 ----------------- cypress/support/commands.ts | 4 ++-- cypress/support/e2e.ts | 27 +++++++++++++++++++++++++++ 5 files changed, 30 insertions(+), 55 deletions(-) diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index 648a1c2c49315..0249ab0bbdec9 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -1,7 +1,3 @@ -import CustomNodeFixture from '../fixtures/Custom_node_n8n_credential.json'; -import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json'; -import CustomCredential from '../fixtures/Custom_credential.json'; - import { NEW_NOTION_ACCOUNT_NAME, NOTION_NODE_NAME, @@ -36,23 +32,6 @@ describe('Credentials', () => { before(() => { cy.resetAll(); cy.setup({ email, firstName, lastName, password }); - - cy.intercept('GET', '/types/nodes.json', (req) => { - req.continue((res) => { - const nodes = res.body; - - nodes.push(CustomNodeFixture, CustomNodeWithCustomCredentialFixture); - res.send(nodes); - }); - }) - cy.intercept('GET', '/types/credentials.json', (req) => { - req.continue((res) => { - const credentials = res.body; - - credentials.push(CustomCredential); - res.send(credentials); - }); - }) }); beforeEach(() => { diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 84eb2d5f383cb..e70fddc777d20 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -1,6 +1,5 @@ import { NodeCreator } from '../pages/features/node-creator'; import { INodeTypeDescription } from 'n8n-workflow'; -import CustomNodeFixture from '../fixtures/Custom_node.json'; import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { randFirstName, randLastName } from '@ngneat/falso'; @@ -19,20 +18,6 @@ describe('Node Creator', () => { beforeEach(() => { cy.signin({ email, password }); - cy.intercept('GET', '/types/nodes.json', (req) => { - // Delete caching headers so that we can intercept the request - ['etag', 'if-none-match', 'if-modified-since'].forEach((header) => { - delete req.headers[header]; - }); - - req.continue((res) => { - const nodes = res.body as INodeTypeDescription[]; - - nodes.push(CustomNodeFixture as INodeTypeDescription); - res.send(nodes); - }); - }).as('nodesIntercept'); - cy.visit(nodeCreatorFeature.url); cy.waitForLoad(); }); @@ -153,6 +138,7 @@ describe('Node Creator', () => { }); it('should render and select community node', () => { + cy.intercept('GET', '/types/nodes.json').as('nodesIntercept'); cy.wait('@nodesIntercept').then(() => { const customCategory = 'Custom Category'; const customNode = 'E2E Node'; diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 22f9db4161f09..d5208e60d25e7 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -1,6 +1,3 @@ -import CustomNodeFixture from '../fixtures/Custom_node_n8n_credential.json'; -import { INodeTypeDescription } from 'n8n-workflow'; - import { WorkflowsPage, WorkflowPage, NDV } from '../pages'; import { v4 as uuid } from 'uuid'; @@ -13,20 +10,6 @@ describe('NDV', () => { cy.resetAll(); cy.skipSetup(); - cy.intercept('GET', '/types/nodes.json', (req) => { - // Delete caching headers so that we can intercept the request - ['etag', 'if-none-match', 'if-modified-since'].forEach((header) => { - delete req.headers[header]; - }); - - req.continue((res) => { - const nodes = res.body as INodeTypeDescription[]; - - nodes.push(CustomNodeFixture as INodeTypeDescription); - res.send(nodes); - }); - }).as('nodesIntercept'); - workflowsPage.actions.createWorkflowFromCard(); workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.saveWorkflowOnButtonClick(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 9a686ab2d0383..a5d9cd24a84df 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -57,8 +57,8 @@ Cypress.Commands.add( ); Cypress.Commands.add('waitForLoad', () => { - cy.getByTestId('node-view-loader').should('not.exist', { timeout: 10000 }); - cy.get('.el-loading-mask').should('not.exist', { timeout: 10000 }); + cy.getByTestId('node-view-loader', { timeout: 10000 }).should('not.exist'); + cy.get('.el-loading-mask', { timeout: 10000 }).should('not.exist'); }); Cypress.Commands.add('signin', ({ email, password }) => { diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index aeef4e42b00c5..3977b482107ae 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -14,3 +14,30 @@ // *********************************************************** import './commands'; +import CustomNodeFixture from '../fixtures/Custom_node.json'; +import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json'; +import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json'; +import CustomCredential from '../fixtures/Custom_credential.json'; + +// Load custom nodes and credentials fixtures +beforeEach(() => { + cy.intercept('GET', '/types/nodes.json', (req) => { + req.continue((res) => { + const nodes = res.body; + + res.headers['cache-control'] = 'no-cache, no-store'; + nodes.push(CustomNodeFixture, CustomNodeWithN8nCredentialFixture, CustomNodeWithCustomCredentialFixture); + res.send(nodes); + }); + }).as('nodesIntercept'); + + cy.intercept('GET', '/types/credentials.json', (req) => { + req.continue((res) => { + const credentials = res.body; + + res.headers['cache-control'] = 'no-cache, no-store'; + credentials.push(CustomCredential); + res.send(credentials); + }); + }).as('credentialsIntercept'); +}) From 687a45fe32e117fb8743c41eddc723089f688715 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Fri, 3 Feb 2023 12:47:21 +0100 Subject: [PATCH 8/8] Console warning if credential is invalid during customApiOptions injection --- packages/cli/src/LoadNodesAndCredentials.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index f388d6367207e..e2718a26ce35f 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -324,7 +324,12 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials { return description.credentials.some(({ name }) => { const credType = this.types.credentials.find((t) => t.name === name); - if (!credType) return false; + if (!credType) { + LoggerProxy.warn( + `Failed to load Custom API options for the node "${description.name}": Unknown credential name "${name}"`, + ); + return false; + } if (credType.authenticate !== undefined) return true; return (