mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-21 20:00:02 +00:00
feat(core): Enable modules to append to workflow context (#18551)
This commit is contained in:
@@ -221,6 +221,48 @@ describe('initModules', () => {
|
|||||||
expect(moduleRegistry.isActive(moduleName as any)).toBe(true);
|
expect(moduleRegistry.isActive(moduleName as any)).toBe(true);
|
||||||
expect(moduleRegistry.getActiveModules()).toEqual([moduleName]);
|
expect(moduleRegistry.getActiveModules()).toEqual([moduleName]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('registers context for module with `context` method', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const moduleName = 'test-module';
|
||||||
|
const moduleContext = { proxy: 'test-proxy', config: { enabled: true } };
|
||||||
|
const ModuleClass: ModuleInterface = {
|
||||||
|
init: jest.fn(),
|
||||||
|
context: jest.fn().mockReturnValue(moduleContext),
|
||||||
|
};
|
||||||
|
const moduleMetadata = mock<ModuleMetadata>({
|
||||||
|
getEntries: jest.fn().mockReturnValue([[moduleName, { class: ModuleClass }]]),
|
||||||
|
});
|
||||||
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
|
|
||||||
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await moduleRegistry.initModules();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(ModuleClass.context).toHaveBeenCalled();
|
||||||
|
expect(moduleRegistry.context.has(moduleName)).toBe(true);
|
||||||
|
expect(moduleRegistry.context.get(moduleName)).toBe(moduleContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not register context for module without `context` method', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const moduleName = 'test-module';
|
||||||
|
const ModuleClass: ModuleInterface = { init: jest.fn() };
|
||||||
|
const moduleMetadata = mock<ModuleMetadata>({
|
||||||
|
getEntries: jest.fn().mockReturnValue([[moduleName, { class: ModuleClass }]]),
|
||||||
|
});
|
||||||
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
|
|
||||||
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await moduleRegistry.initModules();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(moduleRegistry.context.has(moduleName)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadDir', () => {
|
describe('loadDir', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ModuleMetadata } from '@n8n/decorators';
|
import { ModuleMetadata } from '@n8n/decorators';
|
||||||
import type { EntityClass, ModuleSettings } from '@n8n/decorators';
|
import type { EntityClass, ModuleContext, ModuleSettings } from '@n8n/decorators';
|
||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -19,6 +19,8 @@ export class ModuleRegistry {
|
|||||||
|
|
||||||
readonly settings: Map<string, ModuleSettings> = new Map();
|
readonly settings: Map<string, ModuleSettings> = new Map();
|
||||||
|
|
||||||
|
readonly context: Map<string, ModuleContext> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly moduleMetadata: ModuleMetadata,
|
private readonly moduleMetadata: ModuleMetadata,
|
||||||
private readonly licenseState: LicenseState,
|
private readonly licenseState: LicenseState,
|
||||||
@@ -116,6 +118,10 @@ export class ModuleRegistry {
|
|||||||
|
|
||||||
if (moduleSettings) this.settings.set(moduleName, moduleSettings);
|
if (moduleSettings) this.settings.set(moduleName, moduleSettings);
|
||||||
|
|
||||||
|
const moduleContext = await Container.get(ModuleClass).context?.();
|
||||||
|
|
||||||
|
if (moduleContext) this.context.set(moduleName, moduleContext);
|
||||||
|
|
||||||
this.logger.debug(`Initialized module "${moduleName}"`);
|
this.logger.debug(`Initialized module "${moduleName}"`);
|
||||||
|
|
||||||
this.activeModules.push(moduleName);
|
this.activeModules.push(moduleName);
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
export { ModuleInterface, BackendModule, EntityClass, ModuleSettings } from './module';
|
export {
|
||||||
|
ModuleInterface,
|
||||||
|
BackendModule,
|
||||||
|
EntityClass,
|
||||||
|
ModuleSettings,
|
||||||
|
ModuleContext,
|
||||||
|
} from './module';
|
||||||
export { ModuleMetadata } from './module-metadata';
|
export { ModuleMetadata } from './module-metadata';
|
||||||
|
|||||||
@@ -25,14 +25,54 @@ export interface TimestampedEntity {
|
|||||||
export type EntityClass = new () => BaseEntity | TimestampedEntity;
|
export type EntityClass = new () => BaseEntity | TimestampedEntity;
|
||||||
|
|
||||||
export type ModuleSettings = Record<string, unknown>;
|
export type ModuleSettings = Record<string, unknown>;
|
||||||
|
export type ModuleContext = Record<string, unknown>;
|
||||||
|
|
||||||
export interface ModuleInterface {
|
export interface ModuleInterface {
|
||||||
init?(): Promise<void>;
|
init?(): Promise<void>;
|
||||||
shutdown?(): Promise<void>;
|
shutdown?(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of entities to register with the typeorm database connection.
|
||||||
|
*
|
||||||
|
* @example [ InsightsByPeriod, InsightsMetadata, InsightsRaw ]
|
||||||
|
*/
|
||||||
entities?(): Promise<EntityClass[]>;
|
entities?(): Promise<EntityClass[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object with settings to send to the client via `/module-settings`.
|
||||||
|
*
|
||||||
|
* @example { summary: true, dashboard: false }
|
||||||
|
*/
|
||||||
settings?(): Promise<ModuleSettings>;
|
settings?(): Promise<ModuleSettings>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Return an object to merge into workflow context, a.k.a. `WorkflowExecuteAdditionalData`.
|
||||||
|
* This object will be namespaced under the module name set by `@BackendModule('name')`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // at Module.context()
|
||||||
|
* { proxy: Container.get(InsightsProxyService) }
|
||||||
|
*
|
||||||
|
* // at callsite
|
||||||
|
* additionalData.insights.proxy.method()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* For type safety, add the module context to `IWorkflowExecuteAdditionalData`.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* export interface IWorkflowExecuteAdditionalData {
|
||||||
|
* insights?: {
|
||||||
|
* proxy: { method: () => void };
|
||||||
|
* };
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
context?(): Promise<ModuleContext>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a path to a dir to load nodes and credentials from.
|
||||||
|
*
|
||||||
* @returns Path to a dir to load nodes and credentials from. `null` to skip.
|
* @returns Path to a dir to load nodes and credentials from. `null` to skip.
|
||||||
* @example '/Users/nathan/.n8n/nodes/node_modules'
|
* @example '/Users/nathan/.n8n/nodes/node_modules'
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ export async function getBase(
|
|||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
const additionalData: IWorkflowExecuteAdditionalData = {
|
||||||
dataStoreProxyProvider,
|
dataStoreProxyProvider,
|
||||||
currentNodeExecutionIndex: 0,
|
currentNodeExecutionIndex: 0,
|
||||||
credentialsHelper: Container.get(CredentialsHelper),
|
credentialsHelper: Container.get(CredentialsHelper),
|
||||||
@@ -450,4 +450,12 @@ export async function getBase(
|
|||||||
logAiEvent: (eventName: keyof AiEventMap, payload: AiEventPayload) =>
|
logAiEvent: (eventName: keyof AiEventMap, payload: AiEventPayload) =>
|
||||||
eventService.emit(eventName, payload),
|
eventService.emit(eventName, payload),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const [moduleName, moduleContext] of Container.get(ModuleRegistry).context.entries()) {
|
||||||
|
// @ts-expect-error Adding an index signature `[key: string]: unknown`
|
||||||
|
// to `IWorkflowExecuteAdditionalData` triggers complex type errors for derived types.
|
||||||
|
additionalData[moduleName] = moduleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
return additionalData;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user