From 6ca9da6b2069614c4a61ce35013e4d62114ff2d2 Mon Sep 17 00:00:00 2001 From: Piero Maltese Date: Thu, 15 Dec 2022 16:35:35 +0100 Subject: [PATCH 1/2] feat(http-server): controllers now support multiple decorations --- packages/http-server/src/HttpServerModule.ts | 112 +++++---- .../test/unit/HttpServerModule.test.ts | 220 +++++++++++++++++- 2 files changed, 277 insertions(+), 55 deletions(-) diff --git a/packages/http-server/src/HttpServerModule.ts b/packages/http-server/src/HttpServerModule.ts index 7e7cb59b..59e68913 100644 --- a/packages/http-server/src/HttpServerModule.ts +++ b/packages/http-server/src/HttpServerModule.ts @@ -95,60 +95,76 @@ export abstract class HttpServerModule< ); await mapSeries(controllersReflection, ({ controllerInstance, reflection: controllerReflection }) => { - const controllerDecoratorMetadata: ControllerDecoratorMetadata = controllerReflection.decorators.find( - d => d.module === 'http-server' && d.type === 'controller' - ); - const basePath = - controllerDecoratorMetadata?.options?.basePath ?? controllerDecoratorMetadata?.options?.basepath ?? '/'; + const controllerDecoratorMetadatas: Array = + controllerReflection.decorators.filter(d => d.module === 'http-server' && d.type === 'controller'); + + // if the controller is not decorated, a default decoration will be assigned to it + if (controllerDecoratorMetadatas.length === 0) { + controllerDecoratorMetadatas.push({ + module: 'http-server', + type: 'controller', + options: { basePath: '/' } + }); + } - return mapSeries(controllerReflection.methods, async methodReflection => { - const methodDecoratorMetadatas: Array = methodReflection.decorators.filter( - d => d[DecoratorId] === 'http-server.method' - ); - const methodName = methodReflection.name; + return mapSeries(controllerDecoratorMetadatas, controllerDecoratorMetadata => { + const basePath = + controllerDecoratorMetadata?.options?.basePath ?? + controllerDecoratorMetadata?.options?.basepath ?? + '/'; + + return mapSeries(controllerReflection.methods, async methodReflection => { + const methodDecoratorMetadatas: Array = methodReflection.decorators.filter( + d => d[DecoratorId] === 'http-server.method' + ); + const methodName = methodReflection.name; + + if (!methodDecoratorMetadatas.length) return null; + + return mapSeries(methodDecoratorMetadatas, async methodDecoratorMetadata => { + const { + verb, + options: { path } + } = methodDecoratorMetadata; + + let fullPath = pathUtils.join(basePath, path); + if (fullPath.length > 1 && fullPath[fullPath.length - 1] === '/') { + fullPath = fullPath.slice(0, -1); + } - if (!methodDecoratorMetadatas.length) return null; + const parametersConfig = await this.createParametersConfigurations({ + controllerReflection, + methodReflection + }); - return mapSeries(methodDecoratorMetadatas, async methodDecoratorMetadata => { - const { - verb, - options: { path } - } = methodDecoratorMetadata; + const responseStatusCodes = Object.keys(methodDecoratorMetadata.options?.responses ?? {}) + .reduce((acc, statusCodeString) => { + const statusCode = Number(statusCodeString); + if (!Number.isNaN(statusCode)) { + acc.push(statusCode); + } + return acc; + }, []) + .sort(); + + const route: Route = { + path: fullPath, + verb, + parametersConfig, + methodDecoratorMetadata, + methodReflection, + controllerDecoratorMetadata, + controllerReflection, + responseStatusCodes + }; - let fullPath = pathUtils.join(basePath, path); - if (fullPath.length > 1 && fullPath[fullPath.length - 1] === '/') { - fullPath = fullPath.slice(0, -1); - } + this.routes.push(route); - const parametersConfig = await this.createParametersConfigurations({ - controllerReflection, - methodReflection + return this[verb]( + fullPath, + await this.createRequestHandler(controllerInstance, methodName, route) + ); }); - - const responseStatusCodes = Object.keys(methodDecoratorMetadata.options?.responses ?? {}) - .reduce((acc, statusCodeString) => { - const statusCode = Number(statusCodeString); - if (!Number.isNaN(statusCode)) { - acc.push(statusCode); - } - return acc; - }, []) - .sort(); - - const route: Route = { - path: fullPath, - verb, - parametersConfig, - methodDecoratorMetadata, - methodReflection, - controllerDecoratorMetadata, - controllerReflection, - responseStatusCodes - }; - - this.routes.push(route); - - return this[verb](fullPath, await this.createRequestHandler(controllerInstance, methodName, route)); }); }); }); diff --git a/packages/http-server/test/unit/HttpServerModule.test.ts b/packages/http-server/test/unit/HttpServerModule.test.ts index 707b6604..1c03de0b 100644 --- a/packages/http-server/test/unit/HttpServerModule.test.ts +++ b/packages/http-server/test/unit/HttpServerModule.test.ts @@ -141,29 +141,110 @@ describe('HttpServerModule', () => { find() {} @route.post({ path: '/create/', responses: { 201: { description: '' } } }) create() {} + } + class MyDummyHttpServer extends DummyHttpServer { + get() {} + post() {} + } + const dummyHttpServer = new MyDummyHttpServer(); + const app = new App(); + await app.registerController(CustomerController).registerModule(dummyHttpServer); + await app.init(); + + const routes = await dummyHttpServer.createRoutes(); + + expect(routes).to.containSubset([ + { + path: '/api/customers/all', + verb: 'get', + parametersConfig: [], + methodDecoratorMetadata: { + module: 'http-server', + type: 'route', + verb: 'get', + options: { + path: '/all', + responses: { + '200': { + description: '' + }, + '400': { + description: '' + }, + default: { + description: '' + } + } + } + }, + controllerDecoratorMetadata: { + module: 'http-server', + type: 'controller', + options: { + basePath: '/api/customers' + } + }, + responseStatusCodes: [200, 400] + }, + { + path: '/api/customers/create', + verb: 'post', + parametersConfig: [], + methodDecoratorMetadata: { + module: 'http-server', + type: 'route', + verb: 'post', + options: { + path: '/create/', + responses: { + '201': { + description: '' + } + } + } + }, + controllerDecoratorMetadata: { + module: 'http-server', + type: 'controller', + options: { + basePath: '/api/customers' + } + }, + responseStatusCodes: [201] + } + ]); + }); + + it('should support multiple decorations at the controller and method levels', async () => { + @route.controller({ basePath: '/api/customers' }) + @route.controller({ basePath: '/api/users' }) + class PersonController { + @route.get({ + path: '/all', + responses: { 400: { description: '' }, 200: { description: '' }, default: { description: '' } } + }) + find() {} + @route.patch({ path: '/:id', responses: { 201: { description: '' } } }) @route.put({ path: '/:id', responses: { 201: { description: '' } } }) update() {} } class MyDummyHttpServer extends DummyHttpServer { get() {} - post() {} - patch() {} - put() {} } const dummyHttpServer = new MyDummyHttpServer(); const app = new App(); - await app.registerController(CustomerController).registerModule(dummyHttpServer); + await app.registerController(PersonController).registerModule(dummyHttpServer); await app.init(); const routes = await dummyHttpServer.createRoutes(); expect(routes).to.containSubset([ { - path: '/api/customers/all', + path: '/api/users/all', verb: 'get', parametersConfig: [], methodDecoratorMetadata: { @@ -185,24 +266,149 @@ describe('HttpServerModule', () => { } } }, + controllerDecoratorMetadata: { + module: 'http-server', + type: 'controller', + options: { + basePath: '/api/users' + } + }, responseStatusCodes: [200, 400] }, { - path: '/api/customers/create', - verb: 'post', + path: '/api/users/:id', + verb: 'put', + parametersConfig: [], + methodDecoratorMetadata: { + module: 'http-server', + type: 'route', + verb: 'put', + options: { + path: '/:id', + responses: { + '201': { + description: '' + } + } + } + }, + controllerDecoratorMetadata: { + module: 'http-server', + type: 'controller', + options: { + basePath: '/api/users' + } + }, + responseStatusCodes: [201] + }, + { + path: '/api/users/:id', + verb: 'patch', parametersConfig: [], + methodDecoratorMetadata: { + module: 'http-server', + type: 'route', + verb: 'patch', + options: { + path: '/:id', + responses: { + '201': { + description: '' + } + } + } + }, + controllerDecoratorMetadata: { + module: 'http-server', + type: 'controller', + options: { + basePath: '/api/users' + } + }, responseStatusCodes: [201] }, + { + path: '/api/customers/all', + verb: 'get', + parametersConfig: [], + methodDecoratorMetadata: { + module: 'http-server', + type: 'route', + verb: 'get', + options: { + path: '/all', + responses: { + '200': { + description: '' + }, + '400': { + description: '' + }, + default: { + description: '' + } + } + } + }, + controllerDecoratorMetadata: { + module: 'http-server', + type: 'controller', + options: { + basePath: '/api/customers' + } + }, + responseStatusCodes: [200, 400] + }, { path: '/api/customers/:id', verb: 'put', parametersConfig: [], + methodDecoratorMetadata: { + module: 'http-server', + type: 'route', + verb: 'put', + options: { + path: '/:id', + responses: { + '201': { + description: '' + } + } + } + }, + controllerDecoratorMetadata: { + module: 'http-server', + type: 'controller', + options: { + basePath: '/api/customers' + } + }, responseStatusCodes: [201] }, { path: '/api/customers/:id', verb: 'patch', parametersConfig: [], + methodDecoratorMetadata: { + module: 'http-server', + type: 'route', + verb: 'patch', + options: { + path: '/:id', + responses: { + '201': { + description: '' + } + } + } + }, + controllerDecoratorMetadata: { + module: 'http-server', + type: 'controller', + options: { + basePath: '/api/customers' + } + }, responseStatusCodes: [201] } ]); From 18a417ec9f6153720a599012b4f6f08a4fae8786 Mon Sep 17 00:00:00 2001 From: Piero Maltese Date: Thu, 15 Dec 2022 16:43:49 +0100 Subject: [PATCH 2/2] fix: controller decorator allowMultiple: true --- packages/http-server/src/decorators/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-server/src/decorators/route.ts b/packages/http-server/src/decorators/route.ts index d374835b..a802ab26 100644 --- a/packages/http-server/src/decorators/route.ts +++ b/packages/http-server/src/decorators/route.ts @@ -63,7 +63,7 @@ export function controller(options?: ControllerDecoratorOptions): ClassDecorator type: 'controller', options }, - { allowMultiple: false } + { allowMultiple: true } ); }