fix(core): Fix populating of node custom api call options (#5347)
* 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
This commit is contained in:
@@ -12,6 +12,7 @@ import type {
|
||||
ILogger,
|
||||
INodesAndCredentials,
|
||||
KnownNodesAndCredentials,
|
||||
INodeTypeDescription,
|
||||
LoadedNodesAndCredentials,
|
||||
} from 'n8n-workflow';
|
||||
import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||
@@ -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,
|
||||
@@ -66,6 +73,7 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
|
||||
await this.loadNodesFromBasePackages();
|
||||
await this.loadNodesFromDownloadedPackages();
|
||||
await this.loadNodesFromCustomDirectories();
|
||||
this.injectCustomApiCallOptions();
|
||||
}
|
||||
|
||||
async generateTypesForFrontend() {
|
||||
@@ -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];
|
||||
|
||||
@@ -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<INodeTypeDescription[]>((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[] = [];
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user