mirror of
https://github.com/n8n-io/n8n.git
synced 2025-11-20 17:46:34 +00:00
feat(core): Add RootLevelController decorator (no-changelog) (#19972)
This commit is contained in:
parent
249d8f6ee6
commit
9fef8c19c4
@ -26,6 +26,7 @@ describe('ControllerRegistryMetadata', () => {
|
||||
|
||||
expect(metadata).toEqual({
|
||||
basePath: '/',
|
||||
registerOnRootPath: false,
|
||||
middlewares: [],
|
||||
routes: expect.any(Map),
|
||||
});
|
||||
|
||||
@ -9,6 +9,7 @@ describe('@RestController Decorator', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
Container.reset();
|
||||
|
||||
controllerRegistryMetadata = new ControllerRegistryMetadata();
|
||||
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
|
||||
@ -20,6 +21,7 @@ describe('@RestController Decorator', () => {
|
||||
|
||||
const metadata = controllerRegistryMetadata.getControllerMetadata(TestController as Controller);
|
||||
expect(metadata.basePath).toBe('/');
|
||||
expect(metadata.registerOnRootPath).toBe(false);
|
||||
expect(Container.has(TestController)).toBe(true);
|
||||
});
|
||||
|
||||
@ -29,6 +31,7 @@ describe('@RestController Decorator', () => {
|
||||
|
||||
const metadata = controllerRegistryMetadata.getControllerMetadata(TestController as Controller);
|
||||
expect(metadata.basePath).toBe('/test');
|
||||
expect(metadata.registerOnRootPath).toBe(false);
|
||||
expect(Container.has(TestController)).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { ControllerRegistryMetadata } from '../controller-registry-metadata';
|
||||
import { RootLevelController } from '../root-level-controller';
|
||||
import type { Controller } from '../types';
|
||||
|
||||
describe('@RootLevelController Decorator', () => {
|
||||
let controllerRegistryMetadata: ControllerRegistryMetadata;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
Container.reset();
|
||||
|
||||
controllerRegistryMetadata = new ControllerRegistryMetadata();
|
||||
Container.set(ControllerRegistryMetadata, controllerRegistryMetadata);
|
||||
});
|
||||
|
||||
it('should default to root path and register on root', () => {
|
||||
@RootLevelController()
|
||||
class TestController {}
|
||||
|
||||
const metadata = controllerRegistryMetadata.getControllerMetadata(TestController as Controller);
|
||||
expect(metadata.basePath).toBe('/');
|
||||
expect(metadata.registerOnRootPath).toBe(true);
|
||||
expect(Container.has(TestController)).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept custom base path', () => {
|
||||
@RootLevelController('/foo')
|
||||
class FooController {}
|
||||
|
||||
const metadata = controllerRegistryMetadata.getControllerMetadata(FooController as Controller);
|
||||
expect(metadata.basePath).toBe('/foo');
|
||||
expect(metadata.registerOnRootPath).toBe(true);
|
||||
expect(Container.has(FooController)).toBe(true);
|
||||
});
|
||||
|
||||
it('should register multiple controllers with their metadata', () => {
|
||||
@RootLevelController('/users')
|
||||
class UsersController {}
|
||||
|
||||
@RootLevelController('/projects')
|
||||
class ProjectsController {}
|
||||
|
||||
const controllers = Array.from(controllerRegistryMetadata.controllerClasses);
|
||||
expect(controllers).toEqual([UsersController, ProjectsController]);
|
||||
expect(
|
||||
controllerRegistryMetadata.getControllerMetadata(UsersController as Controller)
|
||||
.registerOnRootPath,
|
||||
).toBe(true);
|
||||
expect(
|
||||
controllerRegistryMetadata.getControllerMetadata(ProjectsController as Controller).basePath,
|
||||
).toBe('/projects');
|
||||
});
|
||||
});
|
||||
@ -11,6 +11,7 @@ export class ControllerRegistryMetadata {
|
||||
if (!metadata) {
|
||||
metadata = {
|
||||
basePath: '/',
|
||||
registerOnRootPath: false,
|
||||
middlewares: [],
|
||||
routes: new Map(),
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export { Body, Query, Param } from './args';
|
||||
export { RestController } from './rest-controller';
|
||||
export { RootLevelController } from './root-level-controller';
|
||||
export { Get, Post, Put, Patch, Delete } from './route';
|
||||
export { Middleware } from './middleware';
|
||||
export { ControllerRegistryMetadata } from './controller-registry-metadata';
|
||||
|
||||
@ -10,6 +10,7 @@ export const RestController =
|
||||
target as unknown as Controller,
|
||||
);
|
||||
metadata.basePath = basePath;
|
||||
metadata.registerOnRootPath = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return Service()(target);
|
||||
};
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import { Container, Service } from '@n8n/di';
|
||||
|
||||
import { ControllerRegistryMetadata } from './controller-registry-metadata';
|
||||
import type { Controller } from './types';
|
||||
|
||||
/**
|
||||
* Defines a controller that should be registered on the root path, without any prefix
|
||||
* @param basePath defaults to `/`
|
||||
* @returns ClassDecorator
|
||||
*/
|
||||
export const RootLevelController =
|
||||
(basePath: `/${string}` = '/'): ClassDecorator =>
|
||||
(target) => {
|
||||
const metadata = Container.get(ControllerRegistryMetadata).getControllerMetadata(
|
||||
target as unknown as Controller,
|
||||
);
|
||||
metadata.basePath = basePath;
|
||||
metadata.registerOnRootPath = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return Service()(target);
|
||||
};
|
||||
@ -43,6 +43,8 @@ export interface RouteMetadata {
|
||||
|
||||
export interface ControllerMetadata {
|
||||
basePath: `/${string}`;
|
||||
// If true, the controller will be registered on the root path without the any prefix
|
||||
registerOnRootPath?: boolean;
|
||||
middlewares: HandlerName[];
|
||||
routes: Map<HandlerName, RouteMetadata>;
|
||||
}
|
||||
|
||||
@ -6,7 +6,14 @@ jest.mock('@n8n/backend-common', () => {
|
||||
});
|
||||
|
||||
import type { GlobalConfig } from '@n8n/config';
|
||||
import { ControllerRegistryMetadata, Param, Get, Licensed, RestController } from '@n8n/decorators';
|
||||
import {
|
||||
ControllerRegistryMetadata,
|
||||
Param,
|
||||
Get,
|
||||
Licensed,
|
||||
RestController,
|
||||
RootLevelController,
|
||||
} from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import express from 'express';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
@ -155,4 +162,40 @@ describe('ControllerRegistry', () => {
|
||||
expect(body.data).toEqual({ url: '/args/1234', id: '1234' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Root-level controllers', () => {
|
||||
@RootLevelController('/public')
|
||||
// @ts-expect-error tsc complains about unused class
|
||||
class PublicController {
|
||||
@Get('/info')
|
||||
info() {
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
@RootLevelController()
|
||||
// @ts-expect-error tsc complains about unused class
|
||||
class RootController {
|
||||
@Get('/ping')
|
||||
ping() {
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
authMiddleware.mockImplementation(async (_req, _res, next) => next());
|
||||
lastActiveAtService.middleware.mockImplementation(async (_req, _res, next) => next());
|
||||
});
|
||||
|
||||
it('should mount controller without rest prefix', async () => {
|
||||
const { body } = await agent.get('/public/info').expect(200);
|
||||
expect(body.data).toEqual({ ok: true });
|
||||
await agent.get('/rest/public/info').expect(404);
|
||||
});
|
||||
|
||||
it('should mount default controller at root path', async () => {
|
||||
const { body } = await agent.get('/ping').expect(200);
|
||||
expect(body.data).toEqual({ ok: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -41,10 +41,11 @@ export class ControllerRegistry {
|
||||
const metadata = this.metadata.getControllerMetadata(controllerClass);
|
||||
|
||||
const router = Router({ mergeParams: true });
|
||||
const prefix = `/${this.globalConfig.endpoints.rest}/${metadata.basePath}`
|
||||
.replace(/\/+/g, '/')
|
||||
.replace(/\/$/, '');
|
||||
app.use(prefix, router);
|
||||
const basePath = metadata.registerOnRootPath
|
||||
? metadata.basePath
|
||||
: `/${this.globalConfig.endpoints.rest}/${metadata.basePath}`;
|
||||
const prefix = basePath.replace(/\/+/g, '/').replace(/\/$/, '');
|
||||
app.use(prefix === '' ? '/' : prefix, router);
|
||||
|
||||
const controller = Container.get(controllerClass) as Controller;
|
||||
const controllerMiddlewares = metadata.middlewares.map(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user