From 93bb6fca04c1c027c50f2c863fa7ef4dee4241f4 Mon Sep 17 00:00:00 2001 From: Arcanox Date: Wed, 19 Jul 2017 10:37:43 -0500 Subject: [PATCH] Add @Fallthrough() decorator as well as global option to enable automatic fallthrough. By default, fallthrough is now off. --- src/RoutingControllersOptions.ts | 5 ++ src/decorator/Fallthrough.ts | 15 ++++++ src/driver/BaseDriver.ts | 5 ++ src/driver/Driver.ts | 5 ++ src/driver/express/ExpressDriver.ts | 16 ++++-- src/driver/koa/KoaDriver.ts | 25 ++++++++-- src/index.ts | 7 +++ src/metadata/ActionMetadata.ts | 8 +++ src/metadata/types/ResponseHandlerType.ts | 3 +- test/functional/express-middlewares.spec.ts | 2 +- test/functional/fallthrough-decorator.spec.ts | 50 +++++++++++++++++++ test/functional/koa-middlewares.spec.ts | 2 +- 12 files changed, 130 insertions(+), 13 deletions(-) create mode 100644 src/decorator/Fallthrough.ts create mode 100644 test/functional/fallthrough-decorator.spec.ts diff --git a/src/RoutingControllersOptions.ts b/src/RoutingControllersOptions.ts index f319be54..0ce1008c 100644 --- a/src/RoutingControllersOptions.ts +++ b/src/RoutingControllersOptions.ts @@ -19,6 +19,11 @@ export interface RoutingControllersOptions { */ routePrefix?: string; + /** + * Enable automatic fallthrough from action handlers to future actions or middleware + */ + automaticFallthrough?: boolean; + /** * List of controllers to register in the framework or directories from where to import all your controllers. */ diff --git a/src/decorator/Fallthrough.ts b/src/decorator/Fallthrough.ts new file mode 100644 index 00000000..9f6c1889 --- /dev/null +++ b/src/decorator/Fallthrough.ts @@ -0,0 +1,15 @@ +import {getMetadataArgsStorage} from "../index"; + +/** + * Indicates that this route should fall through to further route handlers or middleware after being executed. + * This is equivalent to a route handler calling next() with no parameters in Express. + */ +export function Fallthrough(): Function { + return function (object: Object, methodName: string) { + getMetadataArgsStorage().responseHandlers.push({ + type: "fallthrough", + target: object.constructor, + method: methodName + }); + }; +} diff --git a/src/driver/BaseDriver.ts b/src/driver/BaseDriver.ts index e1523834..a2983453 100644 --- a/src/driver/BaseDriver.ts +++ b/src/driver/BaseDriver.ts @@ -55,6 +55,11 @@ export class BaseDriver { */ routePrefix: string = ""; + /** + * Enable automatic fallthrough from action handlers to future actions or middleware + */ + automaticFallthrough: boolean; + /** * Indicates if cors are enabled. * This requires installation of additional module (cors for express and kcors for koa). diff --git a/src/driver/Driver.ts b/src/driver/Driver.ts index eefa7c3a..68d55380 100644 --- a/src/driver/Driver.ts +++ b/src/driver/Driver.ts @@ -59,6 +59,11 @@ export interface Driver { */ routePrefix: string; + /** + * Enable automatic fallthrough from action handlers to future actions or middleware + */ + automaticFallthrough: boolean; + /** * Indicates if cors are enabled. * This requires installation of additional module (cors for express and kcors for koa). diff --git a/src/driver/express/ExpressDriver.ts b/src/driver/express/ExpressDriver.ts index e1f2fa15..664ab731 100644 --- a/src/driver/express/ExpressDriver.ts +++ b/src/driver/express/ExpressDriver.ts @@ -258,7 +258,8 @@ export class ExpressDriver extends BaseDriver implements Driver { options.response.redirect(action.redirect); } - options.next(); + if (this.automaticFallthrough || action.fallthrough) + options.next(); } else if (action.renderedTemplate) { // if template is set then render it const renderOptions = result && result instanceof Object ? result : {}; @@ -273,7 +274,9 @@ export class ExpressDriver extends BaseDriver implements Driver { } else if (html) { options.response.send(html); } - options.next(); + + if (this.automaticFallthrough || action.fallthrough) + options.next(); }); } else if (result !== undefined || action.undefinedResultCode) { // send regular result @@ -287,18 +290,21 @@ export class ExpressDriver extends BaseDriver implements Driver { } else { options.response.send(); } - options.next(); + if (this.automaticFallthrough || action.fallthrough) + options.next(); } else { if (action.isJsonTyped) { options.response.json(result); } else { options.response.send(result); } - options.next(); + if (this.automaticFallthrough || action.fallthrough) + options.next(); } } else { - options.next(); + if (this.automaticFallthrough || action.fallthrough) + options.next(); } } diff --git a/src/driver/koa/KoaDriver.ts b/src/driver/koa/KoaDriver.ts index f141433d..5628fa59 100644 --- a/src/driver/koa/KoaDriver.ts +++ b/src/driver/koa/KoaDriver.ts @@ -240,7 +240,10 @@ export class KoaDriver extends BaseDriver implements Driver { options.response.redirect(action.redirect); } - return options.next(); + if (this.automaticFallthrough || action.fallthrough) + return options.next(); + else + return; } else if (action.renderedTemplate) { // if template is set then render it // todo: not working in koa const renderOptions = result && result instanceof Object ? result : {}; @@ -249,7 +252,10 @@ export class KoaDriver extends BaseDriver implements Driver { await ctx.render(action.renderedTemplate, renderOptions); }); - return options.next(); + if (this.automaticFallthrough || action.fallthrough) + return options.next(); + else + return; } else if (result !== undefined || action.undefinedResultCode) { // send regular result if (result === null || (result === undefined && action.undefinedResultCode)) { @@ -268,18 +274,27 @@ export class KoaDriver extends BaseDriver implements Driver { options.response.status = action.undefinedResultCode; } - return options.next(); + if (this.automaticFallthrough || action.fallthrough) + return options.next(); + else + return; } else { if (result instanceof Object) { options.response.body = result; } else { options.response.body = result; } - return options.next(); + if (this.automaticFallthrough || action.fallthrough) + return options.next(); + else + return; } } else { - return options.next(); + if(action.fallthrough) + return options.next(); + else + return; } } diff --git a/src/index.ts b/src/index.ts index d919b2e3..553ca47a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,7 @@ export * from "./decorator/CookieParams"; export * from "./decorator/Ctx"; export * from "./decorator/CurrentUser"; export * from "./decorator/Delete"; +export * from "./decorator/Fallthrough"; export * from "./decorator/Get"; export * from "./decorator/Head"; export * from "./decorator/Header"; @@ -179,6 +180,12 @@ export function createExecutor(driver: Driver, options: RoutingControllersOption driver.isDefaultErrorHandlingEnabled = true; } + if (options.automaticFallthrough !== undefined) { + driver.automaticFallthrough = options.automaticFallthrough; + } else { + driver.automaticFallthrough = false; + } + if (options.classTransformer !== undefined) { driver.useClassTransformer = options.classTransformer; } else { diff --git a/src/metadata/ActionMetadata.ts b/src/metadata/ActionMetadata.ts index 1817dfee..588bc09e 100644 --- a/src/metadata/ActionMetadata.ts +++ b/src/metadata/ActionMetadata.ts @@ -108,6 +108,11 @@ export class ActionMetadata { */ successHttpCode: number; + /** + * Specifies if this action should fall through to future handlers or middleware or not. + */ + fallthrough: boolean; + /** * Specifies redirection url for this action. */ @@ -173,6 +178,7 @@ export class ActionMetadata { const redirectHandler = responseHandlers.find(handler => handler.type === "redirect"); const renderedTemplateHandler = responseHandlers.find(handler => handler.type === "rendered-template"); const authorizedHandler = responseHandlers.find(handler => handler.type === "authorized"); + const fallthroughHandler = responseHandlers.find(handler => handler.type === "fallthrough"); const bodyParam = this.params.find(param => param.type === "body"); if (classTransformerResponseHandler) @@ -187,6 +193,8 @@ export class ActionMetadata { this.redirect = redirectHandler.value; if (renderedTemplateHandler) this.renderedTemplate = renderedTemplateHandler.value; + if (fallthroughHandler) + this.fallthrough = true; this.bodyExtraOptions = bodyParam ? bodyParam.extraOptions : undefined; this.isBodyUsed = !!this.params.find(param => param.type === "body" || param.type === "body-param"); diff --git a/src/metadata/types/ResponseHandlerType.ts b/src/metadata/types/ResponseHandlerType.ts index c0edf142..217bd154 100644 --- a/src/metadata/types/ResponseHandlerType.ts +++ b/src/metadata/types/ResponseHandlerType.ts @@ -11,4 +11,5 @@ export type ResponseHandlerType = "success-code" |"on-null" |"on-undefined" |"response-class-transform-options" - |"authorized"; \ No newline at end of file + |"authorized" + |"fallthrough"; \ No newline at end of file diff --git a/test/functional/express-middlewares.spec.ts b/test/functional/express-middlewares.spec.ts index ca33303f..4bd59e01 100644 --- a/test/functional/express-middlewares.spec.ts +++ b/test/functional/express-middlewares.spec.ts @@ -140,7 +140,7 @@ describe("express middlewares", () => { }); let app: any; - before(done => app = createExpressServer().listen(3001, done)); + before(done => app = createExpressServer({ automaticFallthrough: true }).listen(3001, done)); after(done => app.close(done)); it("should call a global middlewares", () => { diff --git a/test/functional/fallthrough-decorator.spec.ts b/test/functional/fallthrough-decorator.spec.ts new file mode 100644 index 00000000..7e210e1e --- /dev/null +++ b/test/functional/fallthrough-decorator.spec.ts @@ -0,0 +1,50 @@ +import "reflect-metadata"; +import {Controller} from "../../src/decorator/Controller"; +import {Fallthrough} from "../../src/decorator/Fallthrough"; +import {UseAfter} from "../../src/decorator/UseAfter"; +import {Get} from "../../src/decorator/Get"; +import {createExpressServer, createKoaServer, getMetadataArgsStorage} from "../../src/index"; +import {assertRequest} from "./test-utils"; +const chakram = require("chakram"); +const expect = chakram.expect; + +describe("fallthrough-decorator behavior", () => { + let useAfter: boolean; + + beforeEach(() => { + useAfter = undefined; + }); + + before(() => { + // reset metadata args storage + getMetadataArgsStorage().reset(); + + @Controller() + class FallthroughController { + @Get("/books") + @Fallthrough() + @UseAfter(function(req: any, res: any, next: (err?: any) => any) { + useAfter = true; + }) + getAll() { + return "All books"; + } + } + }); + + let expressApp: any, koaApp: any; + before(done => expressApp = createExpressServer().listen(3001, done)); + after(done => expressApp.close(done)); + before(done => koaApp = createKoaServer().listen(3002, done)); + after(done => koaApp.close(done)); + + describe("get should respond with proper status code, headers and body content, and should call after middleware", () => { + assertRequest([3001, 3002], "get", "books", response => { + expect(response).to.have.status(200); + expect(response).to.have.header("content-type", "text/html; charset=utf-8"); + expect(response.body).to.be.equal("All books"); + expect(useAfter).to.be.equal(true); + }); + }); + +}); \ No newline at end of file diff --git a/test/functional/koa-middlewares.spec.ts b/test/functional/koa-middlewares.spec.ts index a15b6659..bc168aa0 100644 --- a/test/functional/koa-middlewares.spec.ts +++ b/test/functional/koa-middlewares.spec.ts @@ -136,7 +136,7 @@ describe("koa middlewares", () => { }); let app: any; - before(done => app = createKoaServer().listen(3001, done)); + before(done => app = createKoaServer({ automaticFallthrough: true }).listen(3001, done)); after(done => app.close(done)); it("should call a global middlewares", () => {