Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Fix populating of node custom api call options #5347

Merged
merged 9 commits into from
Feb 3, 2023
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