From 91ae97a8e12529386a3edb0ab1b3b84688abbf6f Mon Sep 17 00:00:00 2001 From: Evandro Abu Kamel Date: Sat, 25 Nov 2023 08:48:57 -0300 Subject: [PATCH] Add @httpOptions decorator to handle OPTIONS HTTP method. (#313) * Add @httpOptions decorator to handle OPTIONS HTTP method. * Improve unit tests. * fixed build system * remove unnecesary paranthesis caused by casting * remove unused sort package --------- Co-authored-by: Podaru Dragos --- README.md | 21 +++++++++++++++- src/decorators.ts | 25 +++++++++++++------- src/tsconfig-es6.json | 4 ++-- test/features/controller_inheritance.test.ts | 25 +++++++++++++++++++- 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8880b30d..0bf962df 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ Registers the decorated controller method as a request handler for a particular ### `@SHORTCUT(path, [middleware, ...])` -Shortcut decorators which are simply wrappers for `@httpMethod`. Right now these include `@httpGet`, `@httpPost`, `@httpPut`, `@httpPatch`, `@httpHead`, `@httpDelete`, and `@All`. For anything more obscure, use `@httpMethod` (Or make a PR :smile:). +Shortcut decorators which are simply wrappers for `@httpMethod`. Right now these include `@httpGet`, `@httpPost`, `@httpPut`, `@httpPatch`, `@httpHead`, `@httpDelete`, `@httpOptions`, and `@All`. For anything more obscure, use `@httpMethod` (Or make a PR :smile:). ### `@request()` @@ -636,6 +636,25 @@ class Service { The `BaseMiddleware.bind()` method will bind the `TYPES.TraceIdValue` if it hasn't been bound yet or re-bind if it has already been bound. +### Dealing with CORS + +If you access a route from a browser and experience a CORS problem, in other words, your browser stops at the +OPTIONS request, you need to add a route for that method too. You need to write a method in your controller class to +handle the same route but for OPTIONS method, it can have empty body and no parameters though. + +```ts +@controller("/api/example") +class ExampleController extends BaseHttpController { + @httpGet("/:id") + public get(req: Request, res: Response) { + return {}; + } + + @httpOptions("/:id") + public options() { } +} +``` + ## Route Map If we have some controllers like for example: diff --git a/src/decorators.ts b/src/decorators.ts index 0a69a0be..c8538907 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -5,7 +5,7 @@ import type { DecoratorTarget, Middleware, ControllerMetadata, HandlerDecorator, export const injectHttpContext = inject(TYPE.HttpContext); -export function controller(path: string, ...middleware: Array) { +export function controller(path: string, ...middleware: Middleware[]) { return (target: NewableFunction): void => { const currentMetadata: ControllerMetadata = { middleware, @@ -39,57 +39,64 @@ export function controller(path: string, ...middleware: Array) { export function all( path: string, - ...middleware: Array + ...middleware: Middleware[] ): HandlerDecorator { return httpMethod('all', path, ...middleware); } export function httpGet( path: string, - ...middleware: Array + ...middleware: Middleware[] ): HandlerDecorator { return httpMethod('get', path, ...middleware); } export function httpPost( path: string, - ...middleware: Array + ...middleware: Middleware[] ): HandlerDecorator { return httpMethod('post', path, ...middleware); } export function httpPut( path: string, - ...middleware: Array + ...middleware: Middleware[] ): HandlerDecorator { return httpMethod('put', path, ...middleware); } export function httpPatch( path: string, - ...middleware: Array + ...middleware: Middleware[] ): HandlerDecorator { return httpMethod('patch', path, ...middleware); } export function httpHead( path: string, - ...middleware: Array + ...middleware: Middleware[] ): HandlerDecorator { return httpMethod('head', path, ...middleware); } export function httpDelete( path: string, - ...middleware: Array + ...middleware: Middleware[] ): HandlerDecorator { return httpMethod('delete', path, ...middleware); } +export function httpOptions( + path: string, + ...middleware: Middleware[] +): HandlerDecorator { + return httpMethod('options', path, ...middleware); +} + export function httpMethod( method: keyof typeof HTTP_VERBS_ENUM, path: string, - ...middleware: Array + ...middleware: Middleware[] ): HandlerDecorator { return (target: DecoratorTarget, key: string): void => { const metadata: ControllerMethodMetadata = { diff --git a/src/tsconfig-es6.json b/src/tsconfig-es6.json index b2c3b398..fc6ba4e0 100644 --- a/src/tsconfig-es6.json +++ b/src/tsconfig-es6.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "outDir": "../es6", - "target": "ES6" + "outDir": "../es", + "target": "ES2015" }, "extends": "../tsconfig.json" } \ No newline at end of file diff --git a/test/features/controller_inheritance.test.ts b/test/features/controller_inheritance.test.ts index ccac979f..e7b15b6d 100644 --- a/test/features/controller_inheritance.test.ts +++ b/test/features/controller_inheritance.test.ts @@ -2,7 +2,7 @@ import { Container, injectable } from 'inversify'; import supertest from 'supertest'; import { json, urlencoded } from 'express'; import { InversifyExpressServer } from '../../src/server'; -import { controller, httpGet, requestParam, httpDelete, httpPost, httpPut, requestBody, } from '../../src/decorators'; +import { controller, httpGet, requestParam, httpDelete, httpPost, httpPut, requestBody, httpOptions, } from '../../src/decorators'; import { cleanUpMetadata } from '../../src/utils'; type ResponseBody = { args: string, status: number }; @@ -55,6 +55,14 @@ function getDemoServer() { ) { return { status: `BASE DELETE! ${id}` }; } + + @httpOptions('/:id') + public options( + @requestParam('id') id: string + ) { + return { status: `BASE OPTIONS! ${id}` }; + } + } @controller('/api/v1/movies') @@ -188,6 +196,21 @@ describe('Derived controller', () => { }); }); + it('Can access methods decorated with @httpOptions from parent', (done) => { + + const server = getDemoServer(); + const id = 5; + + void supertest(server).options(`/api/v1/movies/${id}`) + .expect(200) + .then(res => { + const r = res.body as ResponseBody; + expect(r.status).toEqual(`BASE OPTIONS! ${id}`); + done(); + }); + + }); + it('Derived controller can have its own methods', done => { const server = getDemoServer(); const movieId = 5;