mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
fix(core): Resolve Custom API Call support through inherited credential types (#35820)
This commit is contained in:
parent
b00fe2708f
commit
88efdb975e
|
|
@ -6,10 +6,12 @@ import { Service } from '@n8n/di';
|
|||
import watcher from '@parcel/watcher';
|
||||
import fs from 'fs/promises';
|
||||
import { CUSTOM_NODES_PACKAGE_NAME, DirectoryLoader } from 'n8n-core';
|
||||
import type { INodeProperties, INodeTypeDescription } from 'n8n-workflow';
|
||||
import type { ICredentialType, INodeProperties, INodeTypeDescription } from 'n8n-workflow';
|
||||
import type { Mock } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_NAME } from '@/constants';
|
||||
|
||||
import { LoadNodesAndCredentials } from '../load-nodes-and-credentials';
|
||||
|
||||
vi.mock('lodash/debounce', () => ({ default: (fn: () => void) => fn }));
|
||||
|
|
@ -515,6 +517,97 @@ describe('LoadNodesAndCredentials', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('injectCustomApiCallOptions', () => {
|
||||
let instance: LoadNodesAndCredentials;
|
||||
|
||||
const makeNode = (credentialName: string): INodeTypeDescription => ({
|
||||
name: 'n8n-nodes-base.test',
|
||||
displayName: 'Test',
|
||||
group: ['transform'],
|
||||
description: 'Test node',
|
||||
version: 1,
|
||||
defaults: {},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [{ name: 'Page', value: 'page' }],
|
||||
default: 'page',
|
||||
},
|
||||
],
|
||||
credentials: [{ name: credentialName }],
|
||||
});
|
||||
|
||||
const makeCredential = (name: string, extendsTypes?: string[]): ICredentialType => ({
|
||||
name,
|
||||
displayName: name,
|
||||
properties: [],
|
||||
...(extendsTypes ? { extends: extendsTypes } : {}),
|
||||
});
|
||||
|
||||
const injectedOption = { name: CUSTOM_API_CALL_NAME, value: CUSTOM_API_CALL_KEY };
|
||||
|
||||
beforeEach(() => {
|
||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock(), mock());
|
||||
});
|
||||
|
||||
it('should inject the option when a credential extends a base OAuth type directly', () => {
|
||||
const node = makeNode('slackOAuth2Api');
|
||||
instance.types.nodes = [node];
|
||||
instance.types.credentials = [makeCredential('slackOAuth2Api', ['oAuth2Api'])];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(instance as any).injectCustomApiCallOptions();
|
||||
|
||||
expect(node.properties[0].options).toEqual([{ name: 'Page', value: 'page' }, injectedOption]);
|
||||
});
|
||||
|
||||
it('should inject the option when a credential reaches a base OAuth type through an intermediate', () => {
|
||||
const node = makeNode('confluenceCloudOAuth2Api');
|
||||
instance.types.nodes = [node];
|
||||
instance.types.credentials = [
|
||||
makeCredential('confluenceCloudOAuth2Api', ['atlassianOAuth2Api']),
|
||||
makeCredential('atlassianOAuth2Api', ['oAuth2Api']),
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(instance as any).injectCustomApiCallOptions();
|
||||
|
||||
expect(node.properties[0].options).toEqual([{ name: 'Page', value: 'page' }, injectedOption]);
|
||||
});
|
||||
|
||||
it('should not inject the option when the extends chain never reaches a base OAuth type', () => {
|
||||
const node = makeNode('someApi');
|
||||
instance.types.nodes = [node];
|
||||
instance.types.credentials = [
|
||||
makeCredential('someApi', ['intermediateApi']),
|
||||
makeCredential('intermediateApi'),
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(instance as any).injectCustomApiCallOptions();
|
||||
|
||||
expect(node.properties[0].options).toEqual([{ name: 'Page', value: 'page' }]);
|
||||
});
|
||||
|
||||
it('should not loop on a cyclic extends chain', () => {
|
||||
const node = makeNode('cyclicApi');
|
||||
instance.types.nodes = [node];
|
||||
instance.types.credentials = [
|
||||
makeCredential('cyclicApi', ['otherCyclicApi']),
|
||||
makeCredential('otherCyclicApi', ['cyclicApi']),
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(instance as any).injectCustomApiCallOptions();
|
||||
|
||||
expect(node.properties[0].options).toEqual([{ name: 'Page', value: 'page' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupHotReload', () => {
|
||||
let instance: LoadNodesAndCredentials;
|
||||
|
||||
|
|
|
|||
|
|
@ -351,12 +351,26 @@ export class LoadNodesAndCredentials {
|
|||
}
|
||||
if (credType.authenticate !== undefined) return true;
|
||||
|
||||
return (
|
||||
Array.isArray(credType.extends) &&
|
||||
credType.extends.some((parentType) =>
|
||||
['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType),
|
||||
)
|
||||
);
|
||||
return this.extendsProxyAuthBaseType(credType);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a credential type reaches one of the OAuth base types through its
|
||||
* `extends` chain. Walks the chain transitively (cycle-guarded), since OAuth
|
||||
* credentials often extend a vendor intermediate (e.g. `atlassianOAuth2Api`)
|
||||
* rather than a base type directly.
|
||||
*/
|
||||
private extendsProxyAuthBaseType(credType: ICredentialType, seen = new Set<string>()): boolean {
|
||||
if (!Array.isArray(credType.extends)) return false;
|
||||
|
||||
return credType.extends.some((parentName) => {
|
||||
if (['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentName)) return true;
|
||||
if (seen.has(parentName)) return false;
|
||||
seen.add(parentName);
|
||||
|
||||
const parent = this.types.credentials.find((t) => t.name === parentName);
|
||||
return parent !== undefined && this.extendsProxyAuthBaseType(parent, seen);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user