mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor(core): Allow modules to define paths to load nodes from (#18193)
This commit is contained in:
@@ -218,3 +218,22 @@ describe('initModules', () => {
|
|||||||
expect(moduleRegistry.getActiveModules()).toEqual([moduleName]);
|
expect(moduleRegistry.getActiveModules()).toEqual([moduleName]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('loadDir', () => {
|
||||||
|
it('should load dirs defined by modules', async () => {
|
||||||
|
const TEST_LOAD_DIR = '/path/to/module/load/dir';
|
||||||
|
const ModuleClass = {
|
||||||
|
entities: jest.fn().mockReturnValue([]),
|
||||||
|
loadDir: jest.fn().mockReturnValue(TEST_LOAD_DIR),
|
||||||
|
};
|
||||||
|
const moduleMetadata = mock<ModuleMetadata>({
|
||||||
|
getClasses: jest.fn().mockReturnValue([ModuleClass]),
|
||||||
|
});
|
||||||
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
|
||||||
|
|
||||||
|
await moduleRegistry.loadModules([]); // empty to skip dynamic imports
|
||||||
|
|
||||||
|
expect(moduleRegistry.loadDirs).toEqual([TEST_LOAD_DIR]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { Logger } from '../logging/logger';
|
|||||||
export class ModuleRegistry {
|
export class ModuleRegistry {
|
||||||
readonly entities: EntityClass[] = [];
|
readonly entities: EntityClass[] = [];
|
||||||
|
|
||||||
|
readonly loadDirs: string[] = [];
|
||||||
|
|
||||||
readonly settings: Map<string, ModuleSettings> = new Map();
|
readonly settings: Map<string, ModuleSettings> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -79,9 +81,11 @@ export class ModuleRegistry {
|
|||||||
for (const ModuleClass of this.moduleMetadata.getClasses()) {
|
for (const ModuleClass of this.moduleMetadata.getClasses()) {
|
||||||
const entities = await Container.get(ModuleClass).entities?.();
|
const entities = await Container.get(ModuleClass).entities?.();
|
||||||
|
|
||||||
if (!entities || entities.length === 0) continue;
|
if (entities?.length) this.entities.push(...entities);
|
||||||
|
|
||||||
this.entities.push(...entities);
|
const loadDir = Container.get(ModuleClass).loadDir?.();
|
||||||
|
|
||||||
|
if (loadDir) this.loadDirs.push(loadDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ export interface ModuleInterface {
|
|||||||
shutdown?(): Promise<void>;
|
shutdown?(): Promise<void>;
|
||||||
entities?(): Promise<EntityClass[]>;
|
entities?(): Promise<EntityClass[]>;
|
||||||
settings?(): Promise<ModuleSettings>;
|
settings?(): Promise<ModuleSettings>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Path to a dir to load nodes and credentials from.
|
||||||
|
* @example '/Users/nathan/.n8n/nodes/node_modules'
|
||||||
|
*/
|
||||||
|
loadDir?(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModuleClass = Constructable<ModuleInterface>;
|
export type ModuleClass = Constructable<ModuleInterface>;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||||||
let instance: LoadNodesAndCredentials;
|
let instance: LoadNodesAndCredentials;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
|
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||||
instance.loaders.package1 = mock<DirectoryLoader>({
|
instance.loaders.package1 = mock<DirectoryLoader>({
|
||||||
directory: '/icons/package1',
|
directory: '/icons/package1',
|
||||||
});
|
});
|
||||||
@@ -58,7 +58,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('convertNodeToAiTool', () => {
|
describe('convertNodeToAiTool', () => {
|
||||||
const instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
|
const instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||||
|
|
||||||
let fullNodeWrapper: { description: INodeTypeDescription };
|
let fullNodeWrapper: { description: INodeTypeDescription };
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||||||
let instance: LoadNodesAndCredentials;
|
let instance: LoadNodesAndCredentials;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
|
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||||
instance.knownNodes['n8n-nodes-base.test'] = {
|
instance.knownNodes['n8n-nodes-base.test'] = {
|
||||||
className: 'Test',
|
className: 'Test',
|
||||||
sourcePath: '/nodes-base/dist/nodes/Test/Test.node.js',
|
sourcePath: '/nodes-base/dist/nodes/Test/Test.node.js',
|
||||||
@@ -330,7 +330,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||||||
let instance: LoadNodesAndCredentials;
|
let instance: LoadNodesAndCredentials;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
|
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||||
instance.types.nodes = [
|
instance.types.nodes = [
|
||||||
{
|
{
|
||||||
name: 'testNode',
|
name: 'testNode',
|
||||||
@@ -455,7 +455,7 @@ describe('LoadNodesAndCredentials', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
|
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
|
||||||
instance.loaders = { CUSTOM: mockLoader };
|
instance.loaders = { CUSTOM: mockLoader };
|
||||||
|
|
||||||
// Allow access to directory
|
// Allow access to directory
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { inTest, isContainedWithin, Logger } from '@n8n/backend-common';
|
import { inTest, isContainedWithin, Logger, ModuleRegistry } from '@n8n/backend-common';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
import type ParcelWatcher from '@parcel/watcher';
|
import type ParcelWatcher from '@parcel/watcher';
|
||||||
@@ -58,6 +58,7 @@ export class LoadNodesAndCredentials {
|
|||||||
private readonly errorReporter: ErrorReporter,
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly instanceSettings: InstanceSettings,
|
private readonly instanceSettings: InstanceSettings,
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly globalConfig: GlobalConfig,
|
||||||
|
private readonly moduleRegistry: ModuleRegistry,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -98,6 +99,10 @@ export class LoadNodesAndCredentials {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const dir of this.moduleRegistry.loadDirs) {
|
||||||
|
await this.loadNodesFromNodeModules(dir);
|
||||||
|
}
|
||||||
|
|
||||||
await this.loadNodesFromCustomDirectories();
|
await this.loadNodesFromCustomDirectories();
|
||||||
await this.postProcessLoaders();
|
await this.postProcessLoaders();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user