perf(core): Skip init for unlicensed modules (#16311)

This commit is contained in:
Iván Ovejero
2025-06-13 11:49:38 +02:00
committed by GitHub
parent 3864f0e1c1
commit cb81826cf1
6 changed files with 43 additions and 19 deletions

View File

@@ -19,7 +19,7 @@ describe('@BackendModule decorator', () => {
initialize() {}
}
const registeredModules = Array.from(moduleMetadata.getModules());
const registeredModules = moduleMetadata.getEntries().map((entry) => entry.class);
expect(registeredModules).toContain(TestModule);
expect(registeredModules).toHaveLength(1);
@@ -41,7 +41,7 @@ describe('@BackendModule decorator', () => {
initialize() {}
}
const registeredModules = Array.from(moduleMetadata.getModules());
const registeredModules = moduleMetadata.getEntries().map((entry) => entry.class);
expect(registeredModules).toContain(FirstModule);
expect(registeredModules).toContain(SecondModule);
@@ -53,7 +53,7 @@ describe('@BackendModule decorator', () => {
@BackendModule()
class TestModule {}
const registeredModules = Array.from(moduleMetadata.getModules());
const registeredModules = moduleMetadata.getEntries().map((entry) => entry.class);
expect(registeredModules).toContain(TestModule);
expect(registeredModules).toHaveLength(1);
@@ -69,7 +69,7 @@ describe('@BackendModule decorator', () => {
}
}
const registeredModules = Array.from(moduleMetadata.getModules());
const registeredModules = moduleMetadata.getEntries().map((entry) => entry.class);
expect(registeredModules).toContain(TestModule);
@@ -87,7 +87,7 @@ describe('@BackendModule decorator', () => {
@BackendModule()
class SecondModule {}
const registeredModules = Array.from(moduleMetadata.getModules());
const registeredModules = moduleMetadata.getEntries().map((entry) => entry.class);
expect(registeredModules).toContain(FirstModule);
expect(registeredModules).toContain(SecondModule);

View File

@@ -1,16 +1,21 @@
import { Service } from '@n8n/di';
import type { ModuleClass } from './module';
import type { LicenseFlag, ModuleClass } from './module';
type ModuleEntry = {
class: ModuleClass;
licenseFlag?: LicenseFlag;
};
@Service()
export class ModuleMetadata {
private readonly modules: Set<ModuleClass> = new Set();
private readonly modules: Map<string, ModuleEntry> = new Map();
register(module: ModuleClass) {
this.modules.add(module);
register(moduleName: string, moduleEntry: ModuleEntry) {
this.modules.set(moduleName, moduleEntry);
}
getModules() {
return this.modules.keys();
getEntries() {
return [...this.modules.values()];
}
}

View File

@@ -1,3 +1,4 @@
import type { LICENSE_FEATURES } from '@n8n/constants';
import { Container, Service, type Constructable } from '@n8n/di';
import { ModuleMetadata } from './module-metadata';
@@ -24,9 +25,16 @@ export interface ModuleInterface {
export type ModuleClass = Constructable<ModuleInterface>;
export const BackendModule = (): ClassDecorator => (target) => {
Container.get(ModuleMetadata).register(target as unknown as ModuleClass);
export type LicenseFlag = (typeof LICENSE_FEATURES)[keyof typeof LICENSE_FEATURES];
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Service()(target);
};
export const BackendModule =
(opts?: { licenseFlag: LicenseFlag }): ClassDecorator =>
(target) => {
Container.get(ModuleMetadata).register(target.name, {
class: target as unknown as ModuleClass,
licenseFlag: opts?.licenseFlag,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Service()(target);
};

View File

@@ -2,7 +2,7 @@ import type { ModuleInterface } from '@n8n/decorators';
import { BackendModule } from '@n8n/decorators';
import { Container } from '@n8n/di';
@BackendModule()
@BackendModule({ licenseFlag: 'feat:externalSecrets' })
export class ExternalSecretsModule implements ModuleInterface {
async init() {
await import('./external-secrets.controller.ee');

View File

@@ -1,3 +1,4 @@
import { LicenseState, Logger } from '@n8n/backend-common';
import { LifecycleMetadata, ModuleMetadata } from '@n8n/decorators';
import type { LifecycleContext, EntityClass } from '@n8n/decorators';
import { Container, Service } from '@n8n/di';
@@ -19,16 +20,22 @@ export class ModuleRegistry {
constructor(
private readonly moduleMetadata: ModuleMetadata,
private readonly lifecycleMetadata: LifecycleMetadata,
private readonly licenseState: LicenseState,
private readonly logger: Logger,
) {}
async initModules() {
for (const ModuleClass of this.moduleMetadata.getModules()) {
for (const { class: ModuleClass, licenseFlag } of this.moduleMetadata.getEntries()) {
if (licenseFlag && !this.licenseState.isLicensed(licenseFlag)) {
this.logger.debug(`Skipped init for unlicensed module "${ModuleClass.name}"`);
continue;
}
await Container.get(ModuleClass).init?.();
}
}
addEntities() {
for (const ModuleClass of this.moduleMetadata.getModules()) {
for (const { class: ModuleClass } of this.moduleMetadata.getEntries()) {
const entities = Container.get(ModuleClass).entities?.();
if (!entities || entities.length === 0) continue;

View File

@@ -1,3 +1,4 @@
import { LicenseState } from '@n8n/backend-common';
import { SettingsRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
@@ -31,6 +32,9 @@ let authMemberAgent: SuperAgentTest;
const mockProvidersInstance = new MockProviders();
mockInstance(ExternalSecretsProviders, mockProvidersInstance);
const licenseMock = mock<LicenseState>();
licenseMock.isLicensed.mockReturnValue(true);
Container.set(LicenseState, licenseMock);
const testServer = setupTestServer({
endpointGroups: ['externalSecrets'],