refactor(core): Decouple database entity registration (#15871)

Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
Iván Ovejero
2025-06-12 19:12:20 +02:00
committed by GitHub
parent a417ed3ac8
commit bcf1a7108b
35 changed files with 225 additions and 179 deletions

View File

@@ -1,9 +1,9 @@
import { Container } from '@n8n/di';
import { N8nModule } from '../module';
import { BackendModule } from '../module';
import { ModuleMetadata } from '../module-metadata';
describe('@N8nModule Decorator', () => {
describe('@BackendModule decorator', () => {
let moduleMetadata: ModuleMetadata;
beforeEach(() => {
@@ -14,7 +14,7 @@ describe('@N8nModule Decorator', () => {
});
it('should register module in ModuleMetadata', () => {
@N8nModule()
@BackendModule()
class TestModule {
initialize() {}
}
@@ -26,17 +26,17 @@ describe('@N8nModule Decorator', () => {
});
it('should register multiple modules', () => {
@N8nModule()
@BackendModule()
class FirstModule {
initialize() {}
}
@N8nModule()
@BackendModule()
class SecondModule {
initialize() {}
}
@N8nModule()
@BackendModule()
class ThirdModule {
initialize() {}
}
@@ -50,7 +50,7 @@ describe('@N8nModule Decorator', () => {
});
it('should work with modules without initialize method', () => {
@N8nModule()
@BackendModule()
class TestModule {}
const registeredModules = Array.from(moduleMetadata.getModules());
@@ -62,7 +62,7 @@ describe('@N8nModule Decorator', () => {
it('should support async initialize method', async () => {
const mockInitialize = jest.fn();
@N8nModule()
@BackendModule()
class TestModule {
async initialize() {
mockInitialize();
@@ -81,10 +81,10 @@ describe('@N8nModule Decorator', () => {
describe('ModuleMetadata', () => {
it('should allow retrieving and checking registered modules', () => {
@N8nModule()
@BackendModule()
class FirstModule {}
@N8nModule()
@BackendModule()
class SecondModule {}
const registeredModules = Array.from(moduleMetadata.getModules());
@@ -95,7 +95,7 @@ describe('@N8nModule Decorator', () => {
});
it('should apply Service decorator', () => {
@N8nModule()
@BackendModule()
class TestModule {}
expect(Container.has(TestModule)).toBe(true);

View File

@@ -1,2 +1,2 @@
export { BaseN8nModule, N8nModule } from './module';
export { ModuleInterface, BackendModule, EntityClass } from './module';
export { ModuleMetadata } from './module-metadata';

View File

@@ -1,12 +1,12 @@
import { Service } from '@n8n/di';
import type { Module } from './module';
import type { ModuleClass } from './module';
@Service()
export class ModuleMetadata {
private readonly modules: Set<Module> = new Set();
private readonly modules: Set<ModuleClass> = new Set();
register(module: Module) {
register(module: ModuleClass) {
this.modules.add(module);
}

View File

@@ -2,14 +2,30 @@ import { Container, Service, type Constructable } from '@n8n/di';
import { ModuleMetadata } from './module-metadata';
export interface BaseN8nModule {
initialize?(): void | Promise<void>;
/**
* Structurally similar (not identical) interface to typeorm's `BaseEntity`
* to prevent importing `@n8n/typeorm` into `@n8n/decorators`.
*/
export interface BaseEntity {
hasId(): boolean;
save(options?: unknown): Promise<this>;
remove(options?: unknown): Promise<this>;
softRemove(options?: unknown): Promise<this>;
recover(options?: unknown): Promise<this>;
reload(): Promise<void>;
}
export type Module = Constructable<BaseN8nModule>;
export type EntityClass = new () => BaseEntity;
export const N8nModule = (): ClassDecorator => (target) => {
Container.get(ModuleMetadata).register(target as unknown as Module);
export interface ModuleInterface {
init?(): void | Promise<void>;
entities?(): EntityClass[];
}
export type ModuleClass = Constructable<ModuleInterface>;
export const BackendModule = (): ClassDecorator => (target) => {
Container.get(ModuleMetadata).register(target as unknown as ModuleClass);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Service()(target);