mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
refactor(core): Move module logic to @n8n/backend-common (#16528)
Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@n8n/config": "workspace:^",
|
"@n8n/config": "workspace:^",
|
||||||
"@n8n/constants": "workspace:^",
|
"@n8n/constants": "workspace:^",
|
||||||
|
"@n8n/decorators": "workspace:^",
|
||||||
"@n8n/di": "workspace:^",
|
"@n8n/di": "workspace:^",
|
||||||
"callsites": "catalog:",
|
"callsites": "catalog:",
|
||||||
"n8n-workflow": "workspace:^",
|
"n8n-workflow": "workspace:^",
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ export * from './types';
|
|||||||
export { inDevelopment, inProduction, inTest } from './environment';
|
export { inDevelopment, inProduction, inTest } from './environment';
|
||||||
export { isObjectLiteral } from './utils/is-object-literal';
|
export { isObjectLiteral } from './utils/is-object-literal';
|
||||||
export { Logger } from './logging/logger';
|
export { Logger } from './logging/logger';
|
||||||
|
export { ModuleRegistry } from './modules/module-registry';
|
||||||
|
export { ModulesConfig, ModuleName } from './modules/modules.config';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { LicenseState } from '@n8n/backend-common';
|
|
||||||
import type { ModuleInterface, ModuleMetadata } from '@n8n/decorators';
|
import type { ModuleInterface, ModuleMetadata } from '@n8n/decorators';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
|
import type { LicenseState } from '../../license-state';
|
||||||
import { ModuleConfusionError } from '../errors/module-confusion.error';
|
import { ModuleConfusionError } from '../errors/module-confusion.error';
|
||||||
import { ModuleRegistry } from '../module-registry';
|
import { ModuleRegistry } from '../module-registry';
|
||||||
import { MODULE_NAMES } from '../modules.config';
|
import { MODULE_NAMES } from '../modules.config';
|
||||||
@@ -43,7 +43,7 @@ describe('loadModules', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock(), mock());
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
|
||||||
|
|
||||||
await moduleRegistry.loadModules([]);
|
await moduleRegistry.loadModules([]);
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ describe('loadModules', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock(), mock());
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
|
||||||
|
|
||||||
await moduleRegistry.loadModules([]);
|
await moduleRegistry.loadModules([]);
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ describe('initModules', () => {
|
|||||||
});
|
});
|
||||||
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
|
|
||||||
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock(), mock());
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
|
||||||
|
|
||||||
await moduleRegistry.initModules();
|
await moduleRegistry.initModules();
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ describe('initModules', () => {
|
|||||||
const licenseState = mock<LicenseState>({ isLicensed: jest.fn().mockReturnValue(true) });
|
const licenseState = mock<LicenseState>({ isLicensed: jest.fn().mockReturnValue(true) });
|
||||||
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
|
|
||||||
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), licenseState, mock(), mock());
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, licenseState, mock(), mock());
|
||||||
|
|
||||||
await moduleRegistry.initModules();
|
await moduleRegistry.initModules();
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ describe('initModules', () => {
|
|||||||
const licenseState = mock<LicenseState>({ isLicensed: jest.fn().mockReturnValue(false) });
|
const licenseState = mock<LicenseState>({ isLicensed: jest.fn().mockReturnValue(false) });
|
||||||
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
|
|
||||||
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), licenseState, mock(), mock());
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, licenseState, mock(), mock());
|
||||||
|
|
||||||
await moduleRegistry.initModules();
|
await moduleRegistry.initModules();
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ describe('initModules', () => {
|
|||||||
|
|
||||||
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
|
|
||||||
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock(), mock());
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
|
||||||
|
|
||||||
await moduleRegistry.initModules();
|
await moduleRegistry.initModules();
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ describe('initModules', () => {
|
|||||||
});
|
});
|
||||||
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
|
|
||||||
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock(), mock());
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
await moduleRegistry.initModules();
|
await moduleRegistry.initModules();
|
||||||
@@ -174,12 +174,13 @@ describe('initModules', () => {
|
|||||||
});
|
});
|
||||||
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
Container.get = jest.fn().mockReturnValue(ModuleClass);
|
||||||
|
|
||||||
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock(), mock());
|
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
await moduleRegistry.initModules();
|
await moduleRegistry.initModules();
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
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]);
|
||||||
});
|
});
|
||||||
113
packages/@n8n/backend-common/src/modules/module-registry.ts
Normal file
113
packages/@n8n/backend-common/src/modules/module-registry.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { ModuleMetadata } from '@n8n/decorators';
|
||||||
|
import type { EntityClass, ModuleSettings } from '@n8n/decorators';
|
||||||
|
import { Container, Service } from '@n8n/di';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { MissingModuleError } from './errors/missing-module.error';
|
||||||
|
import { ModuleConfusionError } from './errors/module-confusion.error';
|
||||||
|
import { ModulesConfig } from './modules.config';
|
||||||
|
import type { ModuleName } from './modules.config';
|
||||||
|
import { LicenseState } from '../license-state';
|
||||||
|
import { Logger } from '../logging/logger';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ModuleRegistry {
|
||||||
|
readonly entities: EntityClass[] = [];
|
||||||
|
|
||||||
|
readonly settings: Map<string, ModuleSettings> = new Map();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly moduleMetadata: ModuleMetadata,
|
||||||
|
private readonly licenseState: LicenseState,
|
||||||
|
private readonly logger: Logger,
|
||||||
|
private readonly modulesConfig: ModulesConfig,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private readonly defaultModules: ModuleName[] = ['insights', 'external-secrets'];
|
||||||
|
|
||||||
|
private readonly activeModules: string[] = [];
|
||||||
|
|
||||||
|
get eligibleModules(): ModuleName[] {
|
||||||
|
const { enabledModules, disabledModules } = this.modulesConfig;
|
||||||
|
|
||||||
|
const doubleListed = enabledModules.filter((m) => disabledModules.includes(m));
|
||||||
|
|
||||||
|
if (doubleListed.length > 0) throw new ModuleConfusionError(doubleListed);
|
||||||
|
|
||||||
|
const defaultPlusEnabled = [...new Set([...this.defaultModules, ...enabledModules])];
|
||||||
|
|
||||||
|
return defaultPlusEnabled.filter((m) => !disabledModules.includes(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads [module name].module.ts for each eligible module.
|
||||||
|
* This only registers the database entities for module and should be done
|
||||||
|
* before instantiating the datasource.
|
||||||
|
*
|
||||||
|
* This will not register routes or do any other kind of module related
|
||||||
|
* setup.
|
||||||
|
*/
|
||||||
|
async loadModules(modules?: ModuleName[]) {
|
||||||
|
const moduleDir = process.env.NODE_ENV === 'test' ? 'src' : 'dist';
|
||||||
|
const modulesDir = path.resolve(__dirname, `../../../../cli/${moduleDir}/modules`);
|
||||||
|
|
||||||
|
for (const moduleName of modules ?? this.eligibleModules) {
|
||||||
|
try {
|
||||||
|
await import(`${modulesDir}/${moduleName}/${moduleName}.module`);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
await import(`${modulesDir}/${moduleName}.ee/${moduleName}.module`);
|
||||||
|
} catch (error) {
|
||||||
|
throw new MissingModuleError(moduleName, error instanceof Error ? error.message : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ModuleClass of this.moduleMetadata.getClasses()) {
|
||||||
|
const entities = Container.get(ModuleClass).entities?.();
|
||||||
|
|
||||||
|
if (!entities || entities.length === 0) continue;
|
||||||
|
|
||||||
|
this.entities.push(...entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls `init` on each eligible module.
|
||||||
|
*
|
||||||
|
* This will do things like registering routes, setup timers or other module
|
||||||
|
* specific setup.
|
||||||
|
*
|
||||||
|
* `ModuleRegistry.loadModules` must have been called before.
|
||||||
|
*/
|
||||||
|
async initModules() {
|
||||||
|
for (const [moduleName, moduleEntry] of this.moduleMetadata.getEntries()) {
|
||||||
|
const { licenseFlag, class: ModuleClass } = moduleEntry;
|
||||||
|
|
||||||
|
if (licenseFlag && !this.licenseState.isLicensed(licenseFlag)) {
|
||||||
|
this.logger.debug(`Skipped init for unlicensed module "${moduleName}"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Container.get(ModuleClass).init?.();
|
||||||
|
|
||||||
|
const moduleSettings = await Container.get(ModuleClass).settings?.();
|
||||||
|
|
||||||
|
if (!moduleSettings) continue;
|
||||||
|
|
||||||
|
this.settings.set(moduleName, moduleSettings);
|
||||||
|
|
||||||
|
this.logger.debug(`Initialized module "${moduleName}"`);
|
||||||
|
|
||||||
|
this.activeModules.push(moduleName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(moduleName: ModuleName) {
|
||||||
|
return this.activeModules.includes(moduleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveModules() {
|
||||||
|
return this.activeModules;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import { inDevelopment, inTest, LicenseState, Logger } from '@n8n/backend-common';
|
import {
|
||||||
|
inDevelopment,
|
||||||
|
inTest,
|
||||||
|
LicenseState,
|
||||||
|
Logger,
|
||||||
|
ModuleRegistry,
|
||||||
|
ModulesConfig,
|
||||||
|
} from '@n8n/backend-common';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
@@ -27,8 +34,6 @@ import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
|||||||
import { ExternalHooks } from '@/external-hooks';
|
import { ExternalHooks } from '@/external-hooks';
|
||||||
import { License } from '@/license';
|
import { License } from '@/license';
|
||||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||||
import { ModuleRegistry } from '@/modules/module-registry';
|
|
||||||
import { ModulesConfig } from '@/modules/modules.config';
|
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
import { PostHogClient } from '@/posthog';
|
import { PostHogClient } from '@/posthog';
|
||||||
import { ShutdownService } from '@/shutdown/shutdown.service';
|
import { ShutdownService } from '@/shutdown/shutdown.service';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ModuleRegistry } from '@n8n/backend-common';
|
||||||
import type { GlobalConfig, InstanceSettingsConfig } from '@n8n/config';
|
import type { GlobalConfig, InstanceSettingsConfig } from '@n8n/config';
|
||||||
import { mysqlMigrations } from '@n8n/db';
|
import { mysqlMigrations } from '@n8n/db';
|
||||||
import { postgresMigrations } from '@n8n/db';
|
import { postgresMigrations } from '@n8n/db';
|
||||||
@@ -5,8 +6,6 @@ import { sqliteMigrations } from '@n8n/db';
|
|||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import type { ModuleRegistry } from '@/modules/module-registry';
|
|
||||||
|
|
||||||
import { DbConnectionOptions } from '../db-connection-options';
|
import { DbConnectionOptions } from '../db-connection-options';
|
||||||
|
|
||||||
describe('DbConnectionOptions', () => {
|
describe('DbConnectionOptions', () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ModuleRegistry } from '@n8n/backend-common';
|
||||||
import { DatabaseConfig, InstanceSettingsConfig } from '@n8n/config';
|
import { DatabaseConfig, InstanceSettingsConfig } from '@n8n/config';
|
||||||
import {
|
import {
|
||||||
entities,
|
entities,
|
||||||
@@ -16,8 +17,6 @@ import { UserError } from 'n8n-workflow';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { TlsOptions } from 'tls';
|
import type { TlsOptions } from 'tls';
|
||||||
|
|
||||||
import { ModuleRegistry } from '@/modules/module-registry';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class DbConnectionOptions {
|
export class DbConnectionOptions {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Logger } from '@n8n/backend-common';
|
import { Logger } from '@n8n/backend-common';
|
||||||
import { ExecutionRepository } from '@n8n/db';
|
import { ExecutionRepository } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { LifecycleMetadata } from '@n8n/decorators';
|
||||||
|
import { Container, Service } from '@n8n/di';
|
||||||
import { stringify } from 'flatted';
|
import { stringify } from 'flatted';
|
||||||
import { ErrorReporter, InstanceSettings, ExecutionLifecycleHooks } from 'n8n-core';
|
import { ErrorReporter, InstanceSettings, ExecutionLifecycleHooks } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
@@ -11,7 +12,6 @@ import type {
|
|||||||
|
|
||||||
import { EventService } from '@/events/event.service';
|
import { EventService } from '@/events/event.service';
|
||||||
import { ExternalHooks } from '@/external-hooks';
|
import { ExternalHooks } from '@/external-hooks';
|
||||||
import { ModuleRegistry } from '@/modules/module-registry';
|
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||||
import { isWorkflowIdValid } from '@/utils';
|
import { isWorkflowIdValid } from '@/utils';
|
||||||
@@ -28,6 +28,72 @@ import {
|
|||||||
} from './shared/shared-hook-functions';
|
} from './shared/shared-hook-functions';
|
||||||
import { type ExecutionSaveSettings, toSaveSettings } from './to-save-settings';
|
import { type ExecutionSaveSettings, toSaveSettings } from './to-save-settings';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
class ModulesHooksRegistry {
|
||||||
|
addHooks(hooks: ExecutionLifecycleHooks) {
|
||||||
|
const handlers = Container.get(LifecycleMetadata).getHandlers();
|
||||||
|
|
||||||
|
for (const { handlerClass, methodName, eventName } of handlers) {
|
||||||
|
const instance = Container.get(handlerClass);
|
||||||
|
|
||||||
|
switch (eventName) {
|
||||||
|
case 'workflowExecuteAfter':
|
||||||
|
hooks.addHandler(eventName, async function (runData, newStaticData) {
|
||||||
|
const context = {
|
||||||
|
type: 'workflowExecuteAfter',
|
||||||
|
workflow: this.workflowData,
|
||||||
|
runData,
|
||||||
|
newStaticData,
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/return-await
|
||||||
|
return await instance[methodName].call(instance, context);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nodeExecuteBefore':
|
||||||
|
hooks.addHandler(eventName, async function (nodeName, taskData) {
|
||||||
|
const context = {
|
||||||
|
type: 'nodeExecuteBefore',
|
||||||
|
workflow: this.workflowData,
|
||||||
|
nodeName,
|
||||||
|
taskData,
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/return-await
|
||||||
|
return await instance[methodName].call(instance, context);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nodeExecuteAfter':
|
||||||
|
hooks.addHandler(eventName, async function (nodeName, taskData, executionData) {
|
||||||
|
const context = {
|
||||||
|
type: 'nodeExecuteAfter',
|
||||||
|
workflow: this.workflowData,
|
||||||
|
nodeName,
|
||||||
|
taskData,
|
||||||
|
executionData,
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/return-await
|
||||||
|
return await instance[methodName].call(instance, context);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'workflowExecuteBefore':
|
||||||
|
hooks.addHandler(eventName, async function (workflowInstance, executionData) {
|
||||||
|
const context = {
|
||||||
|
type: 'workflowExecuteBefore',
|
||||||
|
workflow: this.workflowData,
|
||||||
|
workflowInstance,
|
||||||
|
executionData,
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/return-await
|
||||||
|
return await instance[methodName].call(instance, context);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type HooksSetupParameters = {
|
type HooksSetupParameters = {
|
||||||
saveSettings: ExecutionSaveSettings;
|
saveSettings: ExecutionSaveSettings;
|
||||||
pushRef?: string;
|
pushRef?: string;
|
||||||
@@ -425,7 +491,7 @@ export function getLifecycleHooksForScalingWorker(
|
|||||||
hookFunctionsPush(hooks, optionalParameters);
|
hookFunctionsPush(hooks, optionalParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
Container.get(ModuleRegistry).registerLifecycleHooks(hooks);
|
Container.get(ModulesHooksRegistry).addHooks(hooks);
|
||||||
|
|
||||||
return hooks;
|
return hooks;
|
||||||
}
|
}
|
||||||
@@ -487,7 +553,7 @@ export function getLifecycleHooksForScalingMain(
|
|||||||
hooks.handlers.nodeExecuteBefore = [];
|
hooks.handlers.nodeExecuteBefore = [];
|
||||||
hooks.handlers.nodeExecuteAfter = [];
|
hooks.handlers.nodeExecuteAfter = [];
|
||||||
|
|
||||||
Container.get(ModuleRegistry).registerLifecycleHooks(hooks);
|
Container.get(ModulesHooksRegistry).addHooks(hooks);
|
||||||
|
|
||||||
return hooks;
|
return hooks;
|
||||||
}
|
}
|
||||||
@@ -511,6 +577,6 @@ export function getLifecycleHooksForRegularMain(
|
|||||||
hookFunctionsSaveProgress(hooks, optionalParameters);
|
hookFunctionsSaveProgress(hooks, optionalParameters);
|
||||||
hookFunctionsStatistics(hooks);
|
hookFunctionsStatistics(hooks);
|
||||||
hookFunctionsExternalHooks(hooks);
|
hookFunctionsExternalHooks(hooks);
|
||||||
Container.get(ModuleRegistry).registerLifecycleHooks(hooks);
|
Container.get(ModulesHooksRegistry).addHooks(hooks);
|
||||||
return hooks;
|
return hooks;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,214 +0,0 @@
|
|||||||
import { LicenseState, Logger } from '@n8n/backend-common';
|
|
||||||
import { LifecycleMetadata, ModuleMetadata } from '@n8n/decorators';
|
|
||||||
import type { LifecycleContext, EntityClass, ModuleSettings } from '@n8n/decorators';
|
|
||||||
import { Container, Service } from '@n8n/di';
|
|
||||||
import type { ExecutionLifecycleHooks } from 'n8n-core';
|
|
||||||
import type {
|
|
||||||
IDataObject,
|
|
||||||
IRun,
|
|
||||||
IRunExecutionData,
|
|
||||||
ITaskData,
|
|
||||||
ITaskStartedData,
|
|
||||||
IWorkflowBase,
|
|
||||||
Workflow,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { MissingModuleError } from './errors/missing-module.error';
|
|
||||||
import { ModuleConfusionError } from './errors/module-confusion.error';
|
|
||||||
import { ModulesConfig } from './modules.config';
|
|
||||||
import type { ModuleName } from './modules.config';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class ModuleRegistry {
|
|
||||||
readonly entities: EntityClass[] = [];
|
|
||||||
|
|
||||||
readonly settings: Map<string, ModuleSettings> = new Map();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly moduleMetadata: ModuleMetadata,
|
|
||||||
private readonly lifecycleMetadata: LifecycleMetadata,
|
|
||||||
private readonly licenseState: LicenseState,
|
|
||||||
private readonly logger: Logger,
|
|
||||||
private readonly modulesConfig: ModulesConfig,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private readonly defaultModules: ModuleName[] = ['insights', 'external-secrets'];
|
|
||||||
|
|
||||||
private readonly activeModules: string[] = [];
|
|
||||||
|
|
||||||
get eligibleModules(): ModuleName[] {
|
|
||||||
const { enabledModules, disabledModules } = this.modulesConfig;
|
|
||||||
|
|
||||||
const doubleListed = enabledModules.filter((m) => disabledModules.includes(m));
|
|
||||||
|
|
||||||
if (doubleListed.length > 0) throw new ModuleConfusionError(doubleListed);
|
|
||||||
|
|
||||||
const defaultPlusEnabled = [...new Set([...this.defaultModules, ...enabledModules])];
|
|
||||||
|
|
||||||
return defaultPlusEnabled.filter((m) => !disabledModules.includes(m));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads [module name].module.ts for each eligible module.
|
|
||||||
* This only registers the database entities for module and should be done
|
|
||||||
* before instantiating the datasource.
|
|
||||||
*
|
|
||||||
* This will not register routes or do any other kind of module related
|
|
||||||
* setup.
|
|
||||||
*/
|
|
||||||
async loadModules(modules?: ModuleName[]) {
|
|
||||||
for (const moduleName of modules ?? this.eligibleModules) {
|
|
||||||
try {
|
|
||||||
await import(`../modules/${moduleName}/${moduleName}.module`);
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
await import(`../modules/${moduleName}.ee/${moduleName}.module`);
|
|
||||||
} catch (error) {
|
|
||||||
throw new MissingModuleError(moduleName, error instanceof Error ? error.message : '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const ModuleClass of this.moduleMetadata.getClasses()) {
|
|
||||||
const entities = Container.get(ModuleClass).entities?.();
|
|
||||||
|
|
||||||
if (!entities || entities.length === 0) continue;
|
|
||||||
|
|
||||||
this.entities.push(...entities);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls `init` on each eligible module.
|
|
||||||
*
|
|
||||||
* This will do things like registering routes, setup timers or other module
|
|
||||||
* specific setup.
|
|
||||||
*
|
|
||||||
* `ModuleRegistry.loadModules` must have been called before.
|
|
||||||
*/
|
|
||||||
async initModules() {
|
|
||||||
for (const [moduleName, moduleEntry] of this.moduleMetadata.getEntries()) {
|
|
||||||
const { licenseFlag, class: ModuleClass } = moduleEntry;
|
|
||||||
|
|
||||||
if (licenseFlag && !this.licenseState.isLicensed(licenseFlag)) {
|
|
||||||
this.logger.debug(`Skipped init for unlicensed module "${moduleName}"`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Container.get(ModuleClass).init?.();
|
|
||||||
|
|
||||||
const moduleSettings = await Container.get(ModuleClass).settings?.();
|
|
||||||
|
|
||||||
if (!moduleSettings) continue;
|
|
||||||
|
|
||||||
this.settings.set(moduleName, moduleSettings);
|
|
||||||
|
|
||||||
this.logger.debug(`Initialized module "${moduleName}"`);
|
|
||||||
|
|
||||||
this.activeModules.push(moduleName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isActive(moduleName: ModuleName) {
|
|
||||||
return this.activeModules.includes(moduleName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveModules() {
|
|
||||||
return this.activeModules;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerLifecycleHooks(hooks: ExecutionLifecycleHooks) {
|
|
||||||
const handlers = this.lifecycleMetadata.getHandlers();
|
|
||||||
|
|
||||||
for (const { handlerClass, methodName, eventName } of handlers) {
|
|
||||||
const instance = Container.get(handlerClass);
|
|
||||||
|
|
||||||
switch (eventName) {
|
|
||||||
case 'workflowExecuteAfter':
|
|
||||||
hooks.addHandler(
|
|
||||||
eventName,
|
|
||||||
async function (
|
|
||||||
this: { workflowData: IWorkflowBase },
|
|
||||||
runData: IRun,
|
|
||||||
newStaticData: IDataObject,
|
|
||||||
) {
|
|
||||||
const context: LifecycleContext = {
|
|
||||||
type: 'workflowExecuteAfter',
|
|
||||||
workflow: this.workflowData,
|
|
||||||
runData,
|
|
||||||
newStaticData,
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/return-await
|
|
||||||
return await instance[methodName].call(instance, context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'nodeExecuteBefore':
|
|
||||||
hooks.addHandler(
|
|
||||||
eventName,
|
|
||||||
async function (
|
|
||||||
this: { workflowData: IWorkflowBase },
|
|
||||||
nodeName: string,
|
|
||||||
taskData: ITaskStartedData,
|
|
||||||
) {
|
|
||||||
const context: LifecycleContext = {
|
|
||||||
type: 'nodeExecuteBefore',
|
|
||||||
workflow: this.workflowData,
|
|
||||||
nodeName,
|
|
||||||
taskData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/return-await
|
|
||||||
return await instance[methodName].call(instance, context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'nodeExecuteAfter':
|
|
||||||
hooks.addHandler(
|
|
||||||
eventName,
|
|
||||||
async function (
|
|
||||||
this: { workflowData: IWorkflowBase },
|
|
||||||
nodeName: string,
|
|
||||||
taskData: ITaskData,
|
|
||||||
executionData: IRunExecutionData,
|
|
||||||
) {
|
|
||||||
const context: LifecycleContext = {
|
|
||||||
type: 'nodeExecuteAfter',
|
|
||||||
workflow: this.workflowData,
|
|
||||||
nodeName,
|
|
||||||
taskData,
|
|
||||||
executionData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/return-await
|
|
||||||
return await instance[methodName].call(instance, context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'workflowExecuteBefore':
|
|
||||||
hooks.addHandler(
|
|
||||||
eventName,
|
|
||||||
async function (
|
|
||||||
this: { workflowData: IWorkflowBase },
|
|
||||||
workflowInstance: Workflow,
|
|
||||||
executionData?: IRunExecutionData,
|
|
||||||
) {
|
|
||||||
const context: LifecycleContext = {
|
|
||||||
type: 'workflowExecuteBefore',
|
|
||||||
workflow: this.workflowData,
|
|
||||||
workflowInstance,
|
|
||||||
executionData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/return-await
|
|
||||||
return await instance[methodName].call(instance, context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { FrontendSettings, ITelemetrySettings } from '@n8n/api-types';
|
import type { FrontendSettings, ITelemetrySettings } from '@n8n/api-types';
|
||||||
import { LicenseState, Logger } from '@n8n/backend-common';
|
import { LicenseState, Logger, ModuleRegistry } from '@n8n/backend-common';
|
||||||
import { GlobalConfig, SecurityConfig } from '@n8n/config';
|
import { GlobalConfig, SecurityConfig } from '@n8n/config';
|
||||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
@@ -17,7 +17,6 @@ import { CredentialsOverwrites } from '@/credentials-overwrites';
|
|||||||
import { getLdapLoginLabel } from '@/ldap.ee/helpers.ee';
|
import { getLdapLoginLabel } from '@/ldap.ee/helpers.ee';
|
||||||
import { License } from '@/license';
|
import { License } from '@/license';
|
||||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||||
import { ModuleRegistry } from '@/modules/module-registry';
|
|
||||||
import { isApiEnabled } from '@/public-api';
|
import { isApiEnabled } from '@/public-api';
|
||||||
import { PushConfig } from '@/push/push.config';
|
import { PushConfig } from '@/push/push.config';
|
||||||
import type { CommunityPackagesService } from '@/services/community-packages.service';
|
import type { CommunityPackagesService } from '@/services/community-packages.service';
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { ModuleRegistry } from '@n8n/backend-common';
|
||||||
|
import type { ModuleName } from '@n8n/backend-common';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
|
||||||
import { ModuleRegistry } from '@/modules/module-registry';
|
|
||||||
import type { ModuleName } from '@/modules/modules.config';
|
|
||||||
|
|
||||||
export async function loadModules(moduleNames: ModuleName[]) {
|
export async function loadModules(moduleNames: ModuleName[]) {
|
||||||
await Container.get(ModuleRegistry).loadModules(moduleNames);
|
await Container.get(ModuleRegistry).loadModules(moduleNames);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LicenseState } from '@n8n/backend-common';
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
|
import { ModuleRegistry } from '@n8n/backend-common';
|
||||||
import { mockLogger } from '@n8n/backend-test-utils';
|
import { mockLogger } from '@n8n/backend-test-utils';
|
||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
@@ -14,7 +15,6 @@ import { AUTH_COOKIE_NAME } from '@/constants';
|
|||||||
import { ControllerRegistry } from '@/controller.registry';
|
import { ControllerRegistry } from '@/controller.registry';
|
||||||
import { License } from '@/license';
|
import { License } from '@/license';
|
||||||
import { rawBodyReader, bodyParser } from '@/middlewares';
|
import { rawBodyReader, bodyParser } from '@/middlewares';
|
||||||
import { ModuleRegistry } from '@/modules/module-registry';
|
|
||||||
import { PostHogClient } from '@/posthog';
|
import { PostHogClient } from '@/posthog';
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
import type { APIRequest } from '@/requests';
|
import type { APIRequest } from '@/requests';
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -419,6 +419,9 @@ importers:
|
|||||||
'@n8n/constants':
|
'@n8n/constants':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../constants
|
version: link:../constants
|
||||||
|
'@n8n/decorators':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../decorators
|
||||||
'@n8n/di':
|
'@n8n/di':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../di
|
version: link:../di
|
||||||
|
|||||||
Reference in New Issue
Block a user