Skip to content

Commit

Permalink
feat: allow custom middlewares for controllers and methods (lukeautry…
Browse files Browse the repository at this point in the history
…#1123)

* allow custom middlewares for controllers and methods

* use Reflect API

* use reflect-metadata

* move import to index and type decorator function

* better middleware typings

* removed frameworks types deps

* fixed tests prestep

* Use Array<T> instead of T[]
  • Loading branch information
cheng81 authored Nov 18, 2021
1 parent b0cd953 commit ea976ee
Show file tree
Hide file tree
Showing 17 changed files with 274 additions and 16 deletions.
5 changes: 4 additions & 1 deletion packages/cli/src/routeGeneration/templates/express.hbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* tslint:disable */
/* eslint-disable */
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse } from '@tsoa/runtime';
import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse, fetchMiddlewares } from '@tsoa/runtime';
{{#each controllers}}
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
import { {{name}} } from '{{modulePath}}';
Expand All @@ -15,6 +15,7 @@ const promiseAny = require('promise.any');
import { iocContainer } from '{{iocModule}}';
import { IocContainer, IocContainerFactory } from '@tsoa/runtime';
{{/if}}
import type { RequestHandler } from 'express';
import * as express from 'express';
{{#if useFileUploads}}
const multer = require('multer');
Expand Down Expand Up @@ -68,6 +69,8 @@ export function RegisterRoutes(app: express.Router) {
{{#if uploadFiles}}
upload.array('{{uploadFilesName}}'),
{{/if}}
...(fetchMiddlewares<RequestHandler>({{../name}})),
...(fetchMiddlewares<RequestHandler>({{../name}}.prototype.{{name}})),

{{#if ../../iocModule}}async {{/if}}function {{../name}}_{{name}}(request: any, response: any, next: any) {
const args = {
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/routeGeneration/templates/hapi.hbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* tslint:disable */
/* eslint-disable */
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse } from '@tsoa/runtime';
import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse, fetchMiddlewares } from '@tsoa/runtime';
{{#each controllers}}
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
import { {{name}} } from '{{modulePath}}';
Expand All @@ -16,7 +16,7 @@ import { iocContainer } from '{{iocModule}}';
import { IocContainer, IocContainerFactory } from '@tsoa/runtime';
{{/if}}
import { boomify, isBoom, Payload } from '@hapi/boom';
import { Request } from '@hapi/hapi';
import { Request, RouteOptionsPreAllOptions } from '@hapi/hapi';

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

Expand Down Expand Up @@ -68,13 +68,15 @@ export function RegisterRoutes(server: any) {
{{#if uploadFile}}
{
method: fileUploadMiddleware('{{uploadFileName}}', false)
}
},
{{/if}}
{{#if uploadFiles}}
{
method: fileUploadMiddleware('{{uploadFilesName}}', true)
}
},
{{/if}}
...(fetchMiddlewares<RouteOptionsPreAllOptions>({{../name}})),
...(fetchMiddlewares<RouteOptionsPreAllOptions>({{../name}}.prototype.{{name}})),
],
{{#if uploadFile}}
payload: {
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/routeGeneration/templates/koa.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable */
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
{{#if canImportByAlias}}
import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse } from '@tsoa/runtime';
import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse, fetchMiddlewares } from '@tsoa/runtime';
{{else}}
import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, TsoaResponse, HttpStatusCodeLiteral } from '../../../src';
{{/if}}
Expand All @@ -19,6 +19,7 @@ const promiseAny = require('promise.any');
import { iocContainer } from '{{iocModule}}';
import { IocContainer, IocContainerFactory } from '@tsoa/runtime';
{{/if}}
import type { Middleware } from 'koa';
import * as KoaRouter from '@koa/router';
{{#if useFileUploads}}
const multer = require('@koa/multer');
Expand Down Expand Up @@ -72,6 +73,9 @@ export function RegisterRoutes(router: KoaRouter) {
{{#if uploadFiles}}
upload.array('{{uploadFilesName}}'),
{{/if}}
...(fetchMiddlewares<Middleware>({{../name}})),
...(fetchMiddlewares<Middleware>({{../name}}.prototype.{{name}})),

async function {{../name}}_{{name}}(context: any, next: any) {
const args = {
{{#each parameters}}
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"license": "MIT",
"dependencies": {
"promise.any": "^2.0.2",
"reflect-metadata": "^0.1.13",
"validator": "^13.6.0"
},
"devDependencies": {
Expand Down
51 changes: 51 additions & 0 deletions packages/runtime/src/decorators/middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
type Middleware<T extends Function | Object> = T;

const TSOA_MIDDLEWARES = Symbol('@tsoa:middlewares');

/**
* Helper function to create a decorator
* that can act as a class and method decorator.
* @param fn a callback function that accepts
* the subject of the decorator
* either the constructor or the
* method
* @returns
*/
function decorator(fn: (value: any) => void) {
return (...args: any[]) => {
// class decorator
if (args.length === 1) {
fn(args[0]);
} else if (args.length === 3 && args[2].value) {
// method decorator
const descriptor = args[2] as PropertyDescriptor;
if (descriptor.value) {
fn(descriptor.value);
}
}
};
}

/**
* Install middlewares to the Controller or a specific method.
* @param middlewares
* @returns
*/
export function Middlewares<T>(...mws: Array<Middleware<T>>): ClassDecorator & MethodDecorator {
return decorator(target => {
if (mws) {
const current = fetchMiddlewares<T>(target);
Reflect.defineMetadata(TSOA_MIDDLEWARES, [...current, ...mws], target);
}
});
}

/**
* Internal function used to retrieve installed middlewares
* in controller and methods (used during routes generation)
* @param target
* @returns list of middlewares
*/
export function fetchMiddlewares<T>(target: any): Array<Middleware<T>> {
return Reflect.getMetadata(TSOA_MIDDLEWARES, target) || [];
}
2 changes: 2 additions & 0 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'reflect-metadata';
export * from './decorators/deprecated';
export * from './decorators/example';
export * from './decorators/parameter';
Expand All @@ -7,6 +8,7 @@ export * from './decorators/operationid';
export * from './decorators/route';
export * from './decorators/security';
export * from './decorators/extension';
export * from './decorators/middlewares';
export * from './interfaces/controller';
export * from './interfaces/response';
export * from './interfaces/iocModule';
Expand Down
37 changes: 37 additions & 0 deletions tests/fixtures/controllers/middlewaresExpressController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Route, Get, Middlewares as GenericMiddlewares } from '@tsoa/runtime';

import type {
Request as ExpressRequest,
Response as ExpressResponse,
NextFunction as ExpressNextFunction,
RequestHandler,
} from 'express';

function Middlewares(...mws: RequestHandler[]) {
return GenericMiddlewares<RequestHandler>(...mws);
}

const middlewaresState = {};

export function stateOf(key: string): boolean | undefined {
return middlewaresState[key];
}

function testMiddleware(key: string) {
return async (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => {
middlewaresState[key] = true;
next();
};
}

@GenericMiddlewares<RequestHandler>(
testMiddleware('route'),
)
@Route('MiddlewareTestExpress')
export class MiddlewareExpressController {
@Middlewares(testMiddleware('test1'))
@Get('/test1')
public async test1(): Promise<void> {
return;
}
}
33 changes: 33 additions & 0 deletions tests/fixtures/controllers/middlewaresHapiController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Route, Get, Middlewares as GenericMiddlewares } from '@tsoa/runtime';

import type { Request, ResponseToolkit, RouteOptionsPreAllOptions } from '@hapi/hapi';

function Middlewares(...mws: RouteOptionsPreAllOptions[]) {
return GenericMiddlewares<RouteOptionsPreAllOptions>(...mws);
}

const middlewaresState = {};

export function stateOf(key: string): boolean | undefined {
return middlewaresState[key];
}

function testMiddleware(key: string) {
return async (request: Request, h: ResponseToolkit) => {
middlewaresState[key] = true;
return key;
};
}


@GenericMiddlewares<RouteOptionsPreAllOptions>(
testMiddleware('route'),
)
@Route('MiddlewareTestHapi')
export class MiddlewareHapiController {
@Middlewares(testMiddleware('test1'))
@Get('/test1')
public async test1(): Promise<void> {
return;
}
}
43 changes: 43 additions & 0 deletions tests/fixtures/controllers/middlewaresHierarchyController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Route, Get, Middlewares as GenericMiddlewares, Controller } from '@tsoa/runtime';

import type { Request, Response, NextFunction, RequestHandler } from 'express';

function Middlewares(...mws: RequestHandler[]) {
return GenericMiddlewares<RequestHandler>(...mws);
}

const middlewaresState: string[] = [];

export function state(): string[] {
return middlewaresState;
}

function testMiddleware(key: string) {
return async (req: Request, res: Response, next: NextFunction) => {
middlewaresState.push(key);
next();
};
}

// base class with some middleware
@Middlewares(testMiddleware('base'))
class BaseController extends Controller {}

// another one
@Middlewares(testMiddleware('intermediate'))
class IntermediateController extends BaseController {}

// intermediate controller class without middlewares
class NoopController extends IntermediateController {}

@GenericMiddlewares<RequestHandler>(
testMiddleware('route'),
)
@Route('MiddlewareHierarchyTestExpress')
export class MiddlewareHierarchyTestExpress extends NoopController {
@Middlewares(testMiddleware('test1'))
@Get('/test1')
public async test1(): Promise<void> {
return;
}
}
32 changes: 32 additions & 0 deletions tests/fixtures/controllers/middlewaresKoaController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Route, Get, Middlewares as GenericMiddlewares } from '@tsoa/runtime';

import type { Context as KoaContext, Next as KoaNext, Middleware } from 'koa';

function Middlewares(...mws: Middleware[]) {
return GenericMiddlewares<Middleware>(...mws);
}

const middlewaresState = {};

export function stateOf(key: string): boolean | undefined {
return middlewaresState[key];
}

function testMiddleware(key: string) {
return async (ctx: KoaContext, next: KoaNext) => {
middlewaresState[key] = true;
next();
};
}

@GenericMiddlewares<Middleware>(
testMiddleware('route'),
)
@Route('MiddlewareTestKoa')
export class MiddlewareKoaController {
@Middlewares(testMiddleware('test1'))
@Get('/test1')
public async test1(): Promise<void> {
return;
}
}
3 changes: 3 additions & 0 deletions tests/fixtures/express/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import '../controllers/tagController';
import '../controllers/noExtendsController';
import '../controllers/subresourceController';

import '../controllers/middlewaresExpressController';
import '../controllers/middlewaresHierarchyController';

import { RegisterRoutes } from './routes';

export const app: express.Express = express();
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/hapi/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import '../controllers/validateController';
import '../controllers/noExtendsController';
import '../controllers/subresourceController';

import '../controllers/middlewaresHapiController';

import { RegisterRoutes } from './routes';

export const server = new Server({});
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/koa/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import '../controllers/validateController';
import '../controllers/noExtendsController';
import '../controllers/subresourceController';

import '../controllers/middlewaresKoaController';

import * as bodyParser from 'koa-bodyparser';
import { RegisterRoutes } from './routes';

Expand Down
26 changes: 26 additions & 0 deletions tests/integration/express-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
ValidateMapStringToNumber,
ValidateModel,
} from '../fixtures/testModel';
import { stateOf } from '../fixtures/controllers/middlewaresExpressController';
import { state } from '../fixtures/controllers/middlewaresHierarchyController';
import { readFileSync } from 'fs';
import { resolve } from 'path';

Expand Down Expand Up @@ -363,6 +365,30 @@ describe('Express Server', () => {
);
});

it('can invoke middlewares installed in routes and paths', () => {
expect(stateOf('route')).to.be.undefined;
return verifyGetRequest(
basePath + '/MiddlewareTestExpress/test1',
(err, res) => {
expect(stateOf('route')).to.be.true;
expect(stateOf('test1')).to.be.true;
},
204,
);
});

it('can invoke middlewares in the order they are defined by controller class hierarchy', () => {
expect(state()).to.be.empty;
return verifyGetRequest(
basePath + '/MiddlewareHierarchyTestExpress/test1',
(err, res) => {
const expected = ['base', 'intermediate', 'route', 'test1'];
expect(state()).to.eql(expected);
},
204,
)
});

describe('Controller', () => {
it('should normal status code', () => {
return verifyGetRequest(
Expand Down
Loading

0 comments on commit ea976ee

Please sign in to comment.