Skip to content

Commit

Permalink
fix(core): Fix populating of node custom api call options (#5347)
Browse files Browse the repository at this point in the history
* feat(core): Fix populating of node custom api call options

* lint fixes

* Adress PR comments

* Add e2e test and only inject custom API options for latest version

* Make sure to injectCustomApiCallOption for the latest version of node

* feat(cli): Move apiCallOption injection to LoadNodesAndCredentials and add e2e tests to check for custom nodes credentials

* Load nodes and credentials fixtures from a single place

* Console warning if credential is invalid during customApiOptions injection
  • Loading branch information
OlegIvaniv authored Feb 3, 2023
1 parent 4dab2fe commit 6985500
Show file tree
Hide file tree
Showing 16 changed files with 269 additions and 104 deletions.
22 changes: 20 additions & 2 deletions cypress/e2e/2-credentials.cy.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { HTTP_REQUEST_NODE_TYPE } from './../../packages/editor-ui/src/constants';
import {
NEW_NOTION_ACCOUNT_NAME,
NOTION_NODE_NAME,
PIPEDRIVE_NODE_NAME,
HTTP_REQUEST_NODE_NAME,
NEW_QUERY_AUTH_ACCOUNT_NAME,
} from './../constants';
import { visit } from 'recast';
import {
DEFAULT_USER_EMAIL,
DEFAULT_USER_PASSWORD,
Expand Down Expand Up @@ -252,4 +250,24 @@ describe('Credentials', () => {
credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_QUERY_AUTH_ACCOUNT_NAME);
});

it('should render custom node with n8n credential', () => {
workflowPage.actions.visit();
workflowPage.actions.addNodeToCanvas('Manual Trigger');
workflowPage.actions.addNodeToCanvas('E2E Node with native n8n credential', true, true);
workflowPage.getters.nodeCredentialsLabel().click();
cy.contains('Create New Credential').click();
credentialsModal.getters.editCredentialModal().should('be.visible');
credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API');
})

it('should render custom node with custom credential', () => {
workflowPage.actions.visit();
workflowPage.actions.addNodeToCanvas('Manual Trigger');
workflowPage.actions.addNodeToCanvas('E2E Node with custom credential', true, true);
workflowPage.getters.nodeCredentialsLabel().click();
cy.contains('Create New Credential').click();
credentialsModal.getters.editCredentialModal().should('be.visible');
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
})
});
16 changes: 1 addition & 15 deletions cypress/e2e/4-node-creator.cy.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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();
});
Expand Down Expand Up @@ -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';
Expand Down
2 changes: 2 additions & 0 deletions cypress/e2e/5-ndv.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ describe('NDV', () => {
beforeEach(() => {
cy.resetAll();
cy.skipSetup();

workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.renameWorkflow(uuid());
workflowPage.actions.saveWorkflowOnButtonClick();
});


it('should show up when double clicked on a node and close when Back to canvas clicked', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
workflowPage.getters.canvasNodes().first().dblclick();
Expand Down
19 changes: 19 additions & 0 deletions cypress/fixtures/Custom_credential.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "customE2eCredential",
"displayName": "Custom E2E Credential",
"properties": [{
"displayName": "API Key",
"name": "apiKey",
"type": "string",
"default": "",
"required": false
}],
"authenticate": {
"type": "generic",
"properties": {
"qs": {
"auth": "={{$credentials.apiKey}}"
}
}
}
}
57 changes: 57 additions & 0 deletions cypress/fixtures/Custom_node_custom_credential.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"properties": [
{
"displayName": "Test property",
"name": "testProp",
"type": "string",
"required": true,
"noDataExpression": false,
"default": "Some default"
},
{
"displayName": "Resource",
"name": "resource",
"type": "options",
"noDataExpression": true,
"options": [
{
"name": "option1",
"value": "option1"
},
{
"name": "option2",
"value": "option2"
},
{
"name": "option3",
"value": "option3"
},
{
"name": "option4",
"value": "option4"
}
],
"default": "option2"
}
],
"displayName": "E2E Node with custom credential",
"name": "@e2e/n8n-nodes-e2e-custom-credential",
"group": ["transform"],
"codex": {
"categories": ["Custom Category"]
},
"version": 1,
"description": "Demonstrate rendering of node with custom credential",
"defaults": {
"name": "E2E Node with custom credential"
},
"inputs": ["main"],
"outputs": ["main"],
"icon": "fa:network-wired",
"credentials": [
{
"name": "customE2eCredential",
"required": true
}
]
}
57 changes: 57 additions & 0 deletions cypress/fixtures/Custom_node_n8n_credential.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"properties": [
{
"displayName": "Test property",
"name": "testProp",
"type": "string",
"required": true,
"noDataExpression": false,
"default": "Some default"
},
{
"displayName": "Resource",
"name": "resource",
"type": "options",
"noDataExpression": true,
"options": [
{
"name": "option1",
"value": "option1"
},
{
"name": "option2",
"value": "option2"
},
{
"name": "option3",
"value": "option3"
},
{
"name": "option4",
"value": "option4"
}
],
"default": "option2"
}
],
"displayName": "E2E Node with native n8n credential",
"name": "@e2e/n8n-nodes-e2e-credential",
"group": ["transform"],
"codex": {
"categories": ["Custom Category"]
},
"version": 1,
"description": "Demonstrate rendering of node with native credential",
"defaults": {
"name": "E2E Node with native n8n credential"
},
"inputs": ["main"],
"outputs": ["main"],
"icon": "fa:network-wired",
"credentials": [
{
"name": "notionApi",
"required": true
}
]
}
1 change: 1 addition & 0 deletions cypress/pages/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
4 changes: 2 additions & 2 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
27 changes: 27 additions & 0 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
})
64 changes: 63 additions & 1 deletion packages/cli/src/LoadNodesAndCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
ILogger,
INodesAndCredentials,
KnownNodesAndCredentials,
INodeTypeDescription,
LoadedNodesAndCredentials,
} from 'n8n-workflow';
import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
Expand All @@ -29,7 +30,13 @@ import config from '@/config';
import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { InstalledNodes } from '@db/entities/InstalledNodes';
import { executeCommand } from '@/CommunityNodes/helpers';
import { CLI_DIR, GENERATED_STATIC_DIR, RESPONSE_ERROR_MESSAGES } from '@/constants';
import {
CLI_DIR,
GENERATED_STATIC_DIR,
RESPONSE_ERROR_MESSAGES,
CUSTOM_API_CALL_KEY,
CUSTOM_API_CALL_NAME,
} from '@/constants';
import {
persistInstalledPackageData,
removePackageFromDatabase,
Expand Down Expand Up @@ -66,6 +73,7 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
await this.loadNodesFromBasePackages();
await this.loadNodesFromDownloadedPackages();
await this.loadNodesFromCustomDirectories();
this.injectCustomApiCallOptions();
}

async generateTypesForFrontend() {
Expand Down Expand Up @@ -307,6 +315,60 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
}
}

/**
* 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.types.credentials.find((t) => t.name === name);
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 (
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];
Expand Down
Loading

0 comments on commit 6985500

Please sign in to comment.