diff --git a/.nycrc b/.nycrc index 6b5aa8780b8..4afaeba15a9 100644 --- a/.nycrc +++ b/.nycrc @@ -26,8 +26,8 @@ "lcov" ], "check-coverage": true, - "lines": 99.73, + "lines": 99.8, "statements": 99.81, - "functions": 99.62, - "branches": 88.45 + "functions": 99.69, + "branches": 88.54 } diff --git a/docs/docs/middlewares.md b/docs/docs/middlewares.md index 5e878feb14e..cefcab1b2d1 100644 --- a/docs/docs/middlewares.md +++ b/docs/docs/middlewares.md @@ -169,24 +169,13 @@ The decorator @@OverrideProvider@@ gives you the ability to override some intern <<< @/docs/docs/snippets/middlewares/override-middleware.ts -Since v5.63.0, Ts.ED provide a PlatformResponseMiddleware (it replaces @@SendResponseMiddleware@@ and @@ResponseViewMiddleware@@), -to render a view and send the appropriate response to your consumer according to the executed endpoint. - -You are able to override this middleware to change the initial behavior of this middleware. - -<<< @/docs/docs/snippets/middlewares/override-platform-response-middleware.ts - -Here we use the new [Platform API](/docs/platform-api.md) to write our middleware. By using @@Context@@ decorator and @@PlatformContext@@ class we can get some information: +Here we use the new [Platform API](/docs/platform-api.md) to write our middleware. +By using @@Context@@ decorator and @@PlatformContext@@ class we can get some information: - The data returned by the last executed endpoint, - The @@EndpointMetadata@@ itself, - The @@PlatformRequest@@ and @@PlatformResponse@@ classes abstraction. These classes allow better code abstraction by exposing methods that are agnostic to Express.js. -::: warning -Ts.ED have is own @@GlobalErrorHandlerMiddleware@@. Override this middleware is not necessary, since you can create you own middleware and -register it in your server before the original GlobalErrorHandlerMiddleware. For more details, see our [Exceptions](/docs/exceptions.md#handle-all-errors) documentation page. -::: - ::: tip By default, the server imports automatically your middlewares matching with this rules `${rootDir}/middlewares/**/*.ts` (See [componentScan configuration](/configuration.md)). diff --git a/docs/docs/snippets/middlewares/override-middleware.ts b/docs/docs/snippets/middlewares/override-middleware.ts index 9d2b3dc6d7b..21b18ce5b85 100644 --- a/docs/docs/snippets/middlewares/override-middleware.ts +++ b/docs/docs/snippets/middlewares/override-middleware.ts @@ -1,8 +1,13 @@ -import {OriginalMiddleware, OverrideProvider} from "@tsed/common"; +import {Context, OriginalMiddleware, OverrideProvider} from "@tsed/common"; @OverrideProvider(OriginalMiddleware) export class CustomMiddleware extends OriginalMiddleware { - public use() { + public use(@Context() ctx: Context) { + ctx.response; // Ts.ED response + ctx.request; // Ts.ED resquest + ctx.getResponse(); // return Express.js or Koa.js response + ctx.getRequest(); // return Express.js or Koa.js request + // Do something return super.use(); } diff --git a/docs/docs/snippets/middlewares/override-platform-response-middleware.ts b/docs/docs/snippets/middlewares/override-platform-response-middleware.ts deleted file mode 100644 index f052ebb1885..00000000000 --- a/docs/docs/snippets/middlewares/override-platform-response-middleware.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {PlatformResponseMiddleware, TemplateRenderingError} from "@tsed/common"; -import {isBoolean, isNumber, isStream, isString} from "@tsed/core"; -import {Inject, OverrideProvider} from "@tsed/di"; -import {ConverterService} from "../../converters/services/ConverterService"; -import {Context} from "../decorators/context"; - -@OverrideProvider(PlatformResponseMiddleware) -export class CustomPlatformResponseMiddleware { - @Inject() - mapper: ConverterService; - - public async use(@Context() ctx: Context) { - // Context contain endpoint information, a request and response abstraction and the current data. - const {response, endpoint} = ctx; - let {data} = ctx; - - if (endpoint.view) { - // Render the view if @@View@@ decorator is used - data = await this.render(data, endpoint); - } else if (this.shouldSerialize(data)) { - // Serialize the object - data = this.mapper.serialize(data, {type: endpoint.response.type, withIgnoredProps: false}); - } - - // The response is an instance of PlatformResponse which expose a body method to send response. - // If you want the original response from Express use response.raw => Express.Response - response.body(data); - - return response; // return response is required to stop the next handlers - } - - async render(data: any, ctx: Context) { - const {response, endpoint} = ctx; - - try { - const {path, options} = endpoint.view; - - // NOTE: response is an instance of PlatformResponse class. - // If you want the original response from Express use response.raw => Express.Response - - return response.render(path, {...options, ...data}); - } catch (err) { - throw new TemplateRenderingError(endpoint.target, endpoint.propertyKey, err); - } - } - - protected shouldSerialize(data: any) { - return !(this.shouldBeStreamed(data) || this.shouldBeSent(data) || data === undefined); - } - - protected shouldBeSent(data: any) { - return Buffer.isBuffer(data) || isBoolean(data) || isNumber(data) || isString(data) || data === null; - } - - protected shouldBeStreamed(data: any) { - return isStream(data); - } -} diff --git a/docs/docs/snippets/middlewares/override-response-view.ts b/docs/docs/snippets/middlewares/override-response-view.ts deleted file mode 100644 index 0ae6a7cb4cd..00000000000 --- a/docs/docs/snippets/middlewares/override-response-view.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {EndpointInfo, Res, ResponseViewMiddleware} from "@tsed/common"; -import {Context} from "@tsed/common/src/platform/decorators/context"; -import {OverrideProvider} from "@tsed/di"; - -@OverrideProvider(ResponseViewMiddleware) -export class MyResponseViewMiddleware extends ResponseViewMiddleware { - public use( - @Context() ctx: Context, - @EndpointInfo() endpoint: EndpointInfo, - @Res() response: Res - ): any { - const data = ctx.data; - // DO SOMETHING - - return super.use(data, endpoint, response); - } -} diff --git a/docs/docs/snippets/middlewares/override-send-response.ts b/docs/docs/snippets/middlewares/override-send-response.ts deleted file mode 100644 index 1ca246de107..00000000000 --- a/docs/docs/snippets/middlewares/override-send-response.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {ConverterService, OverrideProvider, Req, Res, SendResponseMiddleware} from "@tsed/common"; -import {isStream} from "@tsed/core/src"; - -@OverrideProvider(SendResponseMiddleware) -export class MySendResponseMiddleware { - constructor(private converterService: ConverterService) { - } - - public use(@Req() request: Req, @Res() response: Res): any { - const {ctx: {data, endpoint}} = request; - - if (data === undefined) { - return response.send(); - } - - if (isStream(data)) { - data.pipe(response); - - return response; - } - - const payload = { - data: this.converterService.serialize(data, {type: endpoint.type}), - errors: [] - }; - - return response.json(payload); - } -} diff --git a/packages/ajv/package.json b/packages/ajv/package.json index 56708d44b1c..297e2fb0581 100644 --- a/packages/ajv/package.json +++ b/packages/ajv/package.json @@ -5,7 +5,8 @@ "main": "./lib/index.js", "typings": "./lib/index.d.ts", "scripts": { - "build": "tsc --build tsconfig.compile.json" + "build": "tsc --build tsconfig.compile.json", + "tsc": "tsc --build tsconfig.check.json" }, "dependencies": { "tslib": "1.11.0" diff --git a/packages/common/src/mvc/errors/TemplateRenderingError.spec.ts b/packages/common/src/mvc/errors/TemplateRenderingError.spec.ts deleted file mode 100644 index 93a4789c66c..00000000000 --- a/packages/common/src/mvc/errors/TemplateRenderingError.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {expect} from "chai"; -import {TemplateRenderingError} from "../../../src/mvc"; - -describe("TemplateRenderingError", () => { - it("should have a message", () => { - const errorInstance = new TemplateRenderingError(class Target {}, "method", new Error("test")); - expect(errorInstance.message).to.equal("Template rendering error: Target.method()\nError: test"); - expect(errorInstance.name).to.equal("TEMPLATING_RENDER_ERROR"); - }); -}); diff --git a/packages/common/src/mvc/index.ts b/packages/common/src/mvc/index.ts index fdb71d79141..6a56832d0a5 100644 --- a/packages/common/src/mvc/index.ts +++ b/packages/common/src/mvc/index.ts @@ -26,7 +26,6 @@ export * from "./pipes/DeserializerPipe"; export * from "./services/ConverterService"; // errors -export * from "./errors/TemplateRenderingError"; export * from "./errors/RequiredValidationError"; export * from "./errors/ValidationError"; diff --git a/packages/common/src/mvc/interfaces/HandlerType.ts b/packages/common/src/mvc/interfaces/HandlerType.ts index abd9ca000c2..8a9f6957819 100644 --- a/packages/common/src/mvc/interfaces/HandlerType.ts +++ b/packages/common/src/mvc/interfaces/HandlerType.ts @@ -1,6 +1,9 @@ export enum HandlerType { - FUNCTION = "function", - $CTX = "context", + CUSTOM = "custom", + ENDPOINT = "endpoint", MIDDLEWARE = "middleware", - CONTROLLER = "controller" + ERR_MIDDLEWARE = "err:middleware", + CTX_FN = "context", + RAW_FN = "raw:middleware", + RAW_ERR_FN = "raw:err:middleware" } diff --git a/packages/common/src/mvc/models/HandlerMetadata.spec.ts b/packages/common/src/mvc/models/HandlerMetadata.spec.ts index c4292f402cb..5a9b0e312b3 100644 --- a/packages/common/src/mvc/models/HandlerMetadata.spec.ts +++ b/packages/common/src/mvc/models/HandlerMetadata.spec.ts @@ -2,8 +2,26 @@ import {Controller, Err, Get, Middleware, Next, ParamMetadata, ParamTypes, Req} import {expect} from "chai"; import {HandlerType} from "../../../src/mvc/interfaces/HandlerType"; import {HandlerMetadata} from "../../../src/mvc/models/HandlerMetadata"; +import {useCtxHandler} from "../../platform/utils/useCtxHandler"; describe("HandlerMetadata", () => { + describe("from useCtxHandler", () => { + it("should create a new handlerMetadata with right metadata", () => { + // GIVEN + const handler = useCtxHandler((ctx: any) => {}); + const options = { + target: handler + }; + // WHEN + const handlerMetadata = new HandlerMetadata(options); + + // THEN + expect(handlerMetadata.injectable).to.eq(false); + expect(handlerMetadata.type).to.eq(HandlerType.CTX_FN); + expect(handlerMetadata.hasNextFunction).to.eq(false); + expect(handlerMetadata.hasErrorParam).to.eq(false); + }); + }); describe("from function", () => { it("should create a new handlerMetadata with right metadata", () => { // GIVEN @@ -15,12 +33,11 @@ describe("HandlerMetadata", () => { // THEN expect(handlerMetadata.injectable).to.eq(false); - expect(handlerMetadata.type).to.eq(HandlerType.FUNCTION); + expect(handlerMetadata.type).to.eq(HandlerType.RAW_FN); expect(handlerMetadata.hasNextFunction).to.eq(true); expect(handlerMetadata.hasErrorParam).to.eq(false); }); }); - describe("from function err", () => { it("should create a new handlerMetadata with right metadata", () => { // GIVEN @@ -32,7 +49,7 @@ describe("HandlerMetadata", () => { // THEN expect(handlerMetadata.injectable).to.eq(false); - expect(handlerMetadata.type).to.eq(HandlerType.FUNCTION); + expect(handlerMetadata.type).to.eq(HandlerType.RAW_ERR_FN); expect(handlerMetadata.hasNextFunction).to.eq(true); expect(handlerMetadata.hasErrorParam).to.eq(true); expect(handlerMetadata.propertyKey).to.eq(undefined); @@ -57,7 +74,6 @@ describe("HandlerMetadata", () => { ]); }); }); - describe("from function without nextFn", () => { it("should create a new handlerMetadata with right metadata", () => { // GIVEN @@ -70,13 +86,12 @@ describe("HandlerMetadata", () => { // THEN expect(handlerMetadata.injectable).to.eq(false); - expect(handlerMetadata.type).to.eq(HandlerType.FUNCTION); + expect(handlerMetadata.type).to.eq(HandlerType.RAW_FN); expect(handlerMetadata.hasNextFunction).to.eq(false); expect(handlerMetadata.hasErrorParam).to.eq(false); expect(handlerMetadata.propertyKey).to.eq(undefined); }); }); - describe("from endpoint/middleware without injection", () => { it("should create a new handlerMetadata with right metadata", () => { // GIVEN @@ -89,20 +104,19 @@ describe("HandlerMetadata", () => { const options = { target: Test, propertyKey: "test", - type: HandlerType.CONTROLLER + type: HandlerType.ENDPOINT }; // WHEN const handlerMetadata = new HandlerMetadata(options); // THEN expect(handlerMetadata.injectable).to.eq(false); - expect(handlerMetadata.type).to.eq(HandlerType.CONTROLLER); + expect(handlerMetadata.type).to.eq(HandlerType.ENDPOINT); expect(handlerMetadata.hasNextFunction).to.eq(true); expect(handlerMetadata.hasErrorParam).to.eq(false); expect(handlerMetadata.propertyKey).to.eq("test"); }); }); - describe("from endpoint/middleware with injection", () => { it("should create a new handlerMetadata with right metadata", () => { // GIVEN @@ -115,14 +129,14 @@ describe("HandlerMetadata", () => { const options = { target: Test, propertyKey: "test", - type: HandlerType.CONTROLLER + type: HandlerType.ENDPOINT }; // WHEN const handlerMetadata = new HandlerMetadata(options); // THEN expect(handlerMetadata.injectable).to.eq(true); - expect(handlerMetadata.type).to.eq(HandlerType.CONTROLLER); + expect(handlerMetadata.type).to.eq(HandlerType.ENDPOINT); expect(handlerMetadata.hasNextFunction).to.eq(true); expect(handlerMetadata.hasErrorParam).to.eq(false); expect(handlerMetadata.propertyKey).to.eq("test"); @@ -131,7 +145,6 @@ describe("HandlerMetadata", () => { expect(handlerMetadata.getParams()[1].paramType).to.deep.eq("NEXT_FN"); }); }); - describe("from middleware without injection and error", () => { it("should create a new handlerMetadata with right metadata", () => { // GIVEN @@ -150,13 +163,12 @@ describe("HandlerMetadata", () => { // THEN expect(handlerMetadata.injectable).to.eq(false); - expect(handlerMetadata.type).to.eq(HandlerType.MIDDLEWARE); + expect(handlerMetadata.type).to.eq(HandlerType.RAW_ERR_FN); expect(handlerMetadata.hasNextFunction).to.eq(true); expect(handlerMetadata.hasErrorParam).to.eq(true); expect(handlerMetadata.propertyKey).to.eq("use"); }); }); - describe("from middleware with injection and error", () => { it("should create a new handlerMetadata with right metadata", () => { // WHEN @@ -175,7 +187,7 @@ describe("HandlerMetadata", () => { // THEN expect(handlerMetadata.injectable).to.eq(true); - expect(handlerMetadata.type).to.eq(HandlerType.MIDDLEWARE); + expect(handlerMetadata.type).to.eq(HandlerType.ERR_MIDDLEWARE); expect(handlerMetadata.hasNextFunction).to.eq(true); expect(handlerMetadata.hasErrorParam).to.eq(true); expect(handlerMetadata.propertyKey).to.eq("use"); diff --git a/packages/common/src/mvc/models/HandlerMetadata.ts b/packages/common/src/mvc/models/HandlerMetadata.ts index e8470e7e473..cdee1e1fd4a 100644 --- a/packages/common/src/mvc/models/HandlerMetadata.ts +++ b/packages/common/src/mvc/models/HandlerMetadata.ts @@ -1,10 +1,12 @@ import {Type} from "@tsed/core"; +import {PlatformRouteOptions, PlatformRouteWithoutHandlers} from "../../platform/interfaces/PlatformRouterMethods"; import {HandlerType} from "../interfaces/HandlerType"; import {ParamMetadata} from "../models/ParamMetadata"; import {ParamTypes} from "./ParamTypes"; -export interface HandlerConstructorOptions { - target: Type | Function; +export interface HandlerMetadataOptions { + target: (Type | Function) & {type?: HandlerType}; + routeOptions?: PlatformRouteWithoutHandlers; token?: Type; propertyKey?: string | symbol; type?: HandlerType; @@ -15,15 +17,16 @@ export class HandlerMetadata { readonly token: Type; readonly propertyKey: string | symbol; readonly injectable: boolean = false; - readonly type: HandlerType = HandlerType.FUNCTION; - readonly hasErrorParam: boolean = false; + readonly type: HandlerType = HandlerType.RAW_FN; readonly hasNextFunction: boolean = false; + readonly routeOptions: Partial; handler: any; - constructor(options: HandlerConstructorOptions) { - const {target, token, propertyKey, type = HandlerType.FUNCTION} = options; + constructor(options: HandlerMetadataOptions) { + const {target, token, propertyKey, type, routeOptions} = options; - this.type = type; + this.type = type || target.type || HandlerType.RAW_FN; + this.routeOptions = routeOptions || {}; this.handler = propertyKey ? target.prototype[propertyKey] : target; if (propertyKey) { @@ -31,20 +34,26 @@ export class HandlerMetadata { this.token = token!; this.propertyKey = propertyKey; this.hasNextFunction = this.hasParamType(ParamTypes.NEXT_FN); - this.hasErrorParam = this.hasParamType(ParamTypes.ERR); + + if (this.hasParamType(ParamTypes.ERR)) { + this.type = HandlerType.ERR_MIDDLEWARE; + } + this.injectable = ParamMetadata.getParams(target as any, propertyKey).length > 0; } if (!this.injectable) { - this.hasErrorParam = this.handler.length === 4; - this.hasNextFunction = this.handler.length >= 3; - - if (this.handler.length === 1) { - this.type = HandlerType.$CTX; + if (this.handler.length === 4) { + this.type = HandlerType.RAW_ERR_FN; } + this.hasNextFunction = this.handler.length >= 3; } } + get hasErrorParam() { + return this.type === HandlerType.ERR_MIDDLEWARE || this.type === HandlerType.RAW_ERR_FN; + } + get parameters(): ParamMetadata[] { if (this.injectable) { return this.getParams(); @@ -101,4 +110,8 @@ export class HandlerMetadata { public hasParamType(paramType: any): boolean { return this.getParams().findIndex((p) => p.paramType === paramType) > -1; } + + public isFinal() { + return this.routeOptions?.isFinal || false; + } } diff --git a/packages/common/src/platform-exceptions/components/ErrorFilter.ts b/packages/common/src/platform-exceptions/components/ErrorFilter.ts index bbe997fc204..89d2412c671 100644 --- a/packages/common/src/platform-exceptions/components/ErrorFilter.ts +++ b/packages/common/src/platform-exceptions/components/ErrorFilter.ts @@ -6,26 +6,30 @@ import type {ExceptionFilterMethods} from "../interfaces/ExceptionFilterMethods" @Catch(Error) export class ErrorFilter implements ExceptionFilterMethods { - catch(error: unknown, ctx: PlatformContext): void { + catch(error: any, ctx: PlatformContext): void { const {response, logger, env} = ctx; - const err = this.mapError(error); + const err = this.mapError(error, env); logger.error({ - error: err + error: {...err, stack: error.stack} }); response + .onEnd(() => { + env === "development" && ctx.injector.logger.error(error); + }) .setHeaders(this.getHeaders(error)) .status(err.status) .body(env === Env.PROD ? "InternalServerError" : err); } - mapError(error: any) { + mapError(error: any, env?: Env) { return { name: error.origin?.name || error.name, message: error.message, status: error.status || 500, - errors: this.getErrors(error) + errors: this.getErrors(error), + stack: env === Env.DEV ? error.stack : undefined }; } diff --git a/packages/common/src/platform-exceptions/components/ExceptionFilter.ts b/packages/common/src/platform-exceptions/components/ExceptionFilter.ts index 78a10e769dc..02d9ea4a322 100644 --- a/packages/common/src/platform-exceptions/components/ExceptionFilter.ts +++ b/packages/common/src/platform-exceptions/components/ExceptionFilter.ts @@ -6,11 +6,12 @@ import {ErrorFilter} from "./ErrorFilter"; @Catch(Exception) export class ExceptionFilter extends ErrorFilter { catch(error: Exception, ctx: PlatformContext) { - const {response, logger} = ctx; - const err = this.mapError(error); + const {response, logger, env} = ctx; + const err = this.mapError(error, env); logger.error({ - error: err + error: err, + stack: error.stack }); response.setHeaders(this.getHeaders(error)).status(error.status).body(err); diff --git a/packages/common/src/platform-exceptions/middlewares/PlatformExceptionsMiddleware.spec.ts b/packages/common/src/platform-exceptions/middlewares/PlatformExceptionsMiddleware.spec.ts index 141187374cf..f958a32b2d8 100644 --- a/packages/common/src/platform-exceptions/middlewares/PlatformExceptionsMiddleware.spec.ts +++ b/packages/common/src/platform-exceptions/middlewares/PlatformExceptionsMiddleware.spec.ts @@ -58,7 +58,8 @@ describe("PlatformExceptionsMiddleware", () => { ], message: "Bad request on ID, innerException: wrong ID", name: "VALIDATION_ERROR", - status: 400 + status: 400, + stack: undefined }); }); it("should map error", () => { @@ -80,7 +81,8 @@ describe("PlatformExceptionsMiddleware", () => { errors: [], message: "My message", name: "Error", - status: 500 + status: 500, + stack: undefined }); }); }); diff --git a/packages/common/src/platform/builder/PlatformControllerBuilder.spec.ts b/packages/common/src/platform/builder/PlatformControllerBuilder.spec.ts index a0cae8a7c02..baa2d116bb6 100644 --- a/packages/common/src/platform/builder/PlatformControllerBuilder.spec.ts +++ b/packages/common/src/platform/builder/PlatformControllerBuilder.spec.ts @@ -1,11 +1,10 @@ -import {All, ControllerProvider, Get, PlatformResponseMiddleware, Use} from "@tsed/common"; +import {All, ControllerProvider, Get, Use} from "@tsed/common"; import {InjectorService} from "@tsed/di"; import {OperationMethods} from "@tsed/schema"; import {expect} from "chai"; import * as Sinon from "sinon"; import {stub} from "../../../../../test/helper/tools"; import {EndpointMetadata} from "../../mvc/models/EndpointMetadata"; -import {PlatformHeadersMiddleware} from "../middlewares/PlatformHeadersMiddleware"; import {Platform} from "../services/Platform"; import {PlatformApplication} from "../services/PlatformApplication"; import {PlatformHandler} from "../services/PlatformHandler"; @@ -84,6 +83,7 @@ describe("PlatformControllerBuilder", () => { beforeEach(() => { // @ts-ignore sandbox.stub(PlatformRouter, "createRawRouter"); + // @ts-ignore sandbox.stub(PlatformRouter.prototype, "mapHandlers").callsFake((o) => o); sandbox.stub(EndpointMetadata, "getEndpoints"); }); @@ -115,9 +115,7 @@ describe("PlatformControllerBuilder", () => { endpoint.beforeMiddlewares[0], endpoint.middlewares[0], endpoint, - PlatformHeadersMiddleware, - endpoint.afterMiddlewares[0], - PlatformResponseMiddleware + endpoint.afterMiddlewares[0] ); }); @@ -142,7 +140,6 @@ describe("PlatformControllerBuilder", () => { endpoint.beforeMiddlewares[0], endpoint.middlewares[0], endpoint, - Sinon.match.func, endpoint.afterMiddlewares[0] ); @@ -167,7 +164,6 @@ describe("PlatformControllerBuilder", () => { endpoint.beforeMiddlewares[0], endpoint.middlewares[0], endpoint, - Sinon.match.func, endpoint.afterMiddlewares[0] ); @@ -195,13 +191,7 @@ describe("PlatformControllerBuilder", () => { // THEN expect(result).to.be.instanceof(PlatformRouter); // ENDPOINT - expect(router.use.getCall(0)).to.have.been.calledWithExactly("/", Sinon.match.func, endpointAll, PlatformHeadersMiddleware); - expect(router.use.getCall(1)).to.have.been.calledWithExactly( - "/", - Sinon.match.func, - endpoint, - Sinon.match.func, - PlatformResponseMiddleware - ); + expect(router.use.getCall(0)).to.have.been.calledWithExactly("/", Sinon.match.func, endpointAll); + expect(router.use.getCall(1)).to.have.been.calledWithExactly("/", Sinon.match.func, endpoint); }); }); diff --git a/packages/common/src/platform/builder/PlatformControllerBuilder.ts b/packages/common/src/platform/builder/PlatformControllerBuilder.ts index 60958ce0506..74f707de785 100644 --- a/packages/common/src/platform/builder/PlatformControllerBuilder.ts +++ b/packages/common/src/platform/builder/PlatformControllerBuilder.ts @@ -1,4 +1,3 @@ -import {PlatformHeadersMiddleware} from "../middlewares/PlatformHeadersMiddleware"; import {Type} from "@tsed/core"; import {InjectorService} from "@tsed/di"; import {JsonMethodPath, OperationMethods} from "@tsed/schema"; @@ -6,8 +5,8 @@ import {EndpointMetadata} from "../../mvc/models/EndpointMetadata"; import {ControllerProvider} from "../domain/ControllerProvider"; import {PlatformRouterMethods} from "../interfaces/PlatformRouterMethods"; import {bindEndpointMiddleware} from "../middlewares/bindEndpointMiddleware"; -import {PlatformResponseMiddleware} from "../middlewares/PlatformResponseMiddleware"; import {PlatformRouter} from "../services/PlatformRouter"; +import {useCtxHandler} from "../utils/useCtxHandler"; function formatMethod(method: string | undefined) { return (method === OperationMethods.CUSTOM ? "use" : method || "use").toLowerCase(); @@ -26,14 +25,15 @@ export class PlatformControllerBuilder { middlewares: {useBefore, useAfter} } = this.provider; - this.provider.router = PlatformRouter.create(injector, routerOptions); + this.provider.setRouter(PlatformRouter.create(injector, routerOptions)); + // Controller lifecycle this.buildMiddlewares(useBefore) // Controller before-middleware .buildEndpoints() // All endpoints and his middlewares .buildMiddlewares(useAfter) // Controller after-middleware .buildChildrenCtrls(injector); // Children controllers - return this.provider.router; + return this.provider.getRouter(); } private buildEndpoints() { @@ -74,26 +74,30 @@ export class PlatformControllerBuilder { private buildEndpoint(endpoint: EndpointMetadata) { const {beforeMiddlewares, middlewares: mldwrs, afterMiddlewares, operation} = endpoint; const { - router, middlewares: {use} } = this.provider; + + const router = this.provider.getRouter(); // Endpoint lifecycle let handlers: any[] = []; handlers = handlers - .concat(bindEndpointMiddleware(endpoint)) + .concat(useCtxHandler(bindEndpointMiddleware(endpoint))) .concat(use) // Controller use-middlewares .concat(beforeMiddlewares) // Endpoint before-middlewares .concat(mldwrs) // Endpoint middlewares .concat(endpoint) // Endpoint metadata - .concat(PlatformHeadersMiddleware) .concat(afterMiddlewares) // Endpoint after-middlewares .filter((item: any) => !!item); // Add handlers to the router operation?.operationPaths.forEach(({path, method, isFinal}) => { - const localHandlers = isFinal ? handlers.concat(PlatformResponseMiddleware) : handlers; - router.addRoute({method: formatMethod(method), path, handlers: localHandlers}); + router.addRoute({ + method: formatMethod(method), + path, + handlers, + isFinal + }); }); if (!operation?.operationPaths.size) { @@ -102,7 +106,8 @@ export class PlatformControllerBuilder { } private buildChildrenCtrls(injector: InjectorService) { - const {children, router} = this.provider; + const {children} = this.provider; + const router = this.provider.getRouter(); children.forEach((child: Type) => { const provider = injector.getProvider(child) as ControllerProvider; @@ -114,12 +119,12 @@ export class PlatformControllerBuilder { new PlatformControllerBuilder(provider).build(injector); - router.use(provider.path, provider.router); + router.use(provider.path, provider.getRouter()); }); } private buildMiddlewares(middlewares: any[]) { - const {router} = this.provider; + const router = this.provider.getRouter(); middlewares .filter((o) => typeof o === "function") diff --git a/packages/common/src/platform/domain/ControllerProvider.ts b/packages/common/src/platform/domain/ControllerProvider.ts index b9b88ddbc26..b0e6f68592a 100644 --- a/packages/common/src/platform/domain/ControllerProvider.ts +++ b/packages/common/src/platform/domain/ControllerProvider.ts @@ -1,19 +1,18 @@ import {Enumerable, NotEnumerable, Type} from "@tsed/core"; import {Provider, ProviderType} from "@tsed/di"; import {JsonEntityStore} from "@tsed/schema"; -import {EndpointMetadata, ControllerMiddlewares} from "../../mvc"; +import {ControllerMiddlewares, EndpointMetadata} from "../../mvc"; import {PlatformRouterMethods} from "../interfaces/PlatformRouterMethods"; export interface IChildrenController extends Type { $parentCtrl?: ControllerProvider; } -export class ControllerProvider extends Provider { - @NotEnumerable() - public router: PlatformRouterMethods; - +export class ControllerProvider extends Provider { @NotEnumerable() readonly entity: JsonEntityStore; + @NotEnumerable() + private router: PlatformRouterMethods; /** * Controllers that depend to this controller. * @type {Array} @@ -145,4 +144,14 @@ export class ControllerProvider extends Provider { public hasParent(): boolean { return !!this.provide.$parentCtrl; } + + public getRouter(): T { + return this.router as any; + } + + public setRouter(router: PlatformRouterMethods) { + this.router = router; + + return this; + } } diff --git a/packages/common/src/platform/domain/HandlerContext.spec.ts b/packages/common/src/platform/domain/HandlerContext.spec.ts index 82766a60dfd..a397bb03059 100644 --- a/packages/common/src/platform/domain/HandlerContext.spec.ts +++ b/packages/common/src/platform/domain/HandlerContext.spec.ts @@ -70,7 +70,7 @@ async function getHandlerContext({token, propertyKey, args}: any = {}) { target: token, token, propertyKey, - type: HandlerType.CONTROLLER + type: HandlerType.ENDPOINT }); const $ctx = createFakePlatformContext(sandbox); diff --git a/packages/common/src/platform/errors/TemplateRenderError.spec.ts b/packages/common/src/platform/errors/TemplateRenderError.spec.ts new file mode 100644 index 00000000000..4e85dcf44ed --- /dev/null +++ b/packages/common/src/platform/errors/TemplateRenderError.spec.ts @@ -0,0 +1,10 @@ +import {expect} from "chai"; +import {TemplateRenderError} from "./TemplateRenderError"; + +describe("TemplateRenderingError", () => { + it("should have a message", () => { + const errorInstance = new TemplateRenderError(class Target {}, "method", new Error("test")); + expect(errorInstance.message).to.equal("Template rendering error: Target.method()\nError: test"); + expect(errorInstance.name).to.equal("TEMPLATE_RENDER_ERROR"); + }); +}); diff --git a/packages/common/src/mvc/errors/TemplateRenderingError.ts b/packages/common/src/platform/errors/TemplateRenderError.ts similarity index 71% rename from packages/common/src/mvc/errors/TemplateRenderingError.ts rename to packages/common/src/platform/errors/TemplateRenderError.ts index 96056dfa1ea..bb5ef43bff1 100644 --- a/packages/common/src/mvc/errors/TemplateRenderingError.ts +++ b/packages/common/src/platform/errors/TemplateRenderError.ts @@ -4,11 +4,11 @@ import {InternalServerError} from "@tsed/exceptions"; /** * @private */ -export class TemplateRenderingError extends InternalServerError { - name = "TEMPLATING_RENDER_ERROR"; +export class TemplateRenderError extends InternalServerError { + name = "TEMPLATE_RENDER_ERROR"; constructor(target: Type | string, method: string | symbol, err: Error) { - super(TemplateRenderingError.buildMessage(target, method, err)); + super(TemplateRenderError.buildMessage(target, method, err)); } /** diff --git a/packages/common/src/platform/index.ts b/packages/common/src/platform/index.ts index 30baaf82433..bc802fa5199 100644 --- a/packages/common/src/platform/index.ts +++ b/packages/common/src/platform/index.ts @@ -13,8 +13,6 @@ export * from "./interfaces/IRoute"; export * from "./interfaces/PlatformRouterMethods"; // middlewares -export * from "./middlewares/PlatformResponseMiddleware"; -export * from "./middlewares/PlatformHeadersMiddleware"; export * from "./middlewares/PlatformLogMiddleware"; export * from "./middlewares/PlatformMulterMiddleware"; export * from "./middlewares/bindEndpointMiddleware"; @@ -29,6 +27,7 @@ export * from "./domain/ControllerProvider"; // errors export * from "./errors/ParamValidationError"; +export * from "./errors/TemplateRenderError"; // providers export * from "./services/Platform"; @@ -41,3 +40,4 @@ export * from "./services/PlatformRequest"; // registries export * from "./registries/ControllerRegistry"; export * from "./utils/createContext"; +export * from "./utils/useCtxHandler"; diff --git a/packages/common/src/platform/interfaces/PlatformRouterMethods.ts b/packages/common/src/platform/interfaces/PlatformRouterMethods.ts index ed01e56dcd0..40b8dc9afae 100644 --- a/packages/common/src/platform/interfaces/PlatformRouterMethods.ts +++ b/packages/common/src/platform/interfaces/PlatformRouterMethods.ts @@ -4,8 +4,11 @@ export interface PlatformRouteOptions { method: string; path: PathParamsType; handlers: any[]; + isFinal?: boolean; } +export type PlatformRouteWithoutHandlers = Partial>; + export interface PlatformRouterMethods { addRoute(routeOptions: PlatformRouteOptions): this; diff --git a/packages/common/src/platform/middlewares/PlatformHeadersMiddleware.ts b/packages/common/src/platform/middlewares/PlatformHeadersMiddleware.ts deleted file mode 100644 index a7dd9896757..00000000000 --- a/packages/common/src/platform/middlewares/PlatformHeadersMiddleware.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {isObject} from "@tsed/core"; -import {Injectable, InjectorService} from "@tsed/di"; -import {Middleware} from "../../mvc/decorators/class/middleware"; -import {Next} from "../../mvc/decorators/params/next"; -import {IMiddleware} from "../../mvc/interfaces/IMiddleware"; -import {Context} from "../decorators/context"; - -function toHeaders(headers: {[key: string]: any}) { - return Object.entries(headers).reduce((headers, [key, item]) => { - return { - ...headers, - [key]: String(item.example) - }; - }, {}); -} - -/** - * Add all headers, contentType, location, redirect and statusCode resolved for the current executed endpoint. - * - * @platform - * @middleware - */ -@Middleware() -export class PlatformHeadersMiddleware implements IMiddleware { - @Injectable() - injector: InjectorService; - - use(@Context() ctx: Context, @Next() next: Next) { - const {endpoint, response} = ctx; - const {operation} = endpoint; - - if (response.statusCode === 200) { - // apply status only if the isn't already modified - response.status(operation.getStatus()); - } - - const headers = operation.getHeadersOf(response.statusCode); - response.setHeaders(toHeaders(headers)); - - if (endpoint.redirect) { - response.redirect(endpoint.redirect.status || 302, endpoint.redirect.url); - } - - if (endpoint.location) { - response.location(endpoint.location); - } - - next(); - } -} diff --git a/packages/common/src/platform/middlewares/PlatformResponseMiddleware.spec.ts b/packages/common/src/platform/middlewares/PlatformResponseMiddleware.spec.ts deleted file mode 100644 index bb4a00aeb02..00000000000 --- a/packages/common/src/platform/middlewares/PlatformResponseMiddleware.spec.ts +++ /dev/null @@ -1,318 +0,0 @@ -import {EndpointMetadata, Get, PlatformResponse, PlatformResponseMiddleware, PlatformTest, View} from "@tsed/common"; -import {Ignore, Property, Returns} from "@tsed/schema"; -import {expect} from "chai"; -import {createReadStream} from "fs"; -import {join} from "path"; -import * as Sinon from "sinon"; -import {FakeResponse} from "../../../../../test/helper"; -import {ABORT} from "../constants/abort"; - -const sandbox = Sinon.createSandbox(); - -function createContext(data: any, model?: any) { - class Test { - @Get("/") - @Returns(200, model) - test() {} - } - - const response: any = new FakeResponse(sandbox); - const ctx = PlatformTest.createRequestContext(); - - ctx.data = data; - ctx.endpoint = EndpointMetadata.get(Test, "test"); - ctx.response = new PlatformResponse(response); - - return ctx; -} - -describe("PlatformResponseMiddleware", () => { - beforeEach(() => PlatformTest.create()); - afterEach(() => PlatformTest.reset()); - - it("should render content", async () => { - class Model { - @Property() - data: string; - - @Ignore() - test: string; - } - - class Test { - @Get("/") - @View("view", {options: "options"}) - @Returns(200, Model) - test() {} - } - - const response: any = new FakeResponse(sandbox); - const ctx = PlatformTest.createRequestContext(); - - ctx.endpoint = EndpointMetadata.get(Test, "test"); - ctx.response = new PlatformResponse(response); - - sandbox.stub(ctx.response, "render").resolves("HTML"); - - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - ctx.data = {data: "data"}; - - await middleware.use(ctx); - expect(ctx.response.render).to.have.been.calledWithExactly("view", { - data: "data", - options: "options" - }); - expect(ctx.response.raw.send).to.have.been.calledWithExactly("HTML"); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - it("should render content with the appropriate content type", async () => { - class Model { - @Property() - data: string; - - @Ignore() - test: string; - } - - class Test { - @Get("/") - @View("view", {options: "options"}) - @(Returns(200).ContentType("text/html")) - test() {} - } - - const response: any = new FakeResponse(sandbox); - const ctx = PlatformTest.createRequestContext(); - - ctx.endpoint = EndpointMetadata.get(Test, "test"); - ctx.response = new PlatformResponse(response); - - sandbox.stub(ctx.response, "render").resolves("HTML"); - - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - ctx.data = {data: "data"}; - - await middleware.use(ctx); - expect(ctx.response.render).to.have.been.calledWithExactly("view", { - data: "data", - options: "options" - }); - expect(ctx.response.raw.send).to.have.been.calledWithExactly("HTML"); - expect(ctx.response.raw.contentType).to.have.been.calledWithExactly("text/html"); - }); - it("should throw an error", async () => { - class Test { - @Get("/") - @View("view", {options: "options"}) - test() {} - } - - const response: any = new FakeResponse(sandbox); - const ctx = PlatformTest.createRequestContext(); - - ctx.endpoint = EndpointMetadata.get(Test, "test"); - ctx.response = new PlatformResponse(response); - sandbox.stub(ctx.response, "render").callsFake(() => { - throw new Error("parser error"); - }); - - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - ctx.data = {data: "data"}; - - let actualError: any; - try { - await middleware.use(ctx); - } catch (er) { - actualError = er; - } - - expect(actualError.message).to.equal("Template rendering error: Test.test()\nError: parser error"); - }); - - describe("when value is a stream", () => { - it("should send the stream", async () => { - // GIVEN - const data = createReadStream(join(__dirname, "__mock__/response.data.json")); - sandbox.stub(data, "pipe"); - - const ctx = createContext(data); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(data.pipe).to.have.been.calledWithExactly(ctx.response.raw); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); - describe("when value is null", () => { - it("should send empty response", async () => { - // GIVEN - const ctx = createContext(null); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.send).to.have.been.calledWithExactly(ctx.data); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); - - describe("when value is false", () => { - it("should send response", async () => { - // GIVEN - const ctx = createContext(false); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.send).to.have.been.calledWithExactly(ctx.data); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); - - describe("when value is true", () => { - it("should send response", async () => { - // GIVEN - const ctx = createContext(true); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.send).to.have.been.calledWithExactly(ctx.data); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); - - describe("when value is an empty string", () => { - it("should send response", async () => { - // GIVEN - const ctx = createContext(""); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.send).to.have.been.calledWithExactly(ctx.data); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); - - describe("when value is string", () => { - it("should send response", async () => { - // GIVEN - const ctx = createContext("test"); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.send).to.have.been.calledWithExactly(ctx.data); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); - - describe("when value is a number", () => { - it("should send response", async () => { - // GIVEN - const ctx = createContext(1); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.send).to.have.been.calledWithExactly(ctx.data); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); - - describe("when value is an object", () => { - it("should send response", async () => { - // GIVEN - const ctx = createContext({data: "data"}); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.json).to.have.been.calledWithExactly(ctx.data); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); - - describe("when value is an object and endpoint type is used", () => { - it("should send response", async () => { - // GIVEN - class Test { - @Property() - data: string; - - @Ignore() - test: string; - } - - const ctx = createContext({data: "data", test: "test"}, Test); - - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.json).to.have.been.calledWithExactly({data: "data"}); - expect(ctx.response.raw.contentType).to.have.been.calledWithExactly("application/json"); - }); - }); - - describe("when value is an array", () => { - it("should send response", async () => { - // GIVEN - const ctx = createContext([{data: "data"}]); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.json).to.have.been.calledWithExactly(ctx.data); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); - - describe("when value is a date", () => { - it("should send response", async () => { - // GIVEN - const ctx = createContext(new Date("2019-01-01")); - const middleware = await PlatformTest.invoke(PlatformResponseMiddleware); - - // WHEN - const result = await middleware.use(ctx); - - // THEN - expect(result).to.eq(ABORT); - expect(ctx.response.raw.send).to.have.been.calledWithExactly("2019-01-01T00:00:00.000Z"); - expect(ctx.response.raw.contentType).not.to.have.been.called; - }); - }); -}); diff --git a/packages/common/src/platform/middlewares/PlatformResponseMiddleware.ts b/packages/common/src/platform/middlewares/PlatformResponseMiddleware.ts deleted file mode 100644 index 610cffa5499..00000000000 --- a/packages/common/src/platform/middlewares/PlatformResponseMiddleware.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {isBoolean, isNumber, isObject, isStream, isString} from "@tsed/core"; -import {Inject} from "@tsed/di"; -import {Middleware} from "../../mvc/decorators/class/middleware"; -import {TemplateRenderingError} from "../../mvc/errors/TemplateRenderingError"; -import {IMiddleware} from "../../mvc/interfaces/IMiddleware"; -import {ConverterService} from "../../mvc/services/ConverterService"; -import {ABORT} from "../constants/abort"; - -import {Context} from "../decorators/context"; - -/** - * Transform data to a response - * - * @platform - * @middleware - */ -@Middleware() -export class PlatformResponseMiddleware implements IMiddleware { - @Inject() - mapper: ConverterService; - - public async use(@Context() ctx: Context) { - const {response, endpoint} = ctx; - let {data} = ctx; - - if (endpoint.view) { - data = await this.render(ctx); - } else if (this.shouldSerialize(data)) { - data = this.mapper.serialize(data, {type: endpoint.type}); - } - - this.setContentType(data, ctx); - response.body(data); - - return ABORT; - } - - protected setContentType(data: any, ctx: Context) { - const {endpoint, response} = ctx; - const {operation} = endpoint; - - const contentType = operation.getContentTypeOf(response.statusCode) || ""; - - if (contentType && contentType !== "*/*") { - if (contentType === "application/json") { - if (isObject(data)) { - response.contentType(contentType); - } - } else { - response.contentType(contentType); - } - } - } - - protected async render(@Context() ctx: Context) { - const {response, endpoint} = ctx; - try { - const {data} = ctx; - const {path, options} = endpoint.view; - - return await response.render(path, {...options, ...data}); - } catch (err) { - throw new TemplateRenderingError(endpoint.targetName, endpoint.propertyKey, err); - } - } - - protected shouldSerialize(data: any) { - return !(this.shouldBeStreamed(data) || this.shouldBeSent(data) || data === undefined); - } - - protected shouldBeSent(data: any) { - return Buffer.isBuffer(data) || isBoolean(data) || isNumber(data) || isString(data) || data === null; - } - - protected shouldBeStreamed(data: any) { - return isStream(data); - } -} diff --git a/packages/common/src/platform/registries/ControllerRegistry.ts b/packages/common/src/platform/registries/ControllerRegistry.ts index 91e61fe332f..a0cc9110627 100644 --- a/packages/common/src/platform/registries/ControllerRegistry.ts +++ b/packages/common/src/platform/registries/ControllerRegistry.ts @@ -7,6 +7,6 @@ export const ControllerRegistry: TypedProvidersRegistry = GlobalProviders.create injectable: false, onInvoke(provider: ControllerProvider, locals: any) { - locals.set(PlatformRouter, provider.router); + locals.set(PlatformRouter, provider.getRouter()); } }); diff --git a/packages/common/src/platform/services/Platform.ts b/packages/common/src/platform/services/Platform.ts index 4bdeb07c835..03b4560bca5 100644 --- a/packages/common/src/platform/services/Platform.ts +++ b/packages/common/src/platform/services/Platform.ts @@ -84,7 +84,7 @@ export class Platform { route, provider }); - this.app.use(route, ...[].concat(provider.router.callback())); + this.app.use(route, ...[].concat(provider.getRouter().callback())); } } } diff --git a/packages/common/src/platform/services/PlatformApplication.spec.ts b/packages/common/src/platform/services/PlatformApplication.spec.ts index e6658d69e52..5d9649fe34f 100644 --- a/packages/common/src/platform/services/PlatformApplication.spec.ts +++ b/packages/common/src/platform/services/PlatformApplication.spec.ts @@ -105,7 +105,7 @@ describe("PlatformApplication", () => { platformApp.use("/", handler); // THEN - expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler); + expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler, {isFinal: false}); expect(platformApp.rawRouter.use).to.have.been.calledWithExactly("/", handler); }); it("should add router to app", async () => { @@ -134,7 +134,11 @@ describe("PlatformApplication", () => { platformApp.get("/", handler); // THEN - expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler); + expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler, { + isFinal: true, + method: "get", + path: "/" + }); expect(platformApp.rawRouter.get).to.have.been.calledWithExactly("/", handler); }); }); @@ -148,7 +152,11 @@ describe("PlatformApplication", () => { platformApp.all("/", handler); // THEN - expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler); + expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler, { + isFinal: true, + method: "all", + path: "/" + }); expect(platformApp.rawRouter.all).to.have.been.calledWithExactly("/", handler); }); }); @@ -162,7 +170,7 @@ describe("PlatformApplication", () => { platformApp.post("/", handler); // THEN - expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler); + expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler, {isFinal: true, method: "post", path: "/"}); expect(platformApp.rawRouter.post).to.have.been.calledWithExactly("/", handler); }); }); @@ -176,7 +184,7 @@ describe("PlatformApplication", () => { platformApp.put("/", handler); // THEN - expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler); + expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler, {isFinal: true, method: "put", path: "/"}); expect(platformApp.rawRouter.put).to.have.been.calledWithExactly("/", handler); }); }); @@ -190,7 +198,7 @@ describe("PlatformApplication", () => { platformApp.patch("/", handler); // THEN - expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler); + expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler, {isFinal: true, method: "patch", path: "/"}); expect(platformApp.rawRouter.patch).to.have.been.calledWithExactly("/", handler); }); }); @@ -204,7 +212,7 @@ describe("PlatformApplication", () => { platformApp.head("/", handler); // THEN - expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler); + expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler, {isFinal: true, method: "head", path: "/"}); expect(platformApp.rawRouter.head).to.have.been.calledWithExactly("/", handler); }); }); @@ -218,7 +226,7 @@ describe("PlatformApplication", () => { platformApp.delete("/", handler); // THEN - expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler); + expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler, {isFinal: true, method: "delete", path: "/"}); expect(platformApp.rawRouter.delete).to.have.been.calledWithExactly("/", handler); }); }); @@ -232,7 +240,7 @@ describe("PlatformApplication", () => { platformApp.options("/", handler); // THEN - expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler); + expect(platformHandler.createHandler).to.have.been.calledWithExactly(handler, {isFinal: true, method: "options", path: "/"}); expect(platformApp.rawRouter.options).to.have.been.calledWithExactly("/", handler); }); }); diff --git a/packages/common/src/platform/services/PlatformHandler.spec.ts b/packages/common/src/platform/services/PlatformHandler.spec.ts index 6deb1e53185..1b2e12d4c25 100644 --- a/packages/common/src/platform/services/PlatformHandler.spec.ts +++ b/packages/common/src/platform/services/PlatformHandler.spec.ts @@ -7,7 +7,8 @@ import { HandlerType, ParamTypes, PlatformTest, - QueryParams + QueryParams, + useCtxHandler } from "@tsed/common"; import {Provider} from "@tsed/di"; import {expect} from "chai"; @@ -49,54 +50,6 @@ describe("PlatformHandler", () => { sandbox.restore(); }); - describe("createHandlerMetadata", () => { - it("should return metadata from Endpoint", async () => { - // GIVEN - - const endpoint = new EndpointMetadata({ - target: Test, - propertyKey: "get" - }); - - const platformHandler = await PlatformTest.invoke(PlatformHandler); - sandbox.stub(PlatformTest.injector, "getProvider").returns(new Provider(Test)); - - // WHEN - const handlerMetadata = platformHandler.createHandlerMetadata(endpoint); - - // THEN - expect(handlerMetadata.target).to.eq(Test); - expect(handlerMetadata.propertyKey).to.eq("get"); - expect(handlerMetadata.type).to.eq(HandlerType.CONTROLLER); - }); - - it("should return metadata from Middleware", async () => { - // GIVEN - const platformHandler = await PlatformTest.invoke(PlatformHandler); - sandbox.stub(PlatformTest.injector, "getProvider").returns(new Provider(Test)); - - // WHEN - const handlerMetadata = platformHandler.createHandlerMetadata(Test); - - // THEN - expect(handlerMetadata.target).to.eq(Test); - expect(handlerMetadata.propertyKey).to.eq("use"); - expect(handlerMetadata.type).to.eq(HandlerType.MIDDLEWARE); - }); - - it("should return metadata from Function", async () => { - const platformHandler = await PlatformTest.invoke(PlatformHandler); - - // GIVEN - sandbox.stub(PlatformTest.injector, "getProvider").returns(undefined); - - // WHEN - const handlerMetadata = platformHandler.createHandlerMetadata(() => {}); - - // THEN - expect(handlerMetadata.type).to.eq(HandlerType.FUNCTION); - }); - }); describe("createHandler", () => { it("should return a native handler (success middleware)", async () => { // GIVEN @@ -107,7 +60,7 @@ describe("PlatformHandler", () => { const handlerMetadata = new HandlerMetadata({ token: Test, target: Test, - type: HandlerType.CONTROLLER, + type: HandlerType.ENDPOINT, propertyKey: "get" }); @@ -132,7 +85,6 @@ describe("PlatformHandler", () => { // THEN expect(nativeHandler).to.eq(handler); }); - it("should do nothing when request is aborted", async () => { // GIVEN const platformHandler = await PlatformTest.invoke(PlatformHandler); @@ -146,7 +98,7 @@ describe("PlatformHandler", () => { const handlerMetadata = new HandlerMetadata({ token: Test, target: Test, - type: HandlerType.CONTROLLER, + type: HandlerType.ENDPOINT, propertyKey: "get" }); diff --git a/packages/common/src/platform/services/PlatformHandler.ts b/packages/common/src/platform/services/PlatformHandler.ts index 3ada65e24fe..d494d47575c 100644 --- a/packages/common/src/platform/services/PlatformHandler.ts +++ b/packages/common/src/platform/services/PlatformHandler.ts @@ -1,19 +1,32 @@ -import {isFunction, isStream, Type} from "@tsed/core"; +import {isBoolean, isFunction, isNumber, isStream, isString, Type} from "@tsed/core"; import {Injectable, InjectorService, ProviderScope} from "@tsed/di"; -import {EndpointMetadata, HandlerConstructorOptions, HandlerMetadata, HandlerType, IPipe, ParamMetadata, ParamTypes} from "../../mvc"; +import {ConverterService, EndpointMetadata, HandlerMetadata, HandlerType, IPipe, ParamMetadata, ParamTypes} from "../../mvc"; import {HandlerContext, HandlerContextStatus} from "../domain/HandlerContext"; import {PlatformContext} from "../domain/PlatformContext"; import {ParamValidationError} from "../errors/ParamValidationError"; +import {PlatformRouteWithoutHandlers} from "../interfaces/PlatformRouterMethods"; +import {createHandlerMetadata} from "../utils/createHandlerMetadata"; +import {renderView} from "../utils/renderView"; +import {setResponseContentType} from "../utils/setResponseContentType"; +import {setResponseHeaders} from "../utils/setResponseHeaders"; export interface OnRequestOptions { $ctx: PlatformContext; - next: any; metadata: HandlerMetadata; + next?: any; err?: any; [key: string]: any; } +function shouldBeSerialized(data: any) { + return !(isStream(data) || shouldBeSent(data) || data === undefined); +} + +function shouldBeSent(data: any) { + return Buffer.isBuffer(data) || isBoolean(data) || isNumber(data) || isString(data) || data === null; +} + /** * Platform Handler abstraction layer. Wrap original class method to a pure platform handler (Express, Koa, etc...). * @platform @@ -26,16 +39,13 @@ export class PlatformHandler { /** * Create a native middleware based on the given metadata and return an instance of HandlerContext - * @param metadata + * @param input + * @param options */ - createHandler(metadata: HandlerMetadata | any) { - if (!(metadata instanceof HandlerMetadata)) { - metadata = this.createHandlerMetadata(metadata); - } + createHandler(input: EndpointMetadata | HandlerMetadata | any, options: PlatformRouteWithoutHandlers = {}) { + const metadata: HandlerMetadata = this.createHandlerMetadata(input, options); - if ([HandlerType.CONTROLLER, HandlerType.MIDDLEWARE].includes(metadata.type)) { - this.sortPipes(metadata); - } + this.sortPipes(metadata); return this.createRawHandler(metadata); } @@ -43,39 +53,10 @@ export class PlatformHandler { /** * Create handler metadata * @param obj + * @param routeOptions */ - public createHandlerMetadata(obj: any | EndpointMetadata) { - const {injector} = this; - let options: HandlerConstructorOptions; - - if (obj instanceof EndpointMetadata) { - const provider = injector.getProvider(obj.token)!; - - options = { - token: provider.token, - target: provider.useClass, - type: HandlerType.CONTROLLER, - propertyKey: obj.propertyKey - }; - } else { - const provider = injector.getProvider(obj); - - if (provider) { - options = { - token: provider.token, - target: provider.useClass, - type: HandlerType.MIDDLEWARE, - propertyKey: "use" - }; - } else { - options = { - target: obj, - type: HandlerType.FUNCTION - }; - } - } - - return new HandlerMetadata(options); + public createHandlerMetadata(obj: any | EndpointMetadata, routeOptions: PlatformRouteWithoutHandlers = {}): HandlerMetadata { + return createHandlerMetadata(this.injector, obj, routeOptions); } /** @@ -142,34 +123,117 @@ export class PlatformHandler { /** * Call handler when a request his handle - * @param h - * @param next + * @param requestOptions */ - protected async onRequest({metadata, $ctx, next, err}: OnRequestOptions): Promise { - const h = new HandlerContext({ - $ctx, - metadata, - args: [], - err - }); + protected async onRequest(requestOptions: OnRequestOptions): Promise { + const {metadata, $ctx, err} = requestOptions; try { - h.args = await this.getArgs(h); + const h = new HandlerContext({ + $ctx, + metadata, + args: [], + err + }); - const data = await h.callHandler(); + h.args = await this.getArgs(h); - if (h.status === HandlerContextStatus.RESOLVED && isFunction(data) && !isStream(data)) { - data($ctx.getRequest(), $ctx.getResponse(), next); + await h.callHandler(); - return; + if (h.status === HandlerContextStatus.RESOLVED) { + // Can be canceled by the handler itself + return await this.onSuccess($ctx.data, requestOptions); } + } catch (er) { + return this.onError(er, requestOptions); + } + } - if (HandlerContextStatus.RESOLVED) { - next(); - } - } catch (error) { - return next(error); + protected onError(er: unknown, requestOptions: OnRequestOptions) { + const {next} = requestOptions; + + if (!next) { + throw er; + } + + return next(er); + } + + /** + * Manage success scenario + * @param data + * @param requestOptions + * @protected + */ + protected async onSuccess(data: any, requestOptions: OnRequestOptions) { + const {metadata, $ctx, next} = requestOptions; + + // set headers each times that an endpoint is called + if (metadata.type === HandlerType.ENDPOINT) { + this.setHeaders($ctx); + } + + // call returned middleware + if (isFunction(data) && !isStream(data)) { + return this.callReturnedMiddleware(data, $ctx, next); + } + + if (metadata.isFinal() && !$ctx.response.isDone()) { + return this.flush(data, $ctx); } + + return !$ctx.response.isDone() && next && next(); + } + + /** + * Call the returned middleware by the handler. + * @param middleware + * @param ctx + * @param next + * @protected + */ + protected callReturnedMiddleware(middleware: any, ctx: PlatformContext, next: any) { + return middleware(ctx.getRequest(), ctx.getResponse(), next); + } + + /** + * Send the response to the consumer. + * @param data + * @param ctx + * @protected + */ + protected async flush(data: any, ctx: PlatformContext) { + const {response, endpoint} = ctx; + + if (endpoint.view) { + data = await this.render(data, ctx); + } else if (shouldBeSerialized(data)) { + data = this.injector.get(ConverterService)!.serialize(data, {type: endpoint.type}); + } + + this.setContentType(data, ctx); + + response.body(data); + } + + /** + * Set the right content type from the current endpoint + * @param data + * @param ctx + * @protected + */ + protected setContentType(data: any, ctx: PlatformContext) { + return setResponseContentType(data, ctx); + } + + /** + * Render the view if the endpoint has a configured view. + * @param data + * @param ctx + * @protected + */ + protected async render(data: any, ctx: PlatformContext) { + return renderView(data, ctx); } /** @@ -177,11 +241,31 @@ export class PlatformHandler { * @param metadata */ protected createRawHandler(metadata: HandlerMetadata): Function { - return (request: any, response: any, next: any) => this.onRequest({metadata, next, $ctx: request.$ctx}); + switch (metadata.type) { + case HandlerType.CUSTOM: + return (ctx: PlatformContext) => this.onRequest({metadata, $ctx: ctx}); + case HandlerType.RAW_ERR_FN: + case HandlerType.RAW_FN: + return metadata.handler; + + default: + case HandlerType.ENDPOINT: + case HandlerType.MIDDLEWARE: + return (request: any, response: any, next: any) => this.onRequest({$ctx: request.$ctx, next, metadata}); + } } /** - * Return arguments to + * Set response headers + * @param ctx + * @protected + */ + protected setHeaders(ctx: PlatformContext) { + return setResponseHeaders(ctx); + } + + /** + * Return arguments to call handler * @param h */ private async getArgs(h: HandlerContext) { @@ -193,10 +277,14 @@ export class PlatformHandler { } /** - * + * Sort pipes before calling it * @param metadata */ private sortPipes(metadata: HandlerMetadata) { + if (!metadata.injectable) { + return; + } + const get = (pipe: Type) => { return this.injector.getProvider(pipe)!.priority || 0; }; @@ -209,7 +297,7 @@ export class PlatformHandler { } /** - * + * Map argument by calling pipe. * @param metadata * @param h */ diff --git a/packages/common/src/platform/services/PlatformResponse.spec.ts b/packages/common/src/platform/services/PlatformResponse.spec.ts index b08cf396fc1..53ccb3fc898 100644 --- a/packages/common/src/platform/services/PlatformResponse.spec.ts +++ b/packages/common/src/platform/services/PlatformResponse.spec.ts @@ -1,5 +1,6 @@ import {PlatformTest} from "@tsed/common"; import {expect} from "chai"; +import {createReadStream} from "fs"; import * as Sinon from "sinon"; import {FakeResponse} from "../../../../../test/helper"; import {PlatformResponse} from "./PlatformResponse"; @@ -85,6 +86,38 @@ describe("PlatformResponse", () => { expect(res.location).to.have.been.calledWithExactly("/path"); }); }); + describe("body()", () => { + it("should call body with undefined", () => { + const {res, response} = createResponse(); + + response.body(undefined); + + expect(res.send).to.have.been.calledWithExactly(); + }); + it("should call body with string", () => { + const {res, response} = createResponse(); + + response.body("string"); + + expect(res.send).to.have.been.calledWithExactly("string"); + }); + it("should call body with stream", () => { + const {res, response} = createResponse(); + const stream = createReadStream(__dirname + "/__mock__/data.txt"); + sandbox.stub(stream, "pipe"); + + response.body(stream); + + expect(stream.pipe).to.have.been.calledWithExactly(res); + }); + it("should call body with {}", () => { + const {res, response} = createResponse(); + + response.body({}); + + expect(res.json).to.have.been.calledWithExactly({}); + }); + }); describe("destroy()", () => { it("should return a string", async () => { const {response} = createResponse(); diff --git a/packages/common/src/platform/services/PlatformResponse.ts b/packages/common/src/platform/services/PlatformResponse.ts index 650832aeeff..b9cdede3228 100644 --- a/packages/common/src/platform/services/PlatformResponse.ts +++ b/packages/common/src/platform/services/PlatformResponse.ts @@ -152,6 +152,10 @@ export class PlatformResponse { * @param data */ body(data: any) { + if (this.isDone()) { + return; + } + if (data === undefined) { this.raw.send(); @@ -179,8 +183,10 @@ export class PlatformResponse { * Add a listener to handler the end of the request/response. * @param cb */ - onEnd(cb: Function) { + onEnd(cb: Function): this { onFinished(this.raw, cb); + + return this; } isDone() { diff --git a/packages/common/src/platform/services/PlatformRouter.ts b/packages/common/src/platform/services/PlatformRouter.ts index 024ae39d6bd..9649317332c 100644 --- a/packages/common/src/platform/services/PlatformRouter.ts +++ b/packages/common/src/platform/services/PlatformRouter.ts @@ -2,7 +2,7 @@ import {Inject, Injectable, InjectorService, ProviderScope} from "@tsed/di"; import {promisify} from "util"; import {PlatformMulter, PlatformMulterSettings, PlatformStaticsOptions} from "../../config"; import {PathParamsType} from "../../mvc/interfaces/PathParamsType"; -import {PlatformRouteOptions} from "../interfaces/PlatformRouterMethods"; +import {PlatformRouteOptions, PlatformRouteWithoutHandlers} from "../interfaces/PlatformRouterMethods"; import {createFakeRawDriver} from "./FakeRawDriver"; import {PlatformHandler} from "./PlatformHandler"; @@ -64,43 +64,44 @@ export class PlatformRouter { return this; } - addRoute({method, path, handlers}: PlatformRouteOptions) { + addRoute(options: PlatformRouteOptions) { + const {method, path, handlers, isFinal} = options; // @ts-ignore - this.getRouter()[method](path, ...this.mapHandlers(handlers)); + this.getRouter()[method](path, ...this.mapHandlers(handlers, {method, path, isFinal})); return this; } all(path: PathParamsType, ...handlers: any[]) { - return this.addRoute({method: "all", path, handlers}); + return this.addRoute({method: "all", path, handlers, isFinal: true}); } get(path: PathParamsType, ...handlers: any[]) { - return this.addRoute({method: "get", path, handlers}); + return this.addRoute({method: "get", path, handlers, isFinal: true}); } post(path: PathParamsType, ...handlers: any[]) { - return this.addRoute({method: "post", path, handlers}); + return this.addRoute({method: "post", path, handlers, isFinal: true}); } put(path: PathParamsType, ...handlers: any[]) { - return this.addRoute({method: "put", path, handlers}); + return this.addRoute({method: "put", path, handlers, isFinal: true}); } delete(path: PathParamsType, ...handlers: any[]) { - return this.addRoute({method: "delete", path, handlers}); + return this.addRoute({method: "delete", path, handlers, isFinal: true}); } patch(path: PathParamsType, ...handlers: any[]) { - return this.addRoute({method: "patch", path, handlers}); + return this.addRoute({method: "patch", path, handlers, isFinal: true}); } head(path: PathParamsType, ...handlers: any[]) { - return this.addRoute({method: "head", path, handlers}); + return this.addRoute({method: "head", path, handlers, isFinal: true}); } options(path: PathParamsType, ...handlers: any[]) { - return this.addRoute({method: "options", path, handlers}); + return this.addRoute({method: "options", path, handlers, isFinal: true}); } statics(path: string, options: PlatformStaticsOptions) { @@ -132,17 +133,22 @@ export class PlatformRouter { return m; } - mapHandlers(handlers: any[]): any[] { - return handlers.reduce((middlewares, handler) => { + protected mapHandlers(handlers: any[], options: PlatformRouteWithoutHandlers = {}): any[] { + return handlers.reduce((list, handler, index) => { if (typeof handler === "string") { - return middlewares.concat(handler); + return list.concat(handler); } if (handler instanceof PlatformRouter) { - return middlewares.concat(handler.callback()); + return list.concat(handler.callback()); } - return middlewares.concat(this.platformHandler.createHandler(handler)); + return list.concat( + this.platformHandler.createHandler(handler, { + ...options, + isFinal: options.isFinal ? index === handlers.length - 1 : false + }) + ); }, []); } } diff --git a/packages/common/src/platform/services/__mock__/data.txt b/packages/common/src/platform/services/__mock__/data.txt new file mode 100644 index 00000000000..707c69abf5f --- /dev/null +++ b/packages/common/src/platform/services/__mock__/data.txt @@ -0,0 +1,3 @@ +{ + "id": "1" +} diff --git a/packages/common/src/platform/utils/createHandlerMetadata.spec.ts b/packages/common/src/platform/utils/createHandlerMetadata.spec.ts new file mode 100644 index 00000000000..5d828d0969e --- /dev/null +++ b/packages/common/src/platform/utils/createHandlerMetadata.spec.ts @@ -0,0 +1,82 @@ +import {EndpointMetadata, Err, Get, HandlerType, PlatformTest, QueryParams, useCtxHandler} from "@tsed/common"; +import {Provider} from "@tsed/di"; +import {expect} from "chai"; +import * as Sinon from "sinon"; +import {createHandlerMetadata} from "./createHandlerMetadata"; + +const sandbox = Sinon.createSandbox(); + +class Test { + @Get("/") + get(@QueryParams("test") v: string) { + return v; + } + + use(@Err() error: any) { + return error; + } + + useErr(err: any, req: any, res: any, next: any) {} +} + +describe("createHandlerMetadata", () => { + beforeEach(PlatformTest.create); + afterEach(PlatformTest.reset); + afterEach(() => { + sandbox.restore(); + }); + + it("should return metadata from Endpoint", async () => { + // GIVEN + + const endpoint = new EndpointMetadata({ + target: Test, + propertyKey: "get" + }); + + sandbox.stub(PlatformTest.injector, "getProvider").returns(new Provider(Test)); + + // WHEN + const handlerMetadata = createHandlerMetadata(PlatformTest.injector, endpoint); + + // THEN + expect(handlerMetadata.target).to.eq(Test); + expect(handlerMetadata.propertyKey).to.eq("get"); + expect(handlerMetadata.type).to.eq(HandlerType.ENDPOINT); + }); + it("should return metadata from Middleware", async () => { + // GIVEN + sandbox.stub(PlatformTest.injector, "getProvider").returns(new Provider(Test)); + + // WHEN + const handlerMetadata = createHandlerMetadata(PlatformTest.injector, Test); + + // THEN + expect(handlerMetadata.target).to.eq(Test); + expect(handlerMetadata.propertyKey).to.eq("use"); + expect(handlerMetadata.type).to.eq(HandlerType.ERR_MIDDLEWARE); + }); + it("should return metadata from Function", async () => { + // GIVEN + sandbox.stub(PlatformTest.injector, "getProvider").returns(undefined); + + // WHEN + const handlerMetadata = createHandlerMetadata(PlatformTest.injector, () => {}); + + // THEN + expect(handlerMetadata.type).to.eq(HandlerType.RAW_FN); + }); + it("should return metadata from useCtxHandler", async () => { + // GIVEN + sandbox.stub(PlatformTest.injector, "getProvider").returns(undefined); + + // WHEN + const handlerMetadata = createHandlerMetadata( + PlatformTest.injector, + useCtxHandler(() => {}) + ); + + // THEN + expect(handlerMetadata.type).to.eq(HandlerType.CTX_FN); + }); +}); diff --git a/packages/common/src/platform/utils/createHandlerMetadata.ts b/packages/common/src/platform/utils/createHandlerMetadata.ts new file mode 100644 index 00000000000..66052812426 --- /dev/null +++ b/packages/common/src/platform/utils/createHandlerMetadata.ts @@ -0,0 +1,51 @@ +import {InjectorService} from "@tsed/di"; +import {HandlerType} from "../../mvc/interfaces/HandlerType"; +import {EndpointMetadata} from "../../mvc/models/EndpointMetadata"; +import {HandlerMetadata, HandlerMetadataOptions} from "../../mvc/models/HandlerMetadata"; +import type {PlatformRouteWithoutHandlers} from "../interfaces/PlatformRouterMethods"; + +function isMetadata(input: any) { + return input instanceof HandlerMetadata; +} + +export function createHandlerMetadata( + injector: InjectorService, + obj: any | EndpointMetadata, + routeOptions: PlatformRouteWithoutHandlers = {} +): HandlerMetadata { + if (isMetadata(obj)) { + return obj as HandlerMetadata; + } + + let options: HandlerMetadataOptions; + + if (obj instanceof EndpointMetadata) { + const provider = injector.getProvider(obj.token)!; + + options = { + token: provider.token, + target: provider.useClass, + type: HandlerType.ENDPOINT, + propertyKey: obj.propertyKey + }; + } else { + const provider = injector.getProvider(obj); + + if (provider) { + options = { + token: provider.token, + target: provider.useClass, + type: HandlerType.MIDDLEWARE, + propertyKey: "use" + }; + } else { + options = { + target: obj + }; + } + } + + options.routeOptions = routeOptions; + + return new HandlerMetadata(options); +} diff --git a/packages/common/src/platform/utils/renderView.spec.ts b/packages/common/src/platform/utils/renderView.spec.ts new file mode 100644 index 00000000000..c56fb177f47 --- /dev/null +++ b/packages/common/src/platform/utils/renderView.spec.ts @@ -0,0 +1,74 @@ +import {EndpointMetadata, Get, PlatformResponse, PlatformResponseMiddleware, PlatformTest, View} from "@tsed/common"; +import {Ignore, Property, Returns} from "@tsed/schema"; +import {expect} from "chai"; +import * as Sinon from "sinon"; +import {FakeResponse} from "../../../../../test/helper"; +import {renderView} from "./renderView"; + +const sandbox = Sinon.createSandbox(); + +describe("renderView", () => { + beforeEach(() => PlatformTest.create()); + afterEach(() => PlatformTest.reset()); + + it("should render content", async () => { + class Model { + @Property() + data: string; + + @Ignore() + test: string; + } + + class Test { + @Get("/") + @View("view", {options: "options"}) + @Returns(200, Model) + test() {} + } + + const response: any = new FakeResponse(sandbox); + const ctx = PlatformTest.createRequestContext(); + + ctx.endpoint = EndpointMetadata.get(Test, "test"); + ctx.response = new PlatformResponse(response); + + sandbox.stub(ctx.response, "render").resolves("HTML"); + + ctx.data = {data: "data"}; + + await renderView(ctx.data, ctx); + + expect(ctx.response.render).to.have.been.calledWithExactly("view", { + data: "data", + options: "options" + }); + }); + it("should throw an error", async () => { + class Test { + @Get("/") + @View("view", {options: "options"}) + test() {} + } + + const response: any = new FakeResponse(sandbox); + const ctx = PlatformTest.createRequestContext(); + + ctx.endpoint = EndpointMetadata.get(Test, "test"); + ctx.response = new PlatformResponse(response); + sandbox.stub(ctx.response, "render").callsFake(() => { + throw new Error("parser error"); + }); + + ctx.data = {data: "data"}; + + let actualError: any; + try { + await renderView(ctx.data, ctx); + } catch (er) { + actualError = er; + } + + expect(actualError.message).to.equal("Template rendering error: Test.test()\nError: parser error"); + }); +}); diff --git a/packages/common/src/platform/utils/renderView.ts b/packages/common/src/platform/utils/renderView.ts new file mode 100644 index 00000000000..b81a4b20f05 --- /dev/null +++ b/packages/common/src/platform/utils/renderView.ts @@ -0,0 +1,14 @@ +import {TemplateRenderError} from "../errors/TemplateRenderError"; +import {PlatformContext} from "../domain/PlatformContext"; + +export async function renderView(data: any, ctx: PlatformContext) { + const {response, endpoint} = ctx; + try { + const {data} = ctx; + const {path, options} = endpoint.view; + + return await response.render(path, {...options, ...data}); + } catch (err) { + throw new TemplateRenderError(endpoint.targetName, endpoint.propertyKey, err); + } +} diff --git a/packages/common/src/platform/utils/setResponseContentType.spec.ts b/packages/common/src/platform/utils/setResponseContentType.spec.ts new file mode 100644 index 00000000000..6c62548e3f6 --- /dev/null +++ b/packages/common/src/platform/utils/setResponseContentType.spec.ts @@ -0,0 +1,76 @@ +import {EndpointMetadata, Get, PlatformResponse, PlatformTest} from "@tsed/common"; +import {Returns} from "@tsed/schema"; +import {expect} from "chai"; +import * as Sinon from "sinon"; +import {FakeResponse} from "../../../../../test/helper"; +import {setResponseContentType} from "./setResponseContentType"; + +const sandbox = Sinon.createSandbox(); + +describe("setResponseContentType", () => { + beforeEach(() => PlatformTest.create()); + afterEach(() => PlatformTest.reset()); + it("should set the appropriate content type", async () => { + class Test { + @Get("/") + @(Returns(200).ContentType("text/html")) + test() {} + } + + const response: any = new FakeResponse(sandbox); + const ctx = PlatformTest.createRequestContext(); + + ctx.endpoint = EndpointMetadata.get(Test, "test"); + ctx.response = new PlatformResponse(response); + + sandbox.stub(ctx.response, "render").resolves("HTML"); + + ctx.data = {data: "data"}; + + await setResponseContentType(ctx.data, ctx); + + expect(ctx.response.raw.contentType).to.have.been.calledWithExactly("text/html"); + }); + it("should set the json content-type", async () => { + class Test { + @Get("/") + @(Returns(200).ContentType("application/json")) + test() {} + } + + const response: any = new FakeResponse(sandbox); + const ctx = PlatformTest.createRequestContext(); + + ctx.endpoint = EndpointMetadata.get(Test, "test"); + ctx.response = new PlatformResponse(response); + + sandbox.stub(ctx.response, "render").resolves("HTML"); + + ctx.data = {data: "data"}; + + await setResponseContentType(ctx.data, ctx); + + expect(ctx.response.raw.contentType).to.have.been.calledWithExactly("application/json"); + }); + it("should not set the json content-type", async () => { + class Test { + @Get("/") + @(Returns(200).ContentType("application/json")) + test() {} + } + + const response: any = new FakeResponse(sandbox); + const ctx = PlatformTest.createRequestContext(); + + ctx.endpoint = EndpointMetadata.get(Test, "test"); + ctx.response = new PlatformResponse(response); + + sandbox.stub(ctx.response, "render").resolves("HTML"); + + ctx.data = "data"; + + await setResponseContentType(ctx.data, ctx); + + expect(ctx.response.raw.contentType).to.not.have.been.called; + }); +}); diff --git a/packages/common/src/platform/utils/setResponseContentType.ts b/packages/common/src/platform/utils/setResponseContentType.ts new file mode 100644 index 00000000000..bb3aff8af00 --- /dev/null +++ b/packages/common/src/platform/utils/setResponseContentType.ts @@ -0,0 +1,19 @@ +import {PlatformContext} from "../domain/PlatformContext"; +import {isObject} from "@tsed/core"; + +export function setResponseContentType(data: any, ctx: PlatformContext) { + const {endpoint, response} = ctx; + const {operation} = endpoint; + + const contentType = operation.getContentTypeOf(response.statusCode) || ""; + + if (contentType && contentType !== "*/*") { + if (contentType === "application/json") { + if (isObject(data)) { + response.contentType(contentType); + } + } else { + response.contentType(contentType); + } + } +} diff --git a/packages/common/src/platform/middlewares/PlatformHeadersMiddleware.spec.ts b/packages/common/src/platform/utils/setResponseHeaders.spec.ts similarity index 66% rename from packages/common/src/platform/middlewares/PlatformHeadersMiddleware.spec.ts rename to packages/common/src/platform/utils/setResponseHeaders.spec.ts index ca7ee462a94..1e5832b0563 100644 --- a/packages/common/src/platform/middlewares/PlatformHeadersMiddleware.spec.ts +++ b/packages/common/src/platform/utils/setResponseHeaders.spec.ts @@ -1,12 +1,13 @@ -import {EndpointMetadata, Get, Location, PlatformHeadersMiddleware, PlatformResponse, PlatformTest, Redirect, Status} from "@tsed/common"; +import {EndpointMetadata, Get, Location, PlatformResponse, PlatformTest, Redirect, Status} from "@tsed/common"; import {expect} from "chai"; import * as Sinon from "sinon"; import {FakeResponse} from "../../../../../test/helper"; +import {setResponseHeaders} from "./setResponseHeaders"; const sandbox = Sinon.createSandbox(); const next = sandbox.stub(); -describe("PlatformHeadersMiddleware", () => { +describe("setResponseHeaders", () => { beforeEach(() => PlatformTest.create()); afterEach(() => PlatformTest.reset()); @@ -18,14 +19,13 @@ describe("PlatformHeadersMiddleware", () => { } const response: any = new FakeResponse(sandbox); - const middleware: any = PlatformTest.get(PlatformHeadersMiddleware); const ctx = PlatformTest.createRequestContext(); ctx.endpoint = EndpointMetadata.get(Test, "test"); ctx.response = new PlatformResponse(response); // WHEN - await middleware.use(ctx, next); + await setResponseHeaders(ctx); // THEN expect(response.set).to.have.been.calledWithExactly("x-header", "test"); @@ -40,14 +40,13 @@ describe("PlatformHeadersMiddleware", () => { } const response: any = new FakeResponse(sandbox); - const middleware: any = PlatformTest.get(PlatformHeadersMiddleware); const ctx = PlatformTest.createRequestContext(); ctx.endpoint = EndpointMetadata.get(Test, "test"); ctx.response = new PlatformResponse(response); // WHEN - await middleware.use(ctx, next); + await setResponseHeaders(ctx); // THEN expect(response.redirect).to.have.been.calledWithExactly(301, "/path"); @@ -61,16 +60,37 @@ describe("PlatformHeadersMiddleware", () => { } const response: any = new FakeResponse(sandbox); - const middleware: any = PlatformTest.get(PlatformHeadersMiddleware); const ctx = PlatformTest.createRequestContext(); ctx.endpoint = EndpointMetadata.get(Test, "test"); ctx.response = new PlatformResponse(response); // WHEN - await middleware.use(ctx, next); + await setResponseHeaders(ctx); // THEN expect(response.location).to.have.been.calledWithExactly("/path"); }); + + it("should do nothing when headers is already sent", async () => { + class Test { + @Get("/") + @(Status(200).Header("x-header", "test")) + test() {} + } + + const response: any = new FakeResponse(sandbox); + response.headersSent = true; + + const ctx = PlatformTest.createRequestContext(); + + ctx.endpoint = EndpointMetadata.get(Test, "test"); + ctx.response = new PlatformResponse(response); + + // WHEN + await setResponseHeaders(ctx); + + // THEN + return expect(response.set).to.not.have.been.called; + }); }); diff --git a/packages/common/src/platform/utils/setResponseHeaders.ts b/packages/common/src/platform/utils/setResponseHeaders.ts new file mode 100644 index 00000000000..92a5e7fc876 --- /dev/null +++ b/packages/common/src/platform/utils/setResponseHeaders.ts @@ -0,0 +1,35 @@ +import {PlatformContext} from "../domain/PlatformContext"; + +function toHeaders(headers: {[key: string]: any}) { + return Object.entries(headers).reduce((headers, [key, item]) => { + return { + ...headers, + [key]: String(item.example) + }; + }, {}); +} + +export function setResponseHeaders(ctx: PlatformContext) { + const {response, endpoint} = ctx; + const {operation} = endpoint; + + if (response.isDone()) { + return; + } + + if (response.statusCode === 200) { + // apply status only if the isn't already modified + response.status(operation.getStatus()); + } + + const headers = operation.getHeadersOf(response.statusCode); + response.setHeaders(toHeaders(headers)); + + if (endpoint.redirect) { + response.redirect(endpoint.redirect.status || 302, endpoint.redirect.url); + } + + if (endpoint.location) { + response.location(endpoint.location); + } +} diff --git a/packages/common/src/platform/utils/useCtxHandler.ts b/packages/common/src/platform/utils/useCtxHandler.ts new file mode 100644 index 00000000000..a4260b72a15 --- /dev/null +++ b/packages/common/src/platform/utils/useCtxHandler.ts @@ -0,0 +1,14 @@ +import {HandlerType} from "../../mvc/interfaces/HandlerType"; +import {PlatformContext} from "../domain/PlatformContext"; + +export type PlatformCtxHandler = ($ctx: PlatformContext) => any | Promise; + +/** + * Create Ts.ED context handler + * @param fn + */ +export function useCtxHandler(fn: PlatformCtxHandler & {type?: HandlerType}) { + fn.type = HandlerType.CTX_FN; + + return fn; +} diff --git a/packages/passport/src/services/ProtocolsService.ts b/packages/passport/src/services/ProtocolsService.ts index 1cc84575d08..61a2640ed98 100644 --- a/packages/passport/src/services/ProtocolsService.ts +++ b/packages/passport/src/services/ProtocolsService.ts @@ -53,24 +53,24 @@ export class ProtocolsService { const handlerMetadata = new HandlerMetadata({ token: provider.provide, target: provider.useClass, - type: HandlerType.CONTROLLER, + type: HandlerType.CUSTOM, propertyKey: "$onVerify" }); const platform = this.injector.get(Platform)!; const middleware = platform.createHandler(handlerMetadata); - return (req: any, ...args: any[]) => { + return async (req: any, ...args: any[]) => { const done = args[args.length - 1]; req.args = args.slice(0, -1); + const ctx = req.$ctx; - return middleware(req, req.res, (err: any) => { - if (err) { - done(err, false, {message: err.message}); - } else { - done(null, ...[].concat(req.$ctx.data)); - } - }); + try { + await middleware(ctx); + done(null, ...[].concat(ctx.data)); + } catch (err) { + done(err, false, {message: err.message}); + } }; } } diff --git a/packages/platform-express/src/services/PlatformExpressHandler.spec.ts b/packages/platform-express/src/services/PlatformExpressHandler.spec.ts index abfb6b4565d..7c261017244 100644 --- a/packages/platform-express/src/services/PlatformExpressHandler.spec.ts +++ b/packages/platform-express/src/services/PlatformExpressHandler.spec.ts @@ -14,14 +14,13 @@ describe("PlatformExpressHandler", () => { }); describe("createHandler", () => { - describe("CONTROLLER", () => { + describe("ENDPOINT", () => { it("should return a native handler with 3 params", async () => { // GIVEN - const platformHandler = await invokePlatformHandler(PlatformExpressHandler); + const platformHandler = await invokePlatformHandler(PlatformExpressHandler); class Test { - get() { - } + get() {} } PlatformTest.invoke(Test); @@ -29,11 +28,10 @@ describe("PlatformExpressHandler", () => { const handlerMetadata = new HandlerMetadata({ token: Test, target: Test, - type: HandlerType.CONTROLLER, + type: HandlerType.ENDPOINT, propertyKey: "get" }); - // WHEN const handler = platformHandler.createHandler(handlerMetadata); @@ -41,33 +39,6 @@ describe("PlatformExpressHandler", () => { expect(handler).to.not.eq(handlerMetadata.handler); expect(handler.length).to.eq(3); }); - it("should return a native handler with 4 params", async () => { - // GIVEN - const platformHandler = await invokePlatformHandler(PlatformExpressHandler); - - class Test { - get(@Err() err: unknown) { - } - } - - PlatformTest.invoke(Test); - - const metadata = new HandlerMetadata({ - token: Test, - target: Test, - type: HandlerType.CONTROLLER, - propertyKey: "get" - }); - - - // WHEN - const handler = platformHandler.createHandler(metadata); - - // THEN - expect(metadata.hasErrorParam).to.eq(true); - expect(handler).to.not.eq(metadata.handler); - expect(handler.length).to.eq(4); - }); }); describe("MIDDLEWARE", () => { it("should return a native handler with 3 params", async () => { @@ -75,8 +46,7 @@ describe("PlatformExpressHandler", () => { const platformHandler = await invokePlatformHandler(PlatformExpressHandler); class Test { - use() { - } + use() {} } PlatformTest.invoke(Test); @@ -88,7 +58,6 @@ describe("PlatformExpressHandler", () => { propertyKey: "use" }); - // WHEN const handler = platformHandler.createHandler(handlerMetadata); @@ -101,8 +70,7 @@ describe("PlatformExpressHandler", () => { const platformHandler = await invokePlatformHandler(PlatformExpressHandler); class Test { - use(@Err() err: unknown) { - } + use(@Err() err: unknown) {} } PlatformTest.invoke(Test); @@ -114,7 +82,6 @@ describe("PlatformExpressHandler", () => { propertyKey: "use" }); - // WHEN const handler = platformHandler.createHandler(metadata); @@ -130,20 +97,17 @@ describe("PlatformExpressHandler", () => { const platformHandler = await invokePlatformHandler(PlatformExpressHandler); class Test { - use() { - } + use() {} } PlatformTest.invoke(Test); const handlerMetadata = new HandlerMetadata({ token: Test, - target: (ctx: any) => { - }, - type: HandlerType.$CTX + target: (ctx: any) => {}, + type: HandlerType.CTX_FN }); - // WHEN const handler = platformHandler.createHandler(handlerMetadata); @@ -158,20 +122,17 @@ describe("PlatformExpressHandler", () => { const platformHandler = await invokePlatformHandler(PlatformExpressHandler); class Test { - use() { - } + use() {} } PlatformTest.invoke(Test); const handlerMetadata = new HandlerMetadata({ token: Test, - target: (req: any, res: any, next: any) => { - }, - type: HandlerType.FUNCTION + target: (req: any, res: any, next: any) => {}, + type: HandlerType.RAW_FN }); - // WHEN const handler = platformHandler.createHandler(handlerMetadata); @@ -180,7 +141,6 @@ describe("PlatformExpressHandler", () => { }); }); }); - describe("getArg()", () => { it("should return REQUEST", async () => { // GIVEN @@ -212,243 +172,219 @@ describe("PlatformExpressHandler", () => { // THEN expect(value).to.deep.eq(response); }); - it( - "should return NEXT", async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.NEXT_FN - }); - - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); - - // THEN - expect(value).to.deep.eq(h.next); + it("should return NEXT", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.NEXT_FN }); - it( - "should return ERR", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.ERR - }); - h.err = new Error(); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // THEN - expect(value).to.deep.eq(h.err); + // THEN + expect(value).to.deep.eq(h.next); + }); + it("should return ERR", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.ERR }); + h.err = new Error(); - it( - "should return $CTX", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.$CTX - }); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(h.err); + }); - // THEN - expect(value).to.deep.eq(h.request.$ctx); + it("should return $CTX", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.$CTX }); - it( - "should return RESPONSE_DATA", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.RESPONSE_DATA - }); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(h.request.$ctx); + }); - // THEN - expect(value).to.deep.eq(h.request.$ctx.data); + it("should return RESPONSE_DATA", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.RESPONSE_DATA }); - it( - "should return ENDPOINT_INFO", - async () => { - // GIVEN - const {param, request, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.ENDPOINT_INFO - }); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - request.$ctx.endpoint = "endpoint"; - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(h.request.$ctx.data); + }); - // THEN - expect(value).to.deep.eq(request.$ctx.endpoint); + it("should return ENDPOINT_INFO", async () => { + // GIVEN + const {param, request, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.ENDPOINT_INFO }); - it( - "should return BODY", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.BODY - }); + // @ts-ignore + request.$ctx.endpoint = "endpoint"; + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(request.$ctx.endpoint); + }); - // THEN - expect(value).to.deep.eq(h.request.body); + it("should return BODY", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.BODY }); - it( - "should return PATH", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.PATH - }); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(h.getRequest().body); + }); - // THEN - expect(value).to.deep.eq(h.request.params); + it("should return PATH", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.PATH }); - it( - "should return QUERY", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.QUERY - }); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(h.getRequest().params); + }); - // THEN - expect(value).to.deep.eq(h.request.query); + it("should return QUERY", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.QUERY }); - it( - "should return HEADER", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.HEADER - }); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(h.getRequest().query); + }); - // THEN - expect(value).to.deep.eq({ - accept: "application/json", - "content-type": "application/json" - }); + it("should return HEADER", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.HEADER }); - it( - "should return COOKIES", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.COOKIES - }); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq({ + accept: "application/json", + "content-type": "application/json" + }); + }); - // THEN - expect(value).to.deep.eq(h.request.cookies); + it("should return COOKIES", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.COOKIES }); - it( - "should return SESSION", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.SESSION - }); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(h.getRequest().cookies); + }); - // THEN - expect(value).to.deep.eq(h.request.session); + it("should return SESSION", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.SESSION }); - it( - "should return LOCALS", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - sandbox, - token: PlatformExpressHandler, - type: ParamTypes.LOCALS - }); - h.err = new Error(); + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(h.request.session); + }); - // THEN - expect(value).to.deep.eq(h.response.locals); + it("should return LOCALS", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + sandbox, + token: PlatformExpressHandler, + type: ParamTypes.LOCALS }); + h.err = new Error(); - it( - "should return request by default", - async () => { - // GIVEN - const {param, h, platformHandler} = await buildPlatformHandler({ - type: "UNKNOWN", - sandbox, - token: PlatformExpressHandler - }); - param.expression = "test"; + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); - // WHEN - // @ts-ignore - const value = platformHandler.getArg(param.paramType, h); + // THEN + expect(value).to.deep.eq(h.response.locals); + }); - // THEN - expect(value).to.deep.eq(h.request); + it("should return request by default", async () => { + // GIVEN + const {param, h, platformHandler} = await buildPlatformHandler({ + type: "UNKNOWN", + sandbox, + token: PlatformExpressHandler }); + param.expression = "test"; + + // WHEN + // @ts-ignore + const value = platformHandler.getArg(param.paramType, h); + + // THEN + expect(value).to.deep.eq(h.getRequest()); + }); }); }); diff --git a/packages/platform-express/src/services/PlatformExpressHandler.ts b/packages/platform-express/src/services/PlatformExpressHandler.ts index 86a575866fb..ac24b71af1a 100644 --- a/packages/platform-express/src/services/PlatformExpressHandler.ts +++ b/packages/platform-express/src/services/PlatformExpressHandler.ts @@ -8,29 +8,23 @@ export class PlatformExpressHandler extends PlatformHandler { protected createRawHandler(metadata: HandlerMetadata): Function { switch (metadata.type) { default: - case HandlerType.FUNCTION: - return metadata.handler; + return super.createRawHandler(metadata); - case HandlerType.$CTX: + case HandlerType.CTX_FN: return async (req: any, res: any, next: any) => { await metadata.handler(req.$ctx); - return next(); + return next && next(); }; - case HandlerType.CONTROLLER: - case HandlerType.MIDDLEWARE: - if (metadata.hasErrorParam) { - return (err: any, request: any, response: any, next: any) => - this.onRequest({ - err, - $ctx: request.$ctx, - next, - metadata - }); - } - - return (request: any, response: any, next: any) => this.onRequest({$ctx: request.$ctx, next, metadata}); + case HandlerType.ERR_MIDDLEWARE: + return (err: any, request: any, response: any, next: any) => + this.onRequest({ + err, + $ctx: request.$ctx, + next, + metadata + }); } } } diff --git a/packages/platform-test-utils/src/tests/testView.ts b/packages/platform-test-utils/src/tests/testView.ts index a9ea0c97dd0..730116e788d 100644 --- a/packages/platform-test-utils/src/tests/testView.ts +++ b/packages/platform-test-utils/src/tests/testView.ts @@ -59,7 +59,7 @@ export function testView(options: PlatformTestOptions) { const response = await request.get("/rest/views/scenario-2").expect(500); expect(response.body).to.deep.equal({ - name: "TEMPLATING_RENDER_ERROR", + name: "TEMPLATE_RENDER_ERROR", message: "Template rendering error: ViewCtrl.testScenario2()\nError: No default engine was specified and no extension was provided.", status: 500, diff --git a/packages/swagger/src/SwaggerModule.ts b/packages/swagger/src/SwaggerModule.ts index 342f8f83e0a..2570ecd141f 100644 --- a/packages/swagger/src/SwaggerModule.ts +++ b/packages/swagger/src/SwaggerModule.ts @@ -6,7 +6,9 @@ import { Module, OnReady, PlatformApplication, - PlatformRouter + PlatformContext, + PlatformRouter, + useCtxHandler } from "@tsed/common"; import * as Fs from "fs"; import {join} from "path"; @@ -52,7 +54,7 @@ export class SwaggerModule implements BeforeRoutesInit, OnReady { this.settings.forEach((conf: SwaggerSettings) => { const {path = "/"} = conf; - this.app.get(path, redirectMiddleware(path)); + this.app.get(path, useCtxHandler(redirectMiddleware(path))); this.app.use(path, this.createRouter(conf, urls)); }); @@ -113,18 +115,18 @@ export class SwaggerModule implements BeforeRoutesInit, OnReady { const {cssPath, jsPath, viewPath = join(__dirname, "../views/index.ejs")} = conf; const router = PlatformRouter.create(this.injector); - router.get("/swagger.json", this.middlewareSwaggerJson(conf)); + router.get("/swagger.json", useCtxHandler(this.middlewareSwaggerJson(conf))); if (viewPath) { if (cssPath) { - router.get("/main.css", cssMiddleware(cssPath)); + router.get("/main.css", useCtxHandler(cssMiddleware(cssPath))); } if (jsPath) { - router.get("/main.js", jsMiddleware(jsPath)); + router.get("/main.js", useCtxHandler(jsMiddleware(jsPath))); } - router.get("/", indexMiddleware(viewPath, {urls, ...conf})); + router.get("/", useCtxHandler(indexMiddleware(viewPath, {urls, ...conf}))); router.statics("/", {root: swaggerUiPath}); } @@ -132,8 +134,8 @@ export class SwaggerModule implements BeforeRoutesInit, OnReady { } private middlewareSwaggerJson(conf: SwaggerSettings) { - return (req: any, res: any) => { - res.status(200).json(this.swaggerService.getOpenAPISpec(conf)); + return (ctx: PlatformContext) => { + ctx.response.status(200).body(this.swaggerService.getOpenAPISpec(conf)); }; } } diff --git a/test/helper/buildPlatformHandler.ts b/test/helper/buildPlatformHandler.ts index 36a83b8ac6c..0cdaf2ed94e 100644 --- a/test/helper/buildPlatformHandler.ts +++ b/test/helper/buildPlatformHandler.ts @@ -10,10 +10,10 @@ export interface TestPlatformHandlerOptions { required?: boolean; } -export function invokePlatformHandler(token: any) { +export function invokePlatformHandler(token: any): T { PlatformTest.injector.getProvider(PlatformHandler)!.useClass = token; - return PlatformTest.invoke(PlatformHandler); + return PlatformTest.invoke(PlatformHandler) as T; } export async function buildPlatformHandler({type, token, sandbox, expression, required}: TestPlatformHandlerOptions) {