fix: Lazy load nodes for credentials testing (#4760)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2022-11-30 10:28:18 +01:00 committed by GitHub
parent 3d67df490c
commit 0a7a2f3e41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 132 additions and 123 deletions

View file

@ -18,6 +18,10 @@ class CredentialTypesClass implements ICredentialTypes {
return this.getCredential(credentialType).type; return this.getCredential(credentialType).type;
} }
getNodeTypesToTestWith(type: string): string[] {
return this.knownCredentials[type]?.nodesToTestWith ?? [];
}
private getCredential(type: string): LoadedClass<ICredentialType> { private getCredential(type: string): LoadedClass<ICredentialType> {
const loadedCredentials = this.loadedCredentials; const loadedCredentials = this.loadedCredentials;
if (type in loadedCredentials) { if (type in loadedCredentials) {

View file

@ -39,6 +39,7 @@ import {
IHttpRequestHelper, IHttpRequestHelper,
INodeTypeData, INodeTypeData,
INodeTypes, INodeTypes,
ICredentialTypes,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
@ -47,25 +48,48 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
import { User } from '@db/entities/User'; import { User } from '@db/entities/User';
import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { whereClause } from './UserManagement/UserManagementHelper'; import { whereClause } from './UserManagement/UserManagementHelper';
import { RESPONSE_ERROR_MESSAGES } from './constants';
const mockNodesData: INodeTypeData = {}; const mockNode = {
const mockNodeTypes: INodeTypes = { name: '',
getAll(): Array<INodeType | IVersionedNodeType> { typeVersion: 1,
return Object.values(mockNodesData).map((data) => data.type); type: 'mock',
position: [0, 0],
parameters: {} as INodeParameters,
} as INode;
const mockNodesData: INodeTypeData = {
mock: {
sourcePath: '',
type: {
description: { properties: [] as INodeProperties[] },
} as INodeType,
}, },
getByNameAndVersion(nodeType: string, version?: number): INodeType | undefined { };
if (mockNodesData[nodeType] === undefined) {
return undefined; const mockNodeTypes: INodeTypes = {
getByName(nodeType: string): INodeType | IVersionedNodeType {
return mockNodesData[nodeType]?.type;
},
getByNameAndVersion(nodeType: string, version?: number): INodeType {
if (!mockNodesData[nodeType]) {
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_NODE}: ${nodeType}`);
} }
return NodeHelpers.getVersionedNodeType(mockNodesData[nodeType].type, version); return NodeHelpers.getVersionedNodeType(mockNodesData[nodeType].type, version);
}, },
}; };
export class CredentialsHelper extends ICredentialsHelper { export class CredentialsHelper extends ICredentialsHelper {
private credentialTypes = CredentialTypes(); constructor(
encryptionKey: string,
private credentialTypes: ICredentialTypes = CredentialTypes(),
private nodeTypes: INodeTypes = NodeTypes(),
) {
super(encryptionKey);
}
/** /**
* Add the required authentication information to the request * Add the required authentication information to the request
@ -387,16 +411,8 @@ export class CredentialsHelper extends ICredentialsHelper {
throw e; throw e;
} }
} else { } else {
const node = {
name: '',
typeVersion: 1,
type: 'mock',
position: [0, 0],
parameters: {} as INodeParameters,
} as INode;
const workflow = new Workflow({ const workflow = new Workflow({
nodes: [node], nodes: [mockNode],
connections: {}, connections: {},
active: false, active: false,
nodeTypes: mockNodeTypes, nodeTypes: mockNodeTypes,
@ -404,7 +420,7 @@ export class CredentialsHelper extends ICredentialsHelper {
// Resolve expressions if any are set // Resolve expressions if any are set
decryptedData = workflow.expression.getComplexParameterValue( decryptedData = workflow.expression.getComplexParameterValue(
node, mockNode,
decryptedData as INodeParameters, decryptedData as INodeParameters,
mode, mode,
defaultTimezone, defaultTimezone,
@ -457,28 +473,27 @@ export class CredentialsHelper extends ICredentialsHelper {
await Db.collections.Credentials.update(findQuery, newCredentialsData); await Db.collections.Credentials.update(findQuery, newCredentialsData);
} }
getCredentialTestFunction( private getCredentialTestFunction(
credentialType: string, credentialType: string,
nodeToTestWith?: string,
): ICredentialTestFunction | ICredentialTestRequestData | undefined { ): ICredentialTestFunction | ICredentialTestRequestData | undefined {
const nodeTypes = NodeTypes(); // Check if test is defined on credentials
const allNodes = nodeTypes.getAll(); const type = this.credentialTypes.getByName(credentialType);
if (type.test) {
return {
testRequest: type.test,
};
}
// Check all the nodes one by one if they have a test function defined const nodeTypesToTestWith = this.credentialTypes.getNodeTypesToTestWith(credentialType);
for (let i = 0; i < allNodes.length; i++) { for (const nodeName of nodeTypesToTestWith) {
const node = allNodes[i]; const node = this.nodeTypes.getByName(nodeName);
if (nodeToTestWith && node.description.name !== nodeToTestWith) {
// eslint-disable-next-line no-continue
continue;
}
// Always set to an array even if node is not versioned to not having // Always set to an array even if node is not versioned to not having
// to duplicate the logic // to duplicate the logic
const allNodeTypes: INodeType[] = []; const allNodeTypes: INodeType[] = [];
if (node instanceof VersionedNodeType) { if (node instanceof VersionedNodeType) {
// Node is versioned // Node is versioned
allNodeTypes.push(...Object.values((node as IVersionedNodeType).nodeVersions)); allNodeTypes.push(...Object.values(node.nodeVersions));
} else { } else {
// Node is not versioned // Node is not versioned
allNodeTypes.push(node as INodeType); allNodeTypes.push(node as INodeType);
@ -487,49 +502,35 @@ export class CredentialsHelper extends ICredentialsHelper {
// Check each of the node versions for credential tests // Check each of the node versions for credential tests
for (const nodeType of allNodeTypes) { for (const nodeType of allNodeTypes) {
// Check each of teh credentials // Check each of teh credentials
for (const credential of nodeType.description.credentials ?? []) { for (const { name, testedBy } of nodeType.description.credentials ?? []) {
if (credential.name === credentialType && !!credential.testedBy) { if (name === credentialType && !!testedBy) {
if (typeof credential.testedBy === 'string') { if (typeof testedBy === 'string') {
if (Object.prototype.hasOwnProperty.call(node, 'nodeVersions')) { if (node instanceof VersionedNodeType) {
// The node is versioned. So check all versions for test function // The node is versioned. So check all versions for test function
// starting with the latest // starting with the latest
const versions = Object.keys((node as IVersionedNodeType).nodeVersions) const versions = Object.keys(node.nodeVersions).sort().reverse();
.sort()
.reverse();
for (const version of versions) { for (const version of versions) {
const versionedNode = (node as IVersionedNodeType).nodeVersions[ const versionedNode = node.nodeVersions[parseInt(version, 10)];
parseInt(version, 10) const credentialTest = versionedNode.methods?.credentialTest;
]; if (credentialTest && testedBy in credentialTest) {
if ( return credentialTest[testedBy];
versionedNode.methods?.credentialTest &&
versionedNode.methods?.credentialTest[credential.testedBy]
) {
return versionedNode.methods?.credentialTest[credential.testedBy];
} }
} }
} }
// Test is defined as string which links to a function // Test is defined as string which links to a function
return (node as unknown as INodeType).methods?.credentialTest![credential.testedBy]; return (node as unknown as INodeType).methods?.credentialTest![testedBy];
} }
// Test is defined as JSON with a definition for the request to make // Test is defined as JSON with a definition for the request to make
return { return {
nodeType, nodeType,
testRequest: credential.testedBy, testRequest: testedBy,
}; };
} }
} }
} }
} }
// Check if test is defined on credentials
const type = this.credentialTypes.getByName(credentialType);
if (type.test) {
return {
testRequest: type.test,
};
}
return undefined; return undefined;
} }
@ -537,9 +538,8 @@ export class CredentialsHelper extends ICredentialsHelper {
user: User, user: User,
credentialType: string, credentialType: string,
credentialsDecrypted: ICredentialsDecrypted, credentialsDecrypted: ICredentialsDecrypted,
nodeToTestWith?: string,
): Promise<INodeCredentialTestResult> { ): Promise<INodeCredentialTestResult> {
const credentialTestFunction = this.getCredentialTestFunction(credentialType, nodeToTestWith); const credentialTestFunction = this.getCredentialTestFunction(credentialType);
if (credentialTestFunction === undefined) { if (credentialTestFunction === undefined) {
return Promise.resolve({ return Promise.resolve({
status: 'Error', status: 'Error',
@ -570,8 +570,7 @@ export class CredentialsHelper extends ICredentialsHelper {
if (credentialTestFunction.nodeType) { if (credentialTestFunction.nodeType) {
nodeType = credentialTestFunction.nodeType; nodeType = credentialTestFunction.nodeType;
} else { } else {
const nodeTypes = NodeTypes(); nodeType = this.nodeTypes.getByNameAndVersion('n8n-nodes-base.noOp');
nodeType = nodeTypes.getByNameAndVersion('n8n-nodes-base.noOp');
} }
const node: INode = { const node: INode = {

View file

@ -15,7 +15,7 @@ import type {
ITelemetrySettings, ITelemetrySettings,
ITelemetryTrackProperties, ITelemetryTrackProperties,
IWorkflowBase as IWorkflowBaseWorkflow, IWorkflowBase as IWorkflowBaseWorkflow,
LoadingDetails, CredentialLoadingDetails,
Workflow, Workflow,
WorkflowActivateMode, WorkflowActivateMode,
WorkflowExecuteMode, WorkflowExecuteMode,
@ -59,7 +59,7 @@ export interface ICustomRequest extends Request {
} }
export interface ICredentialsTypeData { export interface ICredentialsTypeData {
[key: string]: LoadingDetails; [key: string]: CredentialLoadingDetails;
} }
export interface ICredentialsOverwrite { export interface ICredentialsOverwrite {

View file

@ -366,8 +366,12 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
} }
for (const type in known.credentials) { for (const type in known.credentials) {
const { className, sourcePath } = known.credentials[type]; const { className, sourcePath, nodesToTestWith } = known.credentials[type];
this.known.credentials[type] = { className, sourcePath: path.join(dir, sourcePath) }; this.known.credentials[type] = {
className,
sourcePath: path.join(dir, sourcePath),
nodesToTestWith: nodesToTestWith?.map((nodeName) => `${packageName}.${nodeName}`),
};
} }
} }

View file

@ -21,10 +21,6 @@ class NodeTypesClass implements INodeTypes {
} }
} }
getAll(): Array<INodeType | IVersionedNodeType> {
return Object.values(this.loadedNodes).map(({ type }) => type);
}
/** /**
* Variant of `getByNameAndVersion` that includes the node's source path, used to locate a node's translations. * Variant of `getByNameAndVersion` that includes the node's source path, used to locate a node's translations.
*/ */
@ -43,6 +39,10 @@ class NodeTypesClass implements INodeTypes {
return { description: { ...description }, sourcePath: nodeType.sourcePath }; return { description: { ...description }, sourcePath: nodeType.sourcePath };
} }
getByName(nodeType: string): INodeType | IVersionedNodeType {
return this.getNode(nodeType).type;
}
getByNameAndVersion(nodeType: string, version?: number): INodeType { getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedNodeType(this.getNode(nodeType).type, version); return NodeHelpers.getVersionedNodeType(this.getNode(nodeType).type, version);
} }

View file

@ -106,7 +106,7 @@ EECredentialsController.get(
EECredentialsController.post( EECredentialsController.post(
'/test', '/test',
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => { ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
const { credentials, nodeToTestWith } = req.body; const { credentials } = req.body;
const encryptionKey = await EECredentials.getEncryptionKey(); const encryptionKey = await EECredentials.getEncryptionKey();
@ -122,7 +122,7 @@ EECredentialsController.post(
Object.assign(credentials, { data: decryptedData }); Object.assign(credentials, { data: decryptedData });
} }
return EECredentials.test(req.user, encryptionKey, credentials, nodeToTestWith); return EECredentials.test(req.user, encryptionKey, credentials);
}), }),
); );

View file

@ -113,10 +113,10 @@ credentialsController.get(
credentialsController.post( credentialsController.post(
'/test', '/test',
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => { ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
const { credentials, nodeToTestWith } = req.body; const { credentials } = req.body;
const encryptionKey = await CredentialsService.getEncryptionKey(); const encryptionKey = await CredentialsService.getEncryptionKey();
return CredentialsService.test(req.user, encryptionKey, credentials, nodeToTestWith); return CredentialsService.test(req.user, encryptionKey, credentials);
}), }),
); );

View file

@ -282,10 +282,9 @@ export class CredentialsService {
user: User, user: User,
encryptionKey: string, encryptionKey: string,
credentials: ICredentialsDecrypted, credentials: ICredentialsDecrypted,
nodeToTestWith: string | undefined,
): Promise<INodeCredentialTestResult> { ): Promise<INodeCredentialTestResult> {
const helper = new CredentialsHelper(encryptionKey); const helper = new CredentialsHelper(encryptionKey);
return helper.testCredentials(user, credentials.type, credentials, nodeToTestWith); return helper.testCredentials(user, credentials.type, credentials);
} }
} }

View file

@ -2,7 +2,6 @@ import {
IAuthenticateGeneric, IAuthenticateGeneric,
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
ICredentialType, ICredentialType,
ICredentialTypeData,
IHttpRequestOptions, IHttpRequestOptions,
INode, INode,
INodeProperties, INodeProperties,
@ -234,9 +233,13 @@ describe('CredentialsHelper', () => {
}, },
}; };
CredentialTypes(mockNodesAndCredentials); const credentialTypes = CredentialTypes(mockNodesAndCredentials);
const credentialsHelper = new CredentialsHelper(TEST_ENCRYPTION_KEY); const credentialsHelper = new CredentialsHelper(
TEST_ENCRYPTION_KEY,
credentialTypes,
nodeTypes,
);
const result = await credentialsHelper.authenticate( const result = await credentialsHelper.authenticate(
testData.input.credentials, testData.input.credentials,

View file

@ -3,6 +3,7 @@ import {
INodeType, INodeType,
INodeTypeData, INodeTypeData,
INodeTypes, INodeTypes,
IVersionedNodeType,
NodeHelpers, NodeHelpers,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -48,12 +49,8 @@ class NodeTypesClass implements INodeTypes {
} }
} }
getAll(): INodeType[] { getByName(nodeType: string): INodeType | IVersionedNodeType {
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedNodeType(data.type)); return this.nodeTypes[nodeType].type;
}
getByName(nodeType: string): INodeType {
return this.getByNameAndVersion(nodeType);
} }
getByNameAndVersion(nodeType: string, version?: number): INodeType { getByNameAndVersion(nodeType: string, version?: number): INodeType {

View file

@ -22,7 +22,9 @@ const loadClass = (sourcePath) => {
} }
}; };
const generate = (kind) => { const nodesToTestWith = {};
const generate = async (kind) => {
const data = glob const data = glob
.sync(`dist/${kind}/**/*.${kind === 'nodes' ? 'node' : kind}.js`, { .sync(`dist/${kind}/**/*.${kind === 'nodes' ? 'node' : kind}.js`, {
cwd: packageDir, cwd: packageDir,
@ -34,12 +36,28 @@ const generate = (kind) => {
const name = kind === 'nodes' ? instance.description.name : instance.name; const name = kind === 'nodes' ? instance.description.name : instance.name;
if (name in obj) console.error('already loaded', kind, name, sourcePath); if (name in obj) console.error('already loaded', kind, name, sourcePath);
else obj[name] = { className, sourcePath }; else obj[name] = { className, sourcePath };
if (kind === 'nodes') {
const { credentials } = instance.description;
if (credentials && credentials.length) {
for (const credential of credentials) {
nodesToTestWith[credential.name] = nodesToTestWith[credential.name] || [];
nodesToTestWith[credential.name].push(name);
}
}
} else {
if (name in nodesToTestWith) {
obj[name].nodesToTestWith = nodesToTestWith[name];
}
}
return obj; return obj;
}, {}); }, {});
LoggerProxy.info(`Detected ${Object.keys(data).length} ${kind}`); LoggerProxy.info(`Detected ${Object.keys(data).length} ${kind}`);
return writeJSON(`known/${kind}.json`, data); await writeJSON(`known/${kind}.json`, data);
return data;
}; };
(async () => { (async () => {
await Promise.all([generate('credentials'), generate('nodes')]); await generate('nodes');
await generate('credentials');
})(); })();

View file

@ -17,6 +17,7 @@ import {
INodeTypes, INodeTypes,
IRun, IRun,
ITaskData, ITaskData,
IVersionedNodeType,
IWorkflowBase, IWorkflowBase,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
NodeHelpers, NodeHelpers,
@ -805,12 +806,8 @@ class NodeTypesClass implements INodeTypes {
}, },
}; };
getAll(): INodeType[] { getByName(nodeType: string): INodeType | IVersionedNodeType {
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedNodeType(data.type)); return this.nodeTypes[nodeType].type;
}
getByName(nodeType: string): INodeType {
return this.getByNameAndVersion(nodeType);
} }
getByNameAndVersion(nodeType: string, version?: number): INodeType { getByNameAndVersion(nodeType: string, version?: number): INodeType {

View file

@ -335,10 +335,6 @@ export const workflowHelpers = mixins(
const nodeTypes: INodeTypes = { const nodeTypes: INodeTypes = {
nodeTypes: {}, nodeTypes: {},
init: async (nodeTypes?: INodeTypeData): Promise<void> => { }, init: async (nodeTypes?: INodeTypeData): Promise<void> => { },
getAll: (): Array<INodeType | IVersionedNodeType> => {
// Does not get used in Workflow so no need to return it
return [];
},
getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => {
const nodeTypeDescription = this.nodeTypesStore.getNodeType(nodeType, version); const nodeTypeDescription = this.nodeTypesStore.getNodeType(nodeType, version);

View file

@ -221,7 +221,7 @@ export class Mocean implements INodeType {
let endpoint: string; let endpoint: string;
let operation: string; let operation: string;
let requesetMethod: string; let requestMethod: string;
let resource: string; let resource: string;
let text: string; let text: string;
let dlrUrl: string; let dlrUrl: string;
@ -238,7 +238,7 @@ export class Mocean implements INodeType {
resource = this.getNodeParameter('resource', itemIndex, '') as string; resource = this.getNodeParameter('resource', itemIndex, '') as string;
operation = this.getNodeParameter('operation', itemIndex, '') as string; operation = this.getNodeParameter('operation', itemIndex, '') as string;
text = this.getNodeParameter('message', itemIndex, '') as string; text = this.getNodeParameter('message', itemIndex, '') as string;
requesetMethod = 'POST'; requestMethod = 'POST';
body['mocean-from'] = this.getNodeParameter('from', itemIndex, '') as string; body['mocean-from'] = this.getNodeParameter('from', itemIndex, '') as string;
body['mocean-to'] = this.getNodeParameter('to', itemIndex, '') as string; body['mocean-to'] = this.getNodeParameter('to', itemIndex, '') as string;
@ -271,13 +271,7 @@ export class Mocean implements INodeType {
} }
if (operation === 'send') { if (operation === 'send') {
const responseData = await moceanApiRequest.call( const responseData = await moceanApiRequest.call(this, requestMethod, endpoint, body, qs);
this,
requesetMethod,
endpoint,
body,
qs,
);
for (const item of responseData[dataKey] as IDataObject[]) { for (const item of responseData[dataKey] as IDataObject[]) {
item.type = resource; item.type = resource;

View file

@ -181,11 +181,7 @@ export interface IHttpRequestHelper {
helpers: { httpRequest: IAllExecuteFunctions['helpers']['httpRequest'] }; helpers: { httpRequest: IAllExecuteFunctions['helpers']['httpRequest'] };
} }
export abstract class ICredentialsHelper { export abstract class ICredentialsHelper {
encryptionKey: string; constructor(readonly encryptionKey: string) {}
constructor(encryptionKey: string) {
this.encryptionKey = encryptionKey;
}
abstract getParentTypes(name: string): string[]; abstract getParentTypes(name: string): string[];
@ -329,6 +325,7 @@ export interface ICredentialType {
export interface ICredentialTypes { export interface ICredentialTypes {
recognizes(credentialType: string): boolean; recognizes(credentialType: string): boolean;
getByName(credentialType: string): ICredentialType; getByName(credentialType: string): ICredentialType;
getNodeTypesToTestWith(type: string): string[];
} }
// The way the credentials get saved in the database (data encrypted) // The way the credentials get saved in the database (data encrypted)
@ -1209,7 +1206,6 @@ export interface INodeCredentialTestResult {
} }
export interface INodeCredentialTestRequest { export interface INodeCredentialTestRequest {
nodeToTestWith?: string; // node name i.e. slack
credentials: ICredentialsDecrypted; credentials: ICredentialsDecrypted;
} }
@ -1474,18 +1470,24 @@ export type WebhookResponseData = 'allEntries' | 'firstEntryJson' | 'firstEntryB
export type WebhookResponseMode = 'onReceived' | 'lastNode'; export type WebhookResponseMode = 'onReceived' | 'lastNode';
export interface INodeTypes { export interface INodeTypes {
getAll(): Array<INodeType | IVersionedNodeType>; getByName(nodeType: string): INodeType | IVersionedNodeType;
getByNameAndVersion(nodeType: string, version?: number): INodeType | undefined; getByNameAndVersion(nodeType: string, version?: number): INodeType;
} }
export type LoadingDetails = { type LoadingDetails = {
className: string; className: string;
sourcePath: string; sourcePath: string;
}; };
export type CredentialLoadingDetails = LoadingDetails & {
nodesToTestWith?: string[];
};
export type NodeLoadingDetails = LoadingDetails;
export type KnownNodesAndCredentials = { export type KnownNodesAndCredentials = {
nodes: Record<string, LoadingDetails>; nodes: Record<string, NodeLoadingDetails>;
credentials: Record<string, LoadingDetails>; credentials: Record<string, CredentialLoadingDetails>;
}; };
export interface LoadedClass<T> { export interface LoadedClass<T> {

View file

@ -673,12 +673,8 @@ class NodeTypesClass implements INodeTypes {
}, },
}; };
getAll(): INodeType[] { getByName(nodeType: string): INodeType | IVersionedNodeType {
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedNodeType(data.type)); return this.nodeTypes[nodeType].type;
}
getByName(nodeType: string): INodeType | IVersionedNodeType | undefined {
return this.getByNameAndVersion(nodeType);
} }
getByNameAndVersion(nodeType: string, version?: number): INodeType { getByNameAndVersion(nodeType: string, version?: number): INodeType {