diff --git a/docs/docs/snippets/controllers/inject-router.ts b/docs/docs/snippets/controllers/inject-router.ts index c3e492305a5..35c69ba471c 100644 --- a/docs/docs/snippets/controllers/inject-router.ts +++ b/docs/docs/snippets/controllers/inject-router.ts @@ -5,11 +5,6 @@ import {Controller} from "@tsed/di"; export class CalendarCtrl { constructor(router: PlatformRouter) { router.get("/", this.myMethod); - - // GET raw router (Express.Router or Koa.Router) - console.log(router.callback()); - console.log(router.raw); - console.log(router.getRouter()); } myMethod(req: any, res: any, next: any) {} diff --git a/packages/di/jest.config.js b/packages/di/jest.config.js index 03306fa4050..d13685bd92b 100644 --- a/packages/di/jest.config.js +++ b/packages/di/jest.config.js @@ -5,10 +5,10 @@ module.exports = { ...require("@tsed/jest-config")(__dirname, "di"), coverageThreshold: { global: { - statements: 98.3, - branches: 92.63, - functions: 96.44, - lines: 98.39 + statements: 99.23, + branches: 92.91, + functions: 99.23, + lines: 99.34 } } }; diff --git a/packages/platform/common/src/domain/ControllerProvider.spec.ts b/packages/di/src/domain/ControllerProvider.spec.ts similarity index 95% rename from packages/platform/common/src/domain/ControllerProvider.spec.ts rename to packages/di/src/domain/ControllerProvider.spec.ts index cf2a27ab67a..02a9371b2cd 100644 --- a/packages/platform/common/src/domain/ControllerProvider.spec.ts +++ b/packages/di/src/domain/ControllerProvider.spec.ts @@ -1,4 +1,3 @@ -import {getEnumerableKeys} from "@tsed/core"; import {ProviderScope} from "@tsed/di"; import {ControllerProvider} from "./ControllerProvider"; @@ -11,7 +10,6 @@ describe("ControllerProvider", () => { controllerProvider = new ControllerProvider(Test); controllerProvider.path = "/"; controllerProvider.scope = ProviderScope.REQUEST; - controllerProvider.routerOptions = {}; controllerProvider.middlewares = { useBefore: [new Function()], use: [new Function()], diff --git a/packages/platform/common/src/domain/ControllerProvider.ts b/packages/di/src/domain/ControllerProvider.ts similarity index 63% rename from packages/platform/common/src/domain/ControllerProvider.ts rename to packages/di/src/domain/ControllerProvider.ts index 9e01b474c2d..a504cebb4e6 100644 --- a/packages/platform/common/src/domain/ControllerProvider.ts +++ b/packages/di/src/domain/ControllerProvider.ts @@ -1,30 +1,14 @@ -import {ControllerMiddlewares, Provider, ProviderType, TokenProvider} from "@tsed/di"; -import {ROUTER_OPTIONS} from "../constants/routerOptions"; - -let AUTO_INC = 0; +import {ControllerMiddlewares} from "../decorators/controller"; +import {TokenProvider} from "../interfaces/TokenProvider"; +import {Provider} from "./Provider"; +import {ProviderType} from "./ProviderType"; export class ControllerProvider extends Provider { - readonly tokenRouter: string; + public tokenRouter: string; constructor(provide: TokenProvider, options: Partial = {}) { super(provide, options); this.type = ProviderType.CONTROLLER; - this.tokenRouter = `${this.name}_ROUTER_${AUTO_INC++}`; - } - - /** - * - */ - get routerOptions(): any { - return this.store.get(ROUTER_OPTIONS) || ({} as any); - } - - /** - * - * @param value - */ - set routerOptions(value: any) { - this.store.set(ROUTER_OPTIONS, value); } /** @@ -53,6 +37,7 @@ export class ControllerProvider extends Provider { Object.keys(middlewares).forEach((key: string) => { concat(key, mdlwrs, middlewares); }); + this.store.set("middlewares", mdlwrs); } } diff --git a/packages/di/src/domain/DIContext.spec.ts b/packages/di/src/domain/DIContext.spec.ts index a176aad5aad..3df9e01c151 100644 --- a/packages/di/src/domain/DIContext.spec.ts +++ b/packages/di/src/domain/DIContext.spec.ts @@ -51,11 +51,14 @@ describe("DIContext", () => { maxStackSize: 0 }); + context.next = jest.fn(); + expect(context.id).toEqual("id"); expect(context.dateStart).toBeInstanceOf(Date); expect(context.container).toBeInstanceOf(Map); expect(context.env).toEqual("test"); + context.next(); context.logger.info("test"); expect(logger.info).toHaveBeenCalled(); diff --git a/packages/di/src/index.ts b/packages/di/src/index.ts index 8c78e75057c..bb79d19974c 100644 --- a/packages/di/src/index.ts +++ b/packages/di/src/index.ts @@ -20,6 +20,7 @@ export * from "./decorators/useOpts"; export * from "./decorators/value"; export * from "./domain/Container"; export * from "./domain/ContextLogger"; +export * from "./domain/ControllerProvider"; export * from "./domain/DIContext"; export * from "./domain/InjectablePropertyType"; export * from "./domain/LocalsContainer"; diff --git a/packages/di/src/services/DIConfiguration.spec.ts b/packages/di/src/services/DIConfiguration.spec.ts index 528b041117c..83d9d7e55a6 100644 --- a/packages/di/src/services/DIConfiguration.spec.ts +++ b/packages/di/src/services/DIConfiguration.spec.ts @@ -1,6 +1,54 @@ +import {Env} from "@tsed/core"; import {DIConfiguration} from "../../src"; describe("DIConfiguration", () => { + describe("version()", () => { + it("should get version", () => { + // GIVEN + const configuration = new DIConfiguration(); + + configuration.version = "1.0.0"; + expect(configuration.version).toEqual("1.0.0"); + }); + }); + describe("env()", () => { + it("should get env", () => { + // GIVEN + const configuration = new DIConfiguration(); + + configuration.env = Env.DEV; + expect(configuration.env).toEqual(Env.DEV); + }); + }); + describe("debug()", () => { + it("should return debug", () => { + // GIVEN + const configuration = new DIConfiguration(); + + configuration.debug = true; + expect(configuration.debug).toEqual(true); + + configuration.debug = false; + expect(configuration.debug).toEqual(false); + }); + }); + describe("forEach()", () => { + it("should return all key, value", () => { + // GIVEN + const configuration = new DIConfiguration(); + const obj: any = {}; + configuration.forEach((value, key) => { + obj[key] = value; + }); + expect(obj).toEqual({ + imports: [], + logger: {}, + resolvers: [], + routes: [], + scopes: {} + }); + }); + }); describe("scopes()", () => { it("should get scopes", () => { // GIVEN diff --git a/packages/di/src/utils/runInContext.ts b/packages/di/src/utils/runInContext.ts index 1ded8c48b68..ae72736f2da 100644 --- a/packages/di/src/utils/runInContext.ts +++ b/packages/di/src/utils/runInContext.ts @@ -16,5 +16,13 @@ export function runInContext(ctx: DIContext, cb: any) { } export function bindContext(cb: any) { - return AsyncResource.bind(cb); + let localArgs: any; + const newCB: any = AsyncResource.bind(() => cb(...localArgs)); + + // FIXME: remove this hack when the support of v14 will be removed + // see issue: https://github.com/nodejs/node/issues/36051 + return (...args: any) => { + localArgs = args; + return newCB(...args); + }; } diff --git a/packages/graphql/apollo/src/services/ApolloService.spec.ts b/packages/graphql/apollo/src/services/ApolloService.spec.ts index 42eb83ecbcf..baa601a7956 100644 --- a/packages/graphql/apollo/src/services/ApolloService.spec.ts +++ b/packages/graphql/apollo/src/services/ApolloService.spec.ts @@ -1,7 +1,7 @@ -import {PlatformTest} from "@tsed/common"; +import {jest} from "@jest/globals"; +import {PlatformApplication, PlatformTest} from "@tsed/common"; import {ApolloServer} from "apollo-server-express"; import {ApolloService} from "./ApolloService"; -import {jest} from "@jest/globals"; jest.mock("apollo-server-express"); @@ -19,7 +19,14 @@ describe("ApolloService", () => { describe("when server options isn't given", () => { it("should create a server", async () => { // GIVEN - const service = PlatformTest.get(ApolloService); + const service = PlatformTest.get(ApolloService); + const app = PlatformTest.get(PlatformApplication); + + const middleware = function middleware() {}; + // @ts-ignore + ApolloServer.prototype.getMiddleware = jest.fn().mockReturnValue(middleware); + + jest.spyOn(app, "use").mockReturnThis(); // WHEN const result1 = await service.createServer("key", { @@ -35,6 +42,7 @@ describe("ApolloService", () => { expect(result1.getMiddleware).toHaveBeenCalledWith({ path: "/path" }); + expect(app.use).toHaveBeenCalledWith(middleware); }); }); }); diff --git a/packages/graphql/apollo/src/services/ApolloService.ts b/packages/graphql/apollo/src/services/ApolloService.ts index 831e2f3b358..0e9a94c4f97 100644 --- a/packages/graphql/apollo/src/services/ApolloService.ts +++ b/packages/graphql/apollo/src/services/ApolloService.ts @@ -1,6 +1,6 @@ +import {PlatformApplication} from "@tsed/common"; import {Constant, Inject, Service} from "@tsed/di"; import {Logger} from "@tsed/logger"; -import {PlatformApplication} from "@tsed/common"; import type {Config} from "apollo-server-core"; import { ApolloServerBase, @@ -9,10 +9,10 @@ import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core"; import type {GraphQLSchema} from "graphql"; -import type {ApolloServer, ApolloSettings} from "../interfaces/ApolloSettings"; -import {ApolloCustomServerCB} from "../interfaces/ApolloSettings"; import Http from "http"; import Https from "https"; +import type {ApolloServer, ApolloSettings} from "../interfaces/ApolloSettings"; +import {ApolloCustomServerCB} from "../interfaces/ApolloSettings"; @Service() export class ApolloService { @@ -36,7 +36,7 @@ export class ApolloService { > = new Map(); @Inject() - private app: PlatformApplication; + private app: PlatformApplication; @Inject(Http.Server) private httpServer: Http.Server | null; @@ -76,7 +76,7 @@ export class ApolloService { ...middlewareOptions }); - this.app.getRouter().use(middleware); + this.app.use(middleware); return server; } diff --git a/packages/orm/prisma/package.json b/packages/orm/prisma/package.json index 794a9a6c34f..fc919c70da3 100644 --- a/packages/orm/prisma/package.json +++ b/packages/orm/prisma/package.json @@ -32,7 +32,7 @@ "tslib": "2.4.0" }, "devDependencies": { - "@prisma/client": "^3.10.0", + "@prisma/client": "^4.0.0", "@tsed/core": "7.0.0-beta.13", "@tsed/di": "7.0.0-beta.13", "@tsed/json-mapper": "7.0.0-beta.13", diff --git a/packages/platform/common/package.json b/packages/platform/common/package.json index 98b61931984..8ad5fe4feaa 100644 --- a/packages/platform/common/package.json +++ b/packages/platform/common/package.json @@ -82,6 +82,7 @@ "@tsed/platform-params": "7.0.0-beta.13", "@tsed/platform-response-filter": "7.0.0-beta.13", "@tsed/platform-views": "7.0.0-beta.13", + "@tsed/platform-router": "7.0.0-beta.13", "@tsed/schema": "7.0.0-beta.13", "@types/json-schema": "7.0.11", "@types/on-finished": "2.3.1", @@ -102,4 +103,4 @@ } }, "devDependencies": {} -} \ No newline at end of file +} diff --git a/packages/platform/common/src/builder/PlatformBuilder.spec.ts b/packages/platform/common/src/builder/PlatformBuilder.spec.ts index f828179d3e5..f0bdca4ab37 100644 --- a/packages/platform/common/src/builder/PlatformBuilder.spec.ts +++ b/packages/platform/common/src/builder/PlatformBuilder.spec.ts @@ -17,103 +17,153 @@ import {FakeAdapter} from "../services/FakeAdapter"; import {Platform} from "../services/Platform"; import {PlatformBuilder} from "./PlatformBuilder"; -describe("PlatformBuilder", () => { - @Controller("/") - class RestCtrl {} - - class PlatformCustom extends FakeAdapter { - readonly providers = [ - { - provide: class Test {} - } - ]; - - constructor(private platform: PlatformBuilder) { - super(); - } +@Controller("/") +class RestCtrl {} - static create(module: Type, settings: Partial = {}) { - return PlatformBuilder.create(module, { - ...settings, - adapter: PlatformCustom - }); +class PlatformCustom extends FakeAdapter { + readonly providers = [ + { + provide: class Test {} } + ]; - static async bootstrap(module: Type, settings: Partial = {}) { - return PlatformBuilder.build(module, { - ...settings, - adapter: PlatformCustom - }).bootstrap(); - } + constructor(private platform: PlatformBuilder) { + super(); + } - afterLoadRoutes(): Promise { - return Promise.resolve(undefined); - } + static create(module: Type, settings: Partial = {}) { + return PlatformBuilder.create(module, { + ...settings, + adapter: PlatformCustom + }); + } - beforeLoadRoutes(): Promise { - return Promise.resolve(undefined); - } + static async bootstrap(module: Type, settings: Partial = {}) { + return PlatformBuilder.build(module, { + ...settings, + adapter: PlatformCustom + }).bootstrap(); + } - useContext(): any {} + afterLoadRoutes(): Promise { + return Promise.resolve(undefined); + } - useRouter(): any {} + beforeLoadRoutes(): Promise { + return Promise.resolve(undefined); } - @Controller("/") - class HealthCtrl {} + useContext(): any {} - @Module({ - mount: { - "/heath": [HealthCtrl] - } - }) - class HealthModule {} - - const settings = { - logger: { - level: "off" - }, - mount: { - "/rest": [RestCtrl] - }, - acceptMimes: ["application/json"], - imports: [HealthModule] - }; - - @Configuration(settings as any) - class ServerModule implements BeforeInit, AfterInit, BeforeRoutesInit, AfterRoutesInit, BeforeListen, AfterListen, OnReady { - constructor() {} - - $beforeRoutesInit(): void | Promise { - return undefined; - } + useRouter(): any {} +} - $afterRoutesInit(): void | Promise { - console.log("$afterRoutesInit"); - return undefined; - } +@Controller("/") +class HealthCtrl {} - $afterInit(): void | Promise { - return undefined; - } +@Module({ + mount: { + "/heath": [HealthCtrl] + } +}) +class HealthModule {} + +const settings = { + logger: { + level: "off" + }, + mount: { + "/rest": [RestCtrl] + }, + acceptMimes: ["application/json"], + imports: [HealthModule] +}; + +@Configuration(settings as any) +class ServerModule implements BeforeInit, AfterInit, BeforeRoutesInit, AfterRoutesInit, BeforeListen, AfterListen, OnReady { + constructor() {} + + $beforeRoutesInit(): void | Promise { + return undefined; + } - $afterListen(): void | Promise { - return undefined; - } + $afterRoutesInit(): void | Promise { + return undefined; + } - $beforeInit(): void | Promise { - return undefined; - } + $afterInit(): void | Promise { + return undefined; + } - $beforeListen(): void | Promise { - return undefined; - } + $afterListen(): void | Promise { + return undefined; + } - $onReady(): void | Promise { - return undefined; - } + $beforeInit(): void | Promise { + return undefined; } + $beforeListen(): void | Promise { + return undefined; + } + + $onReady(): void | Promise { + return undefined; + } +} + +describe("PlatformBuilder", () => { + describe("loadStatics()", () => { + it("should loadStatics", async () => { + // WHEN + const platform = await PlatformCustom.bootstrap(ServerModule, { + httpPort: false, + httpsPort: false, + statics: { + "/": ["/root", {root: "/root2", test: "test", hook: "$beforeRoutesInit"}] + } + }); + + jest.spyOn(platform.app, "statics").mockReturnValue(undefined as any); + + await platform.loadStatics("$beforeRoutesInit"); + + expect(platform.app.statics).toHaveBeenCalledWith("/", { + hook: "$beforeRoutesInit", + root: "/root2", + test: "test" + }); + }); + }); + describe("loadMiddlewaresFor()", () => { + it("should load middlewares", async () => { + const middlewares: any[] = [ + { + hook: "$beforeRoutesInit", + use: jest.fn() + }, + { + hook: "$afterRoutesInit", + use: jest.fn() + }, + jest.fn() + ]; + // WHEN + const platform = await PlatformCustom.bootstrap(ServerModule, { + httpPort: false, + httpsPort: false, + middlewares + }); + + jest.spyOn(platform.app, "use").mockReturnValue(undefined as any); + + // @ts-ignore + platform.loadMiddlewaresFor("$beforeRoutesInit"); + + expect(platform.app.use).toHaveBeenCalledWith(middlewares[0].use); + expect(platform.app.use).toHaveBeenCalledWith(middlewares[2]); + }); + }); describe("static boostrap()", () => { beforeAll(() => { jest.spyOn(ServerModule.prototype, "$beforeRoutesInit").mockReturnValue(undefined); @@ -123,7 +173,7 @@ describe("PlatformBuilder", () => { jest.spyOn(ServerModule.prototype, "$beforeInit").mockReturnValue(undefined); jest.spyOn(ServerModule.prototype, "$beforeListen").mockReturnValue(undefined); jest.spyOn(ServerModule.prototype, "$onReady").mockReturnValue(undefined); - jest.spyOn(PlatformBuilder.prototype, "loadStatics").mockResolvedValue(undefined); + jest.spyOn(PlatformBuilder.prototype, "loadStatics"); // @ts-ignore jest.spyOn(PlatformBuilder.prototype, "listenServers"); jest.spyOn(InjectorService.prototype, "emit").mockResolvedValue(undefined); @@ -146,7 +196,7 @@ describe("PlatformBuilder", () => { jest.spyOn(ServerModule.prototype, "$beforeInit").mockReturnValue(undefined); jest.spyOn(ServerModule.prototype, "$beforeListen").mockReturnValue(undefined); jest.spyOn(ServerModule.prototype, "$onReady").mockReturnValue(undefined); - jest.spyOn(PlatformBuilder.prototype, "loadStatics").mockResolvedValue(undefined); + jest.spyOn(PlatformBuilder.prototype, "loadStatics"); // @ts-ignore jest.spyOn(PlatformBuilder.prototype, "listenServers"); jest.spyOn(InjectorService.prototype, "emit").mockResolvedValue(undefined); @@ -196,19 +246,30 @@ describe("PlatformBuilder", () => { expect(server.injector.emit).toBeCalledWith("$onDestroy"); }); }); - describe("callback()", () => { - it("should return the callback", async () => { - // WHEN - const server = await PlatformCustom.bootstrap(ServerModule, { - httpPort: false, - httpsPort: false + describe("adapter()", () => { + beforeAll(() => { + jest.spyOn(ServerModule.prototype, "$beforeRoutesInit").mockReturnValue(undefined); + jest.spyOn(ServerModule.prototype, "$afterRoutesInit").mockReturnValue(undefined); + jest.spyOn(ServerModule.prototype, "$afterInit").mockReturnValue(undefined); + jest.spyOn(ServerModule.prototype, "$afterListen").mockReturnValue(undefined); + jest.spyOn(ServerModule.prototype, "$beforeInit").mockReturnValue(undefined); + jest.spyOn(ServerModule.prototype, "$beforeListen").mockReturnValue(undefined); + jest.spyOn(ServerModule.prototype, "$onReady").mockReturnValue(undefined); + jest.spyOn(PlatformBuilder.prototype, "loadStatics").mockResolvedValue(undefined); + // @ts-ignore + jest.spyOn(PlatformBuilder.prototype, "listenServers"); + jest.spyOn(InjectorService.prototype, "emit").mockResolvedValue(undefined); + jest.spyOn(Platform.prototype, "addRoutes").mockReturnValue(undefined); + }); + it("should boostrap a custom platform", async () => { + const platformBuilder = await PlatformBuilder.bootstrap(ServerModule, { + adapter: FakeAdapter }); - expect(server.callback()).toEqual(server.app.raw); - - server.callback({} as any, {} as any); + expect(platformBuilder.adapter).toBeInstanceOf(FakeAdapter); }); }); + describe("useProvider()", () => { it("should add provider", async () => { // WHEN diff --git a/packages/platform/common/src/builder/PlatformBuilder.ts b/packages/platform/common/src/builder/PlatformBuilder.ts index d74a4605f71..73581cf2ff6 100644 --- a/packages/platform/common/src/builder/PlatformBuilder.ts +++ b/packages/platform/common/src/builder/PlatformBuilder.ts @@ -1,9 +1,11 @@ import {nameOf, Type} from "@tsed/core"; import {colors, InjectorService, ProviderOpts, setLoggerConfiguration, TokenProvider} from "@tsed/di"; import {getMiddlewaresForHook} from "@tsed/platform-middlewares"; +import {PlatformLayer} from "@tsed/platform-router"; import type {IncomingMessage, Server, ServerResponse} from "http"; import type Https from "https"; import {PlatformStaticsSettings} from "../config/interfaces/PlatformStaticsSettings"; +import {PlatformRouteDetails} from "../domain/PlatformRouteDetails"; import {Route} from "../interfaces/Route"; import {Platform} from "../services/Platform"; import {PlatformAdapter, PlatformBuilderSettings} from "../services/PlatformAdapter"; @@ -18,24 +20,24 @@ import {printRoutes} from "../utils/printRoutes"; /** * @platform */ -export class PlatformBuilder { - public static adapter: Type>; +export class PlatformBuilder { + public static adapter: Type>; readonly name: string = ""; protected startedAt = new Date(); protected current = new Date(); readonly #injector: InjectorService; readonly #rootModule: Type; - readonly #adapter: PlatformAdapter; + readonly #adapter: PlatformAdapter; #promise: Promise; #servers: (() => Promise)[]; #listeners: (Server | Https.Server)[] = []; - protected constructor(adapter: Type> | undefined, module: Type, settings: Partial) { + protected constructor(adapter: Type> | undefined, module: Type, settings: Partial) { this.#rootModule = module; const configuration = getConfiguration(settings, module); - const adapterKlass: Type> = adapter || (PlatformBuilder.adapter as any); + const adapterKlass: Type> = adapter || (PlatformBuilder.adapter as any); const name = nameOf(adapterKlass).replace("Platform", "").toLowerCase(); configuration.PLATFORM_NAME = name; @@ -46,7 +48,7 @@ export class PlatformBuilder { settings: configuration }); - this.#adapter = this.#injector.get>(PlatformAdapter)!; + this.#adapter = this.#injector.get>(PlatformAdapter)!; this.createHttpServers(); @@ -63,8 +65,8 @@ export class PlatformBuilder { return this.#injector.get(this.#rootModule); } - get app(): PlatformApplication { - return this.injector.get>(PlatformApplication)!; + get app(): PlatformApplication { + return this.injector.get>(PlatformApplication)!; } get platform() { @@ -108,7 +110,7 @@ export class PlatformBuilder { return this.settings.get("logger.disableBootstrapLog"); } - static create(module: Type, settings: PlatformBuilderSettings) { + static create(module: Type, settings: PlatformBuilderSettings) { return this.build(module, { httpsPort: false, httpPort: false, @@ -116,10 +118,7 @@ export class PlatformBuilder { }); } - static build( - module: Type, - {adapter, ...settings}: PlatformBuilderSettings - ) { + static build(module: Type, {adapter, ...settings}: PlatformBuilderSettings) { return new PlatformBuilder(adapter, module, settings); } @@ -128,18 +127,14 @@ export class PlatformBuilder { * @param module * @param settings */ - static async bootstrap(module: Type, settings: PlatformBuilderSettings) { - return this.build(module, settings).bootstrap(); + static async bootstrap(module: Type, settings: PlatformBuilderSettings) { + return this.build(module, settings).bootstrap(); } - callback(): (req: IncomingMessage, res: ServerResponse) => void; - callback(req: IncomingMessage, res: ServerResponse): void; + callback(): (req: IncomingMessage, res: ServerResponse) => any; + callback(req: IncomingMessage, res: ServerResponse): any; callback(req?: IncomingMessage, res?: ServerResponse) { - if (req && res) { - return this.app.callback()(req, res); - } - - return this.app.callback(); + return this.app.callback(req!, res!); } log(...data: any[]) { @@ -187,11 +182,9 @@ export class PlatformBuilder { await this.loadInjector(); - this.#adapter.useContext && this.#adapter.useContext(); - this.#adapter.useRouter && this.#adapter.useRouter(); + this.#adapter.useContext(); await this.loadRoutes(); - await this.logRoutes(); return this; } @@ -307,6 +300,16 @@ export class PlatformBuilder { await this.callHook("$afterRoutesInit"); this.#adapter.afterLoadRoutes && (await this.#adapter.afterLoadRoutes()); + + await this.mapRouters(); + } + + protected async mapRouters() { + const layers = this.platform.getLayers(); + + this.#adapter.mapLayers(layers); + + return this.logRoutes(layers.filter((layer) => layer.isProvider())); } protected diff() { @@ -336,13 +339,23 @@ export class PlatformBuilder { this.#listeners = await Promise.all(this.#servers.map((cb) => cb && cb())); } - protected async logRoutes() { - const {logger, platform} = this; + protected async logRoutes(layers: PlatformLayer[]) { + const {logger} = this; this.log("Routes mounted..."); if (!this.settings.get("logger.disableRoutesSummary") && !this.disableBootstrapLog) { - logger.info(printRoutes(await this.injector.alterAsync("$logRoutes", platform.getRoutes()))); + const routes: PlatformRouteDetails[] = layers.map((layer) => { + return { + url: layer.path, + method: layer.method, + name: layer.opts.name || `${layer.provider.className}.constructor()`, + className: layer.opts.className || layer.provider.className, + methodClassName: layer.opts.methodClassName || "" + } as PlatformRouteDetails; + }); + + logger.info(printRoutes(await this.injector.alterAsync("$logRoutes", routes))); } } } diff --git a/packages/platform/common/src/builder/PlatformControllerBuilder.spec.ts b/packages/platform/common/src/builder/PlatformControllerBuilder.spec.ts deleted file mode 100644 index 7c7814d5c7b..00000000000 --- a/packages/platform/common/src/builder/PlatformControllerBuilder.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import {All, buildRouter, ControllerProvider, Get, Use} from "@tsed/common"; -import {InjectorService} from "@tsed/di"; -import {EndpointMetadata, OperationMethods} from "@tsed/schema"; -import {Platform} from "../services/Platform"; -import {PlatformApplication} from "../services/PlatformApplication"; -import {PlatformHandler} from "../services/PlatformHandler"; -import {PlatformRouter} from "../services/PlatformRouter"; - -function getControllerBuilder({propertyKey = "test", withMiddleware = true}: any = {}) { - class TestCtrl { - @All("/") - allMethod() {} - - @Get("/") - getMethod() {} - - @Use("/") - use() {} - - @Use() - use2() {} - } - - const use = jest.fn(); - const router = { - get: use, - use, - post: use, - all: use - }; - - const injector = new InjectorService(); - - injector.addProvider(PlatformRouter, { - useClass: PlatformRouter - }); - injector.addProvider(PlatformHandler, { - useClass: PlatformHandler - }); - injector.addProvider(PlatformApplication, { - useClass: PlatformApplication - }); - injector.addProvider(Platform, { - useClass: Platform - }); - - const provider = new ControllerProvider(TestCtrl); - - provider.middlewares = { - use: [function controllerUse() {}], - useAfter: [function controllerAfter() {}], - useBefore: [function controllerBefore() {}] - }; - - const endpoint = EndpointMetadata.get(TestCtrl, propertyKey); - - endpoint.before([function endpointBefore() {}]); - endpoint.after([function endpointAfter() {}]); - endpoint.middlewares = [function endpointUse() {}]; - - return {endpoint, router, provider, injector, TestCtrl}; -} - -describe("buildRouter()", () => { - beforeEach(() => { - // @ts-ignore - jest.spyOn(PlatformRouter, "create").mockImplementation(() => ({ - $class: "PlatformRouter", - addRoute: jest.fn(), - get: jest.fn(), - use: jest.fn() - })); - }); - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should build controller with single endpoint", () => { - // GIVEN - const {endpoint, provider, injector} = getControllerBuilder({propertyKey: "getMethod"}); - - endpoint.operation.addOperationPath(OperationMethods.GET, "/", {isFinal: true}); - - // WHEN - const result: any = buildRouter(injector, provider); - - // THEN - expect(result.$class).toEqual("PlatformRouter"); - - // ENDPOINT - expect(result.addRoute).toBeCalledTimes(3); - }); -}); diff --git a/packages/platform/common/src/builder/PlatformControllerBuilder.ts b/packages/platform/common/src/builder/PlatformControllerBuilder.ts deleted file mode 100644 index d4f7b7b0a0b..00000000000 --- a/packages/platform/common/src/builder/PlatformControllerBuilder.ts +++ /dev/null @@ -1,96 +0,0 @@ -import {Type} from "@tsed/core"; -import {GlobalProviders, InjectorService, ProviderType} from "@tsed/di"; -import {getOperationsRoutes, OperationMethods} from "@tsed/schema"; -import {ControllerProvider} from "../domain/ControllerProvider"; -import {PlatformRouter} from "../services/PlatformRouter"; -import {PlatformMiddlewaresChain} from "../services/PlatformMiddlewaresChain"; - -GlobalProviders.createRegistry(ProviderType.CONTROLLER, ControllerProvider, { - onInvoke(provider: ControllerProvider, locals: any, {injector}) { - const router = createRouter(injector, provider); - locals.set(PlatformRouter, router); - } -}); - -/** - * @ignore - */ -function formatMethod(method: string | undefined) { - return (method === OperationMethods.CUSTOM ? "use" : method || "use").toLowerCase(); -} - -/** - * @ignore - */ -export function getRouter(injector: InjectorService, provider: ControllerProvider) { - return injector.get(provider.tokenRouter)!; -} - -/** - * @ignore - */ -export function createRouter(injector: InjectorService, provider: ControllerProvider): PlatformRouter { - const token = provider.tokenRouter; - - if (injector.has(token)) { - return getRouter(injector, provider); - } - - const router = PlatformRouter.create(injector, provider.routerOptions); - - return injector - .add(token, { - useValue: router - }) - .invoke(token); -} - -/** - * @ignore - * @param injector - * @param provider - * @param parentUseBefore - */ -export function buildRouter(injector: InjectorService, provider: ControllerProvider, parentUseBefore: any[] = []) { - const { - middlewares: {useBefore}, - children - } = provider; - - // Controller lifecycle - const router = createRouter(injector, provider); - - if (!router.isBuilt) { - router.isBuilt = true; - - const platformMiddlewaresChain = injector.get(PlatformMiddlewaresChain); - - // build all endpoints and his middlewares - getOperationsRoutes(provider.token).forEach((operationRoute) => { - const handlers = platformMiddlewaresChain?.get(provider, operationRoute, parentUseBefore); - - router.addRoute({ - handlers, - token: operationRoute.token, - method: formatMethod(operationRoute.method), - path: operationRoute.path, - isFinal: operationRoute.isFinal - }); - }); - - const middlewares: any[] = [...parentUseBefore, ...useBefore]; - // build children controllers - children.forEach((child: Type) => { - const childProvider = injector.getProvider(child) as ControllerProvider; - - /* istanbul ignore next */ - if (!childProvider) { - throw new Error("Controller component not found in the ControllerRegistry"); - } - - router.use(childProvider.path, buildRouter(injector, childProvider, middlewares)); - }); - } - - return router; -} diff --git a/packages/platform/common/src/decorators/params/error.ts b/packages/platform/common/src/decorators/params/error.ts index 0356288dbd0..2460f7bdf74 100644 --- a/packages/platform/common/src/decorators/params/error.ts +++ b/packages/platform/common/src/decorators/params/error.ts @@ -9,7 +9,7 @@ import {ParamTypes, UseParam} from "@tsed/platform-params"; export function Err(): Function { return UseParam({ paramType: ParamTypes.ERR, - dataPath: "err", + dataPath: "$ctx.error", useValidation: false, useConverter: false }); diff --git a/packages/platform/common/src/domain/AnyToPromiseWithCtx.spec.ts b/packages/platform/common/src/domain/AnyToPromiseWithCtx.spec.ts new file mode 100644 index 00000000000..09466bf5080 --- /dev/null +++ b/packages/platform/common/src/domain/AnyToPromiseWithCtx.spec.ts @@ -0,0 +1,20 @@ +import {PlatformTest} from "../services/PlatformTest"; +import {AnyToPromiseWithCtx} from "./AnyToPromiseWithCtx"; + +describe("AnyToPromiseWithCtx", () => { + beforeEach(() => PlatformTest.create()); + afterEach(() => PlatformTest.reset()); + + it("should return value", async () => { + const $ctx = PlatformTest.createRequestContext(); + const resolver = new AnyToPromiseWithCtx($ctx); + + const result = await resolver.call(() => "hello"); + + expect(result).toEqual({ + data: "hello", + state: "RESOLVED", + type: "DATA" + }); + }); +}); diff --git a/packages/platform/common/src/domain/AnyToPromiseWithCtx.ts b/packages/platform/common/src/domain/AnyToPromiseWithCtx.ts index 263cf4bf043..cccf733fc33 100644 --- a/packages/platform/common/src/domain/AnyToPromiseWithCtx.ts +++ b/packages/platform/common/src/domain/AnyToPromiseWithCtx.ts @@ -1,22 +1,12 @@ import {AnyPromiseResult, AnyToPromise} from "@tsed/core"; import {PlatformContext} from "./PlatformContext"; -/** - * @ignore - */ -export interface HandlerContextOptions { - $ctx: PlatformContext; - err?: any; -} - export class AnyToPromiseWithCtx extends AnyToPromise { public $ctx: PlatformContext; - public err: unknown; - constructor({$ctx, err}: HandlerContextOptions) { + constructor($ctx: PlatformContext) { super(); this.$ctx = $ctx; - this.err = err; } isDone(): boolean { @@ -30,7 +20,7 @@ export class AnyToPromiseWithCtx extends AnyToPromise { } destroy() { - this.$ctx = null as any; + (this.$ctx as any) = null; } isCanceledResponse(process: any) { diff --git a/packages/platform/common/src/domain/HandlerMetadata.spec.ts b/packages/platform/common/src/domain/HandlerMetadata.spec.ts deleted file mode 100644 index 0690d323fae..00000000000 --- a/packages/platform/common/src/domain/HandlerMetadata.spec.ts +++ /dev/null @@ -1,183 +0,0 @@ -import {Controller, Err, Get, HandlerType, Middleware, Next, Req} from "@tsed/common"; -import {useCtxHandler} from "../utils/useCtxHandler"; -import {HandlerMetadata} from "./HandlerMetadata"; - -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).toEqual(false); - expect(handlerMetadata.type).toEqual(HandlerType.CTX_FN); - expect(handlerMetadata.hasNextFunction).toEqual(false); - expect(handlerMetadata.hasErrorParam).toEqual(false); - expect(handlerMetadata.toString()).toEqual(""); - }); - }); - describe("from function", () => { - it("should create a new handlerMetadata with right metadata", () => { - // GIVEN - const options = { - target(req: any, res: any, next: any) {} - }; - // WHEN - const handlerMetadata = new HandlerMetadata(options); - - // THEN - expect(handlerMetadata.injectable).toEqual(false); - expect(handlerMetadata.type).toEqual(HandlerType.RAW_FN); - expect(handlerMetadata.hasNextFunction).toEqual(true); - expect(handlerMetadata.hasErrorParam).toEqual(false); - expect(handlerMetadata.toString()).toEqual(""); - }); - }); - describe("from function err", () => { - it("should create a new handlerMetadata with right metadata", () => { - // GIVEN - const options = { - target(err: any, req: any, res: any, next: any) {} - }; - // WHEN - const handlerMetadata = new HandlerMetadata(options); - - // THEN - expect(handlerMetadata.injectable).toEqual(false); - expect(handlerMetadata.type).toEqual(HandlerType.RAW_ERR_FN); - expect(handlerMetadata.hasNextFunction).toEqual(true); - expect(handlerMetadata.hasErrorParam).toEqual(true); - expect(handlerMetadata.propertyKey).toBeUndefined(); - expect(handlerMetadata.toString()).toEqual(""); - }); - }); - describe("from function without nextFn", () => { - it("should create a new handlerMetadata with right metadata", () => { - // GIVEN - const options = { - target(req: any, res: any) {} - }; - - // WHEN - const handlerMetadata = new HandlerMetadata(options); - - // THEN - expect(handlerMetadata.injectable).toEqual(false); - expect(handlerMetadata.type).toEqual(HandlerType.RAW_FN); - expect(handlerMetadata.hasNextFunction).toEqual(false); - expect(handlerMetadata.hasErrorParam).toEqual(false); - expect(handlerMetadata.propertyKey).toBeUndefined(); - expect(handlerMetadata.toString()).toEqual(""); - }); - }); - describe("from endpoint/middleware without injection", () => { - it("should create a new handlerMetadata with right metadata", () => { - // GIVEN - @Controller("/") - class Test { - @Get("/") - test(req: any, res: any, next: any) {} - } - - const options = { - target: Test, - propertyKey: "test", - type: HandlerType.ENDPOINT - }; - // WHEN - const handlerMetadata = new HandlerMetadata(options); - - // THEN - expect(handlerMetadata.injectable).toEqual(false); - expect(handlerMetadata.type).toEqual(HandlerType.ENDPOINT); - expect(handlerMetadata.hasNextFunction).toEqual(true); - expect(handlerMetadata.hasErrorParam).toEqual(false); - expect(handlerMetadata.propertyKey).toEqual("test"); - expect(handlerMetadata.toString()).toEqual("Test.test"); - }); - }); - describe("from endpoint/middleware with injection", () => { - it("should create a new handlerMetadata with right metadata", () => { - // GIVEN - @Controller("/") - class Test { - @Get("/") - test(@Req() req: Req, @Next() next: Next) {} - } - - const options = { - target: Test, - propertyKey: "test", - type: HandlerType.ENDPOINT - }; - // WHEN - const handlerMetadata = new HandlerMetadata(options); - - // THEN - expect(handlerMetadata.injectable).toEqual(true); - expect(handlerMetadata.type).toEqual(HandlerType.ENDPOINT); - expect(handlerMetadata.hasNextFunction).toEqual(true); - expect(handlerMetadata.hasErrorParam).toEqual(false); - expect(handlerMetadata.propertyKey).toEqual("test"); - expect(handlerMetadata.toString()).toEqual("Test.test"); - - expect(handlerMetadata.getParams()[0].paramType).toEqual("REQUEST"); - expect(handlerMetadata.getParams()[1].paramType).toEqual("NEXT_FN"); - }); - }); - describe("from middleware without injection and error", () => { - it("should create a new handlerMetadata with right metadata", () => { - // GIVEN - @Middleware() - class Test { - use(error: any, req: any, res: any, next: any) {} - } - - const options = { - target: Test, - propertyKey: "use", - type: HandlerType.MIDDLEWARE - }; - // WHEN - const handlerMetadata = new HandlerMetadata(options); - - // THEN - expect(handlerMetadata.injectable).toEqual(false); - expect(handlerMetadata.type).toEqual(HandlerType.RAW_ERR_FN); - expect(handlerMetadata.hasNextFunction).toEqual(true); - expect(handlerMetadata.hasErrorParam).toEqual(true); - expect(handlerMetadata.propertyKey).toEqual("use"); - expect(handlerMetadata.toString()).toEqual("Test.use"); - }); - }); - describe("from middleware with injection and error", () => { - it("should create a new handlerMetadata with right metadata", () => { - // WHEN - @Middleware() - class Test { - use(@Err() error: any, @Next() next: Next) {} - } - - const options = { - target: Test, - propertyKey: "use", - type: HandlerType.MIDDLEWARE - }; - // WHEN - const handlerMetadata = new HandlerMetadata(options); - - // THEN - expect(handlerMetadata.injectable).toEqual(true); - expect(handlerMetadata.type).toEqual(HandlerType.ERR_MIDDLEWARE); - expect(handlerMetadata.hasNextFunction).toEqual(true); - expect(handlerMetadata.hasErrorParam).toEqual(true); - expect(handlerMetadata.propertyKey).toEqual("use"); - expect(handlerMetadata.toString()).toEqual("Test.use"); - }); - }); -}); diff --git a/packages/platform/common/src/domain/HandlerMetadata.ts b/packages/platform/common/src/domain/HandlerMetadata.ts deleted file mode 100644 index 351b31f6b67..00000000000 --- a/packages/platform/common/src/domain/HandlerMetadata.ts +++ /dev/null @@ -1,79 +0,0 @@ -import {nameOf, Type} from "@tsed/core"; -import {ProviderScope} from "@tsed/di"; -import {ParamTypes} from "@tsed/platform-params"; -import {JsonParameterStore} from "@tsed/schema"; -import {HandlerType} from "../interfaces/HandlerType"; -import {PlatformRouteOptions, PlatformRouteWithoutHandlers} from "../interfaces/PlatformRouteOptions"; - -export interface HandlerMetadataOptions { - target: (Type | Function) & {type?: HandlerType}; - routeOptions?: PlatformRouteWithoutHandlers; - token?: Type; - propertyKey?: string | symbol; - scope?: ProviderScope; - type?: HandlerType; -} - -export class HandlerMetadata { - readonly target: any; - readonly token: Type; - readonly propertyKey: string | symbol; - readonly injectable: boolean = false; - readonly type: HandlerType = HandlerType.RAW_FN; - readonly hasNextFunction: boolean = false; - readonly routeOptions: Partial; - readonly scope: ProviderScope; - - handler: any; - - constructor(options: HandlerMetadataOptions) { - const {target, token, propertyKey, type, scope, routeOptions} = options; - - this.type = type || target.type || HandlerType.RAW_FN; - this.scope = scope || ProviderScope.SINGLETON; - this.routeOptions = routeOptions || {}; - const handler = propertyKey ? target.prototype[propertyKey] : target; - - if (propertyKey) { - this.target = target; - this.token = token!; - this.propertyKey = propertyKey; - this.hasNextFunction = this.hasParamType(ParamTypes.NEXT_FN); - - if (this.hasParamType(ParamTypes.ERR)) { - this.type = HandlerType.ERR_MIDDLEWARE; - } - - this.injectable = JsonParameterStore.getParams(target as any, propertyKey).length > 0; - } else { - this.handler = handler; - } - - if (!this.injectable) { - if (handler.length === 4) { - this.type = HandlerType.RAW_ERR_FN; - } - this.hasNextFunction = handler.length >= 3; - } - } - - get hasErrorParam() { - return this.type === HandlerType.ERR_MIDDLEWARE || this.type === HandlerType.RAW_ERR_FN; - } - - public getParams() { - return JsonParameterStore.getParams(this.target, this.propertyKey) || []; - } - - public hasParamType(paramType: any): boolean { - return this.getParams().findIndex((p) => p.paramType === paramType) > -1; - } - - public isFinal() { - return this.routeOptions?.isFinal || false; - } - - toString() { - return [this.target && nameOf(this.target), this.propertyKey].filter(Boolean).join("."); - } -} diff --git a/packages/platform/common/src/domain/PlatformContext.spec.ts b/packages/platform/common/src/domain/PlatformContext.spec.ts index a6d53b785d3..5390161b61e 100644 --- a/packages/platform/common/src/domain/PlatformContext.spec.ts +++ b/packages/platform/common/src/domain/PlatformContext.spec.ts @@ -1,5 +1,6 @@ import {PlatformApplication, PlatformTest} from "@tsed/common"; import {nameOf} from "@tsed/core"; +import {PlatformHandlerMetadata} from "@tsed/platform-router"; import {PlatformContext} from "./PlatformContext"; describe("PlatformContext", () => { @@ -26,6 +27,9 @@ describe("PlatformContext", () => { context.endpoint = {} as any; context.logger.info("test"); + context.handlerMetadata = {} as any; + context.endpoint = {} as any; + expect(context.id).toEqual("id"); expect(context.dateStart).toBeInstanceOf(Date); expect(context.container).toBeInstanceOf(Map); @@ -35,6 +39,7 @@ describe("PlatformContext", () => { expect(context.getReq()).toEqual(context.request.raw); expect(context.getRes()).toEqual(context.response.raw); expect(context.app).toBeInstanceOf(PlatformApplication); + expect(context.handlerMetadata).toEqual({}); expect(context.endpoint).toEqual({}); expect(nameOf(context.getApp())).toEqual("FakeRawDriver"); }); @@ -87,6 +92,30 @@ describe("PlatformContext", () => { await context.destroy(); + expect(context.isDone()).toEqual(true); + }); + it("should return done when the response is empty", async () => { + // @ts-ignore + const context = new PlatformContext({ + id: "id", + event: { + response: PlatformTest.createResponse(), + request: PlatformTest.createRequest({ + url: "/" + }) + }, + logger: { + info: jest.fn() + }, + injector: PlatformTest.injector, + maxStackSize: 0, + ignoreUrlPatterns: ["/admin"] + }); + + expect(context.isDone()).toEqual(false); + + await context.destroy(); + expect(context.isDone()).toEqual(true); }); }); diff --git a/packages/platform/common/src/domain/PlatformContext.ts b/packages/platform/common/src/domain/PlatformContext.ts index fcadacc4c9a..5ffda6ff417 100644 --- a/packages/platform/common/src/domain/PlatformContext.ts +++ b/packages/platform/common/src/domain/PlatformContext.ts @@ -1,4 +1,5 @@ import {ContextMethods, DIContext, DIContextOptions} from "@tsed/di"; +import {PlatformHandlerMetadata} from "@tsed/platform-router"; import {EndpointMetadata} from "@tsed/schema"; import {IncomingMessage, ServerResponse} from "http"; import {IncomingEvent} from "../interfaces/IncomingEvent"; @@ -31,6 +32,10 @@ export class PlatformContext { - it("should create JsonOperationRoute instance", () => { - class Test { - @Get("/") - get(@QueryParams("test") test: string) {} - } - - const endpoint = JsonEntityStore.fromMethod(Test, "get"); - const routeDetails = new PlatformRouteDetails({ - token: Test, - endpoint, - operationPath: {method: "GET", path: "/"}, - basePath: "/base" - }); - - expect(routeDetails.toJSON()).toMatchObject({ - className: "Test", - method: "GET", - methodClassName: "get", - name: "Test.get()", - rawBody: false, - url: "/base/" - }); - }); -}); diff --git a/packages/platform/common/src/domain/PlatformRouteDetails.ts b/packages/platform/common/src/domain/PlatformRouteDetails.ts index b515373de76..b0aa1c4b22f 100644 --- a/packages/platform/common/src/domain/PlatformRouteDetails.ts +++ b/packages/platform/common/src/domain/PlatformRouteDetails.ts @@ -1,28 +1,7 @@ -import {ParamTypes} from "@tsed/platform-params"; -import {EndpointMetadata, JsonOperationRoute} from "@tsed/schema"; -import {ControllerProvider} from "./ControllerProvider"; - -export class PlatformRouteDetails extends JsonOperationRoute { - readonly rawBody: boolean; - readonly provider: ControllerProvider; - - constructor(options: Partial) { - super(options); - this.rawBody = !!this.endpoint.parameters.find((param) => param.paramType === ParamTypes.RAW_BODY); - - // TMP FIX - this.endpoint.set("route", this.url); - } - - toJSON() { - return { - method: this.method, - name: this.name, - url: this.url, - className: this.className, - methodClassName: this.methodClassName, - parameters: this.parameters, - rawBody: this.rawBody - }; - } +export interface PlatformRouteDetails { + method: string; + name: string; + url: string; + className: string; + methodClassName: string; } diff --git a/packages/platform/common/src/exports.ts b/packages/platform/common/src/exports.ts index b7c8cb5e972..500907d3c53 100644 --- a/packages/platform/common/src/exports.ts +++ b/packages/platform/common/src/exports.ts @@ -9,6 +9,7 @@ export * from "@tsed/platform-cache"; export * from "@tsed/platform-middlewares"; export * from "@tsed/components-scan"; export * from "@tsed/platform-log-middleware"; +export * from "@tsed/platform-router"; export {$log, Logger} from "@tsed/logger"; diff --git a/packages/platform/common/src/index.ts b/packages/platform/common/src/index.ts index 43cecaf3fba..b365fc26103 100644 --- a/packages/platform/common/src/index.ts +++ b/packages/platform/common/src/index.ts @@ -4,7 +4,6 @@ export * from "./exports"; export * from "./builder/PlatformBuilder"; -export * from "./builder/PlatformControllerBuilder"; export * from "./config/interfaces/ConverterSettings"; export * from "./config/interfaces/PlatformLoggerSettings"; export * from "./config/interfaces/PlatformMulterSettings"; @@ -19,9 +18,7 @@ export * from "./decorators/params/next"; export * from "./decorators/params/request"; export * from "./decorators/params/response"; export * from "./domain/AnyToPromiseWithCtx"; -export * from "./domain/ControllerProvider"; export * from "./domain/EndpointMetadata"; -export * from "./domain/HandlerMetadata"; export * from "./domain/PlatformContext"; export * from "./domain/PlatformRouteDetails"; export * from "./interfaces/AfterInit"; @@ -41,7 +38,6 @@ export * from "./interfaces/ResponseErrorObject"; export * from "./interfaces/Route"; export * from "./middlewares/PlatformAcceptMimesMiddleware"; export * from "./middlewares/PlatformMulterMiddleware"; -export * from "./middlewares/bindEndpointMiddleware"; export * from "./services/FakeAdapter"; export * from "./services/Platform"; export * from "./services/PlatformAdapter"; @@ -50,13 +46,12 @@ export * from "./services/PlatformHandler"; export * from "./services/PlatformMiddlewaresChain"; export * from "./services/PlatformRequest"; export * from "./services/PlatformResponse"; -export * from "./services/PlatformRouter"; export * from "./services/PlatformTest"; export * from "./utils/createContext"; -export * from "./utils/createHandlerMetadata"; export * from "./utils/createHttpServer"; export * from "./utils/createHttpsServer"; export * from "./utils/createInjector"; +export * from "./utils/ensureContext"; export * from "./utils/getStaticsOptions"; export * from "./utils/listenServer"; export * from "./utils/mapReturnedResponse"; diff --git a/packages/platform/common/src/interfaces/Route.ts b/packages/platform/common/src/interfaces/Route.ts index fb8632ff564..55cf51319b0 100644 --- a/packages/platform/common/src/interfaces/Route.ts +++ b/packages/platform/common/src/interfaces/Route.ts @@ -1,5 +1,4 @@ -import {TokenProvider} from "@tsed/di"; -import {ControllerProvider} from "../domain/ControllerProvider"; +import {Provider, TokenProvider} from "@tsed/di"; export interface Route extends Record { route: string; @@ -8,5 +7,5 @@ export interface Route extends Record { export interface RouteController { route: string; - provider: ControllerProvider; + provider: Provider; } diff --git a/packages/platform/common/src/middlewares/PlatformAcceptMimesMiddleware.spec.ts b/packages/platform/common/src/middlewares/PlatformAcceptMimesMiddleware.spec.ts index f3ea4857253..87d248c5f33 100644 --- a/packages/platform/common/src/middlewares/PlatformAcceptMimesMiddleware.spec.ts +++ b/packages/platform/common/src/middlewares/PlatformAcceptMimesMiddleware.spec.ts @@ -53,6 +53,7 @@ describe("PlatformMimesMiddleware", () => { event: {request}, endpoint }); + jest.spyOn(request, "accepts"); const middleware = await PlatformTest.invoke(PlatformAcceptMimesMiddleware); diff --git a/packages/platform/common/src/middlewares/PlatformMulterMiddleware.ts b/packages/platform/common/src/middlewares/PlatformMulterMiddleware.ts index 90391498ed0..fd60bb77bd0 100644 --- a/packages/platform/common/src/middlewares/PlatformMulterMiddleware.ts +++ b/packages/platform/common/src/middlewares/PlatformMulterMiddleware.ts @@ -1,8 +1,8 @@ -import type {MulterError} from "multer"; -import {Constant, Inject, Value} from "@tsed/di"; +import {Inject, Value} from "@tsed/di"; import {BadRequest} from "@tsed/exceptions"; import {Middleware, MiddlewareMethods} from "@tsed/platform-middlewares"; import {Context} from "@tsed/platform-params"; +import type {MulterError} from "multer"; import {PlatformMulterField, PlatformMulterSettings} from "../config/interfaces/PlatformMulterSettings"; import {PlatformApplication} from "../services/PlatformApplication"; diff --git a/packages/platform/common/src/middlewares/bindEndpointMiddleware.spec.ts b/packages/platform/common/src/middlewares/bindEndpointMiddleware.spec.ts deleted file mode 100644 index f68cc4e2e40..00000000000 --- a/packages/platform/common/src/middlewares/bindEndpointMiddleware.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {bindEndpointMiddleware, EndpointMetadata, PlatformTest} from "@tsed/common"; - -class Test { - test() {} -} - -describe("bindEndpointMiddleware", () => { - beforeEach(() => PlatformTest.create()); - afterEach(() => PlatformTest.reset()); - it("should bind endpoint to the request", async () => { - const endpoint = new EndpointMetadata({ - target: Test, - propertyKey: "test" - } as any); - - const ctx = PlatformTest.createRequestContext({endpoint}); - - bindEndpointMiddleware(endpoint)(ctx); - - expect(ctx.endpoint).toEqual(endpoint); - }); -}); diff --git a/packages/platform/common/src/middlewares/bindEndpointMiddleware.ts b/packages/platform/common/src/middlewares/bindEndpointMiddleware.ts deleted file mode 100644 index 8592715c97c..00000000000 --- a/packages/platform/common/src/middlewares/bindEndpointMiddleware.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type {EndpointMetadata} from "../domain/EndpointMetadata"; -import type {PlatformContext} from "../domain/PlatformContext"; - -/** - * @ignore - */ -export function bindEndpointMiddleware(endpoint: EndpointMetadata) { - return (ctx: PlatformContext) => { - ctx.endpoint = endpoint; - }; -} diff --git a/packages/platform/common/src/services/FakeAdapter.ts b/packages/platform/common/src/services/FakeAdapter.ts index 81c5e5dda60..a1d7b29efba 100644 --- a/packages/platform/common/src/services/FakeAdapter.ts +++ b/packages/platform/common/src/services/FakeAdapter.ts @@ -1,9 +1,11 @@ import {ProviderOpts} from "@tsed/di"; -import {PlatformAdapter} from "./PlatformAdapter"; +import {PlatformContextHandler, PlatformLayer} from "@tsed/platform-router"; import {PlatformMulter, PlatformMulterSettings} from "../config/interfaces/PlatformMulterSettings"; import {PlatformStaticsOptions} from "../config/interfaces/PlatformStaticsSettings"; +import {PlatformContext} from "../domain/PlatformContext"; +import {PlatformAdapter} from "./PlatformAdapter"; -export class FakeAdapter implements PlatformAdapter { +export class FakeAdapter implements PlatformAdapter { providers: ProviderOpts[] = []; static createFakeRawDriver() { @@ -59,4 +61,12 @@ export class FakeAdapter implements PlatformAdapter { bodyParser(type: string): any { return () => {}; } + + mapLayers(layers: PlatformLayer[]) {} + + mapHandler(handler: PlatformContextHandler) { + return handler; + } + + useContext() {} } diff --git a/packages/platform/common/src/services/Platform.spec.ts b/packages/platform/common/src/services/Platform.spec.ts index 89fe2909d4e..3ba783dde9b 100644 --- a/packages/platform/common/src/services/Platform.spec.ts +++ b/packages/platform/common/src/services/Platform.spec.ts @@ -1,10 +1,7 @@ import {Controller} from "@tsed/di"; +import {Get} from "@tsed/schema"; import {Platform} from "./Platform"; -import {PlatformApplication} from "./PlatformApplication"; import {PlatformTest} from "./PlatformTest"; -import {Get} from "@tsed/schema"; -import {PlatformRouter} from "./PlatformRouter"; -import {ControllerProvider} from "../domain/ControllerProvider"; @Controller("/my-route") class MyCtrl { @@ -35,87 +32,30 @@ describe("Platform", () => { describe("addRoute", () => { it("should add a route", async () => { // GIVEN - const provider: ControllerProvider = PlatformTest.injector.getProvider(MyCtrl)! as ControllerProvider; const platform = await PlatformTest.get(Platform); jest.spyOn(platform.app, "use"); // WHEN - platform.addRoute("/test", MyCtrl); - const router = PlatformTest.get(provider.tokenRouter); - - // THEN - expect(platform.getMountedControllers()).toEqual([{provider, route: "/test/my-route"}]); - expect(platform.app.use).toBeCalledWith("/test/my-route", router.raw); + platform.addRoutes([{route: "/test", token: MyCtrl}]); }); it("should add nested controllers", async () => { // GIVEN - const nestedProvider: ControllerProvider = PlatformTest.injector.getProvider(MyNestedCtrl)! as ControllerProvider; - const subProvider: ControllerProvider = PlatformTest.injector.getProvider(MySubCtrl)! as ControllerProvider; const platform = await PlatformTest.get(Platform); jest.spyOn(platform.app, "use"); // WHEN - platform.addRoute("/test", MyNestedCtrl); - const router = PlatformTest.get(nestedProvider.tokenRouter); + platform.addRoutes([{route: "/rest", token: MyNestedCtrl}]); + platform.addRoutes([{route: "/rest", token: MySubCtrl}]); - // THEN - expect(platform.getMountedControllers()).toEqual([ - {provider: subProvider, route: "/test/my-route/my-sub-route"}, - {provider: nestedProvider, route: "/test/my-route"} - ]); - expect(platform.app.use).toBeCalledWith("/test/my-route", router.raw); - }); - }); - describe("getRoutes", () => { - it("should add a route", async () => { - // GIVEN - const driver = { - use: jest.fn(), - raw: { - use: jest.fn() - } - }; - - const platform = await PlatformTest.invoke(Platform, [ - { - token: PlatformApplication, - use: driver - } - ]); - - // WHEN - platform.addRoute("/test", MyCtrl); - platform.addRoute("/test-2", class Test {}); - - const result = platform.getRoutes(); - - // THEN - expect(result.map((o) => o.toJSON())).toEqual([ - { - className: "MyCtrl", - method: "GET", - methodClassName: "get", - name: "MyCtrl.get()", - parameters: [], - rawBody: false, - url: "/test/my-route/" - } - ]); + platform.getLayers(); + const result = platform.getMountedControllers(); // THEN - expect(platform.routes.map((o) => o.toJSON())).toEqual([ - { - className: "MyCtrl", - method: "GET", - methodClassName: "get", - name: "MyCtrl.get()", - parameters: [], - rawBody: false, - url: "/test/my-route/" - } - ]); + expect(result.length).toEqual(2); + expect(result[0].route).toEqual("/rest"); + expect(result[1].route).toEqual("/rest/my-route"); }); }); }); diff --git a/packages/platform/common/src/services/Platform.ts b/packages/platform/common/src/services/Platform.ts index 46b600029f1..9c1f5973f77 100644 --- a/packages/platform/common/src/services/Platform.ts +++ b/packages/platform/common/src/services/Platform.ts @@ -1,11 +1,9 @@ -import {Type} from "@tsed/core"; -import {Injectable, InjectorService, ProviderScope, ProviderType, TokenProvider} from "@tsed/di"; -import {concatPath, EndpointMetadata, getJsonEntityStore, getOperationsRoutes, JsonEntityStore} from "@tsed/schema"; -import {buildRouter, createRouter, getRouter} from "../builder/PlatformControllerBuilder"; -import {ControllerProvider} from "../domain/ControllerProvider"; -import {PlatformRouteDetails} from "../domain/PlatformRouteDetails"; +import {Injectable, InjectorService, ProviderScope, TokenProvider} from "@tsed/di"; +import {PlatformHandlerMetadata, PlatformRouters} from "@tsed/platform-router"; +import {ControllerProvider} from "@tsed/di"; import {Route, RouteController} from "../interfaces/Route"; import {PlatformApplication} from "./PlatformApplication"; +import {PlatformHandler} from "./PlatformHandler"; import {PlatformMiddlewaresChain} from "./PlatformMiddlewaresChain"; /** @@ -15,70 +13,67 @@ import {PlatformMiddlewaresChain} from "./PlatformMiddlewaresChain"; */ @Injectable({ scope: ProviderScope.SINGLETON, - imports: [PlatformMiddlewaresChain] + imports: [] }) export class Platform { - private _routes: PlatformRouteDetails[] = []; - private _controllers: RouteController[] = []; - - constructor(readonly injector: InjectorService, readonly platformApplication: PlatformApplication) { - this.createRouters(); + #controllers: Map = new Map(); + + constructor( + readonly injector: InjectorService, + readonly platformApplication: PlatformApplication, + readonly platformRouters: PlatformRouters, + readonly platformHandler: PlatformHandler, + readonly platformMiddlewaresChain: PlatformMiddlewaresChain + ) { + // configure the router module + platformRouters.hooks + .on("alterEndpointHandlers", platformMiddlewaresChain.get.bind(platformMiddlewaresChain)) + .on("alterHandler", (handler: Function, handlerMetadata: PlatformHandlerMetadata) => { + handler = handlerMetadata.isRawMiddleware() ? handler : this.platformHandler.createHandler(handler as any, handlerMetadata); + + return platformApplication.adapter.mapHandler(handler, handlerMetadata); + }); + + platformRouters.prebuild(); } get app() { return this.platformApplication; } - get routes(): PlatformRouteDetails[] { - return this._routes; - } - public addRoutes(routes: Route[]) { routes.forEach((routeSettings) => { this.addRoute(routeSettings.route, routeSettings.token); }); } - public addRoute(basePath: string, token: TokenProvider) { - const {injector} = this; - const provider = injector.getProvider(token) as ControllerProvider; + public addRoute(route: string, token: TokenProvider) { + const provider = this.injector.getProvider(token) as ControllerProvider; if (!provider || provider.hasParent()) { - return; + return this; } - this._controllers.push(...this.getAllControllers(basePath, token)); + const router = this.platformRouters.from(provider.token); - const ctrlPath = concatPath(basePath, JsonEntityStore.from(provider.token).path); + this.app.use(route, router); - this.app.use(ctrlPath, ...[].concat(getRouter(injector, provider).callback())); + return this; + } - this._routes = getOperationsRoutes(provider.token, { - withChildren: true, - basePath - }).reduce((routes, operationRoute) => { - if (injector.hasProvider(token)) { - const provider = injector.getProvider(operationRoute.token) as ControllerProvider; - const route = new PlatformRouteDetails({ - ...operationRoute, - provider - }); + public getLayers() { + this.#controllers = new Map(); - routes = routes.concat(route); + return this.platformRouters.getLayers(this.app).map((layer) => { + if (layer.isProvider()) { + this.#controllers.set(layer.provider.token, { + route: String(layer.path).split(layer.provider.path)[0], + provider: layer.provider + }); } - return routes; - }, this._routes); - - return this; - } - - /** - * Get all routes built by TsExpressDecorators and mounted on Express application. - * @returns {PlatformRouteDetails[]} - */ - public getRoutes(): PlatformRouteDetails[] { - return this._routes; + return layer; + }); } /** @@ -86,58 +81,6 @@ export class Platform { * @returns {RouteController[]} */ public getMountedControllers(): RouteController[] { - return this._controllers; - } - - protected $onInit() { - this.buildControllers(); - } - - /** - * Create routers from the collected controllers. - * @private - */ - private createRouters() { - const {injector} = this; - - injector.getProviders(ProviderType.CONTROLLER).map((provider: ControllerProvider) => { - createRouter(injector, provider); - }); - } - - /** - * Get all router controllers from the controller token. - * @private - */ - private getAllControllers(basePath: string, token: Type | any): RouteController[] { - const store: JsonEntityStore = token.isStore ? token : getJsonEntityStore(token); - const ctrlPath = concatPath(basePath, JsonEntityStore.from(token).path); - const children = store.get("childrenControllers", []); - - return children - .reduce((controllers, token) => { - const childBasePath = concatPath(basePath, store.path); - return controllers.concat(this.getAllControllers(childBasePath, token)); - }, []) - .concat([ - { - route: ctrlPath, - provider: this.injector.getProvider(token) as ControllerProvider - } - ]); - } - - /** - * Create controllers from DI - * @private - */ - private buildControllers() { - const {injector} = this; - - injector.getProviders(ProviderType.CONTROLLER).map((provider: ControllerProvider) => { - if (!provider.hasParent()) { - return buildRouter(injector, provider); - } - }); + return [...this.#controllers.values()]; } } diff --git a/packages/platform/common/src/services/PlatformAdapter.ts b/packages/platform/common/src/services/PlatformAdapter.ts index cb4fb6332c2..9c69fb8eec1 100644 --- a/packages/platform/common/src/services/PlatformAdapter.ts +++ b/packages/platform/common/src/services/PlatformAdapter.ts @@ -1,11 +1,12 @@ import {Type} from "@tsed/core"; import {InjectorService, ProviderOpts, registerProvider} from "@tsed/di"; +import {PlatformContextHandler, PlatformHandlerMetadata, PlatformLayer} from "@tsed/platform-router"; import {PlatformMulter, PlatformMulterSettings} from "../config/interfaces/PlatformMulterSettings"; import {PlatformStaticsOptions} from "../config/interfaces/PlatformStaticsSettings"; -import {RouterOptions} from "express"; +import {PlatformContext} from "../domain/PlatformContext"; import {FakeAdapter} from "./FakeAdapter"; -export abstract class PlatformAdapter { +export abstract class PlatformAdapter { /** * Load providers in top priority */ @@ -16,12 +17,24 @@ export abstract class PlatformAdapter any; beforeLoadRoutes?: () => Promise; afterLoadRoutes?: () => Promise; - useRouter?: () => any; - useContext?: () => any; + /** + * create initial context + */ + abstract useContext: () => any; + /** + * Map router layer to the targeted framework + */ + abstract mapLayers: (layer: PlatformLayer[]) => void; + /** + * Map handler to the targeted framework + */ + abstract mapHandler: (handler: Function, layer: PlatformHandlerMetadata) => Function; + /** + * Return the app instance + */ abstract app(): {app: App; callback(): any}; - abstract router(routerOptions?: Partial): {router: Router; callback(): any}; /** * Return the statics middlewares * @param endpoint @@ -43,8 +56,8 @@ export abstract class PlatformAdapter): any; } -export interface PlatformBuilderSettings extends Partial { - adapter?: Type>; +export interface PlatformBuilderSettings extends Partial { + adapter?: Type>; } registerProvider({ diff --git a/packages/platform/common/src/services/PlatformApplication.spec.ts b/packages/platform/common/src/services/PlatformApplication.spec.ts index 7096a20a4f4..793c26a988d 100644 --- a/packages/platform/common/src/services/PlatformApplication.spec.ts +++ b/packages/platform/common/src/services/PlatformApplication.spec.ts @@ -1,7 +1,9 @@ -import {PlatformRouter, PlatformTest} from "@tsed/common"; +import {createContext, PlatformTest} from "@tsed/common"; import {PlatformApplication} from "./PlatformApplication"; import {PlatformHandler} from "./PlatformHandler"; +jest.mock("../utils/createContext"); + function createDriver() { return { use: jest.fn(), @@ -20,15 +22,14 @@ async function getPlatformApp() { const platformHandler = { createHandler: jest.fn().mockImplementation((o) => o) }; - const platformApp = await PlatformTest.invoke>(PlatformApplication, [ + const platformApp = await PlatformTest.invoke>(PlatformApplication, [ { token: PlatformHandler, use: platformHandler } ]); platformApp.injector.settings.logger = {}; - platformApp.rawRouter = createDriver() as any; - platformApp.rawApp = platformApp.raw = createDriver() as any; + platformApp.rawApp = createDriver() as any; return {platformApp, platformHandler}; } @@ -43,213 +44,54 @@ describe("PlatformApplication", () => { const {platformApp} = await getPlatformApp(); // WHEN - expect(platformApp.getApp()).toEqual(platformApp.raw); - }); - }); - describe("getRouter()", () => { - it("should return app", async () => { - // GIVEN - const {platformApp} = await getPlatformApp(); - - // WHEN - expect(platformApp.getRouter()).toEqual(platformApp.rawRouter); + expect(platformApp.getApp()).toEqual(platformApp.rawApp); }); }); - describe("use()", () => { - afterEach(() => { - jest.resetAllMocks(); - }); - it("should create a PlatformApplication and add handler", async () => { - // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const handler = jest.fn(); - - // WHEN - platformApp.use("/", handler); - - // THEN - expect(platformHandler.createHandler).toBeCalledWith(handler, {isFinal: false}); - expect(platformApp.rawRouter.use).toBeCalledWith("/", handler); - }); - it("should add router to app", async () => { + describe("statics()", () => { + it("should call statics", async () => { // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const router = createDriver(); - const adapter = { - router() { - return { - router, - callback() { - return router; - } - }; - } - }; - - // @ts-ignore - const handler = new PlatformRouter(platformHandler as any, adapter); - - // WHEN - platformApp.use("/", handler); + const {platformApp} = await getPlatformApp(); - // THEN - expect(platformApp.rawRouter.use).toBeCalledWith("/", handler.raw); - }); - }); - describe("get()", () => { - it("should create a PlatformApplication and add handler", async () => { - // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const handler = jest.fn(); + jest.spyOn(console, "warn"); // WHEN - platformApp.get("/", handler); - - // THEN - expect(platformHandler.createHandler).toBeCalledWith(handler, { - isFinal: true, - method: "get", - path: "/" - }); - expect(platformApp.rawRouter.get).toBeCalledWith("/", handler); + platformApp.statics("/", {root: "/root"}); }); }); - describe("all()", () => { - it("should create a PlatformApplication and add handler", async () => { + describe("multer()", () => { + it("should call statics", async () => { // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const handler = jest.fn(); + const {platformApp} = await getPlatformApp(); - // WHEN - platformApp.all("/", handler); - - // THEN - expect(platformHandler.createHandler).toBeCalledWith(handler, { - isFinal: true, - method: "all", - path: "/" - }); - expect(platformApp.rawRouter.all).toBeCalledWith("/", handler); - }); - }); - describe("post()", () => { - it("should create a PlatformApplication and add handler", async () => { - // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const handler = jest.fn(); + jest.spyOn(console, "warn"); // WHEN - platformApp.post("/", handler); - - // THEN - expect(platformHandler.createHandler).toBeCalledWith(handler, { - isFinal: true, - method: "post", - path: "/" - }); - expect(platformApp.rawRouter.post).toBeCalledWith("/", handler); + platformApp.multer({}); }); }); - describe("put()", () => { - it("should create a PlatformApplication and add handler", async () => { - // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const handler = jest.fn(); - // WHEN - platformApp.put("/", handler); - - // THEN - expect(platformHandler.createHandler).toBeCalledWith(handler, { - isFinal: true, - method: "put", - path: "/" - }); - expect(platformApp.rawRouter.put).toBeCalledWith("/", handler); - }); - }); - describe("patch()", () => { - it("should create a PlatformApplication and add handler", async () => { - // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const handler = jest.fn(); + describe("callback()", () => { + it("should return the callback", async () => { + const $ctx = { + runInContext: jest.fn().mockImplementation((cb) => { + cb(); + }) + }; - // WHEN - platformApp.patch("/", handler); - - // THEN - expect(platformHandler.createHandler).toBeCalledWith(handler, { - isFinal: true, - method: "patch", - path: "/" - }); - expect(platformApp.rawRouter.patch).toBeCalledWith("/", handler); - }); - }); - describe("head()", () => { - it("should create a PlatformApplication and add handler", async () => { - // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const handler = jest.fn(); + const {platformApp} = await getPlatformApp(); - // WHEN - platformApp.head("/", handler); - - // THEN - expect(platformHandler.createHandler).toBeCalledWith(handler, { - isFinal: true, - method: "head", - path: "/" - }); - expect(platformApp.rawRouter.head).toBeCalledWith("/", handler); - }); - }); - describe("delete()", () => { - it("should create a PlatformApplication and add handler", async () => { - // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const handler = jest.fn(); + (createContext as any).mockReturnValue(() => Promise.resolve($ctx)); // WHEN - platformApp.delete("/", handler); - - // THEN - expect(platformHandler.createHandler).toBeCalledWith(handler, { - isFinal: true, - method: "delete", - path: "/" - }); - expect(platformApp.rawRouter.delete).toBeCalledWith("/", handler); - }); - }); - describe("options()", () => { - it("should create a PlatformApplication and add handler", async () => { - // GIVEN - const {platformApp, platformHandler} = await getPlatformApp(); - const handler = jest.fn(); + const callback = jest.fn(); - // WHEN - platformApp.options("/", handler); - - // THEN - expect(platformHandler.createHandler).toBeCalledWith(handler, { - isFinal: true, - method: "options", - path: "/" - }); - expect(platformApp.rawRouter.options).toBeCalledWith("/", handler); - }); - }); - describe("statics()", () => { - it("should call statics", async () => { - // GIVEN - const {platformApp} = await getPlatformApp(); + jest.spyOn(platformApp, "rawCallback").mockReturnValue(callback); - jest.spyOn(console, "warn"); + await platformApp.callback({} as any, {} as any); - // WHEN - platformApp.statics("/", {root: "/root"}); + expect(platformApp.rawCallback).toHaveBeenCalledWith(); + expect(callback).toHaveBeenCalledWith({}, {}); }); }); }); diff --git a/packages/platform/common/src/services/PlatformApplication.ts b/packages/platform/common/src/services/PlatformApplication.ts index b573031fe46..7665106c268 100644 --- a/packages/platform/common/src/services/PlatformApplication.ts +++ b/packages/platform/common/src/services/PlatformApplication.ts @@ -1,6 +1,8 @@ -import {Injectable, ProviderScope} from "@tsed/di"; -import {PlatformHandler} from "./PlatformHandler"; -import {PlatformRouter} from "./PlatformRouter"; +import {Injectable, InjectorService, ProviderScope} from "@tsed/di"; +import {PlatformRouter} from "@tsed/platform-router"; +import {IncomingMessage, ServerResponse} from "http"; +import {PlatformMulterSettings} from "../config/interfaces/PlatformMulterSettings"; +import {createContext} from "../utils/createContext"; import {PlatformAdapter} from "./PlatformAdapter"; declare global { @@ -18,20 +20,39 @@ declare global { @Injectable({ scope: ProviderScope.SINGLETON }) -export class PlatformApplication extends PlatformRouter { - raw: App; +export class PlatformApplication extends PlatformRouter { rawApp: App; - declare rawRouter: Router; + rawCallback: () => any; - constructor(platformHandler: PlatformHandler, adapter: PlatformAdapter) { - super(platformHandler, adapter); + constructor(public adapter: PlatformAdapter, public injector: InjectorService) { + super(injector); const {app, callback} = adapter.app(); - this.rawApp = this.raw = app; - this.callback = callback; + this.rawApp = app; + this.rawCallback = callback; } getApp(): App { - return this.raw; + return this.rawApp; + } + + multer(options: PlatformMulterSettings) { + return this.adapter.multipart(options); + } + + callback(): (req: IncomingMessage, res: ServerResponse) => any; + callback(req: IncomingMessage, res: ServerResponse): any; + callback(req?: IncomingMessage, res?: ServerResponse) { + if (req && res) { + return this.callback()(req, res); + } + + const invoke = createContext(this.injector); + + return (req: IncomingMessage, res: ServerResponse) => { + const cb = this.rawCallback(); + + return invoke({request: req, response: res}).then(($ctx) => $ctx.runInContext(() => cb(req, res))); + }; } } diff --git a/packages/platform/common/src/services/PlatformHandler.spec.ts b/packages/platform/common/src/services/PlatformHandler.spec.ts deleted file mode 100644 index dde56eb3741..00000000000 --- a/packages/platform/common/src/services/PlatformHandler.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import {Context, EndpointMetadata, Err, Get, HandlerMetadata, HandlerType, Middleware, PlatformTest, QueryParams} from "@tsed/common"; -import {PlatformHandler} from "./PlatformHandler"; - -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) {} -} - -class CustomPlatformHandler extends PlatformHandler {} - -describe("PlatformHandler", () => { - beforeEach(PlatformTest.create); - beforeEach(() => { - PlatformTest.injector.getProvider(PlatformHandler)!.useClass = CustomPlatformHandler; - }); - afterEach(PlatformTest.reset); - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("createHandler", () => { - it("should return a native handler (success middleware)", async () => { - // GIVEN - jest.spyOn(Test.prototype, "get").mockImplementation((o) => o); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: Test, - type: HandlerType.ENDPOINT, - propertyKey: "get" - }); - - const platformHandler = await PlatformTest.invoke(PlatformHandler); - await PlatformTest.invoke(Test); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - - // THEN - expect(handler).toBeInstanceOf(Function); - }); - it("should return a native metadata (from native metadata)", async () => { - // GIVEN - const platformHandler = await PlatformTest.invoke(PlatformHandler); - jest.spyOn(Test.prototype, "get").mockImplementation((o) => o); - - const nativeHandler = (req: any, res: any, next: any) => {}; - - // WHEN - const handler = platformHandler.createHandler(nativeHandler); - - // THEN - expect(nativeHandler).toEqual(handler); - }); - it("should do nothing when request is aborted", async () => { - // GIVEN - const platformHandler = await PlatformTest.invoke(PlatformHandler); - - const $ctx = PlatformTest.createRequestContext(); - - $ctx.request.raw.aborted = true; - $ctx.endpoint = EndpointMetadata.get(Test, "get"); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: Test, - type: HandlerType.ENDPOINT, - propertyKey: "get" - }); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - const next = jest.fn(); - - await handler($ctx.getRequest(), $ctx.getResponse(), next); - - // THEN - return expect(next).not.toBeCalled(); - }); - it("should call returned function", async () => { - // GIVEN - const internalMiddleware = jest.fn().mockReturnValue("hello"); - - @Middleware() - class Test { - use(@Context() ctx: Context) { - return internalMiddleware; - } - } - - const platformHandler = await PlatformTest.invoke(PlatformHandler); - await PlatformTest.invoke(Test); - - const ctx = PlatformTest.createRequestContext(); - const next = jest.fn(); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: Test, - type: HandlerType.MIDDLEWARE, - propertyKey: "use" - }); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - - await handler(ctx.getRequest(), ctx.getResponse(), next); - // THEN - expect(internalMiddleware).toBeCalledWith(ctx.getRequest(), ctx.getResponse(), next); - }); - }); -}); diff --git a/packages/platform/common/src/services/PlatformHandler.ts b/packages/platform/common/src/services/PlatformHandler.ts index feeec38376f..1d8a4cfd020 100644 --- a/packages/platform/common/src/services/PlatformHandler.ts +++ b/packages/platform/common/src/services/PlatformHandler.ts @@ -1,27 +1,14 @@ -import {AnyToPromiseStatus, isFunction, isStream} from "@tsed/core"; -import {Inject, Injectable, InjectorService, Provider, ProviderScope} from "@tsed/di"; +import {AnyToPromiseStatus, catchAsyncError, isFunction, isStream} from "@tsed/core"; +import {Inject, Injectable, Provider, ProviderScope} from "@tsed/di"; import {$log} from "@tsed/logger"; -import {ArgScope, HandlerWithScope, PlatformParams} from "@tsed/platform-params"; +import {PlatformParams, PlatformParamsCallback} from "@tsed/platform-params"; import {PlatformResponseFilter} from "@tsed/platform-response-filter"; -import {EndpointMetadata} from "@tsed/schema"; +import {PlatformContextHandler, PlatformHandlerMetadata, PlatformHandlerType, PlatformRouters} from "@tsed/platform-router"; +import {promisify} from "util"; import {AnyToPromiseWithCtx} from "../domain/AnyToPromiseWithCtx"; -import {HandlerMetadata} from "../domain/HandlerMetadata"; import {PlatformContext} from "../domain/PlatformContext"; -import {HandlerType} from "../interfaces/HandlerType"; -import {PlatformRouteWithoutHandlers} from "../interfaces/PlatformRouteOptions"; -import {createHandlerMetadata} from "../utils/createHandlerMetadata"; import {setResponseHeaders} from "../utils/setResponseHeaders"; -export interface OnRequestOptions { - $ctx: PlatformContext; - metadata: HandlerMetadata; - handler: HandlerWithScope; - next?: any; - err?: any; - - [key: string]: any; -} - /** * Platform Handler abstraction layer. Wrap original class method to a pure platform handler (Express, Koa, etc...). * @platform @@ -33,244 +20,160 @@ export class PlatformHandler { @Inject() protected responseFilter: PlatformResponseFilter; - constructor(protected injector: InjectorService, protected params: PlatformParams) {} + @Inject() + protected platformRouters: PlatformRouters; - /** - * Create a native middleware based on the given metadata and return an instance of AnyToPromiseWithCtx - * @param input - * @param options - */ - createHandler(input: EndpointMetadata | HandlerMetadata | any, options: PlatformRouteWithoutHandlers = {}) { - return this.createRawHandler(createHandlerMetadata(this.injector, input, options)); + @Inject() + protected platformParams: PlatformParams; + + createHandler(handler: PlatformParamsCallback, handlerMetadata: PlatformHandlerMetadata): PlatformContextHandler { + return async ($ctx: PlatformContext) => { + $ctx.handlerMetadata = handlerMetadata; + + const error = await catchAsyncError(() => this.onRequest(handler, $ctx)); + + return this.next(error, $ctx.next, $ctx); + }; } /** - * Create injectable handler from the given provider * @param provider * @param propertyKey */ createCustomHandler(provider: Provider, propertyKey: string) { - const handler = this.compileHandler( - new HandlerMetadata({ - token: provider.provide, - target: provider.useClass, - type: HandlerType.CUSTOM, - scope: provider.scope, - propertyKey - }) - ); - - return async ($ctx: PlatformContext, next?: any) => - handler({ - $ctx, - next - }); + const metadata = new PlatformHandlerMetadata({ + provider, + type: PlatformHandlerType.CUSTOM, + propertyKey + }); + + const handler = this.platformParams.compileHandler(metadata.store); + + return async ($ctx: PlatformContext) => { + $ctx.set(PlatformHandlerMetadata, metadata); + + // @ts-ignore + return this.onRequest(handler, $ctx); + }; } /** * Send the response to the consumer. - * @param data - * @param ctx * @protected + * @param $ctx */ - async flush(data: any, ctx: PlatformContext) { - const {response} = ctx; + async flush($ctx: PlatformContext) { + const {response} = $ctx; if (!response.isDone()) { - data = await this.responseFilter.serialize(data, ctx); - data = await this.responseFilter.transform(data, ctx); + let data = await this.responseFilter.serialize($ctx.data, $ctx); + data = await this.responseFilter.transform(data, $ctx); response.body(data); } + + return response; } - compileHandler(metadata: HandlerMetadata) { - if (metadata.type === HandlerType.CTX_FN) { - return async (scope: Record) => { - // @ts-ignore - return this.onCtxRequest({ - ...scope, - handler: () => metadata.handler(scope.$ctx), - metadata - }); - }; + public setResponseHeaders($ctx: PlatformContext) { + if (!$ctx.response.isDone()) { + return setResponseHeaders($ctx); } - - const promise = this.params.compileHandler({ - token: metadata.token, - propertyKey: metadata.propertyKey, - getCustomArgs: metadata.injectable ? undefined : this.getDefaultArgs(metadata) - }); - - return async (scope: Record) => { - const handler = await promise; - // @ts-ignore - return this.onRequest({ - ...scope, - handler, - metadata - }); - }; } - protected async onCtxRequest(requestOptions: OnRequestOptions): Promise { - await requestOptions.handler(requestOptions); + /** + * @param error + * @param next + * @param $ctx + */ + next(error: unknown, next: Function, $ctx: PlatformContext) { + if (isStream($ctx.data) || $ctx.isDone()) { + return; + } - return this.next(requestOptions); + return error ? next(error) : next(); } /** * Call handler when a request his handle - * @param requestOptions */ - protected async onRequest(requestOptions: OnRequestOptions): Promise { - const {$ctx, metadata, err, handler} = requestOptions; - // istanbul ignore next - if (!$ctx || $ctx.isDone()) { + async onRequest(handler: PlatformParamsCallback, $ctx: PlatformContext): Promise { + // istanbul ignore next$ + if ($ctx.isDone()) { $log.error({ name: "HEADERS_SENT", - message: `An endpoint is called but the response is already send to the client. The call comes from the handler: ${metadata.toString()}` + message: `An endpoint is called but the response is already send to the client. The call comes from the handler: ${$ctx.handlerMetadata.toString()}` }); return; } - return $ctx.runInContext(async () => { - const resolver = new AnyToPromiseWithCtx({$ctx, err}); - - try { - const {state, data, status, headers} = await resolver.call(handler); + try { + const {handlerMetadata} = $ctx; + if (handlerMetadata.type === PlatformHandlerType.CTX_FN) { + return await handler({$ctx}); + } - if (state === AnyToPromiseStatus.RESOLVED) { - if (status) { - $ctx.response.status(status); - } + const resolver = new AnyToPromiseWithCtx($ctx); - if (headers) { - $ctx.response.setHeaders(headers); - } + const {state, data, status, headers} = await resolver.call(handler); - if (data !== undefined) { - $ctx.data = data; - } + // Note: restore previous handler metadata (for OIDC) + $ctx.handlerMetadata = handlerMetadata; - // Can be canceled by the handler itself - return await this.onSuccess($ctx.data, requestOptions); + if (state === AnyToPromiseStatus.RESOLVED) { + if (status) { + $ctx.response.status(status); } - } catch (er) { - return this.onError(er, requestOptions); - } - }); - } - protected async onError(er: Error, requestOptions: OnRequestOptions) { - const {next, $ctx, metadata} = requestOptions; + if (headers) { + $ctx.response.setHeaders(headers); + } - if ($ctx) { - $ctx.data = er; - } + if (data !== undefined) { + $ctx.data = data; + } - if (!next) { - throw er; + // Can be canceled by the handler itself + return await this.onSuccess($ctx); + } + } catch (error) { + return this.onError(error, $ctx); } + } - // istanbul ignore next - if (!$ctx || $ctx.isDone()) { - $log.warn({ - name: "HEADERS_SENT", - message: `An error was caught after the headers were sent to the client. The error comes from the handler: ${metadata.toString()}`, - stack: er.stack, - origin: er, - request_id: $ctx?.id - }); - - return; - } + protected async onError(error: unknown, $ctx: PlatformContext) { + $ctx.error = error; - return next(er); + throw error; } /** * Manage success scenario - * @param data - * @param requestOptions + * @param $ctx * @protected */ - protected async onSuccess(data: any, requestOptions: OnRequestOptions) { - const {metadata, $ctx, next} = requestOptions; - + protected async onSuccess($ctx: PlatformContext) { // istanbul ignore next - if ($ctx?.isDone()) { + if ($ctx.isDone()) { return; } + $ctx.error = null; + + const metadata = $ctx.handlerMetadata; + // set headers each times that an endpoint is called - if (metadata.type === HandlerType.ENDPOINT) { - this.setHeaders($ctx); + + if (metadata.isEndpoint()) { + this.setResponseHeaders($ctx); } // call returned middleware - if (isFunction(data) && !isStream(data)) { - return this.callReturnedMiddleware(data, $ctx, next); + if (isFunction($ctx.data) && !isStream($ctx.data)) { + return promisify($ctx.data)($ctx.getRequest(), $ctx.getResponse()); } if (metadata.isFinal()) { - return this.flush(data, $ctx); - } - - return this.next(requestOptions); - } - - /** - * 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); - } - - /** - * create Raw handler - * @param metadata - */ - protected createRawHandler(metadata: HandlerMetadata) { - if ([HandlerType.RAW_ERR_FN, HandlerType.RAW_FN].includes(metadata.type)) { - return metadata.handler; + return this.flush($ctx); } - - const handler = this.compileHandler(metadata); - - return async (request: any, response: any, next: any) => { - return handler({ - next, - $ctx: request.$ctx - }); - }; - } - - /** - * Set response headers - * @param ctx - * @protected - */ - protected setHeaders(ctx: PlatformContext) { - return setResponseHeaders(ctx); - } - - protected next(requestOptions: OnRequestOptions) { - const {$ctx, next} = requestOptions; - - return !$ctx.response.isDone() && next && next(); - } - - protected getDefaultArgs(metadata: HandlerMetadata) { - return async (scope: ArgScope) => - [ - metadata.hasErrorParam && scope.err, - scope.$ctx.request.request, - scope.$ctx.response.response, - metadata.hasNextFunction && scope.next - ].filter(Boolean); } } diff --git a/packages/platform/common/src/services/PlatformMiddlewaresChain.spec.ts b/packages/platform/common/src/services/PlatformMiddlewaresChain.spec.ts index 8f7fb0ed6e3..9ac2496d313 100644 --- a/packages/platform/common/src/services/PlatformMiddlewaresChain.spec.ts +++ b/packages/platform/common/src/services/PlatformMiddlewaresChain.spec.ts @@ -28,52 +28,19 @@ describe("PlatformMiddlewaresChain", () => { const provider = PlatformTest.injector.getProvider(TestCtrl)!; const platformMiddlewaresChain = PlatformTest.get(PlatformMiddlewaresChain); const operationRoutes = getOperationsRoutes(provider.token); - const endpoint = EndpointMetadata.get(TestCtrl, "getMethod"); const parentsMiddlewares = [function parentControllerBefore() {}]; - provider.middlewares = { - use: [function controllerUse() {}], - useAfter: [function controllerAfter() {}], - useBefore: [function controllerBefore() {}] - }; + const chain = platformMiddlewaresChain.get(parentsMiddlewares, operationRoutes[1]); - endpoint.before([function endpointBefore() {}]); - endpoint.after([function endpointAfter() {}]); - endpoint.middlewares = [function endpointUse() {}]; - - const chain = platformMiddlewaresChain.get(provider, operationRoutes[1], parentsMiddlewares); - - expect(chain[0].type).toEqual("context"); - expect(chain[1]).toEqual(parentsMiddlewares[0]); - expect(chain[2]).toEqual(provider.middlewares.useBefore[0]); - expect(chain[3]).toEqual(endpoint.beforeMiddlewares[0]); - expect(chain[4]).toEqual(provider.middlewares.use[0]); - expect(chain[5]).toEqual(endpoint.middlewares[0]); - expect(chain[6]).toEqual(endpoint); - expect(chain[7]).toEqual(endpoint.afterMiddlewares[0]); - expect(chain[8]).toEqual(provider.middlewares.useAfter[0]); + expect(chain[0]).toEqual(parentsMiddlewares[0]); }); it("should return the middlewares for a given endpoint (allMethod)", () => { const provider = PlatformTest.injector.getProvider(TestCtrl)!; const platformMiddlewaresChain = PlatformTest.get(PlatformMiddlewaresChain); const operationRoutes = getOperationsRoutes(provider.token); - const endpoint = EndpointMetadata.get(TestCtrl, "allMethod"); const parentsMiddlewares = [function parentControllerBefore() {}]; + const chain = platformMiddlewaresChain.get(parentsMiddlewares, operationRoutes[1]); - provider.middlewares = { - use: [function controllerUse() {}], - useAfter: [function controllerAfter() {}], - useBefore: [function controllerBefore() {}] - }; - - endpoint.before([function endpointBefore() {}]); - endpoint.after([function endpointAfter() {}]); - endpoint.middlewares = [function endpointUse() {}]; - - const chain = platformMiddlewaresChain.get(provider, operationRoutes[1], parentsMiddlewares); - - expect(chain[0].type).toEqual("context"); - expect(chain[1]).toEqual(parentsMiddlewares[0]); - expect(chain[2].name).toEqual("controllerBefore"); + expect(chain[0]).toEqual(parentsMiddlewares[0]); }); }); diff --git a/packages/platform/common/src/services/PlatformMiddlewaresChain.ts b/packages/platform/common/src/services/PlatformMiddlewaresChain.ts index fbc4f418ae2..c6e442a2e8e 100644 --- a/packages/platform/common/src/services/PlatformMiddlewaresChain.ts +++ b/packages/platform/common/src/services/PlatformMiddlewaresChain.ts @@ -1,13 +1,10 @@ +import {isClass} from "@tsed/core"; import {Constant, Inject, Injectable} from "@tsed/di"; import {ParamTypes} from "@tsed/platform-params"; import {JsonEntityStore, JsonOperationRoute} from "@tsed/schema"; -import {ControllerProvider} from "../domain/ControllerProvider"; -import {bindEndpointMiddleware} from "../middlewares/bindEndpointMiddleware"; import {PlatformAcceptMimesMiddleware} from "../middlewares/PlatformAcceptMimesMiddleware"; import {PlatformMulterMiddleware} from "../middlewares/PlatformMulterMiddleware"; -import {useCtxHandler} from "../utils/useCtxHandler"; import {PlatformAdapter} from "../services/PlatformAdapter"; -import {isClass} from "@tsed/core"; @Injectable() export class PlatformMiddlewaresChain { @@ -17,29 +14,10 @@ export class PlatformMiddlewaresChain { @Inject() protected adapter: PlatformAdapter; - get(provider: ControllerProvider, operationRoute: JsonOperationRoute, parentMiddlewares: any[] = []) { - const {endpoint} = operationRoute; - const {beforeMiddlewares, middlewares: mldwrs, afterMiddlewares} = endpoint; - - const { - middlewares: {useBefore, use, useAfter} - } = provider; - - const allMiddlewares = [ - ...parentMiddlewares, - ...useBefore, - ...beforeMiddlewares, - ...use, - ...mldwrs, - endpoint, - ...afterMiddlewares, - ...useAfter - ]; - + get(allMiddlewares: any[], operationRoute: JsonOperationRoute) { const {ACCEPT_MIMES, FILE, RAW_BODY, BODY} = this.getParamTypes(allMiddlewares, operationRoute); return [ - useCtxHandler(bindEndpointMiddleware(endpoint)), ACCEPT_MIMES && PlatformAcceptMimesMiddleware, FILE && PlatformMulterMiddleware, !FILE && RAW_BODY && this.adapter.bodyParser("raw"), diff --git a/packages/platform/common/src/services/PlatformRequest.spec.ts b/packages/platform/common/src/services/PlatformRequest.spec.ts index 1842e0da672..1cece7aac02 100644 --- a/packages/platform/common/src/services/PlatformRequest.spec.ts +++ b/packages/platform/common/src/services/PlatformRequest.spec.ts @@ -1,4 +1,4 @@ -import {PlatformTest} from "@tsed/common"; +import {PlatformHandlerMetadata, PlatformTest} from "@tsed/common"; import {PlatformRequest} from "./PlatformRequest"; function createRequest() { @@ -46,8 +46,10 @@ describe("PlatformRequest", () => { describe("route()", () => { it("should return route", () => { const $ctx = PlatformTest.createRequestContext(); - $ctx.endpoint = new Map() as any; - $ctx.endpoint.set("route", "/id"); + $ctx.handlerMetadata = new PlatformHandlerMetadata({ + handler: () => {} + }); + $ctx.handlerMetadata.path = "/id"; expect($ctx.request.route).toEqual("/id"); }); diff --git a/packages/platform/common/src/services/PlatformRequest.ts b/packages/platform/common/src/services/PlatformRequest.ts index 469f773191b..2eba6fa75f1 100644 --- a/packages/platform/common/src/services/PlatformRequest.ts +++ b/packages/platform/common/src/services/PlatformRequest.ts @@ -8,10 +8,6 @@ declare global { // @ts-ignore export interface Request { id: string; - /** - * @deprecated - */ - $ctx: PlatformContext; } } } @@ -115,7 +111,7 @@ export class PlatformRequest { } get route() { - return this.$ctx.endpoint?.get("route"); + return this.$ctx.handlerMetadata?.path; } /** @@ -178,6 +174,6 @@ export class PlatformRequest { * Return the Node.js response object */ getReq(): IncomingMessage { - return this.raw as any; + return this.$ctx.event.request; } } diff --git a/packages/platform/common/src/services/PlatformResponse.ts b/packages/platform/common/src/services/PlatformResponse.ts index af6e7e281cf..26911d52b74 100644 --- a/packages/platform/common/src/services/PlatformResponse.ts +++ b/packages/platform/common/src/services/PlatformResponse.ts @@ -2,21 +2,16 @@ import {isBoolean, isNumber, isStream, isString} from "@tsed/core"; import {Injectable, ProviderScope, Scope} from "@tsed/di"; import {OutgoingHttpHeaders, ServerResponse} from "http"; import onFinished from "on-finished"; -import {IncomingEvent} from "../interfaces/IncomingEvent"; -import type {PlatformRequest} from "./PlatformRequest"; import type {PlatformContext} from "../domain/PlatformContext"; +import {PlatformRequest} from "./PlatformRequest"; declare global { namespace TsED { // @ts-ignore - export interface Response { - // req: any; - } + export interface Response {} } } -export type HeaderValue = Array | boolean | number | string; - /** * Platform Response abstraction layer. * @platform @@ -101,7 +96,7 @@ export class PlatformResponse = any> { * Return the Node.js response object */ getRes(): ServerResponse { - return this.raw as any; + return this.$ctx.event.response; } hasStatus() { @@ -307,7 +302,7 @@ export class PlatformResponse = any> { * @param cb */ onEnd(cb: (er: Error | null, message: string) => void): this { - PlatformResponse.onFinished(this.getRes(), cb); + PlatformResponse.onFinished(this.$ctx.event.response, cb); return this; } diff --git a/packages/platform/common/src/services/PlatformRouter.spec.ts b/packages/platform/common/src/services/PlatformRouter.spec.ts deleted file mode 100644 index 4a6d4fd6f5e..00000000000 --- a/packages/platform/common/src/services/PlatformRouter.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {InjectorService, PLATFORM_ROUTER_OPTIONS} from "@tsed/common"; -import {PlatformRouter} from "./PlatformRouter"; - -describe("PlatformRouter", () => { - describe("create()", () => { - it("should create a new router", async () => { - // GIVEN - const injector = new InjectorService(); - jest.spyOn(injector, "invoke").mockReturnValue(undefined); - const routerOptions: any = { - test: "options" - }; - - // WHEN - PlatformRouter.create(injector, routerOptions); - - // THEN - const locals = new Map(); - locals.set(PLATFORM_ROUTER_OPTIONS, routerOptions); - expect(injector.invoke).toBeCalledWith(PlatformRouter, locals); - }); - }); -}); diff --git a/packages/platform/common/src/services/PlatformRouter.ts b/packages/platform/common/src/services/PlatformRouter.ts deleted file mode 100644 index 131481723b5..00000000000 --- a/packages/platform/common/src/services/PlatformRouter.ts +++ /dev/null @@ -1,139 +0,0 @@ -import {Inject, Injectable, InjectorService, PathType, ProviderScope} from "@tsed/di"; -import {PlatformHandler} from "./PlatformHandler"; -import {PlatformRouteOptions, PlatformRouteWithoutHandlers} from "../interfaces/PlatformRouteOptions"; -import {PlatformStaticsOptions} from "../config/interfaces/PlatformStaticsSettings"; -import {PlatformMulter, PlatformMulterSettings} from "../config/interfaces/PlatformMulterSettings"; -import {PlatformAdapter} from "./PlatformAdapter"; -import {RouterOptions} from "express"; - -/** - * @ignore - */ -export const PLATFORM_ROUTER_OPTIONS = Symbol.for("PlatformRouterOptions"); - -declare global { - namespace TsED { - // @ts-ignore - export interface Router {} - } -} - -/** - * Platform Router abstraction layer. - * @platform - */ -@Injectable({ - scope: ProviderScope.INSTANCE -}) -export class PlatformRouter { - rawRouter: Router; - raw: any; - isBuilt: boolean = false; - - @Inject() - injector: InjectorService; - - callback: () => any; - - constructor( - protected platformHandler: PlatformHandler, - readonly adapter: PlatformAdapter, - @Inject(PLATFORM_ROUTER_OPTIONS) routerOptions: Partial = {} - ) { - const {router, callback} = adapter.router(routerOptions); - - this.rawRouter = this.raw = router; - this.callback = callback; - } - - /** - * Create a new instance of PlatformRouter - * @param injector - * @param routerOptions - */ - static create(injector: InjectorService, routerOptions: any = {}) { - const locals = new Map(); - locals.set(PLATFORM_ROUTER_OPTIONS, routerOptions); - - return injector.invoke(PlatformRouter, locals); - } - - getRouter(): Router { - return this.rawRouter; - } - - use(...handlers: any[]) { - // @ts-ignore - this.getRouter().use(...this.mapHandlers(handlers)); - - return this; - } - - addRoute(options: Partial) { - const {method, path, handlers, ...opts} = options; - - // @ts-ignore - this.getRouter()[method](path, ...this.mapHandlers(handlers, {method, path, ...opts})); - - return this; - } - - all(path: PathType, ...handlers: any[]) { - return this.addRoute({method: "all", path, handlers, isFinal: true}); - } - - get(path: PathType, ...handlers: any[]) { - return this.addRoute({method: "get", path, handlers, isFinal: true}); - } - - post(path: PathType, ...handlers: any[]) { - return this.addRoute({method: "post", path, handlers, isFinal: true}); - } - - put(path: PathType, ...handlers: any[]) { - return this.addRoute({method: "put", path, handlers, isFinal: true}); - } - - delete(path: PathType, ...handlers: any[]) { - return this.addRoute({method: "delete", path, handlers, isFinal: true}); - } - - patch(path: PathType, ...handlers: any[]) { - return this.addRoute({method: "patch", path, handlers, isFinal: true}); - } - - head(path: PathType, ...handlers: any[]) { - return this.addRoute({method: "head", path, handlers, isFinal: true}); - } - - options(path: PathType, ...handlers: any[]) { - return this.addRoute({method: "options", path, handlers, isFinal: true}); - } - - statics(path: string, options: PlatformStaticsOptions) { - return this.use(path, this.adapter.statics(path, options)); - } - - multer(options: PlatformMulterSettings): PlatformMulter { - return this.adapter.multipart(options); - } - - protected mapHandlers(handlers: any[], options: PlatformRouteWithoutHandlers = {}): any[] { - return handlers.reduce((list, handler, index) => { - if (typeof handler === "string") { - return list.concat(handler); - } - - if (handler instanceof PlatformRouter) { - return list.concat(handler.callback()); - } - - return list.concat( - this.platformHandler.createHandler(handler, { - ...options, - isFinal: options.isFinal ? index === handlers.length - 1 : false - }) - ); - }, []); - } -} diff --git a/packages/platform/common/src/utils/createContext.spec.ts b/packages/platform/common/src/utils/createContext.spec.ts index 3461c5ec174..8ce029fe77d 100644 --- a/packages/platform/common/src/utils/createContext.spec.ts +++ b/packages/platform/common/src/utils/createContext.spec.ts @@ -42,14 +42,12 @@ describe("createContext", () => { const response = PlatformTest.createResponse(); response.req = request; - jest.spyOn(PlatformResponse.prototype, "setHeader"); - // WHEN const invoke = createContext(injector); const ctx = await invoke({request, response}); // THEN - expect(ctx.response.setHeader).toBeCalledWith("x-request-id", expect.any(String)); + expect(ctx.response.raw.headers["x-request-id"]).toBeDefined(); }); it("should use an existing x-request-id request header for the response x-request-id header", async () => { @@ -71,6 +69,6 @@ describe("createContext", () => { const ctx = await invoke({request, response}); // THEN - expect(ctx.response.setHeader).toBeCalledWith("x-request-id", "test-id"); + expect(ctx.response.raw.headers["x-request-id"]).toEqual("test-id"); }); }); diff --git a/packages/platform/common/src/utils/createContext.ts b/packages/platform/common/src/utils/createContext.ts index f5b082af650..2e9947acf28 100644 --- a/packages/platform/common/src/utils/createContext.ts +++ b/packages/platform/common/src/utils/createContext.ts @@ -1,12 +1,12 @@ import {InjectorService} from "@tsed/di"; +import {v4} from "uuid"; import {PlatformContext} from "../domain/PlatformContext"; +import {IncomingEvent} from "../interfaces/IncomingEvent"; import {PlatformRequest} from "../services/PlatformRequest"; import {PlatformResponse} from "../services/PlatformResponse"; -import {IncomingEvent} from "../interfaces/IncomingEvent"; -import {v4} from "uuid"; function defaultReqIdBuilder(req: any) { - return req.get("x-request-id") || v4().split("-").join(""); + return req.headers["x-request-id"] || v4().split("-").join(""); } function mapIgnoreUrlPatterns(ignoreUrlPatterns: any[]) { diff --git a/packages/platform/common/src/utils/createHandlerMetadata.spec.ts b/packages/platform/common/src/utils/createHandlerMetadata.spec.ts deleted file mode 100644 index e51062acbe7..00000000000 --- a/packages/platform/common/src/utils/createHandlerMetadata.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {EndpointMetadata, Err, Get, HandlerType, PlatformTest, QueryParams, useCtxHandler} from "@tsed/common"; -import {Provider} from "@tsed/di"; -import {createHandlerMetadata} from "./createHandlerMetadata"; - -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(() => { - jest.resetAllMocks(); - }); - - it("should return metadata from Endpoint", async () => { - // GIVEN - - const endpoint = new EndpointMetadata({ - target: Test, - propertyKey: "get" - } as any); - - jest.spyOn(PlatformTest.injector, "getProvider").mockReturnValue(new Provider(Test)); - - // WHEN - const handlerMetadata = createHandlerMetadata(PlatformTest.injector, endpoint); - - // THEN - expect(handlerMetadata.target).toEqual(Test); - expect(handlerMetadata.propertyKey).toEqual("get"); - expect(handlerMetadata.type).toEqual(HandlerType.ENDPOINT); - }); - it("should return metadata from Middleware", async () => { - // GIVEN - jest.spyOn(PlatformTest.injector, "getProvider").mockReturnValue(new Provider(Test)); - - // WHEN - const handlerMetadata = createHandlerMetadata(PlatformTest.injector, Test); - - // THEN - expect(handlerMetadata.target).toEqual(Test); - expect(handlerMetadata.propertyKey).toEqual("use"); - expect(handlerMetadata.type).toEqual(HandlerType.ERR_MIDDLEWARE); - }); - it("should return metadata from Function", async () => { - // GIVEN - jest.spyOn(PlatformTest.injector, "getProvider").mockReturnValue(undefined); - - // WHEN - const handlerMetadata = createHandlerMetadata(PlatformTest.injector, () => {}); - - // THEN - expect(handlerMetadata.type).toEqual(HandlerType.RAW_FN); - }); - it("should return metadata from useCtxHandler", async () => { - // GIVEN - jest.spyOn(PlatformTest.injector, "getProvider").mockReturnValue(undefined); - - // WHEN - const handlerMetadata = createHandlerMetadata( - PlatformTest.injector, - useCtxHandler(() => {}) - ); - - // THEN - expect(handlerMetadata.type).toEqual(HandlerType.CTX_FN); - }); -}); diff --git a/packages/platform/common/src/utils/createHandlerMetadata.ts b/packages/platform/common/src/utils/createHandlerMetadata.ts deleted file mode 100644 index f02c793e43b..00000000000 --- a/packages/platform/common/src/utils/createHandlerMetadata.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {InjectorService} from "@tsed/di"; -import {EndpointMetadata} from "@tsed/schema"; -import {HandlerMetadata, HandlerMetadataOptions} from "../domain/HandlerMetadata"; -import {HandlerType} from "../interfaces/HandlerType"; -import {PlatformRouteWithoutHandlers} from "../interfaces/PlatformRouteOptions"; - -function isMetadata(input: any) { - return input instanceof HandlerMetadata; -} - -/** - * @ignore - */ -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(routeOptions.token)!; - - options = { - token: provider.token, - target: provider.useClass, - scope: provider.scope, - type: HandlerType.ENDPOINT, - propertyKey: obj.propertyKey - }; - } else { - const provider = injector.getProvider(obj); - - if (provider) { - options = { - token: provider.token, - target: provider.useClass, - scope: provider.scope, - type: HandlerType.MIDDLEWARE, - propertyKey: "use" - }; - } else { - options = { - target: obj - }; - } - } - - options.routeOptions = routeOptions; - - return new HandlerMetadata(options); -} diff --git a/packages/platform/common/src/utils/createInjector.ts b/packages/platform/common/src/utils/createInjector.ts index eb2654258d7..e76a97b9db2 100644 --- a/packages/platform/common/src/utils/createInjector.ts +++ b/packages/platform/common/src/utils/createInjector.ts @@ -8,7 +8,6 @@ import {PlatformApplication} from "../services/PlatformApplication"; import {PlatformHandler} from "../services/PlatformHandler"; import {PlatformRequest} from "../services/PlatformRequest"; import {PlatformResponse} from "../services/PlatformResponse"; -import {PlatformRouter} from "../services/PlatformRouter"; $log.name = "TSED"; @@ -16,13 +15,12 @@ const DEFAULT_PROVIDERS = [ {provide: PlatformHandler}, {provide: PlatformResponse}, {provide: PlatformRequest}, - {provide: PlatformRouter}, {provide: PlatformApplication}, {provide: Platform} ]; interface CreateInjectorOptions { - adapter?: Type>; + adapter?: Type>; settings?: Partial; } diff --git a/packages/platform/common/src/utils/ensureContext.spec.ts b/packages/platform/common/src/utils/ensureContext.spec.ts new file mode 100644 index 00000000000..db86f28a79a --- /dev/null +++ b/packages/platform/common/src/utils/ensureContext.spec.ts @@ -0,0 +1,39 @@ +import {PlatformTest} from "../services/PlatformTest"; +import {ensureContext} from "./ensureContext"; + +describe("ensureContext()", () => { + beforeEach(() => PlatformTest.create()); + afterEach(() => PlatformTest.reset()); + + it("should return context (no async_hook)", async () => { + const $ctx = PlatformTest.createRequestContext(); + const stub = jest.fn(); + const req = { + $ctx + }; + + await ensureContext(req, stub); + + expect(stub).toHaveBeenCalledWith($ctx); + }); + + it("should return context (with async_hook)", async () => { + const $ctx = PlatformTest.createRequestContext(); + const stub = jest.fn(); + const req = {}; + + await $ctx.runInContext(() => ensureContext(req, stub)); + + expect(stub).toHaveBeenCalledWith($ctx); + }); + + it("should return context (no async_hook, no context)", async () => { + const stub = jest.fn(); + const fallback = jest.fn(); + const req = {}; + + await ensureContext(req, stub, fallback); + + expect(fallback).toHaveBeenCalledWith(); + }); +}); diff --git a/packages/platform/common/src/utils/ensureContext.ts b/packages/platform/common/src/utils/ensureContext.ts new file mode 100644 index 00000000000..cacc33a4f14 --- /dev/null +++ b/packages/platform/common/src/utils/ensureContext.ts @@ -0,0 +1,16 @@ +import {getContext} from "@tsed/di"; +import {PlatformContext} from "../domain/PlatformContext"; + +export function ensureContext(request: any, cb: ($ctx: PlatformContext) => any, fallback?: () => void) { + const $ctx = getContext(); + + if ($ctx) { + return cb($ctx); + } + + if (!request?.$ctx && fallback) { + return fallback(); + } + + return request.$ctx.runInContext(() => cb(request.$ctx)); +} diff --git a/packages/platform/common/src/utils/printRoutes.ts b/packages/platform/common/src/utils/printRoutes.ts index c53fa0d20c4..49b680dcf13 100644 --- a/packages/platform/common/src/utils/printRoutes.ts +++ b/packages/platform/common/src/utils/printRoutes.ts @@ -8,21 +8,22 @@ export function printRoutes(routes: PlatformRouteDetails[]) { PUT: "blue", DELETE: "red", PATCH: "magenta", - ALL: "cyan" + ALL: "cyan", + STATICS: "white" }; const list = routes.map((route) => { - const obj = route.toJSON(); - const method = obj.method.toUpperCase(); + const method = route.method.toUpperCase(); - obj.method = { - length: method.length, - toString: () => { - return colorize(method, mapColor[method]); + return { + ...route, + method: { + length: method.length, + toString: () => { + return colorize(method, mapColor[method]); + } } - } as any; - - return obj; + }; }); const str = $log.drawTable(list, { diff --git a/packages/platform/common/src/utils/setResponseHeaders.ts b/packages/platform/common/src/utils/setResponseHeaders.ts index 4100b942e56..32ec2a0cad9 100644 --- a/packages/platform/common/src/utils/setResponseHeaders.ts +++ b/packages/platform/common/src/utils/setResponseHeaders.ts @@ -19,7 +19,7 @@ export function setResponseHeaders(ctx: PlatformContext) { const statusCode = response.statusCode; const headers = operation.getHeadersOf(statusCode); - Object.entries(headers).forEach(([key, item]) => { + Object.entries(headers).forEach(([key, item]: any[]) => { if (!response.get(key)) { response.setHeader(key, String(item.example)); } diff --git a/packages/platform/common/src/utils/useCtxHandler.ts b/packages/platform/common/src/utils/useCtxHandler.ts index 92740e1f4b4..981f993b507 100644 --- a/packages/platform/common/src/utils/useCtxHandler.ts +++ b/packages/platform/common/src/utils/useCtxHandler.ts @@ -1,15 +1,8 @@ -import {PlatformContext} from "../domain/PlatformContext"; -import {HandlerType} from "../interfaces/HandlerType"; - -export type PlatformCtxHandler = ($ctx: PlatformContext) => any | Promise; +import {useContextHandler} from "@tsed/platform-router"; /** * Create Ts.ED context handler * @param fn - * @ignore + * @deprecated Use useContextHandler from "@tsed/platform-router" */ -export function useCtxHandler(fn: PlatformCtxHandler & {type?: HandlerType}) { - fn.type = HandlerType.CTX_FN; - - return fn; -} +export const useCtxHandler = useContextHandler; diff --git a/packages/platform/common/test/integration/platform.spec.ts b/packages/platform/common/test/integration/platform.spec.ts new file mode 100644 index 00000000000..cf78985a7e2 --- /dev/null +++ b/packages/platform/common/test/integration/platform.spec.ts @@ -0,0 +1,58 @@ +import "@tsed/ajv"; +import {Configuration, Controller, Get, PlatformTest} from "@tsed/common"; +import {PlatformExpress} from "@tsed/platform-express"; +import {PlatformTestUtils} from "@tsed/platform-test-utils"; +import bodyParser from "body-parser"; +import compress from "compression"; +import cookieParser from "cookie-parser"; +import methodOverride from "method-override"; +import SuperTest from "supertest"; + +@Controller("/hello") +class TestHelloWorld { + @Get("/") + test() { + return {hello: "world"}; + } +} + +@Configuration({ + port: 8081, + logger: { + level: "info" + }, + middlewares: [cookieParser(), compress({}), methodOverride(), bodyParser.json()], + mount: { + "/rest": [TestHelloWorld] + } +}) +export class Server {} + +const utils = PlatformTestUtils.create({ + rootDir: __dirname, + platform: PlatformExpress, + server: Server, + logger: { + level: "off" + } +}); + +describe("QueryParser", () => { + let request: SuperTest.SuperTest; + + beforeEach(utils.bootstrap()); + beforeEach(() => { + request = SuperTest(PlatformTest.callback()); + }); + + afterEach(utils.reset); + describe("scenario test", () => { + it("should return hello", async () => { + const response = await request.get(`/rest/hello`).expect(200); + + expect(response.body).toEqual({ + hello: "world" + }); + }); + }); +}); diff --git a/packages/platform/platform-exceptions/src/interfaces/ExceptionFilterMethods.ts b/packages/platform/platform-exceptions/src/interfaces/ExceptionFilterMethods.ts index 16509e28f5c..1cc77b68a27 100644 --- a/packages/platform/platform-exceptions/src/interfaces/ExceptionFilterMethods.ts +++ b/packages/platform/platform-exceptions/src/interfaces/ExceptionFilterMethods.ts @@ -1,5 +1,5 @@ -import {BaseContext} from "@tsed/di"; +import {DIContext} from "@tsed/di"; export interface ExceptionFilterMethods { - catch(error: T, ctx: BaseContext): void; + catch(error: T, ctx: DIContext): void; } diff --git a/packages/platform/platform-exceptions/src/services/PlatformExceptions.ts b/packages/platform/platform-exceptions/src/services/PlatformExceptions.ts index e2af8b13da9..af0cb4768a3 100644 --- a/packages/platform/platform-exceptions/src/services/PlatformExceptions.ts +++ b/packages/platform/platform-exceptions/src/services/PlatformExceptions.ts @@ -1,5 +1,5 @@ import {ancestorsOf, classOf, nameOf} from "@tsed/core"; -import {BaseContext, Inject, Injectable, InjectorService} from "@tsed/di"; +import {BaseContext, DIContext, Inject, Injectable, InjectorService} from "@tsed/di"; import {ErrorFilter} from "../components/ErrorFilter"; import {ExceptionFilter} from "../components/ExceptionFilter"; import {MongooseErrorFilter} from "../components/MongooseErrorFilter"; @@ -28,7 +28,7 @@ export class PlatformExceptions { }); } - catch(error: unknown, ctx: BaseContext) { + catch(error: unknown, ctx: DIContext) { const name = nameOf(classOf(error)); if (name && this.types.has(name)) { @@ -47,7 +47,7 @@ export class PlatformExceptions { return this.types.get(Error)!.catch(error, ctx); } - resourceNotFound(ctx: BaseContext) { + resourceNotFound(ctx: DIContext) { return this.catch(new ResourceNotFound(ctx.request.url), ctx); } } diff --git a/packages/platform/platform-express/package.json b/packages/platform/platform-express/package.json index 788ebbac476..644ef7142c5 100644 --- a/packages/platform/platform-express/package.json +++ b/packages/platform/platform-express/package.json @@ -123,4 +123,4 @@ "optional": false } } -} \ No newline at end of file +} diff --git a/packages/platform/platform-express/src/components/PlatformExpress.ts b/packages/platform/platform-express/src/components/PlatformExpress.ts index 9a3acc95299..6a05edfad0f 100644 --- a/packages/platform/platform-express/src/components/PlatformExpress.ts +++ b/packages/platform/platform-express/src/components/PlatformExpress.ts @@ -1,29 +1,25 @@ import { - createContext, + bindContext, + getContext, InjectorService, PlatformAdapter, PlatformApplication, PlatformBuilder, PlatformContext, PlatformExceptions, - PlatformHandler, PlatformMulter, PlatformMulterSettings, - PlatformRequest, - PlatformResponse, PlatformStaticsOptions } from "@tsed/common"; import {Env, isFunction, nameOf, Type} from "@tsed/core"; +import {PlatformHandlerMetadata, PlatformHandlerType, PlatformLayer} from "@tsed/platform-router"; import type {PlatformViews} from "@tsed/platform-views"; import {OptionsJson, OptionsText, OptionsUrlencoded} from "body-parser"; -import Express, {RouterOptions} from "express"; +import Express from "express"; import type multer from "multer"; import {promisify} from "util"; import {PlatformExpressStaticsOptions} from "../interfaces/PlatformExpressStaticsOptions"; import {staticsMiddleware} from "../middlewares/staticsMiddleware"; -import {PlatformExpressHandler} from "../services/PlatformExpressHandler"; -import {PlatformExpressRequest} from "../services/PlatformExpressRequest"; -import {PlatformExpressResponse} from "../services/PlatformExpressResponse"; declare module "express" { export interface Request { @@ -34,7 +30,7 @@ declare module "express" { declare global { namespace TsED { - export interface Router extends Express.Router {} + // export interface Router extends Express.Router {} export interface Application extends Express.Application {} @@ -51,21 +47,8 @@ declare global { * @platform * @express */ -export class PlatformExpress implements PlatformAdapter { - readonly providers = [ - { - provide: PlatformHandler, - useClass: PlatformExpressHandler - }, - { - provide: PlatformResponse, - useClass: PlatformExpressResponse - }, - { - provide: PlatformRequest, - useClass: PlatformExpressRequest - } - ]; +export class PlatformExpress implements PlatformAdapter { + readonly providers = []; #multer: typeof multer; constructor(protected injector: InjectorService) { @@ -78,7 +61,7 @@ export class PlatformExpress implements PlatformAdapter, settings: Partial = {}) { - return PlatformBuilder.create(module, { + return PlatformBuilder.create(module, { ...settings, adapter: PlatformExpress }); @@ -90,7 +73,7 @@ export class PlatformExpress implements PlatformAdapter, settings: Partial = {}) { - return PlatformBuilder.bootstrap(module, { + return PlatformBuilder.bootstrap(module, { ...settings, adapter: PlatformExpress }); @@ -113,16 +96,6 @@ export class PlatformExpress implements PlatformAdapter>(PlatformApplication)!; - - logger.debug("Mount app router"); - app.getApp().use(app.getRouter()); - - return this; - } - async beforeLoadRoutes() { const injector = this.injector; const app = this.injector.get>(PlatformApplication)!; @@ -131,33 +104,73 @@ export class PlatformExpress implements PlatformAdapter>(PlatformApplication)!; + const platformExceptions = this.injector.get(PlatformExceptions)!; // NOT FOUND app.use((req: any, res: any, next: any) => { - !res.headersSent && injector.get(PlatformExceptions)?.resourceNotFound(req.$ctx); + const $ctx = getContext()!; + !$ctx.isDone() && platformExceptions.resourceNotFound($ctx); }); // EXCEPTION FILTERS app.use((err: any, req: any, res: any, next: any) => { - !res.headersSent && injector.get(PlatformExceptions)?.catch(err, req.$ctx); + const $ctx = getContext()!; + + !$ctx.isDone() && platformExceptions.catch(err, $ctx); }); } - useContext(): this { - const {logger} = this.injector; + mapLayers(layers: PlatformLayer[]) { + const app = this.getPlatformApplication(); + const rawApp: any = app.getApp(); - logger.debug("Mount app context"); + layers.forEach((layer) => { + switch (layer.method) { + case "statics": + rawApp.use(layer.path, this.statics(layer.path as string, layer.opts as any)); + return; + } - const invoke = createContext(this.injector); - const app = this.injector.get>(PlatformApplication)!; + rawApp[layer.method](...layer.getArgs()); + }); + } + + mapHandler(handler: Function, metadata: PlatformHandlerMetadata) { + switch (metadata.type) { + case PlatformHandlerType.RAW_ERR_FN: + return (error: unknown, req: any, res: any, next: any) => handler(error, req, res, bindContext(next)); + case PlatformHandlerType.RAW_FN: + return (req: any, res: any, next: any) => handler(req, res, bindContext(next)); + case PlatformHandlerType.ERR_MIDDLEWARE: + return async (error: unknown, req: any, res: any, next: any) => { + const $ctx = getContext()!; + $ctx.next = next; + $ctx.error = error; + + await handler($ctx); + }; + default: + return async (req: any, res: any, next: any) => { + const $ctx = getContext()!; + $ctx.next = next; + + await handler($ctx); + }; + } + } + + useContext(): this { + const app = this.getPlatformApplication(); + + app.use(async (request: any, response: any, next: any) => { + const $ctx = getContext()!; - app.getApp().use(async (request: any, response: any, next: any) => { - await invoke({request, response}); + $ctx.upgrade({request, response}); return next(); }); @@ -165,6 +178,16 @@ export class PlatformExpress implements PlatformAdapter = {}) { - const options = Object.assign( - { - mergeParams: true - }, - this.injector.settings.get("express.router", {}), - routerOptions - ); - - const router = Express.Router(options); - - return { - router, - callback() { - return router; - } - }; - } - statics(endpoint: string, options: PlatformStaticsOptions) { const {root, ...props} = options; @@ -247,6 +241,10 @@ export class PlatformExpress implements PlatformAdapter>(PlatformApplication)!; + } + private async configureViewsEngine() { const injector = this.injector; const app = this.injector.get>(PlatformApplication)!; diff --git a/packages/platform/platform-express/src/decorators/caseSensitive.ts b/packages/platform/platform-express/src/decorators/caseSensitive.ts index 7e70e66aaf5..6878bf685da 100644 --- a/packages/platform/platform-express/src/decorators/caseSensitive.ts +++ b/packages/platform/platform-express/src/decorators/caseSensitive.ts @@ -21,6 +21,7 @@ import {RouterSettings} from "./routerSettings"; * @returns {Function} * @decorator * @express + * @deprecated */ export function CaseSensitive(caseSensitive: boolean) { return RouterSettings({caseSensitive}); diff --git a/packages/platform/platform-express/src/decorators/mergeParams.ts b/packages/platform/platform-express/src/decorators/mergeParams.ts index 57ac4faa7fb..597d490d872 100644 --- a/packages/platform/platform-express/src/decorators/mergeParams.ts +++ b/packages/platform/platform-express/src/decorators/mergeParams.ts @@ -21,6 +21,7 @@ import {RouterSettings} from "./routerSettings"; * @returns {Function} * @decorator * @express + * @deprecated */ export function MergeParams(mergeParams: boolean = true) { return RouterSettings({mergeParams}); diff --git a/packages/platform/platform-express/src/decorators/routerSettings.ts b/packages/platform/platform-express/src/decorators/routerSettings.ts index 689671f59c2..685cc70fee4 100644 --- a/packages/platform/platform-express/src/decorators/routerSettings.ts +++ b/packages/platform/platform-express/src/decorators/routerSettings.ts @@ -23,6 +23,7 @@ import {RouterOptions} from "express"; * @decorator * @param routerOptions * @express + * @deprecated */ export function RouterSettings(routerOptions: RouterOptions): Function { return StoreMerge(ROUTER_OPTIONS, routerOptions); diff --git a/packages/platform/platform-express/src/decorators/strict.ts b/packages/platform/platform-express/src/decorators/strict.ts index 834e83a5eed..d913af67c49 100644 --- a/packages/platform/platform-express/src/decorators/strict.ts +++ b/packages/platform/platform-express/src/decorators/strict.ts @@ -21,6 +21,7 @@ import {RouterSettings} from "./routerSettings"; * @returns {Function} * @decorator * @express + * @deprecated */ export function Strict(strict: boolean) { return RouterSettings({strict}); diff --git a/packages/platform/platform-express/src/index.ts b/packages/platform/platform-express/src/index.ts index 72ab6cc4915..1df5085cc4d 100644 --- a/packages/platform/platform-express/src/index.ts +++ b/packages/platform/platform-express/src/index.ts @@ -12,6 +12,3 @@ export * from "./interfaces/PlatformExpressSettings"; export * from "./interfaces/PlatformExpressStaticsOptions"; export * from "./interfaces/interfaces"; export * from "./middlewares/staticsMiddleware"; -export * from "./services/PlatformExpressHandler"; -export * from "./services/PlatformExpressRequest"; -export * from "./services/PlatformExpressResponse"; diff --git a/packages/platform/platform-express/src/services/PlatformExpressApplication.spec.ts b/packages/platform/platform-express/src/services/PlatformExpressApplication.spec.ts deleted file mode 100644 index 3662fba0fa3..00000000000 --- a/packages/platform/platform-express/src/services/PlatformExpressApplication.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {PlatformApplication, PlatformTest} from "@tsed/common"; -import {PlatformExpress} from "@tsed/platform-express"; -import Express from "express"; - -describe("PlatformExpressApplication", () => { - class TestServer {} - - beforeEach( - PlatformTest.bootstrap(TestServer, { - platform: PlatformExpress - }) - ); - afterEach(() => PlatformTest.reset()); - - describe("statics()", () => { - it("should create a PlatformApplication", async () => { - const middlewareServeStatic = jest.fn(); - jest.spyOn(Express, "static").mockReturnValue(middlewareServeStatic); - - const app = await PlatformTest.invoke(PlatformApplication); - - jest.spyOn(app, "use").mockReturnThis(); - - app.statics("/path", { - root: "/publics", - options: "options" - }); - - expect(Express.static).toBeCalledWith(expect.stringContaining("/publics"), {options: "options"}); - expect(app.use).toBeCalledWith("/path", expect.any(Function)); - }); - }); -}); diff --git a/packages/platform/platform-express/src/services/PlatformExpressHandler.spec.ts b/packages/platform/platform-express/src/services/PlatformExpressHandler.spec.ts deleted file mode 100644 index a6a2f64c975..00000000000 --- a/packages/platform/platform-express/src/services/PlatformExpressHandler.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -import {Err, HandlerMetadata, HandlerType, PlatformTest} from "@tsed/common"; -import {PlatformExpressHandler} from "@tsed/platform-express"; -import {invokePlatformHandler} from "../../../../../test/helper/invokePlatformHandler"; - -describe("PlatformExpressHandler", () => { - beforeEach(PlatformTest.create); - afterEach(PlatformTest.reset); - - describe("createHandler", () => { - describe("ENDPOINT", () => { - it("should return a native handler with 3 params", async () => { - // GIVEN - const platformHandler = await invokePlatformHandler(PlatformExpressHandler); - - class Test { - get() {} - } - - PlatformTest.invoke(Test); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: Test, - type: HandlerType.ENDPOINT, - propertyKey: "get" - }); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - - // THEN - expect(handler).not.toEqual(handlerMetadata.handler); - expect(handler.length).toEqual(3); - }); - }); - describe("MIDDLEWARE", () => { - it("should return a native handler with 3 params", async () => { - // GIVEN - const platformHandler = await invokePlatformHandler(PlatformExpressHandler); - - class Test { - use() {} - } - - PlatformTest.invoke(Test); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: Test, - type: HandlerType.MIDDLEWARE, - propertyKey: "use" - }); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - - // THEN - expect(handler).not.toEqual(handlerMetadata.handler); - expect(handler.length).toEqual(3); - }); - it("should return a native handler with 4 params", async () => { - // GIVEN - const platformHandler = await invokePlatformHandler(PlatformExpressHandler); - - class Test { - use(@Err() err: unknown) {} - } - - PlatformTest.invoke(Test); - - const metadata = new HandlerMetadata({ - token: Test, - target: Test, - type: HandlerType.MIDDLEWARE, - propertyKey: "use" - }); - - // WHEN - const handler = platformHandler.createHandler(metadata); - - // THEN - expect(metadata.hasErrorParam).toEqual(true); - expect(handler).not.toEqual(metadata.handler); - expect(handler.length).toEqual(4); - }); - }); - describe("$CTX", () => { - it("should return a native handler with 3 params", async () => { - // GIVEN - const platformHandler = await invokePlatformHandler(PlatformExpressHandler); - - class Test { - use() {} - } - - PlatformTest.invoke(Test); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: (ctx: any) => {}, - type: HandlerType.CTX_FN - }); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - - // THEN - expect(handler).not.toEqual(handlerMetadata.handler); - expect(handler.length).toEqual(3); - }); - it("should catch error from handler", async () => { - // GIVEN - const platformHandler = await invokePlatformHandler(PlatformExpressHandler); - - class Test { - use() {} - } - - const error = new Error("test"); - PlatformTest.invoke(Test); - - const $ctx = PlatformTest.createRequestContext(); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: (ctx: any) => { - throw error; - }, - type: HandlerType.CTX_FN - }); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - - // THEN - expect(handler).not.toEqual(handlerMetadata.handler); - expect(handler.length).toEqual(3); - - const next = jest.fn(); - - await handler($ctx.getRequest(), $ctx.getResponse(), next); - - expect(next).toBeCalledWith(error); - }); - }); - describe("FUNCTION", () => { - it("should return a native handler with 3 params", async () => { - // GIVEN - const platformHandler = await invokePlatformHandler(PlatformExpressHandler); - - class Test { - use() {} - } - - PlatformTest.invoke(Test); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: (req: any, res: any, next: any) => {}, - type: HandlerType.RAW_FN - }); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - - // THEN - expect(handler).toEqual(handlerMetadata.handler); - }); - }); - }); -}); diff --git a/packages/platform/platform-express/src/services/PlatformExpressHandler.ts b/packages/platform/platform-express/src/services/PlatformExpressHandler.ts deleted file mode 100644 index 529243f4f90..00000000000 --- a/packages/platform/platform-express/src/services/PlatformExpressHandler.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {HandlerMetadata, HandlerType, OnRequestOptions, PlatformHandler} from "@tsed/common"; - -/** - * @platform - * @express - */ -export class PlatformExpressHandler extends PlatformHandler { - protected createRawHandler(metadata: HandlerMetadata): Function { - if (metadata.type === HandlerType.ERR_MIDDLEWARE) { - const handler = this.compileHandler(metadata); - return async (err: any, req: any, res: any, next: any) => - handler({ - err, - next, - $ctx: req.$ctx - }); - } - - return super.createRawHandler(metadata); - } - - protected async onCtxRequest(requestOptions: OnRequestOptions): Promise { - try { - return await super.onCtxRequest(requestOptions); - } catch (er) { - return this.onError(er, requestOptions); - } - } -} diff --git a/packages/platform/platform-express/src/services/PlatformExpressRequest.ts b/packages/platform/platform-express/src/services/PlatformExpressRequest.ts deleted file mode 100644 index 6a9eb27e1ca..00000000000 --- a/packages/platform/platform-express/src/services/PlatformExpressRequest.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {PlatformContext, PlatformRequest} from "@tsed/common"; -import type Express from "express"; - -/** - * @platform - * @express - */ -export class PlatformExpressRequest extends PlatformRequest {} diff --git a/packages/platform/platform-express/src/services/PlatformExpressResponse.spec.ts b/packages/platform/platform-express/src/services/PlatformExpressResponse.spec.ts deleted file mode 100644 index 9a67edd76c8..00000000000 --- a/packages/platform/platform-express/src/services/PlatformExpressResponse.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {PlatformTest} from "@tsed/common"; -import {PlatformExpressResponse} from "@tsed/platform-express"; -import "./PlatformExpressResponse"; - -describe("PlatformExpressResponse", () => { - beforeEach(() => PlatformTest.create()); - afterEach(() => PlatformTest.reset()); - it("should create a PlatformResponse instance", () => { - const response = PlatformTest.createResponse(); - const ctx = PlatformTest.createRequestContext({ - event: { - response - }, - ResponseKlass: PlatformExpressResponse - }); - - expect(ctx.response.raw).toEqual(response); - }); -}); diff --git a/packages/platform/platform-express/src/services/PlatformExpressResponse.ts b/packages/platform/platform-express/src/services/PlatformExpressResponse.ts deleted file mode 100644 index 72467691219..00000000000 --- a/packages/platform/platform-express/src/services/PlatformExpressResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {PlatformResponse} from "@tsed/common"; -import type Express from "express"; - -declare global { - namespace TsED { - export interface Response extends Express.Response {} - } -} - -/** - * @platform - * @express - */ -export class PlatformExpressResponse extends PlatformResponse {} diff --git a/packages/platform/platform-express/src/services/PlatformExpressRouter.spec.ts b/packages/platform/platform-express/src/services/PlatformExpressRouter.spec.ts deleted file mode 100644 index 01a188ca16d..00000000000 --- a/packages/platform/platform-express/src/services/PlatformExpressRouter.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {PlatformAdapter, PlatformHandler, PlatformRouter} from "@tsed/common"; -import {InjectorService} from "@tsed/di"; -import {PlatformExpress} from "@tsed/platform-express"; -import Express from "express"; - -jest.mock("express"); - -describe("PlatformExpressRouter", () => { - describe("create()", () => { - it("should create a new router", async () => { - // GIVEN - const nativeDriver = { - use: jest.fn(), - all: jest.fn(), - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), - delete: jest.fn(), - patch: jest.fn(), - head: jest.fn(), - options: jest.fn() - }; - - (Express.Router as jest.Mock).mockReturnValue(nativeDriver); - - const platformHandler = { - createHandler: jest.fn().mockImplementation((o) => o) - }; - - const injector = new InjectorService(); - injector.addProvider(PlatformHandler, { - useValue: platformHandler - }); - - injector.addProvider(PlatformAdapter, { - useValue: new PlatformExpress({ - settings: injector.settings, - injector - } as any) - }); - - injector.settings.set("express", { - router: { - mergeParams: true - } - }); - - const routerOptions: any = { - test: "options" - }; - - // WHEN - const router = PlatformRouter.create(injector, routerOptions); - - // THEN - expect(Express.Router).toBeCalledWith({...injector.settings.get("express.router"), ...routerOptions}); - - expect(router.raw).toEqual(nativeDriver); - }); - }); -}); diff --git a/packages/platform/platform-express/test/__snapshots__/array-body.spec.ts.snap b/packages/platform/platform-express/test/__snapshots__/array-body.spec.ts.snap new file mode 100644 index 00000000000..201be2cd67f --- /dev/null +++ b/packages/platform/platform-express/test/__snapshots__/array-body.spec.ts.snap @@ -0,0 +1,246 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Array Body swagger should return the swagger (OS3) 1`] = ` +Object { + "components": Object { + "schemas": Object { + "MyModel": Object { + "properties": Object { + "test": Object { + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "info": Object { + "title": "Api documentation", + "version": "1.0.0", + }, + "openapi": "3.0.1", + "paths": Object { + "/rest/array/1": Object { + "post": Object { + "operationId": "testArrayBodyCtrlScenario1", + "parameters": Array [], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "items": Object { + "nullable": true, + "oneOf": Array [ + Object { + "type": "integer", + }, + Object { + "type": "number", + }, + Object { + "type": "string", + }, + Object { + "type": "boolean", + }, + Object { + "type": "array", + }, + Object { + "type": "object", + }, + ], + }, + "type": "array", + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "TestArrayBodyCtrl", + ], + }, + }, + "/rest/array/2": Object { + "post": Object { + "operationId": "testArrayBodyCtrlScenario2", + "parameters": Array [], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "items": Object { + "nullable": true, + "oneOf": Array [ + Object { + "type": "integer", + }, + Object { + "type": "number", + }, + Object { + "type": "string", + }, + Object { + "type": "boolean", + }, + Object { + "type": "array", + }, + Object { + "type": "object", + }, + ], + }, + "type": "array", + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "TestArrayBodyCtrl", + ], + }, + }, + "/rest/array/3": Object { + "post": Object { + "operationId": "testArrayBodyCtrlScenario3", + "parameters": Array [], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "items": Object { + "type": "number", + }, + "type": "array", + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "TestArrayBodyCtrl", + ], + }, + }, + "/rest/array/4": Object { + "post": Object { + "operationId": "testArrayBodyCtrlScenario4", + "parameters": Array [], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "items": Object { + "$ref": "#/components/schemas/MyModel", + }, + "type": "array", + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "TestArrayBodyCtrl", + ], + }, + }, + "/rest/array/5": Object { + "post": Object { + "operationId": "testArrayBodyCtrlScenario5", + "parameters": Array [], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "type": "object", + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "TestArrayBodyCtrl", + ], + }, + }, + "/rest/array/6": Object { + "post": Object { + "operationId": "testArrayBodyCtrlScenario6", + "parameters": Array [], + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "nullable": true, + "oneOf": Array [ + Object { + "type": "integer", + }, + Object { + "type": "number", + }, + Object { + "type": "string", + }, + Object { + "type": "boolean", + }, + Object { + "type": "array", + }, + Object { + "type": "object", + }, + ], + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "TestArrayBodyCtrl", + ], + }, + }, + }, + "tags": Array [ + Object { + "name": "TestArrayBodyCtrl", + }, + ], +} +`; diff --git a/packages/platform/platform-express/test/array-body.spec.ts b/packages/platform/platform-express/test/array-body.spec.ts index 64e24149764..1478021f55a 100644 --- a/packages/platform/platform-express/test/array-body.spec.ts +++ b/packages/platform/platform-express/test/array-body.spec.ts @@ -76,236 +76,7 @@ describe("Array Body", () => { it("should return the swagger (OS3)", async () => { const {body} = await request.get("/v3/docs/swagger.json").expect(200); - expect(body).toEqual({ - components: { - schemas: { - MyModel: { - properties: { - test: { - type: "string" - } - }, - type: "object" - } - } - }, - info: { - title: "Api documentation", - version: "1.0.0" - }, - openapi: "3.0.1", - paths: { - "/rest/array/1": { - post: { - operationId: "testArrayBodyCtrlScenario1", - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - items: { - nullable: true, - oneOf: [ - { - type: "integer" - }, - { - type: "number" - }, - { - type: "string" - }, - { - type: "boolean" - }, - { - type: "array" - }, - { - type: "object" - } - ] - }, - type: "array" - } - } - }, - required: false - }, - responses: { - "200": { - description: "Success" - } - }, - tags: ["TestArrayBodyCtrl"] - } - }, - "/rest/array/2": { - post: { - operationId: "testArrayBodyCtrlScenario2", - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - items: { - nullable: true, - oneOf: [ - { - type: "integer" - }, - { - type: "number" - }, - { - type: "string" - }, - { - type: "boolean" - }, - { - type: "array" - }, - { - type: "object" - } - ] - }, - type: "array" - } - } - }, - required: false - }, - responses: { - "200": { - description: "Success" - } - }, - tags: ["TestArrayBodyCtrl"] - } - }, - "/rest/array/3": { - post: { - operationId: "testArrayBodyCtrlScenario3", - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - items: { - type: "number" - }, - type: "array" - } - } - }, - required: false - }, - responses: { - "200": { - description: "Success" - } - }, - tags: ["TestArrayBodyCtrl"] - } - }, - "/rest/array/4": { - post: { - operationId: "testArrayBodyCtrlScenario4", - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - items: { - $ref: "#/components/schemas/MyModel" - }, - type: "array" - } - } - }, - required: false - }, - responses: { - "200": { - description: "Success" - } - }, - tags: ["TestArrayBodyCtrl"] - } - }, - "/rest/array/5": { - post: { - operationId: "testArrayBodyCtrlScenario5", - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - type: "object" - } - } - }, - required: false - }, - responses: { - "200": { - description: "Success" - } - }, - tags: ["TestArrayBodyCtrl"] - } - }, - "/rest/array/6": { - post: { - operationId: "testArrayBodyCtrlScenario6", - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - nullable: true, - oneOf: [ - { - type: "integer" - }, - { - type: "number" - }, - { - type: "string" - }, - { - type: "boolean" - }, - { - type: "array" - }, - { - type: "object" - } - ] - } - } - }, - required: false - }, - responses: { - "200": { - description: "Success" - } - }, - tags: ["TestArrayBodyCtrl"] - } - } - }, - tags: [ - { - name: "TestArrayBodyCtrl" - } - ] - }); + expect(body).toMatchSnapshot(); }); }); diff --git a/packages/platform/platform-express/test/fullpayload.spec.ts b/packages/platform/platform-express/test/fullpayload.spec.ts index 3b489901e88..919c74a4f49 100644 --- a/packages/platform/platform-express/test/fullpayload.spec.ts +++ b/packages/platform/platform-express/test/fullpayload.spec.ts @@ -1,6 +1,5 @@ import {BodyParams, Controller, Get, PlatformTest, Post} from "@tsed/common"; import {PlatformTestUtils} from "@tsed/platform-test-utils"; -import {getSpec, Returns, SpecTypes} from "@tsed/schema"; import SuperTest from "supertest"; import {PlatformExpress} from "../src"; import {rootDir, Server} from "./app/Server"; diff --git a/packages/platform/platform-express/test/response-filter.spec.ts b/packages/platform/platform-express/test/response-filter.spec.ts index 33554c7a046..e7fc4bd9482 100644 --- a/packages/platform/platform-express/test/response-filter.spec.ts +++ b/packages/platform/platform-express/test/response-filter.spec.ts @@ -1,6 +1,7 @@ import {Context, Controller, Get, PlatformTest, Res, ResponseFilter} from "@tsed/common"; import {PlatformTestUtils} from "@tsed/platform-test-utils"; import {Returns} from "@tsed/schema"; +import {ServerResponse} from "http"; import SuperTest from "supertest"; import {PlatformExpress} from "../src"; import {rootDir, Server} from "./app/Server"; @@ -34,7 +35,7 @@ class TestPageableCtrl { } @Get("/scenario-2") - async scenario2(@Res() response: Res) { + async scenario2(@Res() response: ServerResponse) { const raw = "..."; response.setHeader("Content-Type", "image/png"); diff --git a/packages/platform/platform-koa/jest.config.js b/packages/platform/platform-koa/jest.config.js index 15474484e23..6debeab4fce 100644 --- a/packages/platform/platform-koa/jest.config.js +++ b/packages/platform/platform-koa/jest.config.js @@ -5,10 +5,10 @@ module.exports = { ...require("@tsed/jest-config")(__dirname, "platform-koa"), coverageThreshold: { global: { - statements: 99.5, + statements: 99.46, branches: 85.45, functions: 100, - lines: 99.49 + lines: 99.45 } } }; diff --git a/packages/platform/platform-koa/package.json b/packages/platform/platform-koa/package.json index d416f643ec8..f26522f4d78 100644 --- a/packages/platform/platform-koa/package.json +++ b/packages/platform/platform-koa/package.json @@ -126,4 +126,4 @@ "optional": false } } -} \ No newline at end of file +} diff --git a/packages/platform/platform-koa/src/components/PlatformKoa.ts b/packages/platform/platform-koa/src/components/PlatformKoa.ts index 6b97a51b886..605fb5054e9 100644 --- a/packages/platform/platform-koa/src/components/PlatformKoa.ts +++ b/packages/platform/platform-koa/src/components/PlatformKoa.ts @@ -1,10 +1,11 @@ import KoaRouter, {RouterOptions as KoaRouterOptions} from "@koa/router"; import { - createContext, + getContext, InjectorService, PlatformAdapter, PlatformApplication, PlatformBuilder, + PlatformContext, PlatformHandler, PlatformMulter, PlatformMulterSettings, @@ -14,6 +15,7 @@ import { } from "@tsed/common"; import {PlatformExceptions} from "@tsed/platform-exceptions"; import {isFunction, Type} from "@tsed/core"; +import {PlatformHandlerMetadata, PlatformLayer} from "@tsed/platform-router"; import Koa, {Context, Next} from "koa"; import koaBodyParser, {Options} from "koa-bodyparser"; // @ts-ignore @@ -32,7 +34,7 @@ declare global { } namespace TsED { - export interface Router extends KoaRouter {} + // export interface Router extends KoaRouter {} export interface RouterOptions extends KoaRouterOptions {} @@ -57,7 +59,7 @@ KoaRouter.prototype.match = function match(...args: any[]) { * @platform * @koa */ -export class PlatformKoa implements PlatformAdapter { +export class PlatformKoa implements PlatformAdapter { readonly providers = [ { provide: PlatformResponse, @@ -81,7 +83,7 @@ export class PlatformKoa implements PlatformAdapter { * @param settings */ static create(module: Type, settings: Partial = {}) { - return PlatformBuilder.create(module, { + return PlatformBuilder.create(module, { ...settings, adapter: PlatformKoa }); @@ -93,7 +95,7 @@ export class PlatformKoa implements PlatformAdapter { * @param settings */ static async bootstrap(module: Type, settings: Partial = {}) { - return PlatformBuilder.bootstrap(module, { + return PlatformBuilder.bootstrap(module, { ...settings, adapter: PlatformKoa }); @@ -101,37 +103,65 @@ export class PlatformKoa implements PlatformAdapter { onInit() { const injector = this.injector; - const app = this.injector.get(PlatformApplication)!; + const app = this.getPlatformApplication(); const listener: any = (error: any, ctx: Koa.Context) => { - injector.get(PlatformExceptions)?.catch(error, ctx.request.$ctx); + const $ctx = getContext()!; + injector.get(PlatformExceptions)?.catch(error, $ctx); }; app.getApp().silent = true; app.getApp().on("error", listener); } - useRouter(): this { + async beforeLoadRoutes() { const app = this.injector.get>(PlatformApplication)!; - app.getApp().use(resourceNotFoundMiddleware); - app.getApp().use(app.getRouter().routes()).use(app.getRouter().allowedMethods()); + this.useContext(); - return this; + app.use(resourceNotFoundMiddleware); + } + + mapLayers(layers: PlatformLayer[]) { + const {settings} = this.injector; + const app = this.getPlatformApplication(); + const options = settings.get("koa.router", {}); + const rawRouter = new KoaRouter(options) as any; + + layers.forEach((layer) => { + switch (layer.method) { + case "statics": + rawRouter.use(layer.path, this.statics(layer.path as string, layer.opts as any)); + break; + + default: + rawRouter[layer.method](...layer.getArgs()); + } + }); + + app.getApp().use(rawRouter.routes()).use(rawRouter.allowedMethods()); + } + + mapHandler(handler: Function, metadata: PlatformHandlerMetadata) { + if (metadata.isRawMiddleware()) { + return handler; + } + + return async (koaContext: Koa.Context, next: Koa.Next) => { + const $ctx = getContext()!; + $ctx.next = next; + + await handler($ctx); + }; } useContext(): this { - const app = this.injector.get>(PlatformApplication)!; - const {logger} = this.injector; - logger.info("Mount app context"); + const app = this.getPlatformApplication(); - const invoke = createContext(this.injector); + app.use(async (koaContext: Context, next: Next) => { + const $ctx = getContext>()!; - app.getApp().use(async (ctx: Context, next: Next) => { - await invoke({ - request: ctx.request as any, - response: ctx.response as any - }); + $ctx.upgrade({request: koaContext.request as any, response: koaContext.response as any, koaContext}); return next(); }); @@ -151,20 +181,6 @@ export class PlatformKoa implements PlatformAdapter { }; } - router(routerOptions: any = {}) { - const {settings} = this.injector; - - const options = Object.assign({}, settings.get("koa.router", {}), routerOptions); - const router = new KoaRouter(options) as any; - - return { - router, - callback() { - return [router.routes(), router.allowedMethods()]; - } - }; - } - multipart(options: PlatformMulterSettings): PlatformMulter { return getMulter(options); } @@ -186,4 +202,8 @@ export class PlatformKoa implements PlatformAdapter { return parser({...options, ...additionalOptions}); } + + private getPlatformApplication() { + return this.injector.get(PlatformApplication)!; + } } diff --git a/packages/platform/platform-koa/src/decorators/caseSensitive.ts b/packages/platform/platform-koa/src/decorators/caseSensitive.ts deleted file mode 100644 index c88d5c3a183..00000000000 --- a/packages/platform/platform-koa/src/decorators/caseSensitive.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {RouterSettings} from "./routerSettings"; - -/** - * Specify the behavior of the router controller. - * - * ```typescript - * @Controller("/") - * @CaseSensitive(true) - * class MyCtrl { - * - * } - * ``` - * - * Property | Description | Default - * ---|---|--- - * CaseSensitive | Enable case sensitivity. | Disabled by default, treating “/Foo” and “/foo” as the same. - * Strict | Enable strict routing. | Disabled by default, “/foo” and “/foo/” are treated the same by the router. - * - * @returns {Function} - * @decorator - * @koa - * @param sensitive - */ -export function CaseSensitive(sensitive: boolean) { - return RouterSettings({sensitive}); -} diff --git a/packages/platform/platform-koa/src/decorators/routerSettings.spec.ts b/packages/platform/platform-koa/src/decorators/routerSettings.spec.ts deleted file mode 100644 index f68da330c73..00000000000 --- a/packages/platform/platform-koa/src/decorators/routerSettings.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {ROUTER_OPTIONS} from "@tsed/common"; -import {Store} from "@tsed/core"; -import {CaseSensitive, Strict} from "@tsed/platform-koa"; - -class Test {} - -describe("RouterSettings", () => { - describe("CaseSensitive", () => { - it("should call merge method for mergeParams options", () => { - CaseSensitive(true)(Test); - const store = Store.from(Test).get(ROUTER_OPTIONS); - expect(store.sensitive).toEqual(true); - }); - }); - - describe("Strict", () => { - it("should call merge method for mergeParams options", () => { - Strict(true)(Test); - const store = Store.from(Test).get(ROUTER_OPTIONS); - expect(store.strict).toEqual(true); - }); - }); - - describe("Strict", () => { - it("should call merge method for mergeParams options", () => { - Strict(true)(Test); - const store = Store.from(Test).get(ROUTER_OPTIONS); - expect(store.strict).toEqual(true); - }); - }); -}); diff --git a/packages/platform/platform-koa/src/decorators/routerSettings.ts b/packages/platform/platform-koa/src/decorators/routerSettings.ts deleted file mode 100644 index 5ef8feb74ca..00000000000 --- a/packages/platform/platform-koa/src/decorators/routerSettings.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {ROUTER_OPTIONS} from "@tsed/common"; -import {StoreMerge} from "@tsed/core"; -import {RouterOptions} from "@koa/router"; - -/** - * Specify the behavior of the router controller. - * - * ```typescript - * @Controller("/") - * @RouterSettings({sensitive: true}) - * class MyCtrl { - * - * } - * ``` - * - * Property | Description | Default - * ---|---|--- - * sensitive | Enable case sensitivity. | Disabled by default, treating “/Foo” and “/foo” as the same. - * strict | Enable strict routing. | Disabled by default, “/foo” and “/foo/” are treated the same by the router. - * - * @returns {(target:any)=>void} - * @decorator - * @param routerOptions - * @koa - */ -export function RouterSettings(routerOptions: RouterOptions): Function { - return StoreMerge(ROUTER_OPTIONS, routerOptions); -} diff --git a/packages/platform/platform-koa/src/decorators/strict.ts b/packages/platform/platform-koa/src/decorators/strict.ts deleted file mode 100644 index 7f0a3fdaae6..00000000000 --- a/packages/platform/platform-koa/src/decorators/strict.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {RouterSettings} from "./routerSettings"; - -/** - * Specify the behavior of the router controller. - * - * ```typescript - * @Controller("/") - * @Strict(true) - * class MyCtrl { - * - * } - * ``` - * - * Property | Description | Default - * ---|---|--- - * CaseSensitive | Enable case sensitivity. | Disabled by default, treating “/Foo” and “/foo” as the same. - * MergeParams | Preserve the req.params values from the parent router. If the parent and the child have conflicting param names, the child’s value take precedence. | false - * Strict | Enable strict routing. | Disabled by default, “/foo” and “/foo/” are treated the same by the router. - * - * @param strict - * @returns {Function} - * @decorator - * @koa - */ -export function Strict(strict: boolean) { - return RouterSettings({strict}); -} diff --git a/packages/platform/platform-koa/src/index.ts b/packages/platform/platform-koa/src/index.ts index f35afd165ef..3bcb929fa1b 100644 --- a/packages/platform/platform-koa/src/index.ts +++ b/packages/platform/platform-koa/src/index.ts @@ -4,11 +4,8 @@ export * from "./exports"; export * from "./components/PlatformKoa"; -export * from "./decorators/caseSensitive"; export * from "./decorators/ctx"; -export * from "./decorators/routerSettings"; export * from "./decorators/state"; -export * from "./decorators/strict"; export * from "./interfaces/PlatformKoaSettings"; export * from "./interfaces/interfaces"; export * from "./middlewares/resourceNotFoundMiddleware"; diff --git a/packages/platform/platform-koa/src/middlewares/resourceNotFoundMiddleware.ts b/packages/platform/platform-koa/src/middlewares/resourceNotFoundMiddleware.ts index 0bf9ac6adff..e920a971dad 100644 --- a/packages/platform/platform-koa/src/middlewares/resourceNotFoundMiddleware.ts +++ b/packages/platform/platform-koa/src/middlewares/resourceNotFoundMiddleware.ts @@ -1,4 +1,5 @@ import {PlatformContext} from "@tsed/common"; +import {getContext} from "@tsed/di"; import {PlatformExceptions} from "@tsed/platform-exceptions"; /** @@ -7,7 +8,7 @@ import {PlatformExceptions} from "@tsed/platform-exceptions"; * @param next */ export async function resourceNotFoundMiddleware(ctx: any, next: any) { - const $ctx: PlatformContext = ctx.request.$ctx; + const $ctx = getContext()!; await next(); const status = ctx.status || 404; diff --git a/packages/platform/platform-koa/src/services/PlatformKoaHandler.spec.ts b/packages/platform/platform-koa/src/services/PlatformKoaHandler.spec.ts deleted file mode 100644 index f8bd9a50752..00000000000 --- a/packages/platform/platform-koa/src/services/PlatformKoaHandler.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import {HandlerMetadata, HandlerType, PlatformTest} from "@tsed/common"; -import {PlatformKoaHandler} from "@tsed/platform-koa"; -import {invokePlatformHandler} from "../../../../../test/helper/invokePlatformHandler"; - -describe("PlatformKoaHandler", () => { - beforeEach(PlatformTest.create); - afterEach(PlatformTest.reset); - - describe("createHandler", () => { - describe("$CTX", () => { - it("should return a native handler with 3 params", async () => { - // GIVEN - const platformHandler = await invokePlatformHandler(PlatformKoaHandler); - - class Test { - use() {} - } - - PlatformTest.invoke(Test); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: (ctx: any) => {}, - type: HandlerType.CTX_FN - }); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - - // THEN - expect(handler).not.toEqual(handlerMetadata.handler); - expect(handler.length).toEqual(2); - }); - it("should catch error from handler", async () => { - // GIVEN - const platformHandler = await invokePlatformHandler(PlatformKoaHandler); - - class Test { - use() {} - } - - const error = new Error("test"); - - PlatformTest.invoke(Test); - - const $ctx = PlatformTest.createRequestContext(); - const next = jest.fn(); - const ctx = { - request: $ctx.getRequest(), - response: $ctx.getResponse() - }; - $ctx.getRequest().ctx = ctx; - $ctx.getApp().emit = jest.fn(); - - const handlerMetadata = new HandlerMetadata({ - token: Test, - target: (ctx: any) => { - throw error; - }, - type: HandlerType.CTX_FN - }); - - // WHEN - const handler = platformHandler.createHandler(handlerMetadata); - - // THEN - expect(handler).not.toEqual(handlerMetadata.handler); - expect(handler.length).toEqual(2); - - await handler(ctx, next); - - expect($ctx.getApp().emit).toBeCalledWith("error", error, ctx); - }); - }); - }); -}); diff --git a/packages/platform/platform-koa/src/services/PlatformKoaHandler.ts b/packages/platform/platform-koa/src/services/PlatformKoaHandler.ts index 3a648f9b2c0..6b4ae9c64c0 100644 --- a/packages/platform/platform-koa/src/services/PlatformKoaHandler.ts +++ b/packages/platform/platform-koa/src/services/PlatformKoaHandler.ts @@ -1,71 +1,35 @@ -import {HandlerMetadata, HandlerType, OnRequestOptions, PlatformContext, PlatformHandler} from "@tsed/common"; -import Koa from "koa"; +import {PlatformContext, PlatformHandler, PlatformParamsCallback} from "@tsed/common"; +import {isStream} from "@tsed/core"; +import {PlatformContextHandler} from "@tsed/platform-router"; import "./PlatformKoaRequest"; -const OVERRIDE_TYPES = [HandlerType.ENDPOINT, HandlerType.MIDDLEWARE, HandlerType.ERR_MIDDLEWARE, HandlerType.CTX_FN]; - export class PlatformKoaHandler extends PlatformHandler { - public async flush(data: any, ctx: PlatformContext): Promise { - if (data === undefined && ctx.getResponse().body) { - data = ctx.getResponse().body; - } - - return super.flush(data, ctx); - } - - protected createRawHandler(metadata: HandlerMetadata) { - if (OVERRIDE_TYPES.includes(metadata.type)) { - const handler = this.compileHandler(metadata); - return async (ctx: Koa.Context, next: Koa.Next) => handler({next, $ctx: ctx.request.$ctx}); + public async flush($ctx: PlatformContext) { + if ($ctx.data === undefined && $ctx.getResponse().body) { + $ctx.data = $ctx.getResponse().body; } - return super.createRawHandler(metadata); + return super.flush($ctx); } - protected async onRequest(requestOptions: OnRequestOptions): Promise { - const {metadata, $ctx} = requestOptions; - - if ($ctx.data instanceof Error) { - if (metadata.hasErrorParam) { - requestOptions.err = $ctx.data; - } else { - return this.onError($ctx.data, requestOptions); - } + async onRequest(handler: PlatformParamsCallback, $ctx: PlatformContext): Promise { + if ($ctx.error instanceof Error && !$ctx.handlerMetadata.hasErrorParam) { + return; } - return super.onRequest(requestOptions); + return super.onRequest(handler, $ctx); } - protected onError(error: unknown, requestOptions: OnRequestOptions) { - const {next, $ctx, metadata} = requestOptions; - - // istanbul ignore next - if ($ctx.isDone()) { + public next(error: unknown, next: any, $ctx: PlatformContext) { + if (isStream($ctx.data) || $ctx.isDone()) { return; } - $ctx.data = error; - - if (!next || metadata.isFinal()) { - this.throwError(error, $ctx); - + if (error && !($ctx.handlerMetadata.isEndpoint() && !$ctx.handlerMetadata.isFinal())) { + $ctx.getApp().emit("error", error, $ctx.getRequest().ctx); return; } - return next(); - } - - protected async onCtxRequest(requestOptions: OnRequestOptions): Promise { - const {$ctx} = requestOptions; - - try { - return await super.onCtxRequest(requestOptions); - } catch (error) { - this.throwError(error, $ctx); - } - } - - protected throwError(error: any, $ctx: PlatformContext) { - $ctx.getApp().emit("error", error, $ctx.getRequest().ctx); + return !$ctx.handlerMetadata.isFinal() && next(); } } diff --git a/packages/platform/platform-koa/src/services/PlatformKoaRequest.ts b/packages/platform/platform-koa/src/services/PlatformKoaRequest.ts index 5954a391f47..40cb8a79b10 100644 --- a/packages/platform/platform-koa/src/services/PlatformKoaRequest.ts +++ b/packages/platform/platform-koa/src/services/PlatformKoaRequest.ts @@ -21,7 +21,7 @@ declare global { */ export class PlatformKoaRequest extends PlatformRequest { get ctx(): Koa.Context { - return this.raw.ctx; + return this.$ctx.event.koaContext as Koa.Context; } get protocol(): string { diff --git a/packages/platform/platform-log-middleware/src/middlewares/PlatformLogMiddleware.spec.ts b/packages/platform/platform-log-middleware/src/middlewares/PlatformLogMiddleware.spec.ts index f4dd46d54a6..eb222ad28be 100644 --- a/packages/platform/platform-log-middleware/src/middlewares/PlatformLogMiddleware.spec.ts +++ b/packages/platform/platform-log-middleware/src/middlewares/PlatformLogMiddleware.spec.ts @@ -1,4 +1,4 @@ -import {PlatformTest} from "@tsed/common"; +import {PlatformHandlerMetadata, PlatformTest} from "@tsed/common"; import {PlatformLogMiddleware} from "./PlatformLogMiddleware"; async function createMiddlewareFixture() { @@ -19,6 +19,10 @@ async function createMiddlewareFixture() { logger: PlatformTest.injector.logger }); + ctx.handlerMetadata = new PlatformHandlerMetadata({ + handler: () => {} + }); + jest.spyOn(PlatformTest.injector.logger, "info").mockResolvedValue(undefined); jest.spyOn(PlatformTest.injector.logger, "warn").mockResolvedValue(undefined); jest.spyOn(PlatformTest.injector.logger, "trace").mockResolvedValue(undefined); @@ -44,12 +48,13 @@ describe("PlatformLogMiddleware", () => { // GIVEN const {request, ctx, middleware} = await createMiddlewareFixture(); request.originalUrl = "/originalUrl"; - ctx.endpoint.set("route", "/:id"); + ctx.handlerMetadata.path = "/:id"; + // WHEN middleware.use(ctx); // THEN - middleware.$onResponse(request.$ctx); + middleware.$onResponse(ctx); // THEN expect(PlatformTest.injector.logger.info).toHaveBeenCalledWith( @@ -92,13 +97,13 @@ describe("PlatformLogMiddleware", () => { it("should configure request and create context logger (debug, logRequest)", async () => { // GIVEN - const {request, ctx, middleware} = await createMiddlewareFixture(); + const {ctx, middleware} = await createMiddlewareFixture(); // WHEN middleware.use(ctx); // THEN - middleware.$onResponse(request.$ctx as any); + middleware.$onResponse(ctx as any); // THEN expect(PlatformTest.injector.logger.debug).toHaveBeenCalledWith( @@ -137,7 +142,7 @@ describe("PlatformLogMiddleware", () => { middleware.use(ctx); // THEN - middleware.$onResponse(request.$ctx as any); + middleware.$onResponse(ctx as any); // THEN expect(PlatformTest.injector.logger.info).toHaveBeenCalledWith( @@ -160,13 +165,13 @@ describe("PlatformLogMiddleware", () => { afterEach(() => PlatformTest.reset()); it("should configure request and create context logger (no debug, logRequest, logStart)", async () => { // GIVEN - const {request, ctx, middleware} = await createMiddlewareFixture(); + const {ctx, middleware} = await createMiddlewareFixture(); // WHEN middleware.use(ctx); // THEN - middleware.$onResponse(request.$ctx as any); + middleware.$onResponse(ctx as any); // THEN expect(PlatformTest.injector.logger.info).toHaveBeenCalledWith( diff --git a/packages/platform/platform-params/src/builder/PlatformParams.spec.ts b/packages/platform/platform-params/src/builder/PlatformParams.spec.ts index 9ce38eca39b..a9af769e1f1 100644 --- a/packages/platform/platform-params/src/builder/PlatformParams.spec.ts +++ b/packages/platform/platform-params/src/builder/PlatformParams.spec.ts @@ -109,16 +109,16 @@ describe("PlatformParams", () => { // GIVEN const {param, h, platformParams} = await buildPlatformParams({ paramType: ParamTypes.ERR, - dataPath: "err" + dataPath: "$ctx.error" }); - h.err = new Error(); + h.$ctx.error = new Error(); // WHEN const pipes = await platformParams.getPipes(param); const value = await platformParams.getArg(h, pipes, param); // THEN - expect(value).toEqual(h.err); + expect(value).toEqual(h.$ctx.error); }); it("should return $CTX", async () => { // GIVEN @@ -262,7 +262,8 @@ describe("PlatformParams", () => { paramType: ParamTypes.LOCALS, dataPath: "$ctx.response.locals" }); - h.err = new Error(); + + h.$ctx.error = new Error(); // WHEN const pipes = await platformParams.getPipes(param); @@ -391,49 +392,5 @@ describe("PlatformParams", () => { expect(result).toEqual("test"); }); - it("should with default args", async () => { - const platformParams = await invokePlatformParams(); - - @Injectable() - class MyCtrTest { - get(query: any, params: any) { - return {query, params}; - } - } - - const $ctx = PlatformTest.createRequestContext({ - event: { - request: { - query: { - test: "test" - }, - params: { - s: "s" - } - } - } - }); - - const handler = await platformParams.compileHandler({ - token: MyCtrTest, - propertyKey: "get", - async getCustomArgs(scope: any) { - return [scope.$ctx.request.query, scope.$ctx.request.params]; - } - }); - - const result = await handler({ - $ctx - }); - - expect(result).toEqual({ - params: { - s: "s" - }, - query: { - test: "test" - } - }); - }); }); }); diff --git a/packages/platform/platform-params/src/builder/PlatformParams.ts b/packages/platform/platform-params/src/builder/PlatformParams.ts index e9810a5846f..9cdb7d4a1fd 100644 --- a/packages/platform/platform-params/src/builder/PlatformParams.ts +++ b/packages/platform/platform-params/src/builder/PlatformParams.ts @@ -1,16 +1,10 @@ import {DIContext, Inject, Injectable, InjectorService, ProviderScope, TokenProvider} from "@tsed/di"; -import {JsonEntityStore, JsonParameterStore, PipeMethods} from "@tsed/schema"; +import {JsonMethodStore, JsonParameterStore, PipeMethods} from "@tsed/schema"; import {ParamValidationError} from "../errors/ParamValidationError"; import {ParseExpressionPipe} from "../pipes/ParseExpressionPipe"; -export type ArgScope = {$ctx: Context} & Record; -export type HandlerWithScope = (scope: ArgScope) => any; - -export interface CompileHandlerOptions extends Record { - token: TokenProvider; - propertyKey: string | symbol; - getCustomArgs?: (scope: ArgScope) => Promise; -} +export type PlatformParamsScope = {$ctx: Context} & Record; +export type PlatformParamsCallback = (scope: PlatformParamsScope) => Promise; /** * Platform Params abstraction layer. @@ -18,47 +12,61 @@ export interface CompileHandlerOptions ex */ @Injectable({ scope: ProviderScope.SINGLETON, - imports: [] + imports: [ParseExpressionPipe] }) export class PlatformParams { @Inject() protected injector: InjectorService; - async getPipes(param: JsonParameterStore) { + getPipes(param: JsonParameterStore) { const get = (pipe: TokenProvider) => { return this.injector.getProvider(pipe)!.priority || 0; }; const sort = (p1: TokenProvider, p2: TokenProvider) => (get(p1) < get(p2) ? -1 : get(p1) > get(p2) ? 1 : 0); - const map = (token: TokenProvider) => this.injector.invoke(token)!; - const promises = await Promise.all([ParseExpressionPipe, ...param.pipes.sort(sort)].map(map)); + const map = (token: TokenProvider) => this.injector.get(token)!; - return promises.filter(Boolean); + return [ParseExpressionPipe, ...param.pipes.sort(sort)].map(map).filter(Boolean); } - async compile(entity: JsonEntityStore) { - const params = JsonParameterStore.getParams(entity.target, entity.propertyKey); + /** + * Return a handler with injectable parameters + * @param handlerMetadata + */ + compileHandler({ + propertyKey, + token + }: { + propertyKey: string | symbol; + token: any; + }): PlatformParamsCallback { + const store = JsonMethodStore.fromMethod(token, propertyKey); + const getArguments = this.compile(store); + + return async (scope: PlatformParamsScope) => { + const [instance, args] = await Promise.all([this.injector.invoke(token, scope.$ctx.container), getArguments(scope)]); - const argsPipes = await Promise.all( - params.map(async (param) => { - return { - param, - pipes: await this.getPipes(param) - }; - }) - ); + return instance[propertyKey].call(instance, ...args, scope.$ctx); + }; + } - return (scope: ArgScope) => { - const promises = argsPipes.map(({param, pipes}) => { - return this.getArg(scope, pipes, param); - }); + compile(entity: JsonMethodStore): PlatformParamsCallback { + const params = JsonParameterStore.getParams(entity.target, entity.propertyKey); + const argsPipes = params.map((param) => { + return { + param, + pipes: this.getPipes(param) + }; + }); + return (scope) => { + const promises = argsPipes.map(({param, pipes}) => this.getArg(scope, pipes, param)); return Promise.all(promises); }; } - async getArg(scope: ArgScope, pipes: PipeMethods[], param: JsonParameterStore) { - return pipes.reduce(async (value, pipe) => { + async getArg(scope: PlatformParamsScope, pipes: PipeMethods[], param: JsonParameterStore) { + return pipes.reduce(async (value: any | Promise, pipe) => { value = await value; try { @@ -66,31 +74,6 @@ export class PlatformParams { } catch (er) { throw ParamValidationError.from(param, er); } - }, scope as any); - } - - async compileHandler( - metadata: CompileHandlerOptions - ): Promise> { - const {token, propertyKey, getCustomArgs} = metadata; - - const provider = this.injector.getProvider(token); - const getArguments = getCustomArgs || (await this.compile(JsonEntityStore.fromMethod(token, propertyKey!))); - - if (!provider || !provider.scope || provider.scope === ProviderScope.SINGLETON) { - const instance = await this.injector.invoke(token); - - return async (scope) => { - const args = await getArguments(scope); - - return instance[propertyKey!].call(instance, ...args, scope.$ctx); - }; - } - - return async (scope) => { - const [instance, args] = await Promise.all([this.injector.invoke(token, scope.$ctx.container), getArguments(scope)]); - - return instance[propertyKey].call(instance, ...args, scope.$ctx); - }; + }, scope); } } diff --git a/packages/platform/platform-params/src/pipes/ParseExpressionPipe.ts b/packages/platform/platform-params/src/pipes/ParseExpressionPipe.ts index 0e4f28f3fe7..2e7a3dd851f 100644 --- a/packages/platform/platform-params/src/pipes/ParseExpressionPipe.ts +++ b/packages/platform/platform-params/src/pipes/ParseExpressionPipe.ts @@ -1,14 +1,14 @@ import {getValue} from "@tsed/core"; import {Injectable} from "@tsed/di"; import {JsonParameterStore, PipeMethods} from "@tsed/schema"; +import {PlatformParamsScope} from "../builder/PlatformParams"; import {ParamTypes} from "../domain/ParamTypes"; -import type {ArgScope} from "../builder/PlatformParams"; @Injectable({ priority: -1000 }) export class ParseExpressionPipe implements PipeMethods { - transform(scope: ArgScope, param: JsonParameterStore) { + transform(scope: PlatformParamsScope, param: JsonParameterStore) { const {paramType, type} = param; const value = getValue(scope, this.getKey(param)); diff --git a/packages/platform/platform-router/.npmignore b/packages/platform/platform-router/.npmignore new file mode 100644 index 00000000000..33eaaa499a1 --- /dev/null +++ b/packages/platform/platform-router/.npmignore @@ -0,0 +1,6 @@ +src +test +tsconfig.compile.json +tsconfig.json +__mock__ +*.spec.js diff --git a/packages/platform/platform-router/jest.config.js b/packages/platform/platform-router/jest.config.js new file mode 100644 index 00000000000..fa5f945dd2b --- /dev/null +++ b/packages/platform/platform-router/jest.config.js @@ -0,0 +1,14 @@ +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + ...require("@tsed/jest-config")(__dirname, "platform-router"), + coverageThreshold: { + global: { + statements: 100, + branches: 78.26, + functions: 100, + lines: 100 + } + } +}; diff --git a/packages/platform/platform-router/package.json b/packages/platform/platform-router/package.json new file mode 100644 index 00000000000..640ccf9a9ae --- /dev/null +++ b/packages/platform/platform-router/package.json @@ -0,0 +1,62 @@ +{ + "name": "@tsed/platform-router", + "version": "7.0.0-beta.13", + "description": "Router", + "private": false, + "source": "./src/index.ts", + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "typings": "./lib/types/index.d.ts", + "exports": { + "types": "./lib/types/index.d.ts", + "import": "./lib/esm/index.js", + "require": "./lib/cjs/index.js", + "default": "./lib/esm/index.js" + }, + "scripts": { + "build": "yarn barrels && yarn run build:esm && yarn run build:cjs", + "build:cjs": "tsc --build tsconfig.compile.json", + "build:esm": "tsc --build tsconfig.compile.esm.json", + "barrels": "yarn barrelsby --delete -d ./src -e \"\\.spec\\.ts\" -e \"__mock__\" -e \".benchmark.ts\"", + "test": "cross-env NODE_ENV=test yarn jest --max-workers=2" + }, + "dependencies": { + "tslib": "2.4.0" + }, + "devDependencies": { + "@tsed/core": "7.0.0-beta.13", + "@tsed/di": "7.0.0-beta.13", + "@tsed/exceptions": "7.0.0-beta.13", + "@tsed/json-mapper": "7.0.0-beta.13", + "@tsed/platform-params": "7.0.0-beta.13", + "@tsed/schema": "7.0.0-beta.13" + }, + "peerDependencies": { + "@tsed/core": "^7.0.0-beta.13", + "@tsed/di": "^7.0.0-beta.13", + "@tsed/exceptions": "^7.0.0-beta.13", + "@tsed/json-mapper": "^7.0.0-beta.13", + "@tsed/platform-params": "7.0.0-beta.13", + "@tsed/schema": "^7.0.0-beta.13" + }, + "peerDependenciesMeta": { + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/exceptions": { + "optional": false + }, + "@tsed/json-mapper": { + "optional": false + }, + "@tsed/schema": { + "optional": false + }, + "@tsed/platform-params": { + "optional": false + } + } +} diff --git a/packages/platform/platform-router/readme.md b/packages/platform/platform-router/readme.md new file mode 100644 index 00000000000..eaa21a5929a --- /dev/null +++ b/packages/platform/platform-router/readme.md @@ -0,0 +1,176 @@ +

+ Ts.ED logo +

+ +
+

@tsed/platform-router

+ +[![Build & Release](https://github.com/tsedio/tsed/workflows/Build%20&%20Release/badge.svg)](https://github.com/tsedio/tsed/actions?query=workflow%3A%22Build+%26+Release%22) +[![PR Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/tsedio/tsed/blob/master/CONTRIBUTING.md) +[![Coverage Status](https://coveralls.io/repos/github/tsedio/tsed/badge.svg?branch=production)](https://coveralls.io/github/tsedio/tsed?branch=production) +[![npm version](https://badge.fury.io/js/%40tsed%2Fcommon.svg)](https://badge.fury.io/js/%40tsed%2Fcommon) +[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![github](https://img.shields.io/static/v1?label=Github%20sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/romakita) +[![opencollective](https://img.shields.io/static/v1?label=OpenCollective%20sponsor&message=%E2%9D%A4&logo=OpenCollective&color=%23fe8e86)](https://opencollective.com/tsed) + +
+ +
+ Website +   •   + Getting started +   •   + Slack +   •   + Twitter +
+ +
+ +A package of Ts.ED framework. See website: https://tsed.io/ + +# Installation + +```bash +npm install --save @tsed/platform-router +``` + +## Usage + +This module generates routers from decorated class. + +```typescript +import express from "express"; +import {Controller, InjectorService} from "@tsed/di"; +import {PlatformHandlerType, PlatformHandlerMetadata, PlatformRouter, PlatformLayer} from "@tsed/platform-router"; +import {Delete, Get, Head, Options, Patch, Post, Put} from "@tsed/schema"; + +@Controller("/nested") +class NestedController { + @Get("/") + get() {} + + @Post("/") + post() {} + + @Put("/:id") + put() {} + + @Delete("/:id") + delete() {} + + @Head("/:id") + head() {} + + @Options("/:id") + option() {} + + @Patch("/:id") + patch() {} +} + +@Controller({path: "/controller", children: [NestedController]}) +@UseBefore(function useBefore() {}) +class MyController { + @Get("/") + get() {} + + @Post("/") + post() {} + + @Put("/:id") + put() {} + + @Delete("/:id") + delete() {} + + @Head("/:id") + head() {} + + @Options("/:id") + option() {} + + @Patch("/:id") + patch() {} +} + +const injector = new InjectorService(); +const expressApp = express(); + +injector.addProvider(MyController); +injector.addProvider(NestedController); + +const appRouter = new PlatformRouter(injector); + +appRouter.use("/rest", MyController); + +// transform handlerMetadata to a compatible handler for the targeted framework (Express.js, Koa.js, etc...) +PlatformRouter.hooks.on("alterHandler", (handlerMetadata: PlatformHandlerMetadata) => { + const handler = handlerMetadata.compileHandler(injector); + + switch (handlerMetadata.type) { + default: + case PlatformHandlerType.RAW_FN: // native express.js/koa.js handler + return handler; + case PlatformHandlerType.MIDDLEWARE: + case PlatformHandlerType.ENDPOINT: + return (req, res, next) => { + return next(); + }; + case PlatformHandlerType.ERR_MIDDLEWARE: + return (error: unknown, req, res, next) => { + return next(); + }; + case PlatformHandlerType.CUSTOM: + return (req, res, next) => { + return next(); + }; + } +}); + +// bind layers to the framework router +appRouter.getLayers().forEach((layer: PlatformLayer) => { + const {method, args} = layer; + + expressApp[method](...layer.args); +}); + +expressApp.listen(3000); +``` + +## Contributors + +Please read [contributing guidelines here](https://tsed.io/CONTRIBUTING.html). + + + +## Backers + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/tsed#backer)] + + + +## Sponsors + +Support this project by becoming a sponsor. Your logo will show up here with a link to your +website. [[Become a sponsor](https://opencollective.com/tsed#sponsor)] + +## License + +The MIT License (MIT) + +Copyright (c) 2016 - 2018 Romain Lenzotti + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts new file mode 100644 index 00000000000..d33d0bde5e3 --- /dev/null +++ b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts @@ -0,0 +1,150 @@ +import {Err, Next, Req} from "@tsed/common"; +import {Controller, InjectorService} from "@tsed/di"; +import {Middleware} from "@tsed/platform-middlewares"; +import {Get, JsonMethodStore} from "@tsed/schema"; +import {useContextHandler} from "../utils/useContextHandler"; +import {PlatformHandlerMetadata} from "./PlatformHandlerMetadata"; +import {PlatformHandlerType} from "./PlatformHandlerType"; + +describe("PlatformHandlerMetadata", () => { + describe("from()", () => { + it("should return PlatformMetadata", () => { + const meta = new PlatformHandlerMetadata({ + type: PlatformHandlerType.CUSTOM, + handler: () => {} + }); + const result = PlatformHandlerMetadata.from({} as any, meta); + expect(result).toEqual(meta); + }); + }); + describe("from useContextHandler", () => { + it("should create a new handlerMetadata with right metadata", () => { + // GIVEN + const handler = useContextHandler((ctx: any) => {}); + + // WHEN + const handlerMetadata = new PlatformHandlerMetadata({ + handler + }); + + // THEN + expect(handlerMetadata.type).toEqual(PlatformHandlerType.CTX_FN); + expect(handlerMetadata.hasNextFunction).toEqual(false); + expect(handlerMetadata.hasErrorParam).toEqual(false); + expect(handlerMetadata.toString()).toEqual(""); + }); + }); + describe("from function", () => { + it("should create a new handlerMetadata with right metadata", () => { + // GIVEN + const options = { + handler(req: any, res: any, next: any) {} + }; + // WHEN + const handlerMetadata = new PlatformHandlerMetadata(options); + + // THEN + expect(handlerMetadata.type).toEqual(PlatformHandlerType.RAW_FN); + expect(handlerMetadata.hasNextFunction).toEqual(true); + expect(handlerMetadata.hasErrorParam).toEqual(false); + expect(handlerMetadata.toString()).toEqual("handler"); + }); + }); + describe("from function err", () => { + it("should create a new handlerMetadata with right metadata", () => { + // GIVEN + const options = { + handler(err: any, req: any, res: any, next: any) {} + }; + // WHEN + const handlerMetadata = new PlatformHandlerMetadata(options); + + // THEN + expect(handlerMetadata.type).toEqual(PlatformHandlerType.RAW_ERR_FN); + expect(handlerMetadata.hasNextFunction).toEqual(true); + expect(handlerMetadata.hasErrorParam).toEqual(true); + expect(handlerMetadata.propertyKey).toBeUndefined(); + expect(handlerMetadata.toString()).toEqual("handler"); + }); + }); + describe("from function without nextFn", () => { + it("should create a new handlerMetadata with right metadata", () => { + // GIVEN + const options = { + handler(req: any, res: any) {} + }; + + // WHEN + const handlerMetadata = new PlatformHandlerMetadata(options); + + // THEN + expect(handlerMetadata.type).toEqual(PlatformHandlerType.RAW_FN); + expect(handlerMetadata.hasNextFunction).toEqual(false); + expect(handlerMetadata.hasErrorParam).toEqual(false); + expect(handlerMetadata.propertyKey).toBeUndefined(); + expect(handlerMetadata.toString()).toEqual("handler"); + }); + }); + describe("from endpoint/middleware with injection", () => { + it("should create a new handlerMetadata with right metadata", () => { + // GIVEN + @Controller("/") + class Test { + @Get("/") + test(@Req() req: Req, @Next() next: Next) {} + } + + const injector = new InjectorService(); + injector.addProvider(Test); + + const options = { + provider: injector.getProvider(Test), + propertyKey: "test", + type: PlatformHandlerType.ENDPOINT + }; + // WHEN + const handlerMetadata = new PlatformHandlerMetadata(options); + + // THEN + expect(handlerMetadata.type).toEqual(PlatformHandlerType.ENDPOINT); + expect(handlerMetadata.hasNextFunction).toEqual(true); + expect(handlerMetadata.hasErrorParam).toEqual(false); + expect(handlerMetadata.propertyKey).toEqual("test"); + expect(handlerMetadata.scope).toEqual("singleton"); + expect(handlerMetadata.toString()).toEqual("Test.test"); + expect(handlerMetadata.store).toBeInstanceOf(JsonMethodStore); + expect(handlerMetadata.isFinal()).toEqual(false); + + expect(handlerMetadata.getParams()[0].paramType).toEqual("REQUEST"); + expect(handlerMetadata.getParams()[1].paramType).toEqual("NEXT_FN"); + }); + }); + + describe("from middleware with injection and error", () => { + it("should create a new handlerMetadata with right metadata", () => { + // WHEN + @Middleware() + class Test { + use(@Err() error: any, @Next() next: Next) {} + } + + const injector = new InjectorService(); + injector.addProvider(Test); + + const options = { + provider: injector.getProvider(Test), + propertyKey: "use", + type: PlatformHandlerType.MIDDLEWARE + }; + // WHEN + const handlerMetadata = new PlatformHandlerMetadata(options); + + // THEN + expect(handlerMetadata.type).toEqual(PlatformHandlerType.ERR_MIDDLEWARE); + expect(handlerMetadata.hasNextFunction).toEqual(true); + expect(handlerMetadata.hasErrorParam).toEqual(true); + expect(handlerMetadata.propertyKey).toEqual("use"); + expect(handlerMetadata.toString()).toEqual("Test.use"); + }); + }); +}); diff --git a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts new file mode 100644 index 00000000000..da193c2d1d2 --- /dev/null +++ b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts @@ -0,0 +1,141 @@ +import {nameOf} from "@tsed/core"; +import {InjectorService, Provider, ProviderScope, TokenProvider} from "@tsed/di"; +import {ParamTypes} from "@tsed/platform-params"; +import {EndpointMetadata, JsonEntityStore, JsonParameterStore} from "@tsed/schema"; +import {PlatformHandlerType} from "./PlatformHandlerType"; +import {SinglePathType} from "./SinglePathType"; + +export interface PlatformHandlerMetadataOpts extends Record { + token?: TokenProvider; + isFinal?: boolean; +} + +export interface PlatformHandlerMetadataProps { + provider?: Provider; + handler?: any; + opts?: PlatformHandlerMetadataOpts; + propertyKey?: string | symbol; + type?: PlatformHandlerType; +} + +export class PlatformHandlerMetadata { + path: SinglePathType; + + readonly provider?: Provider; + readonly propertyKey: string | symbol; + readonly type: PlatformHandlerType = PlatformHandlerType.RAW_FN; + readonly hasNextFunction: boolean = false; + readonly opts: PlatformHandlerMetadataOpts = {}; + + #handler: any; + + constructor(props: PlatformHandlerMetadataProps) { + const {propertyKey, type, provider, handler, opts} = props; + + this.provider = provider; + this.type = type || handler.type || PlatformHandlerType.RAW_FN; + this.opts = opts || {}; + + this.#handler = propertyKey ? this.target.prototype[propertyKey] : handler; + + if (propertyKey) { + this.propertyKey = propertyKey; + this.hasNextFunction = this.hasParamType(ParamTypes.NEXT_FN); + + if (this.hasParamType(ParamTypes.ERR)) { + this.type = PlatformHandlerType.ERR_MIDDLEWARE; + } + } else { + if (this.#handler.length === 4) { + this.type = PlatformHandlerType.RAW_ERR_FN; + } + this.hasNextFunction = this.#handler.length >= 3; + } + } + + get target() { + return this.provider?.useClass || this.#handler; + } + + get token() { + return this.provider?.token || this.#handler; + } + + get handler() { + return this.#handler; + } + + get scope() { + return this.provider?.scope || ProviderScope.SINGLETON; + } + + get hasErrorParam() { + return this.type === PlatformHandlerType.ERR_MIDDLEWARE || this.type === PlatformHandlerType.RAW_ERR_FN; + } + + get store() { + return JsonEntityStore.fromMethod(this.provider!.useClass, this.propertyKey!); + } + + static from(injector: InjectorService, input: any, opts: PlatformHandlerMetadataOpts = {}): PlatformHandlerMetadata { + if (input instanceof PlatformHandlerMetadata) { + return input; + } + + if (input instanceof EndpointMetadata) { + const provider = injector.getProvider(opts.token)!; + + return new PlatformHandlerMetadata({ + provider, + type: PlatformHandlerType.ENDPOINT, + propertyKey: input.propertyKey, + opts + }); + } + + const provider = injector.getProvider(input); + + if (provider) { + return new PlatformHandlerMetadata({ + provider, + type: PlatformHandlerType.MIDDLEWARE, + propertyKey: "use", + opts + }); + } + + return new PlatformHandlerMetadata({ + handler: input, + type: input.type, + opts + }); + } + + public getParams() { + return JsonParameterStore.getParams(this.target, this.propertyKey) || []; + } + + public hasParamType(paramType: any): boolean { + return this.getParams().findIndex((p) => p.paramType === paramType) > -1; + } + + public isFinal() { + return this.opts?.isFinal || false; + } + + public isRawMiddleware() { + return [PlatformHandlerType.RAW_ERR_FN, PlatformHandlerType.RAW_FN].includes(this.type); + } + + public isEndpoint() { + return this.type === PlatformHandlerType.ENDPOINT; + } + + public isCtxFn() { + return this.type === PlatformHandlerType.CTX_FN; + } + + toString() { + return [nameOf(this.target), this.propertyKey].filter(Boolean).join("."); + } +} diff --git a/packages/platform/platform-router/src/domain/PlatformHandlerType.ts b/packages/platform/platform-router/src/domain/PlatformHandlerType.ts new file mode 100644 index 00000000000..792ebbf503f --- /dev/null +++ b/packages/platform/platform-router/src/domain/PlatformHandlerType.ts @@ -0,0 +1,9 @@ +export enum PlatformHandlerType { + CUSTOM = "custom", + ENDPOINT = "endpoint", + MIDDLEWARE = "middleware", + ERR_MIDDLEWARE = "err:middleware", + CTX_FN = "context", + RAW_FN = "raw:middleware", + RAW_ERR_FN = "raw:err:middleware" +} diff --git a/packages/platform/platform-router/src/domain/PlatformLayer.ts b/packages/platform/platform-router/src/domain/PlatformLayer.ts new file mode 100644 index 00000000000..084defcb508 --- /dev/null +++ b/packages/platform/platform-router/src/domain/PlatformLayer.ts @@ -0,0 +1,61 @@ +import {isPrimitive, nameOf, Type} from "@tsed/core"; +import {Provider} from "@tsed/di"; +import {PlatformParamsCallback} from "@tsed/platform-params"; +import {PlatformHandlerMetadata} from "./PlatformHandlerMetadata"; +import type {PlatformRouter} from "./PlatformRouter"; +import {SinglePathType} from "./SinglePathType"; + +export interface PlatformLayerOptions extends Record { + token?: Type; + isFinal?: boolean; +} + +export interface PlatformLayerProps { + provider: Provider; + path: SinglePathType; + method: string; + handlers: any[]; + router: PlatformRouter; + opts: PlatformLayerOptions; +} + +export class PlatformLayer { + public provider: Provider; + public path: SinglePathType = ""; + public method: string; + public handlers: PlatformHandlerMetadata[] = []; + public router?: PlatformRouter; + public opts: PlatformLayerOptions = {}; + + #args: PlatformParamsCallback[]; + + constructor(props: Partial = {}) { + Object.assign(this, props); + } + + set(args: PlatformParamsCallback[]) { + this.#args = args; + } + + getArgs() { + return [this.path, ...this.#args].filter(Boolean); + } + + isProvider() { + return !!this.provider; + } + + inspect() { + return { + path: this.path, + method: this.method, + handlers: this.handlers.map((item: any) => String(item)), + opts: Object.entries(this.opts).reduce((obj, [key, value]) => { + return { + ...obj, + [key]: isPrimitive(value) ? value : nameOf(value) + }; + }, {}) + }; + } +} diff --git a/packages/platform/platform-router/src/domain/PlatformRouter.ts b/packages/platform/platform-router/src/domain/PlatformRouter.ts new file mode 100644 index 00000000000..ac413e31757 --- /dev/null +++ b/packages/platform/platform-router/src/domain/PlatformRouter.ts @@ -0,0 +1,127 @@ +import {isString} from "@tsed/core"; +import {Injectable, InjectorService, Provider, ProviderScope, Scope} from "@tsed/di"; +import {concatPath} from "@tsed/schema"; +import {formatMethod} from "../utils/formatMethod"; +import {PlatformHandlerMetadata} from "./PlatformHandlerMetadata"; +import {PlatformLayer, PlatformLayerOptions} from "./PlatformLayer"; +import {SinglePathType} from "./SinglePathType"; + +function printHandler(handler: any) { + return handler.toString().split("{")[0].trim(); +} + +@Injectable() +@Scope(ProviderScope.INSTANCE) +export class PlatformRouter { + #isBuilt = false; + + readonly layers: PlatformLayer[] = []; + + provider: Provider; + + constructor(protected readonly injector: InjectorService) {} + + use(...handlers: any[]) { + const layer = handlers.reduce((layer: PlatformLayer, item) => { + if (isString(item) || item instanceof RegExp) { + layer.path = item; + } else { + if (item instanceof PlatformRouter) { + layer.router = item; + + if (!this.provider && item.provider) { + layer.path = concatPath(layer.path, item.provider.path); + } else { + layer.path = layer.path || item.provider.path; + } + } else { + item = PlatformHandlerMetadata.from(this.injector, item); + } + + layer.handlers.push(item); + } + + return layer; + }, new PlatformLayer({method: "use", provider: this.provider})); + + this.layers.push(layer); + + return this; + } + + addRoute(method: string, path: SinglePathType, handlers: any[], opts: PlatformLayerOptions = {}) { + const layer = new PlatformLayer({ + provider: this.provider, + method: formatMethod(method), + path, + handlers: handlers.map((input, index, handlers) => { + const isFinal = opts.isFinal ? index === handlers.length - 1 : false; + + return PlatformHandlerMetadata.from(this.injector, input, {...opts, isFinal}); + }), + opts + }); + + this.layers.push(layer); + + return this; + } + + all(path: SinglePathType, ...handlers: any[]) { + return this.addRoute("all", path, handlers, {isFinal: true}); + } + + get(path: SinglePathType, ...handlers: any[]) { + return this.addRoute("get", path, handlers, {isFinal: true}); + } + + post(path: SinglePathType, ...handlers: any[]) { + return this.addRoute("post", path, handlers, {isFinal: true}); + } + + put(path: SinglePathType, ...handlers: any[]) { + return this.addRoute("put", path, handlers, {isFinal: true}); + } + + delete(path: SinglePathType, ...handlers: any[]) { + return this.addRoute("delete", path, handlers, {isFinal: true}); + } + + patch(path: SinglePathType, ...handlers: any[]) { + return this.addRoute("patch", path, handlers, {isFinal: true}); + } + + head(path: SinglePathType, ...handlers: any[]) { + return this.addRoute("head", path, handlers, {isFinal: true}); + } + + options(path: SinglePathType, ...handlers: any[]) { + return this.addRoute("options", path, handlers, {isFinal: true}); + } + + statics(path: string, options: any) { + return this.addRoute("statics", path, [], options); + } + + inspect() { + return this.layers.map((layer) => { + const obj = layer.inspect(); + + return { + ...obj, + handlers: obj.handlers.map(printHandler), + path: concatPath(this.provider?.path, obj.path) + }; + }); + } + + isBuilt() { + if (this.#isBuilt) { + return true; + } + + this.#isBuilt = true; + + return false; + } +} diff --git a/packages/platform/platform-router/src/domain/PlatformRouters.ts b/packages/platform/platform-router/src/domain/PlatformRouters.ts new file mode 100644 index 00000000000..db3f57264e5 --- /dev/null +++ b/packages/platform/platform-router/src/domain/PlatformRouters.ts @@ -0,0 +1,158 @@ +import {getValue, Hooks, Type} from "@tsed/core"; +import {ControllerProvider, GlobalProviders, Injectable, InjectorService, Provider, ProviderType, TokenProvider} from "@tsed/di"; +import {PlatformParams, PlatformParamsScope} from "@tsed/platform-params"; +import {concatPath, EndpointMetadata, getOperationsRoutes} from "@tsed/schema"; +import {useContextHandler} from "../utils/useContextHandler"; +import {PlatformLayer} from "./PlatformLayer"; +import {PlatformRouter} from "./PlatformRouter"; + +let AUTO_INC = 0; + +function getInjectableRouter(injector: InjectorService, provider: Provider): PlatformRouter { + return injector.get(provider.tokenRouter)!; +} + +function createTokenRouter(provider: ControllerProvider) { + return (provider.tokenRouter = provider.tokenRouter || `${provider.name}_ROUTER_${AUTO_INC++}`); +} + +function createInjectableRouter(injector: InjectorService, provider: ControllerProvider): PlatformRouter { + const tokenRouter = createTokenRouter(provider); + + if (injector.has(tokenRouter)) { + return getInjectableRouter(injector, provider); + } + + const router = injector.invoke(PlatformRouter); + router.provider = provider; + + return injector + .add(tokenRouter, { + useValue: router + }) + .invoke(tokenRouter); +} + +GlobalProviders.createRegistry(ProviderType.CONTROLLER, ControllerProvider, { + onInvoke(provider: ControllerProvider, locals: any, {injector}) { + const router = createInjectableRouter(injector, provider); + locals.set(PlatformRouter, router); + } +}); + +@Injectable() +export class PlatformRouters { + readonly hooks = new Hooks(); + + constructor(protected readonly injector: InjectorService, protected readonly platformParams: PlatformParams) {} + + prebuild() { + this.injector.getProviders(ProviderType.CONTROLLER).forEach((provider: ControllerProvider) => { + createInjectableRouter(this.injector, provider); + }); + } + + from(token: TokenProvider, parentMiddlewares: any[] = []) { + const {injector} = this; + const provider = injector.getProvider(token)!; + + if (!provider) { + throw new Error("Token not found in the provider registry"); + } + + const router = createInjectableRouter(injector, provider); + + if (router.isBuilt()) { + return router; + } + + const useBefore = getValue(provider, "middlewares.useBefore", []); + + const {children} = provider; + + getOperationsRoutes(provider.token).forEach((operationRoute) => { + const {endpoint} = operationRoute; + const {beforeMiddlewares, middlewares: mldwrs, afterMiddlewares} = endpoint; + + const useBefore = getValue(provider, "middlewares.useBefore", []); + const use = getValue(provider, "middlewares.use", []); + const useAfter = getValue(provider, "middlewares.useAfter", []); + + const handlers = this.hooks.alter( + "alterEndpointHandlers", + [ + ...parentMiddlewares, + ...useBefore, + ...beforeMiddlewares, + ...use, + ...mldwrs, + operationRoute.endpoint, + ...afterMiddlewares, + ...useAfter + ], + [operationRoute], + this + ); + + router.addRoute( + operationRoute.method, + operationRoute.path || "", + [ + useContextHandler(($ctx) => { + $ctx.set(EndpointMetadata, operationRoute.endpoint); + }), + ...handlers + ], + operationRoute + ); + }); + + const middlewares: any[] = [...parentMiddlewares, ...useBefore]; + + children.forEach((token: Type) => { + const nested = this.from(token, middlewares); + + router.use(nested); + }); + + return router; + } + + getLayers(router: PlatformRouter): PlatformLayer[] { + return router.layers + .flatMap((layer) => { + if (layer.router) { + return this.getLayers(layer.router).map((subLayer: PlatformLayer) => { + return new PlatformLayer({ + ...subLayer, + path: concatPath(layer.path, subLayer.path) + }); + }); + } + + return new PlatformLayer(layer); + }) + .map((layer) => { + const handlers = layer.handlers.map((handlerMetadata) => { + // set path on handler metadata to retrieve it later in $ctx + handlerMetadata.path = layer.path; + + let handler: any; + + if (handlerMetadata.isRawMiddleware()) { + handler = handlerMetadata.handler; + } else { + handler = handlerMetadata.isCtxFn() + ? (scope: PlatformParamsScope) => handlerMetadata.handler(scope.$ctx) + : this.platformParams.compileHandler(handlerMetadata); + } + + return this.hooks.alter("alterHandler", handler, [handlerMetadata]); + }); + + layer.set(handlers); + + return layer; + }); + } +} diff --git a/packages/platform/platform-router/src/domain/SinglePathType.ts b/packages/platform/platform-router/src/domain/SinglePathType.ts new file mode 100644 index 00000000000..dda93796d16 --- /dev/null +++ b/packages/platform/platform-router/src/domain/SinglePathType.ts @@ -0,0 +1 @@ +export type SinglePathType = string | RegExp; diff --git a/packages/platform/platform-router/src/index.ts b/packages/platform/platform-router/src/index.ts new file mode 100644 index 00000000000..0b17e7614d3 --- /dev/null +++ b/packages/platform/platform-router/src/index.ts @@ -0,0 +1,12 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from "./domain/PlatformHandlerMetadata"; +export * from "./domain/PlatformHandlerType"; +export * from "./domain/PlatformLayer"; +export * from "./domain/PlatformRouter"; +export * from "./domain/PlatformRouters"; +export * from "./domain/SinglePathType"; +export * from "./utils/formatMethod"; +export * from "./utils/useContextHandler"; diff --git a/packages/platform/platform-router/src/utils/formatMethod.ts b/packages/platform/platform-router/src/utils/formatMethod.ts new file mode 100644 index 00000000000..4649a4b95b7 --- /dev/null +++ b/packages/platform/platform-router/src/utils/formatMethod.ts @@ -0,0 +1,5 @@ +import {OperationMethods} from "@tsed/schema"; + +export function formatMethod(method: string | undefined) { + return (method === OperationMethods.CUSTOM ? "use" : method || "use").toLowerCase(); +} diff --git a/packages/platform/platform-router/src/utils/useContextHandler.ts b/packages/platform/platform-router/src/utils/useContextHandler.ts new file mode 100644 index 00000000000..9ddcf2c3bba --- /dev/null +++ b/packages/platform/platform-router/src/utils/useContextHandler.ts @@ -0,0 +1,15 @@ +import {DIContext} from "@tsed/di"; +import {PlatformHandlerType} from "../domain/PlatformHandlerType"; + +export type PlatformContextHandler = ($ctx: Context) => any | Promise; + +/** + * Create Ts.ED context handler + * @param fn + * @ignore + */ +export function useContextHandler(fn: PlatformContextHandler & {type?: PlatformHandlerType}) { + fn.type = PlatformHandlerType.CTX_FN; + + return fn; +} diff --git a/packages/platform/platform-router/test/__snapshots__/routers.integration.spec.ts.snap b/packages/platform/platform-router/test/__snapshots__/routers.integration.spec.ts.snap new file mode 100644 index 00000000000..837adc0afeb --- /dev/null +++ b/packages/platform/platform-router/test/__snapshots__/routers.integration.spec.ts.snap @@ -0,0 +1,355 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`routers integration getLayers() should declare router - appRouter 1`] = ` +Array [ + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.get", + ], + "method": "get", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:get", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/rest/controller", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.post", + ], + "method": "post", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:post", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/rest/controller", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.put", + ], + "method": "put", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:put", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/rest/controller/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.delete", + ], + "method": "delete", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:delete", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/rest/controller/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.head", + ], + "method": "head", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:head", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/rest/controller/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.option", + ], + "method": "options", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:option", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/rest/controller/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.patch", + ], + "method": "patch", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:patch", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/rest/controller/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "NestedController.get", + ], + "method": "get", + "opts": Object { + "basePath": "/nested", + "endpoint": "NestedController:get", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "NestedController", + }, + "path": "/rest/controller/nested", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "NestedController.post", + ], + "method": "post", + "opts": Object { + "basePath": "/nested", + "endpoint": "NestedController:post", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "NestedController", + }, + "path": "/rest/controller/nested", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "NestedController.put", + ], + "method": "put", + "opts": Object { + "basePath": "/nested", + "endpoint": "NestedController:put", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "NestedController", + }, + "path": "/rest/controller/nested/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "NestedController.delete", + ], + "method": "delete", + "opts": Object { + "basePath": "/nested", + "endpoint": "NestedController:delete", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "NestedController", + }, + "path": "/rest/controller/nested/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "NestedController.head", + ], + "method": "head", + "opts": Object { + "basePath": "/nested", + "endpoint": "NestedController:head", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "NestedController", + }, + "path": "/rest/controller/nested/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "NestedController.option", + ], + "method": "options", + "opts": Object { + "basePath": "/nested", + "endpoint": "NestedController:option", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "NestedController", + }, + "path": "/rest/controller/nested/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "NestedController.patch", + ], + "method": "patch", + "opts": Object { + "basePath": "/nested", + "endpoint": "NestedController:patch", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "NestedController", + }, + "path": "/rest/controller/nested/:id", + }, +] +`; + +exports[`routers integration getLayers() should declare router 1`] = ` +Array [ + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.get", + ], + "method": "get", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:get", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/controller", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.post", + ], + "method": "post", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:post", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/controller", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.put", + ], + "method": "put", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:put", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/controller/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.delete", + ], + "method": "delete", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:delete", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/controller/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.head", + ], + "method": "head", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:head", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/controller/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.option", + ], + "method": "options", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:option", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/controller/:id", + }, + Object { + "handlers": Array [ + "", + "useBefore", + "MyController.patch", + ], + "method": "patch", + "opts": Object { + "basePath": "/controller", + "endpoint": "MyController:patch", + "operationPath": "[object Object]", + "paramsTypes": "[object Object]", + "token": "MyController", + }, + "path": "/controller/:id", + }, + Object { + "handlers": Array [ + "[object Object]", + ], + "method": "use", + "opts": Object {}, + "path": "/controller/nested", + }, +] +`; diff --git a/packages/platform/platform-router/test/routers-alter-endpoint-handlers.integration.spec.ts b/packages/platform/platform-router/test/routers-alter-endpoint-handlers.integration.spec.ts new file mode 100644 index 00000000000..862122cc04a --- /dev/null +++ b/packages/platform/platform-router/test/routers-alter-endpoint-handlers.integration.spec.ts @@ -0,0 +1,60 @@ +import {PlatformTest} from "@tsed/common"; +import {Controller, DIContext, InjectorService} from "@tsed/di"; +import {UseBefore} from "@tsed/platform-middlewares"; +import {Context, PlatformParams} from "@tsed/platform-params"; +import {EndpointMetadata, Get, JsonOperationRoute} from "@tsed/schema"; +import {PlatformRouter} from "../src/domain/PlatformRouter"; +import {PlatformRouters} from "../src/domain/PlatformRouters"; +import {useContextHandler} from "../src/index"; + +@Controller("/controller") +@UseBefore(function useBefore() {}) +class MyController { + @Get("/") + get(@Context() $ctx: Context) { + return $ctx; + } +} + +function createAppRouterFixture() { + const injector = new InjectorService(); + const platformRouters = injector.invoke(PlatformRouters); + const platformParams = injector.invoke(PlatformParams); + const appRouter = injector.invoke(PlatformRouter); + + injector.addProvider(MyController, {}); + + return {injector, appRouter, platformRouters, platformParams}; +} + +describe("routers with alter handlers", () => { + beforeEach(() => PlatformTest.create()); + afterEach(() => PlatformTest.reset()); + it("should declare router - appRouter", async () => { + const {appRouter, platformRouters} = createAppRouterFixture(); + + platformRouters.hooks.on("alterEndpointHandlers", (allMiddlewares: any[], operationRoute: JsonOperationRoute) => { + const {endpoint} = operationRoute; + + return [ + useContextHandler(($ctx: DIContext) => { + $ctx.set(EndpointMetadata, endpoint); + }), + ...allMiddlewares + ]; + }); + + const router = platformRouters.from(MyController); + + appRouter.use("/rest", router); + + const layers = platformRouters.getLayers(appRouter); + + expect(layers.length).toEqual(1); + + const $ctx = new Map(); + await (layers[0].getArgs() as any)[1]({$ctx}); + + expect($ctx.get(EndpointMetadata)).toBeDefined(); + }); +}); diff --git a/packages/platform/platform-router/test/routers-injection.integration.spec.ts b/packages/platform/platform-router/test/routers-injection.integration.spec.ts new file mode 100644 index 00000000000..512b205ecf5 --- /dev/null +++ b/packages/platform/platform-router/test/routers-injection.integration.spec.ts @@ -0,0 +1,49 @@ +import {Controller, ControllerProvider, InjectorService} from "@tsed/di"; +import {PlatformParams} from "@tsed/platform-params"; +import {PlatformRouter} from "../src/domain/PlatformRouter"; +import {PlatformRouters} from "../src/domain/PlatformRouters"; + +@Controller("/statics") +class CustomStaticsCtrl { + constructor(public router: PlatformRouter) { + router.statics("/", { + root: "/assets" + }); + } +} + +function createAppRouterFixture() { + const injector = new InjectorService(); + const platformRouters = injector.invoke(PlatformRouters); + const platformParams = injector.invoke(PlatformParams); + const appRouter = injector.invoke(PlatformRouter); + + injector.addProvider(CustomStaticsCtrl, {}); + + return {injector, appRouter, platformRouters, platformParams}; +} + +describe("Routers injection", () => { + it("should load router and inject router to the given controller", () => { + const {injector, platformRouters} = createAppRouterFixture(); + + // prebuild controllers to inject router in controller + platformRouters.prebuild(); + + const router = platformRouters.from(CustomStaticsCtrl); + const router1 = platformRouters.from(CustomStaticsCtrl); + + const provider = injector.getProvider(CustomStaticsCtrl)!; + const router2 = injector.get(provider.tokenRouter); + const controller = injector.invoke(CustomStaticsCtrl)!; + + expect(router).toEqual(router1); + expect(router).toEqual(router2); + expect(controller.router).toEqual(router2); + + const layers = platformRouters.getLayers(router); + + expect(layers[0].path).toEqual("/"); + expect(layers[0].method).toEqual("statics"); + }); +}); diff --git a/packages/platform/platform-router/test/routers-middlewares.integration.spec.ts b/packages/platform/platform-router/test/routers-middlewares.integration.spec.ts new file mode 100644 index 00000000000..3fb4bb76017 --- /dev/null +++ b/packages/platform/platform-router/test/routers-middlewares.integration.spec.ts @@ -0,0 +1,53 @@ +import {PlatformTest, UseBeforeEach} from "@tsed/common"; +import {Controller, InjectorService} from "@tsed/di"; +import {Middleware} from "@tsed/platform-middlewares"; +import {Context, PlatformParams} from "@tsed/platform-params"; +import {Get} from "@tsed/schema"; +import {PlatformRouter} from "../src/domain/PlatformRouter"; +import {PlatformRouters} from "../src/domain/PlatformRouters"; + +@Middleware() +class MyMiddleware { + use() {} +} + +@Controller("/controller") +@UseBeforeEach(MyMiddleware) +class MyController { + @Get("/") + get(@Context() $ctx: Context) { + return $ctx; + } +} + +function createAppRouterFixture() { + const injector = new InjectorService(); + const platformRouters = injector.invoke(PlatformRouters); + const platformParams = injector.invoke(PlatformParams); + const appRouter = injector.invoke(PlatformRouter); + + injector.addProvider(MyMiddleware); + injector.addProvider(MyController, {}); + + return {injector, appRouter, platformRouters, platformParams}; +} + +describe("routers with middlewares", () => { + beforeEach(() => PlatformTest.create()); + afterEach(() => PlatformTest.reset()); + it("should declare router", async () => { + const {appRouter, platformRouters} = createAppRouterFixture(); + + const router = platformRouters.from(MyController); + + appRouter.use("/rest", router); + + const layers = platformRouters.getLayers(appRouter); + + expect(layers.length).toEqual(1); + expect(layers[0].getArgs().length).toEqual(4); + + expect(layers[0].handlers[1].type).toEqual("middleware"); + expect(layers[0].handlers[1].isEndpoint()).toEqual(false); + }); +}); diff --git a/packages/platform/platform-router/test/routers.integration.spec.ts b/packages/platform/platform-router/test/routers.integration.spec.ts new file mode 100644 index 00000000000..78a2bc8b957 --- /dev/null +++ b/packages/platform/platform-router/test/routers.integration.spec.ts @@ -0,0 +1,307 @@ +import {PlatformContext, PlatformTest} from "@tsed/common"; +import {catchError} from "@tsed/core"; +import {Controller, InjectorService} from "@tsed/di"; +import {UseBefore} from "@tsed/platform-middlewares"; +import {Context, PlatformParams} from "@tsed/platform-params"; +import {Delete, Get, Head, Options, Patch, Post, Put} from "@tsed/schema"; +import {PlatformRouter} from "../src/domain/PlatformRouter"; +import {PlatformRouters} from "../src/domain/PlatformRouters"; + +@Controller("/nested") +class NestedController { + @Get("/") + get() {} + + @Post("/") + post() {} + + @Put("/:id") + put() {} + + @Delete("/:id") + delete() {} + + @Head("/:id") + head() {} + + @Options("/:id") + option() {} + + @Patch("/:id") + patch() {} +} + +@Controller({path: "/controller", children: [NestedController]}) +@UseBefore(function useBefore() {}) +class MyController { + @Get("/") + get(@Context() $ctx: Context) { + return $ctx; + } + + @Post("/") + post() {} + + @Put("/:id") + put() {} + + @Delete("/:id") + delete() {} + + @Head("/:id") + head() {} + + @Options("/:id") + option() {} + + @Patch("/:id") + patch() {} +} + +function createAppRouterFixture() { + const injector = new InjectorService(); + const platformRouters = injector.invoke(PlatformRouters); + const platformParams = injector.invoke(PlatformParams); + const appRouter = injector.invoke(PlatformRouter); + + injector.addProvider(NestedController, {}); + + return {injector, appRouter, platformRouters, platformParams}; +} + +describe("routers integration", () => { + beforeEach(() => PlatformTest.create()); + afterEach(() => PlatformTest.reset()); + describe("getLayers()", () => { + it("should declare router", () => { + const {injector, platformRouters} = createAppRouterFixture(); + injector.addProvider(MyController, {}); + + const hookStub = jest.fn().mockImplementation((o) => o); + + platformRouters.hooks.on("alterEndpointHandlers", hookStub); + + const router = platformRouters.from(MyController); + + expect(hookStub).toHaveBeenCalled(); + expect(router.inspect()).toMatchSnapshot(); + }); + it("should declare router - appRouter", async () => { + const {injector, appRouter, platformRouters} = createAppRouterFixture(); + injector.addProvider(MyController, {}); + + const router = platformRouters.from(MyController); + + appRouter.use("/rest", router); + + // prebuild controllers + platformRouters.prebuild(); + + // returns layers + const layers = platformRouters.getLayers(appRouter); + + expect(layers.length).toEqual(14); + expect(layers.map((l) => l.inspect())).toMatchSnapshot(); + expect(layers.find((layer) => layer.method == "use")).toEqual(undefined); + + const args = layers[0].getArgs(); + expect(layers[0].isProvider()).toEqual(true); + expect(args[0]).toEqual("/rest/controller"); + expect(layers[0].getArgs().length).toEqual(4); + + const $ctx = PlatformTest.createRequestContext(); + const result = await (args[3] as any)!({$ctx}); + + expect(result).toBeInstanceOf(PlatformContext); + }); + it("should throw an error when the controller isn't found", () => { + const {platformRouters} = createAppRouterFixture(); + + const error: any = catchError(() => platformRouters.from(class Test {})); + + expect(error?.message).toEqual("Token not found in the provider registry"); + }); + }); + + describe("use()", () => { + it("should call method", () => { + const injector = new InjectorService(); + injector.addProvider(NestedController, {}); + + const router = new PlatformRouter(injector); + + router.use("/hello", function h() {}); + + expect(router.inspect()).toEqual([ + { + handlers: ["h"], + method: "use", + opts: {}, + path: "/hello" + } + ]); + }); + }); + describe("get()", () => { + it("should call method", () => { + const {appRouter} = createAppRouterFixture(); + + appRouter.get("/hello", function h() {}); + + expect(appRouter.inspect()).toEqual([ + { + handlers: ["h"], + method: "get", + opts: { + isFinal: true + }, + path: "/hello" + } + ]); + }); + }); + describe("post()", () => { + it("should call method", () => { + const {appRouter} = createAppRouterFixture(); + + appRouter.post("/hello", function h() {}); + + expect(appRouter.inspect()).toEqual([ + { + handlers: ["h"], + method: "post", + opts: { + isFinal: true + }, + path: "/hello" + } + ]); + }); + }); + describe("put()", () => { + it("should call method", () => { + const {appRouter} = createAppRouterFixture(); + + appRouter.put("/hello", function h() {}); + + expect(appRouter.inspect()).toEqual([ + { + handlers: ["h"], + method: "put", + opts: { + isFinal: true + }, + path: "/hello" + } + ]); + }); + }); + describe("patch()", () => { + it("should call method", () => { + const {appRouter} = createAppRouterFixture(); + + appRouter.patch("/hello", function h() {}); + + expect(appRouter.inspect()).toEqual([ + { + handlers: ["h"], + method: "patch", + opts: { + isFinal: true + }, + path: "/hello" + } + ]); + }); + }); + describe("head()", () => { + it("should call method", () => { + const {appRouter} = createAppRouterFixture(); + + appRouter.head("/hello", function h() {}); + + expect(appRouter.inspect()).toEqual([ + { + handlers: ["h"], + method: "head", + opts: { + isFinal: true + }, + path: "/hello" + } + ]); + }); + }); + describe("delete()", () => { + it("should call method", () => { + const {appRouter} = createAppRouterFixture(); + + appRouter.delete("/hello", function h() {}); + + expect(appRouter.inspect()).toEqual([ + { + handlers: ["h"], + method: "delete", + opts: { + isFinal: true + }, + path: "/hello" + } + ]); + }); + }); + describe("option()", () => { + it("should call method", () => { + const {appRouter} = createAppRouterFixture(); + + appRouter.options("/hello", function h() {}); + + expect(appRouter.inspect()).toEqual([ + { + handlers: ["h"], + method: "options", + opts: { + isFinal: true + }, + path: "/hello" + } + ]); + }); + }); + describe("all()", () => { + it("should call method", () => { + const {appRouter} = createAppRouterFixture(); + + appRouter.all("/hello", function h() {}); + + expect(appRouter.inspect()).toEqual([ + { + handlers: ["h"], + method: "all", + opts: { + isFinal: true + }, + path: "/hello" + } + ]); + }); + }); + describe("statics()", () => { + it("should call method", () => { + const {appRouter} = createAppRouterFixture(); + + appRouter.statics("/hello", {root: "root"}); + + expect(appRouter.inspect()).toEqual([ + { + handlers: [], + method: "statics", + opts: { + root: "root" + }, + path: "/hello" + } + ]); + }); + }); +}); diff --git a/packages/platform/platform-router/tsconfig.compile.esm.json b/packages/platform/platform-router/tsconfig.compile.esm.json new file mode 100644 index 00000000000..06456ae6499 --- /dev/null +++ b/packages/platform/platform-router/tsconfig.compile.esm.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.compile.json", + "compilerOptions": { + "baseUrl": ".", + "module": "ES2020", + "rootDir": "src", + "outDir": "./lib/esm", + "declaration": true, + "declarationDir": "./lib/types" + }, + "exclude": ["node_modules", "test", "lib", "benchmark", "coverage", "spec", "**/*.benchmark.ts", "**/*.spec.ts", "keys", "jest.config.js"] +} diff --git a/packages/platform/platform-router/tsconfig.compile.json b/packages/platform/platform-router/tsconfig.compile.json new file mode 100644 index 00000000000..97476d245bb --- /dev/null +++ b/packages/platform/platform-router/tsconfig.compile.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.compile.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/cjs", + "declaration": false + }, + "exclude": ["node_modules", "test", "lib", "benchmark", "coverage", "spec", "**/*.benchmark.ts", "**/*.spec.ts", "keys", "jest.config.js"] +} diff --git a/packages/platform/platform-serverless-http/src/builder/PlatformServerlessHttp.ts b/packages/platform/platform-serverless-http/src/builder/PlatformServerlessHttp.ts index 234a5ff7a70..0c0ed314918 100644 --- a/packages/platform/platform-serverless-http/src/builder/PlatformServerlessHttp.ts +++ b/packages/platform/platform-serverless-http/src/builder/PlatformServerlessHttp.ts @@ -4,7 +4,7 @@ import type {Handler} from "aws-lambda"; import serverless from "serverless-http"; export class PlatformServerlessHttp { - static bootstrap(module: Type, settings: PlatformBuilderSettings): PlatformBuilder & {handler(): Handler} { + static bootstrap(module: Type, settings: PlatformBuilderSettings): PlatformBuilder & {handler(): Handler} { const platform = PlatformBuilder.create(module, settings); const promise = platform.listen(); diff --git a/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts b/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts index feb1e083f89..a24c3603781 100644 --- a/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts +++ b/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts @@ -152,14 +152,14 @@ export class PlatformServerlessTest extends DITest { static request = LambdaClientRequest; static bootstrap( - serverless: {bootstrap: (server: Type, settings: PlatformBuilderSettings) => PlatformBuilder}, - {server, ...settings}: PlatformBuilderSettings & {server: Type} + serverless: {bootstrap: (server: Type, settings: PlatformBuilderSettings) => PlatformBuilder}, + {server, ...settings}: PlatformBuilderSettings & {server: Type} ): () => Promise; static bootstrap( serverless: {bootstrap: (settings: Partial & {lambda?: Type[]}) => any}, - {server, ...settings}: PlatformBuilderSettings + {server, ...settings}: PlatformBuilderSettings ): () => Promise; - static bootstrap(serverless: any, {server, ...settings}: PlatformBuilderSettings) { + static bootstrap(serverless: any, {server, ...settings}: PlatformBuilderSettings) { return async function before(): Promise { settings = DITest.configure(settings); diff --git a/packages/platform/platform-serverless/src/builder/PlatformServerlessHandler.ts b/packages/platform/platform-serverless/src/builder/PlatformServerlessHandler.ts index 31ea419babe..7fdb4ecfcbb 100644 --- a/packages/platform/platform-serverless/src/builder/PlatformServerlessHandler.ts +++ b/packages/platform/platform-serverless/src/builder/PlatformServerlessHandler.ts @@ -1,8 +1,9 @@ import {AnyPromiseResult, AnyToPromise, isSerializable} from "@tsed/core"; -import {BaseContext, Inject, Injectable, InjectorService, LazyInject, ProviderScope, runInContext, TokenProvider} from "@tsed/di"; +import {BaseContext, Inject, Injectable, InjectorService, LazyInject, ProviderScope, TokenProvider} from "@tsed/di"; import {serialize} from "@tsed/json-mapper"; -import {DeserializerPipe, PlatformParams, ValidationPipe} from "@tsed/platform-params"; import type {PlatformExceptions} from "@tsed/platform-exceptions"; +import {DeserializerPipe, PlatformParams, ValidationPipe} from "@tsed/platform-params"; +import {JsonMethodStore} from "@tsed/schema"; import {ServerlessContext} from "../domain/ServerlessContext"; import {setResponseHeaders} from "../utils/setResponseHeaders"; @@ -21,10 +22,7 @@ export class PlatformServerlessHandler { protected exceptionsManager: Promise; createHandler(token: TokenProvider, propertyKey: string | symbol) { - const promisedHandler = this.params.compileHandler({ - token, - propertyKey - }); + const promisedHandler = this.params.compileHandler({token, propertyKey}); return async ($ctx: ServerlessContext) => { await $ctx.runInContext(async () => { diff --git a/packages/platform/platform-test-utils/src/tests/testHandlers.ts b/packages/platform/platform-test-utils/src/tests/testHandlers.ts index 415b165ce38..4295cc4ca41 100644 --- a/packages/platform/platform-test-utils/src/tests/testHandlers.ts +++ b/packages/platform/platform-test-utils/src/tests/testHandlers.ts @@ -16,9 +16,9 @@ export class MyModel { @Controller("/handlers") export class HandlersCtrl { @Get("/scenario-1/:id") // Express style - public scenario1(request: any, response: any): MyModel { + public scenario1(@PathParams("id") id: string): MyModel { const model = new MyModel(); - model.id = request.params.id; + model.id = id; model.name = "test"; return model; @@ -57,6 +57,9 @@ export function testHandlers(options: PlatformTestOptions) { beforeAll( PlatformTest.bootstrap(options.server, { ...options, + logger: { + leve: "info" + }, mount: { "/rest": [HandlersCtrl] } diff --git a/packages/platform/platform-test-utils/src/tests/testStatics.ts b/packages/platform/platform-test-utils/src/tests/testStatics.ts index dd8223b05f6..db68c308a04 100644 --- a/packages/platform/platform-test-utils/src/tests/testStatics.ts +++ b/packages/platform/platform-test-utils/src/tests/testStatics.ts @@ -1,6 +1,7 @@ -import {Controller, PlatformRouter, PlatformTest} from "@tsed/common"; +import {Controller, PlatformTest} from "@tsed/common"; import {getValue} from "@tsed/core"; import {Configuration} from "@tsed/di"; +import {PlatformRouter} from "@tsed/platform-router"; import {readFileSync} from "fs"; import SuperTest from "supertest"; import {PlatformTestOptions} from "../interfaces"; @@ -19,6 +20,9 @@ export function testStatics(options: PlatformTestOptions) { beforeEach( PlatformTest.bootstrap(options.server, { ...options, + logger: { + level: "info" + }, mount: { "/rest": [CustomStaticsCtrl] } diff --git a/packages/security/oidc-provider/src/middlewares/OidcInteractionMiddleware.ts b/packages/security/oidc-provider/src/middlewares/OidcInteractionMiddleware.ts index 3999a461e04..d570c2c3bf3 100644 --- a/packages/security/oidc-provider/src/middlewares/OidcInteractionMiddleware.ts +++ b/packages/security/oidc-provider/src/middlewares/OidcInteractionMiddleware.ts @@ -2,8 +2,8 @@ import {Context, Inject, InjectorService, Middleware} from "@tsed/common"; import { INTERACTION_CONTEXT, INTERACTION_DETAILS, - INTERACTION_PARAMS, INTERACTION_GRANT_ID, + INTERACTION_PARAMS, INTERACTION_PROMPT, INTERACTION_SESSION, INTERACTION_UID diff --git a/packages/security/oidc-provider/src/services/OidcInteractionContext.ts b/packages/security/oidc-provider/src/services/OidcInteractionContext.ts index c65b08d708d..a0c5d37ca63 100644 --- a/packages/security/oidc-provider/src/services/OidcInteractionContext.ts +++ b/packages/security/oidc-provider/src/services/OidcInteractionContext.ts @@ -22,7 +22,7 @@ export class OidcInteractionContext { protected oidcInteractions: OidcInteractions; @Inject() - protected context: PlatformContext; + protected $ctx: PlatformContext; protected raw: OidcInteraction; @@ -50,26 +50,26 @@ export class OidcInteractionContext { const handler = this.oidcInteractions.getInteractionHandler(name); if (handler) { - await handler(this.context); + await handler(this.$ctx); } } async interactionDetails(): Promise { - this.raw = await this.oidcProvider.get().interactionDetails(this.context.getReq(), this.context.getRes()); + this.raw = await this.oidcProvider.get().interactionDetails(this.$ctx.getReq(), this.$ctx.getRes()); return this.raw; } async interactionFinished(result: InteractionResults, options: {mergeWithLastSubmission?: boolean} = {mergeWithLastSubmission: false}) { - return this.oidcProvider.get().interactionFinished(this.context.getReq(), this.context.getRes(), result, options); + return this.oidcProvider.get().interactionFinished(this.$ctx.getReq(), this.$ctx.getRes(), result, options); } async interactionResult(result: InteractionResults, options: {mergeWithLastSubmission?: boolean} = {mergeWithLastSubmission: false}) { - return this.oidcProvider.get().interactionResult(this.context.getReq(), this.context.getRes(), result, options); + return this.oidcProvider.get().interactionResult(this.$ctx.getReq(), this.$ctx.getRes(), result, options); } async render(view: string, result: any): Promise { - return this.context.response.render(view, result); + return this.$ctx.response.render(view, result); } async save(ttl: number): Promise { diff --git a/packages/security/oidc-provider/src/services/OidcInteractions.ts b/packages/security/oidc-provider/src/services/OidcInteractions.ts index fe55170c9ae..56439e0d6b6 100644 --- a/packages/security/oidc-provider/src/services/OidcInteractions.ts +++ b/packages/security/oidc-provider/src/services/OidcInteractions.ts @@ -52,10 +52,10 @@ export class OidcInteractions { const interaction = this.getInteractionProvider(name); if (interaction) { + const endpoint = EndpointMetadata.get(interaction.useClass, "$prompt"); return (ctx: PlatformContext) => { // Add current endpoint metadata to ctx - ctx.endpoint = EndpointMetadata.get(interaction.useClass, "$prompt"); - + ctx.endpoint = endpoint; return interaction.store.get("$prompt")(ctx); }; } diff --git a/packages/security/passport/package.json b/packages/security/passport/package.json index d7d6acc42a5..a1dfc8da50c 100644 --- a/packages/security/passport/package.json +++ b/packages/security/passport/package.json @@ -28,6 +28,7 @@ "@tsed/common": "7.0.0-beta.13", "@tsed/core": "7.0.0-beta.13", "@tsed/di": "7.0.0-beta.13", + "@tsed/platform-router": "7.0.0-beta.13", "@types/passport": "1.0.7", "@types/passport-http": "0.3.9", "@types/passport-local": "1.0.34", @@ -46,4 +47,4 @@ "optional": false } } -} \ No newline at end of file +} diff --git a/packages/security/passport/src/services/ProtocolsService.spec.ts b/packages/security/passport/src/services/ProtocolsService.spec.ts index ba1a300fb3d..605fd6e86a7 100644 --- a/packages/security/passport/src/services/ProtocolsService.spec.ts +++ b/packages/security/passport/src/services/ProtocolsService.spec.ts @@ -85,17 +85,20 @@ describe("ProtocolsService", () => { // GIVEN (LocalProtocol.prototype.$onVerify as any).mockReturnValue({id: 0}); const provider = PlatformTest.injector.getProvider(LocalProtocol)!; - const ctx = PlatformTest.createRequestContext(); + const $ctx = PlatformTest.createRequestContext(); // WHEN - const result = await protocolService.invoke(provider); + const protocol = await protocolService.invoke(provider); + const resultDone: any = await new Promise((resolve) => { - Strategy.mock.calls[0][1](ctx.getRequest(), "test", (...args: any[]) => resolve(args)); + const verify = Strategy.mock.calls[0][1]; + + return $ctx.runInContext(() => verify($ctx.getRequest(), "test", (...args: any[]) => resolve(args))); }); // THEN - expect(result.$onVerify).toHaveBeenCalledWith(ctx.getRequest(), ctx); expect(resultDone).toEqual([null, {id: 0}]); + expect(protocol.$onVerify).toHaveBeenCalledWith($ctx.getRequest(), $ctx); }); it("should call metadata and catch error", async () => { @@ -105,16 +108,18 @@ describe("ProtocolsService", () => { (LocalProtocol.prototype.$onVerify as any).mockRejectedValue(error); const provider = PlatformTest.injector.getProvider(LocalProtocol)!; - const ctx = PlatformTest.createRequestContext(); + const $ctx = PlatformTest.createRequestContext(); // WHEN const result = await protocolService.invoke(provider); const resultDone: any = await new Promise((resolve) => { - Strategy.mock.calls[0][1](ctx.getRequest(), "test", (...args: any[]) => resolve(args)); + const verify = Strategy.mock.calls[0][1]; + + return $ctx.runInContext(() => verify($ctx.getRequest(), "test", (...args: any[]) => resolve(args))); }); // THEN - expect(result.$onVerify).toHaveBeenCalledWith(ctx.getRequest(), ctx); + expect(result.$onVerify).toHaveBeenCalledWith($ctx.getRequest(), $ctx); expect(resultDone).toEqual([error, false, {message: "message"}]); }); it("should call metadata and catch missing $ctx", async () => { diff --git a/packages/security/passport/src/services/ProtocolsService.ts b/packages/security/passport/src/services/ProtocolsService.ts index 145fb241173..7deeed9033d 100644 --- a/packages/security/passport/src/services/ProtocolsService.ts +++ b/packages/security/passport/src/services/ProtocolsService.ts @@ -1,11 +1,11 @@ import {PlatformContext, PlatformHandler} from "@tsed/common"; import {ancestorsOf} from "@tsed/core"; -import {Injectable, InjectorService, Provider} from "@tsed/di"; +import {getContext, Inject, Injectable, InjectorService, Provider} from "@tsed/di"; import {Unauthorized} from "@tsed/exceptions"; import Passport, {Strategy} from "passport"; -import {PassportException} from "../errors/PassportException"; -import {PROVIDER_TYPE_PROTOCOL} from "../contants/constants"; import {promisify} from "util"; +import {PROVIDER_TYPE_PROTOCOL} from "../contants/constants"; +import {PassportException} from "../errors/PassportException"; import type {ProtocolMethods} from "../interfaces/ProtocolMethods"; import type {ProtocolOptions} from "../interfaces/ProtocolOptions"; @@ -16,7 +16,11 @@ import type {ProtocolOptions} from "../interfaces/ProtocolOptions"; export class ProtocolsService { readonly strategies: Map = new Map(); - constructor(private injector: InjectorService) {} + @Inject() + protected platformHandler: PlatformHandler; + + @Inject() + private injector: InjectorService; public getProtocols(): Provider[] { return this.injector.getProviders(PROVIDER_TYPE_PROTOCOL); @@ -134,19 +138,20 @@ export class ProtocolsService { * @param provider * @private */ - private createHandler(provider: Provider) { - const platformHandler = this.injector.get(PlatformHandler)!; - const middleware = platformHandler.createCustomHandler(provider, "$onVerify"); + private createHandler(provider: Provider) { + const middleware = this.platformHandler.createCustomHandler(provider, "$onVerify"); return async (req: any, ...args: any[]) => { + const $ctx = getContext(); const done = args[args.length - 1]; - if (req.$ctx) { - req.$ctx.set("PROTOCOL_ARGS", args.slice(0, -1)); + if ($ctx) { + $ctx.set("PROTOCOL_ARGS", args.slice(0, -1)); try { - await middleware(req.$ctx); - done(null, ...[].concat(req.$ctx.data)); + await middleware($ctx); + + done(null, ...[].concat($ctx.data)); } catch (err) { done(err, false, {message: err.message}); } diff --git a/packages/specs/schema/jest.config.js b/packages/specs/schema/jest.config.js index 0ede37d2da4..8d506385d65 100644 --- a/packages/specs/schema/jest.config.js +++ b/packages/specs/schema/jest.config.js @@ -5,10 +5,10 @@ module.exports = { ...require("@tsed/jest-config")(__dirname, "schema"), coverageThreshold: { global: { - statements: 98.68, + statements: 98.61, branches: 89.91, - functions: 99.54, - lines: 98.77 + functions: 99.28, + lines: 98.74 } } }; diff --git a/packages/specs/schema/src/domain/JsonEntityStore.ts b/packages/specs/schema/src/domain/JsonEntityStore.ts index 378bad277bc..d57bcf36ca6 100644 --- a/packages/specs/schema/src/domain/JsonEntityStore.ts +++ b/packages/specs/schema/src/domain/JsonEntityStore.ts @@ -277,6 +277,10 @@ export abstract class JsonEntityStore implements JsonEntityStoreOptions { } } + toString() { + return [this.targetName, this.propertyName, this.index].filter((o) => o !== undefined).join(":"); + } + static get(target: Type, propertyKey: string | symbol, descriptor?: any) { return JsonEntityStore.from(prototypeOf(target), propertyKey, descriptor); } diff --git a/packages/specs/schema/src/domain/JsonOperationRoute.spec.ts b/packages/specs/schema/src/domain/JsonOperationRoute.spec.ts index 6bf12baea59..96b0d09fc96 100644 --- a/packages/specs/schema/src/domain/JsonOperationRoute.spec.ts +++ b/packages/specs/schema/src/domain/JsonOperationRoute.spec.ts @@ -22,8 +22,8 @@ describe("JsonOperationRoute", () => { }); expect(operationRoute.method).toEqual("GET"); expect(operationRoute.path).toEqual("/"); - expect(operationRoute.fullPath).toEqual("/base/"); - expect(operationRoute.url).toEqual("/base/"); + expect(operationRoute.fullPath).toEqual("/base"); + expect(operationRoute.url).toEqual("/base"); expect(operationRoute.isFinal).toEqual(false); expect(operationRoute.name).toEqual("Test.get()"); expect(operationRoute.className).toEqual("Test"); @@ -56,8 +56,8 @@ describe("JsonOperationRoute", () => { }); expect(operationRoute.method).toEqual("GET"); expect(operationRoute.path).toEqual("/"); - expect(operationRoute.fullPath).toEqual("/base/"); - expect(operationRoute.url).toEqual("/base/"); + expect(operationRoute.fullPath).toEqual("/base"); + expect(operationRoute.url).toEqual("/base"); expect(operationRoute.isFinal).toEqual(false); expect(operationRoute.name).toEqual("Test.get()"); expect(operationRoute.className).toEqual("Test"); @@ -89,8 +89,8 @@ describe("JsonOperationRoute", () => { }); expect(operationRoute.method).toEqual("GET"); expect(operationRoute.path).toEqual("/"); - expect(operationRoute.fullPath).toEqual("/base/"); - expect(operationRoute.url).toEqual("/base/"); + expect(operationRoute.fullPath).toEqual("/base"); + expect(operationRoute.url).toEqual("/base"); expect(operationRoute.isFinal).toEqual(false); expect(operationRoute.name).toEqual("Test.get()"); expect(operationRoute.className).toEqual("Test"); diff --git a/packages/specs/schema/src/utils/concatPath.spec.ts b/packages/specs/schema/src/utils/concatPath.spec.ts new file mode 100644 index 00000000000..23bf8b69def --- /dev/null +++ b/packages/specs/schema/src/utils/concatPath.spec.ts @@ -0,0 +1,22 @@ +import {concatPath} from "./concatPath"; + +describe("concatPath", () => { + it("should return path", () => { + expect(concatPath("/controllers/", "/path")).toEqual("/controllers/path"); + expect(concatPath("/controllers", "/path")).toEqual("/controllers/path"); + expect(concatPath("/controllers/", "path")).toEqual("/controllers/path"); + expect(concatPath(undefined, "/path")).toEqual("/path"); + expect(concatPath("/controllers/", undefined)).toEqual("/controllers"); + expect(concatPath("/", "/:id")).toEqual("/:id"); + expect(concatPath("/", "/")).toEqual("/"); + expect(concatPath("/", undefined)).toEqual("/"); + expect(concatPath(undefined, "/")).toEqual("/"); + }); + it("should concat regexp", () => { + expect(concatPath(/\/(.*)/, /\/test(.*)/).source).toEqual("\\/(.*)\\/test(.*)"); + expect(concatPath(undefined, /\/test(.*)/).source).toEqual("\\/test(.*)"); + expect(concatPath(/\/(.*)/, undefined).source).toEqual("\\/(.*)"); + expect(concatPath(/\/(.*)/, "/test").source).toEqual("\\/(.*)\\/test"); + expect(concatPath("/test", /\/test(.*)/).source).toEqual("\\/test\\/test(.*)"); + }); +}); diff --git a/packages/specs/schema/src/utils/concatPath.ts b/packages/specs/schema/src/utils/concatPath.ts index 960da4eea9b..3d324cd6171 100644 --- a/packages/specs/schema/src/utils/concatPath.ts +++ b/packages/specs/schema/src/utils/concatPath.ts @@ -1,3 +1,24 @@ -export function concatPath(basePath: string | undefined, path: string | undefined): string { - return ((basePath || "") + (path || "")).replace(/\/\//gi, "/"); +export function concatPath(basePath: string | undefined | RegExp, path: string | undefined | RegExp): any { + if (basePath instanceof RegExp || path instanceof RegExp) { + if (!basePath) { + return path; + } + + if (!path) { + return basePath; + } + + const r1 = basePath instanceof RegExp ? basePath : new RegExp(basePath, "gi"); + const r2 = path instanceof RegExp ? path : new RegExp(path, "gi"); + + return new RegExp(r1.source + r2.source, (r1.global ? "g" : "") + (r1.ignoreCase ? "i" : "") + (r1.multiline ? "m" : "")); + } + + if (basePath && path && basePath.endsWith("/") && path.startsWith("/")) { + path = path.slice(1); + } + + const result = (basePath || "") + (path || ""); + + return result.endsWith("/") && result.length > 1 ? result.slice(0, -1) : result; } diff --git a/packages/specs/schema/src/utils/getOperationsRoutes.spec.ts b/packages/specs/schema/src/utils/getOperationsRoutes.spec.ts index d7d8cb551c1..42f07e86769 100644 --- a/packages/specs/schema/src/utils/getOperationsRoutes.spec.ts +++ b/packages/specs/schema/src/utils/getOperationsRoutes.spec.ts @@ -57,7 +57,7 @@ describe("getOperationsRoutes()", () => { expect(operationsRoutes.map(getData)).toEqual([ { - fullPath: "/test/", + fullPath: "/test", method: "GET", path: "/", propertyKey: "method", @@ -67,7 +67,7 @@ describe("getOperationsRoutes()", () => { isFinal: false }, { - fullPath: "/test/", + fullPath: "/test", method: "GET", path: "/", propertyKey: "method3", @@ -87,7 +87,7 @@ describe("getOperationsRoutes()", () => { isFinal: true }, { - fullPath: "/test/", + fullPath: "/test", method: "GET", path: "/", propertyKey: "method2", @@ -97,7 +97,7 @@ describe("getOperationsRoutes()", () => { isFinal: false }, { - fullPath: "/test/", + fullPath: "/test", method: "GET", path: "/", propertyKey: "method1", @@ -155,7 +155,7 @@ describe("getOperationsRoutes()", () => { expect(operationsRoutes.map(getData)).toEqual([ { - fullPath: "/test/children/deep/", + fullPath: "/test/children/deep", isFinal: true, method: "GET", path: "/", @@ -165,7 +165,7 @@ describe("getOperationsRoutes()", () => { token: TestChild2 }, { - fullPath: "/test/children/", + fullPath: "/test/children", isFinal: true, method: "GET", path: "/", @@ -175,7 +175,7 @@ describe("getOperationsRoutes()", () => { token: TestChild }, { - fullPath: "/test/", + fullPath: "/test", isFinal: false, method: "GET", path: "/", @@ -185,7 +185,7 @@ describe("getOperationsRoutes()", () => { token: Test }, { - fullPath: "/test/", + fullPath: "/test", isFinal: true, method: "GET", path: "/", @@ -241,7 +241,7 @@ describe("getOperationsRoutes()", () => { expect(operationsRoutes.map(getData)).toEqual([ { - fullPath: "/rest/test/children/deep/", + fullPath: "/rest/test/children/deep", isFinal: true, method: "GET", path: "/", @@ -251,7 +251,7 @@ describe("getOperationsRoutes()", () => { token: TestChild2 }, { - fullPath: "/rest/test/children/", + fullPath: "/rest/test/children", isFinal: true, method: "GET", path: "/", @@ -261,7 +261,7 @@ describe("getOperationsRoutes()", () => { token: TestChild }, { - fullPath: "/rest/test/", + fullPath: "/rest/test", isFinal: false, method: "GET", path: "/", @@ -271,7 +271,7 @@ describe("getOperationsRoutes()", () => { token: Test }, { - fullPath: "/rest/test/", + fullPath: "/rest/test", isFinal: true, method: "GET", path: "/", diff --git a/packages/specs/swagger/src/SwaggerModule.ts b/packages/specs/swagger/src/SwaggerModule.ts index 25f15b75aaf..2a9bae2e1e5 100644 --- a/packages/specs/swagger/src/SwaggerModule.ts +++ b/packages/specs/swagger/src/SwaggerModule.ts @@ -8,20 +8,19 @@ import { normalizePath, OnReady, PlatformApplication, - PlatformContext, - PlatformRouter, - useCtxHandler + PlatformContext } from "@tsed/common"; +import {PlatformRouter, useContextHandler} from "@tsed/platform-router"; import Fs from "fs"; import {join} from "path"; +import {Env} from "@tsed/core"; +import {absolutePath} from "swagger-ui-dist"; import {SwaggerSettings} from "./interfaces/SwaggerSettings"; import {cssMiddleware} from "./middlewares/cssMiddleware"; import {indexMiddleware} from "./middlewares/indexMiddleware"; import {jsMiddleware} from "./middlewares/jsMiddleware"; import {redirectMiddleware} from "./middlewares/redirectMiddleware"; import {SwaggerService} from "./services/SwaggerService"; -import {Env} from "@tsed/core"; -import {absolutePath} from "swagger-ui-dist"; /** * @ignore @@ -62,7 +61,7 @@ export class SwaggerModule implements OnRoutesInit, OnReady { this.settings.forEach((conf: SwaggerSettings) => { const {path = "/"} = conf; - this.app.get(path, useCtxHandler(redirectMiddleware(path))); + this.app.get(path, useContextHandler(redirectMiddleware(path))); this.app.use(path, this.createRouter(conf, urls)); }); @@ -125,22 +124,22 @@ export class SwaggerModule implements OnRoutesInit, OnReady { */ private createRouter(conf: SwaggerSettings, urls: string[]) { const {disableSpec = false, fileName = "swagger.json", cssPath, jsPath, viewPath = join(__dirname, "../views/index.ejs")} = conf; - const router = PlatformRouter.create(this.injector); + const router = new PlatformRouter(this.injector); if (!disableSpec) { - router.get(normalizePath("/", fileName), useCtxHandler(this.middlewareSwaggerJson(conf))); + router.get(normalizePath("/", fileName), useContextHandler(this.middlewareSwaggerJson(conf))); } if (viewPath) { if (cssPath) { - router.get("/main.css", useCtxHandler(cssMiddleware(cssPath))); + router.get("/main.css", useContextHandler(cssMiddleware(cssPath))); } if (jsPath) { - router.get("/main.js", useCtxHandler(jsMiddleware(jsPath))); + router.get("/main.js", useContextHandler(jsMiddleware(jsPath))); } - router.get("/", useCtxHandler(indexMiddleware(viewPath, {urls, ...conf}))); + router.get("/", useContextHandler(indexMiddleware(viewPath, {urls, ...conf}))); router.statics("/", {root: absolutePath()}); } diff --git a/packages/specs/swagger/src/services/SwaggerService.ts b/packages/specs/swagger/src/services/SwaggerService.ts index 44922c36937..0648b6cf4ad 100644 --- a/packages/specs/swagger/src/services/SwaggerService.ts +++ b/packages/specs/swagger/src/services/SwaggerService.ts @@ -29,7 +29,7 @@ export class SwaggerService { const tokens = this.platform .getMountedControllers() .filter(({route, provider}) => includeRoute(route, provider, conf)) - .map(({route, provider}) => ({token: provider.token, rootPath: route.replace(provider.path, "")})); + .map(({route, provider}) => ({token: provider.token, rootPath: route})); const spec = await generateSpec({ tokens, diff --git a/packages/specs/swagger/src/utils/includeRoute.ts b/packages/specs/swagger/src/utils/includeRoute.ts index e07606fad92..c390ea00013 100644 --- a/packages/specs/swagger/src/utils/includeRoute.ts +++ b/packages/specs/swagger/src/utils/includeRoute.ts @@ -1,8 +1,8 @@ -import {ControllerProvider} from "@tsed/common"; -import {matchPath} from "./matchPath"; +import {Provider} from "@tsed/di"; import {SwaggerSettings} from "../interfaces/SwaggerSettings"; +import {matchPath} from "./matchPath"; -export function includeRoute(route: string, provider: ControllerProvider, conf: SwaggerSettings): boolean { +export function includeRoute(route: string, provider: Provider, conf: SwaggerSettings): boolean { const hidden = provider.store.get("hidden"); const docs = provider.store.get("docs") || []; const {doc, pathPatterns} = conf; diff --git a/packages/specs/swagger/test/__snapshots__/swagger.integration.spec.ts.snap b/packages/specs/swagger/test/__snapshots__/swagger.integration.spec.ts.snap new file mode 100644 index 00000000000..6d2ab49329e --- /dev/null +++ b/packages/specs/swagger/test/__snapshots__/swagger.integration.spec.ts.snap @@ -0,0 +1,285 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Swagger integration OpenSpec2 should swagger spec 1`] = ` +Object { + "consumes": Array [ + "application/json", + ], + "definitions": Object { + "Calendar": Object { + "properties": Object { + "id": Object { + "type": "string", + }, + "name": Object { + "minLength": 1, + "type": "string", + }, + }, + "required": Array [ + "name", + ], + "type": "object", + }, + }, + "info": Object { + "title": "Swagger title", + "version": "1.2.0", + }, + "paths": Object { + "/rest/calendars": Object { + "get": Object { + "operationId": "calendarsControllerGetAll", + "parameters": Array [], + "produces": Array [ + "application/json", + ], + "responses": Object { + "200": Object { + "description": "Success", + "schema": Object { + "items": Object { + "$ref": "#/definitions/Calendar", + }, + "type": "array", + }, + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + "/rest/calendars/csv": Object { + "post": Object { + "consumes": Array [ + "text/plain", + ], + "operationId": "calendarsControllerCsv", + "parameters": Array [ + Object { + "in": "body", + "name": "body", + "required": false, + "schema": Object { + "type": "string", + }, + }, + ], + "produces": Array [ + "text/plain", + ], + "responses": Object { + "200": Object { + "description": "Success", + "schema": Object { + "type": "string", + }, + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + "/rest/calendars/events": Object { + "get": Object { + "description": "Events", + "operationId": "eventCtrlGet", + "parameters": Array [], + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "EventCtrl", + ], + }, + }, + "/rest/calendars/{id}": Object { + "get": Object { + "operationId": "calendarsControllerGet", + "parameters": Array [ + Object { + "description": "An ObjectID", + "in": "path", + "name": "id", + "pattern": "^[0-9a-fA-F]{24}$", + "required": true, + "type": "string", + }, + ], + "produces": Array [ + "application/json", + ], + "responses": Object { + "200": Object { + "description": "Success", + "schema": Object { + "$ref": "#/definitions/Calendar", + }, + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + }, + "produces": Array [ + "application/json", + ], + "swagger": "2.0", + "tags": Array [ + Object { + "name": "CalendarsController", + }, + Object { + "name": "EventCtrl", + }, + ], +} +`; + +exports[`Swagger integration OpenSpec3 should swagger spec 1`] = ` +Object { + "components": Object { + "schemas": Object { + "Calendar": Object { + "properties": Object { + "id": Object { + "type": "string", + }, + "name": Object { + "minLength": 1, + "type": "string", + }, + }, + "required": Array [ + "name", + ], + "type": "object", + }, + }, + }, + "info": Object { + "title": "Api documentation", + "version": "1.0.0", + }, + "openapi": "3.0.1", + "paths": Object { + "/rest/calendars": Object { + "get": Object { + "operationId": "calendarsControllerGetAll", + "parameters": Array [], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "items": Object { + "$ref": "#/components/schemas/Calendar", + }, + "type": "array", + }, + }, + }, + "description": "Success", + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + "/rest/calendars/csv": Object { + "post": Object { + "operationId": "calendarsControllerCsv", + "parameters": Array [], + "requestBody": Object { + "content": Object { + "text/plain": Object { + "schema": Object { + "type": "string", + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "content": Object { + "text/plain": Object { + "schema": Object { + "type": "string", + }, + }, + }, + "description": "Success", + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + "/rest/calendars/events": Object { + "get": Object { + "description": "Events", + "operationId": "eventCtrlGet", + "parameters": Array [], + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "EventCtrl", + ], + }, + }, + "/rest/calendars/{id}": Object { + "get": Object { + "operationId": "calendarsControllerGet", + "parameters": Array [ + Object { + "description": "An ObjectID", + "in": "path", + "name": "id", + "required": true, + "schema": Object { + "example": "5ce7ad3028890bd71749d477", + "pattern": "^[0-9a-fA-F]{24}$", + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/Calendar", + }, + }, + }, + "description": "Success", + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + }, + "tags": Array [ + Object { + "name": "CalendarsController", + }, + Object { + "name": "EventCtrl", + }, + ], +} +`; diff --git a/packages/specs/swagger/test/__snapshots__/swagger.operationId.spec.ts.snap b/packages/specs/swagger/test/__snapshots__/swagger.operationId.spec.ts.snap new file mode 100644 index 00000000000..39fad87d4b5 --- /dev/null +++ b/packages/specs/swagger/test/__snapshots__/swagger.operationId.spec.ts.snap @@ -0,0 +1,285 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Swagger integration OpenSpec should swagger spec 2 1`] = ` +Object { + "consumes": Array [ + "application/json", + ], + "definitions": Object { + "Calendar": Object { + "properties": Object { + "id": Object { + "type": "string", + }, + "name": Object { + "minLength": 1, + "type": "string", + }, + }, + "required": Array [ + "name", + ], + "type": "object", + }, + }, + "info": Object { + "title": "Swagger title", + "version": "1.2.0", + }, + "paths": Object { + "/rest/calendars": Object { + "get": Object { + "operationId": "CalendarsController_getAll", + "parameters": Array [], + "produces": Array [ + "application/json", + ], + "responses": Object { + "200": Object { + "description": "Success", + "schema": Object { + "items": Object { + "$ref": "#/definitions/Calendar", + }, + "type": "array", + }, + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + "/rest/calendars/csv": Object { + "post": Object { + "consumes": Array [ + "text/plain", + ], + "operationId": "CalendarsController_csv", + "parameters": Array [ + Object { + "in": "body", + "name": "body", + "required": false, + "schema": Object { + "type": "string", + }, + }, + ], + "produces": Array [ + "text/plain", + ], + "responses": Object { + "200": Object { + "description": "Success", + "schema": Object { + "type": "string", + }, + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + "/rest/calendars/events": Object { + "get": Object { + "description": "Events", + "operationId": "EventCtrl_get", + "parameters": Array [], + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "EventCtrl", + ], + }, + }, + "/rest/calendars/{id}": Object { + "get": Object { + "operationId": "CalendarsController_get", + "parameters": Array [ + Object { + "description": "An ObjectID", + "in": "path", + "name": "id", + "pattern": "^[0-9a-fA-F]{24}$", + "required": true, + "type": "string", + }, + ], + "produces": Array [ + "application/json", + ], + "responses": Object { + "200": Object { + "description": "Success", + "schema": Object { + "$ref": "#/definitions/Calendar", + }, + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + }, + "produces": Array [ + "application/json", + ], + "swagger": "2.0", + "tags": Array [ + Object { + "name": "CalendarsController", + }, + Object { + "name": "EventCtrl", + }, + ], +} +`; + +exports[`Swagger integration OpenSpec should swagger spec 3 1`] = ` +Object { + "components": Object { + "schemas": Object { + "Calendar": Object { + "properties": Object { + "id": Object { + "type": "string", + }, + "name": Object { + "minLength": 1, + "type": "string", + }, + }, + "required": Array [ + "name", + ], + "type": "object", + }, + }, + }, + "info": Object { + "title": "Api documentation", + "version": "1.0.0", + }, + "openapi": "3.0.1", + "paths": Object { + "/rest/calendars": Object { + "get": Object { + "operationId": "CalendarsController__getAll", + "parameters": Array [], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "items": Object { + "$ref": "#/components/schemas/Calendar", + }, + "type": "array", + }, + }, + }, + "description": "Success", + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + "/rest/calendars/csv": Object { + "post": Object { + "operationId": "CalendarsController__csv", + "parameters": Array [], + "requestBody": Object { + "content": Object { + "text/plain": Object { + "schema": Object { + "type": "string", + }, + }, + }, + "required": false, + }, + "responses": Object { + "200": Object { + "content": Object { + "text/plain": Object { + "schema": Object { + "type": "string", + }, + }, + }, + "description": "Success", + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + "/rest/calendars/events": Object { + "get": Object { + "description": "Events", + "operationId": "EventCtrl__get", + "parameters": Array [], + "responses": Object { + "200": Object { + "description": "Success", + }, + }, + "tags": Array [ + "EventCtrl", + ], + }, + }, + "/rest/calendars/{id}": Object { + "get": Object { + "operationId": "CalendarsController__get", + "parameters": Array [ + Object { + "description": "An ObjectID", + "in": "path", + "name": "id", + "required": true, + "schema": Object { + "example": "5ce7ad3028890bd71749d477", + "pattern": "^[0-9a-fA-F]{24}$", + "type": "string", + }, + }, + ], + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/Calendar", + }, + }, + }, + "description": "Success", + }, + }, + "tags": Array [ + "CalendarsController", + ], + }, + }, + }, + "tags": Array [ + Object { + "name": "CalendarsController", + }, + Object { + "name": "EventCtrl", + }, + ], +} +`; diff --git a/packages/specs/swagger/test/app/index.ts b/packages/specs/swagger/test/app/index.ts index 47a0e0f2dd0..50db729e392 100644 --- a/packages/specs/swagger/test/app/index.ts +++ b/packages/specs/swagger/test/app/index.ts @@ -13,7 +13,18 @@ if (process.env.NODE_ENV !== "test") { value: string; } - @Controller("/hello") + @Controller("/nested") + class HelloWorld3 { + @Get("/") + get() { + return {test: "Hello world"}; + } + } + + @Controller({ + path: "/hello", + children: [HelloWorld3] + }) class HelloWorld { @Get("/") get() { diff --git a/packages/specs/swagger/test/swagger.integration.spec.ts b/packages/specs/swagger/test/swagger.integration.spec.ts index f1bac727721..9571729092e 100644 --- a/packages/specs/swagger/test/swagger.integration.spec.ts +++ b/packages/specs/swagger/test/swagger.integration.spec.ts @@ -1,6 +1,6 @@ import {BodyParams, Controller, Get, PathParams, PlatformTest, Post} from "@tsed/common"; import {ObjectID} from "@tsed/mongoose"; -import {MergeParams, PlatformExpress} from "@tsed/platform-express"; +import {PlatformExpress} from "@tsed/platform-express"; import {Consumes, Description, Returns} from "@tsed/schema"; import {Docs, Hidden} from "@tsed/swagger"; import SuperTest from "supertest"; @@ -15,7 +15,6 @@ class AdminCtrl { } @Controller("/events") -@MergeParams(true) class EventCtrl { @Get("/") @Description("Events") @@ -90,123 +89,7 @@ describe("Swagger integration", () => { name: "name" } ]); - expect(response.body).toEqual({ - consumes: ["application/json"], - definitions: { - Calendar: { - properties: { - id: { - type: "string" - }, - name: { - minLength: 1, - type: "string" - } - }, - required: ["name"], - type: "object" - } - }, - info: { - title: "Swagger title", - version: "1.2.0" - }, - paths: { - "/rest/calendars": { - get: { - operationId: "calendarsControllerGetAll", - parameters: [], - produces: ["application/json"], - responses: { - "200": { - description: "Success", - schema: { - items: { - $ref: "#/definitions/Calendar" - }, - type: "array" - } - } - }, - tags: ["CalendarsController"] - } - }, - "/rest/calendars/csv": { - post: { - consumes: ["text/plain"], - operationId: "calendarsControllerCsv", - parameters: [ - { - in: "body", - name: "body", - required: false, - schema: { - type: "string" - } - } - ], - produces: ["text/plain"], - responses: { - "200": { - description: "Success", - schema: { - type: "string" - } - } - }, - tags: ["CalendarsController"] - } - }, - "/rest/calendars/events": { - get: { - description: "Events", - operationId: "eventCtrlGet", - parameters: [], - responses: { - "200": { - description: "Success" - } - }, - tags: ["EventCtrl"] - } - }, - "/rest/calendars/{id}": { - get: { - operationId: "calendarsControllerGet", - parameters: [ - { - description: "An ObjectID", - in: "path", - name: "id", - pattern: "^[0-9a-fA-F]{24}$", - required: true, - type: "string" - } - ], - produces: ["application/json"], - responses: { - "200": { - description: "Success", - schema: { - $ref: "#/definitions/Calendar" - } - } - }, - tags: ["CalendarsController"] - } - } - }, - produces: ["application/json"], - swagger: "2.0", - tags: [ - { - name: "EventCtrl" - }, - { - name: "CalendarsController" - } - ] - }); + expect(response.body).toMatchSnapshot(); }); }); describe("OpenSpec3", () => { @@ -239,105 +122,7 @@ describe("Swagger integration", () => { } ]); - expect(response.body).toEqual({ - info: {version: "1.0.0", title: "Api documentation"}, - openapi: "3.0.1", - paths: { - "/rest/calendars/events": { - get: { - operationId: "eventCtrlGet", - parameters: [], - responses: {"200": {description: "Success"}}, - description: "Events", - tags: ["EventCtrl"] - } - }, - "/rest/calendars/{id}": { - get: { - operationId: "calendarsControllerGet", - parameters: [ - { - description: "An ObjectID", - in: "path", - name: "id", - required: true, - schema: { - example: "5ce7ad3028890bd71749d477", - pattern: "^[0-9a-fA-F]{24}$", - type: "string" - } - } - ], - responses: { - "200": { - content: {"application/json": {schema: {$ref: "#/components/schemas/Calendar"}}}, - description: "Success" - } - }, - tags: ["CalendarsController"] - } - }, - "/rest/calendars": { - get: { - operationId: "calendarsControllerGetAll", - parameters: [], - responses: { - "200": { - content: { - "application/json": { - schema: { - type: "array", - items: {$ref: "#/components/schemas/Calendar"} - } - } - }, - description: "Success" - } - }, - tags: ["CalendarsController"] - } - }, - "/rest/calendars/csv": { - post: { - operationId: "calendarsControllerCsv", - parameters: [], - requestBody: { - content: { - "text/plain": { - schema: { - type: "string" - } - } - }, - required: false - }, - responses: { - "200": { - content: { - "text/plain": { - schema: { - type: "string" - } - } - }, - description: "Success" - } - }, - tags: ["CalendarsController"] - } - } - }, - tags: [{name: "EventCtrl"}, {name: "CalendarsController"}], - components: { - schemas: { - Calendar: { - type: "object", - properties: {id: {type: "string"}, name: {type: "string", minLength: 1}}, - required: ["name"] - } - } - } - }); + expect(response.body).toMatchSnapshot(); }); }); }); diff --git a/packages/specs/swagger/test/swagger.operationId.spec.ts b/packages/specs/swagger/test/swagger.operationId.spec.ts index 4e1b95fd48b..a75e49f8b08 100644 --- a/packages/specs/swagger/test/swagger.operationId.spec.ts +++ b/packages/specs/swagger/test/swagger.operationId.spec.ts @@ -1,6 +1,6 @@ import {BodyParams, Controller, Get, PathParams, PlatformTest, Post} from "@tsed/common"; import {ObjectID} from "@tsed/mongoose"; -import {MergeParams, PlatformExpress} from "@tsed/platform-express"; +import {PlatformExpress} from "@tsed/platform-express"; import {Consumes, Description, Returns} from "@tsed/schema"; import {Docs, Hidden} from "@tsed/swagger"; import SuperTest from "supertest"; @@ -15,7 +15,6 @@ class AdminCtrl { } @Controller("/events") -@MergeParams(true) class EventCtrl { @Get("/") @Description("Events") @@ -96,256 +95,12 @@ describe("Swagger integration", () => { it("should swagger spec 2", async () => { const response = await request.get("/v2/doc/swagger.json").expect(200); - expect(response.body).toEqual({ - consumes: ["application/json"], - definitions: { - Calendar: { - properties: { - id: { - type: "string" - }, - name: { - minLength: 1, - type: "string" - } - }, - required: ["name"], - type: "object" - } - }, - info: { - title: "Swagger title", - version: "1.2.0" - }, - paths: { - "/rest/calendars": { - get: { - operationId: "CalendarsController_getAll", - parameters: [], - produces: ["application/json"], - responses: { - "200": { - description: "Success", - schema: { - items: { - $ref: "#/definitions/Calendar" - }, - type: "array" - } - } - }, - tags: ["CalendarsController"] - } - }, - "/rest/calendars/csv": { - post: { - consumes: ["text/plain"], - operationId: "CalendarsController_csv", - parameters: [ - { - in: "body", - name: "body", - required: false, - schema: { - type: "string" - } - } - ], - produces: ["text/plain"], - responses: { - "200": { - description: "Success", - schema: { - type: "string" - } - } - }, - tags: ["CalendarsController"] - } - }, - "/rest/calendars/events": { - get: { - description: "Events", - operationId: "EventCtrl_get", - parameters: [], - responses: { - "200": { - description: "Success" - } - }, - tags: ["EventCtrl"] - } - }, - "/rest/calendars/{id}": { - get: { - operationId: "CalendarsController_get", - parameters: [ - { - description: "An ObjectID", - in: "path", - name: "id", - pattern: "^[0-9a-fA-F]{24}$", - required: true, - type: "string" - } - ], - produces: ["application/json"], - responses: { - "200": { - description: "Success", - schema: { - $ref: "#/definitions/Calendar" - } - } - }, - tags: ["CalendarsController"] - } - } - }, - produces: ["application/json"], - swagger: "2.0", - tags: [ - { - name: "EventCtrl" - }, - { - name: "CalendarsController" - } - ] - }); + expect(response.body).toMatchSnapshot(); }); it("should swagger spec 3", async () => { const response = await request.get("/v3/doc/swagger.json").expect(200); - expect(response.body).toEqual({ - components: { - schemas: { - Calendar: { - properties: { - id: { - type: "string" - }, - name: { - minLength: 1, - type: "string" - } - }, - required: ["name"], - type: "object" - } - } - }, - info: { - title: "Api documentation", - version: "1.0.0" - }, - openapi: "3.0.1", - paths: { - "/rest/calendars": { - get: { - operationId: "CalendarsController__getAll", - parameters: [], - responses: { - "200": { - content: { - "application/json": { - schema: { - items: { - $ref: "#/components/schemas/Calendar" - }, - type: "array" - } - } - }, - description: "Success" - } - }, - tags: ["CalendarsController"] - } - }, - "/rest/calendars/csv": { - post: { - operationId: "CalendarsController__csv", - parameters: [], - requestBody: { - content: { - "text/plain": { - schema: { - type: "string" - } - } - }, - required: false - }, - responses: { - "200": { - content: { - "text/plain": { - schema: { - type: "string" - } - } - }, - description: "Success" - } - }, - tags: ["CalendarsController"] - } - }, - "/rest/calendars/events": { - get: { - description: "Events", - operationId: "EventCtrl__get", - parameters: [], - responses: { - "200": { - description: "Success" - } - }, - tags: ["EventCtrl"] - } - }, - "/rest/calendars/{id}": { - get: { - operationId: "CalendarsController__get", - parameters: [ - { - description: "An ObjectID", - in: "path", - name: "id", - required: true, - schema: { - example: "5ce7ad3028890bd71749d477", - pattern: "^[0-9a-fA-F]{24}$", - type: "string" - } - } - ], - responses: { - "200": { - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/Calendar" - } - } - }, - description: "Success" - } - }, - tags: ["CalendarsController"] - } - } - }, - tags: [ - { - name: "EventCtrl" - }, - { - name: "CalendarsController" - } - ] - }); + expect(response.body).toMatchSnapshot(); }); }); }); diff --git a/packages/third-parties/formio/src/FormioModule.spec.ts b/packages/third-parties/formio/src/FormioModule.spec.ts index bec2c9f7e06..e511c845682 100644 --- a/packages/third-parties/formio/src/FormioModule.spec.ts +++ b/packages/third-parties/formio/src/FormioModule.spec.ts @@ -65,7 +65,6 @@ describe("FormioModule", () => { await service.$onRoutesInit(); - expect(app.getRouter).toHaveBeenCalledWith(); expect(app.use).toHaveBeenCalledWith("/", formio.middleware.restrictRequestTypes, formio.router); expect(installer.install).toHaveBeenCalledWith(template, root); }); @@ -98,14 +97,12 @@ describe("FormioModule", () => { const results = await service.$logRoutes(routes); - expect(results.map((o) => o.toJSON())).toEqual([ + expect(results).toEqual([ { className: "formio", method: "get", methodClassName: "operationId", name: "operationId", - parameters: [], - rawBody: false, url: "/projects/path/to" } ]); diff --git a/packages/third-parties/formio/src/FormioModule.ts b/packages/third-parties/formio/src/FormioModule.ts index 1d15ecea64d..a7707254071 100644 --- a/packages/third-parties/formio/src/FormioModule.ts +++ b/packages/third-parties/formio/src/FormioModule.ts @@ -79,9 +79,7 @@ export class FormioModule implements OnRoutesInit, OnReady { async $onRoutesInit() { if (this.formio.isInit()) { - const router: any = this.app.getRouter(); - - router.use(this.baseUrl, this.formio.middleware.restrictRequestTypes, this.formio.router); + this.app.use(this.baseUrl, this.formio.middleware.restrictRequestTypes, this.formio.router); if (await this.shouldInstall()) { await this.installer.install(this.template!, this.root); @@ -107,18 +105,12 @@ export class FormioModule implements OnRoutesInit, OnReady { Object.entries(spec.paths).forEach(([path, methods]: [string, any]) => { Object.entries(methods).forEach(([method, operation]: [string, any]) => { routes.push({ - toJSON() { - return { - method, - name: operation.operationId, - url: normalizePath(baseUrl, path.replace(/\/{(.*)}/gi, "/:$1")), - className: "formio", - methodClassName: operation.operationId, - parameters: [], - rawBody: false - }; - } - } as any); + method, + name: operation.operationId, + url: normalizePath(baseUrl, path.replace(/\/{(.*)}/gi, "/:$1")), + className: "formio", + methodClassName: operation.operationId + }); }); }); } diff --git a/packages/third-parties/formio/src/components/AlterActions.spec.ts b/packages/third-parties/formio/src/components/AlterActions.spec.ts index cb8b8f95cf3..dc749aefeec 100644 --- a/packages/third-parties/formio/src/components/AlterActions.spec.ts +++ b/packages/third-parties/formio/src/components/AlterActions.spec.ts @@ -74,13 +74,15 @@ describe("AlterActions", () => { const instance = new actions.custom(info as any, ctx.getRequest(), ctx.getResponse()); new Promise((resolve) => { - instance.resolve( - "handler", - "method", - ctx.getRequest(), - ctx.getResponse(), - (err: any, result: any) => resolve(result), - "setActionItemMessage" as any + ctx.runInContext(() => + instance.resolve( + "handler", + "method", + ctx.getRequest(), + ctx.getResponse(), + (err: any, result: any) => resolve(result), + "setActionItemMessage" as any + ) ); }); @@ -154,13 +156,15 @@ describe("AlterActions", () => { const instance = new actions.custom(info as any, ctx.getRequest(), ctx.getResponse()); new Promise((resolve) => { - instance.resolve( - "handler", - "method", - ctx.getRequest(), - ctx.getResponse(), - (err: any, result: any) => resolve(result), - "setActionItemMessage" as any + ctx.runInContext(() => + instance.resolve( + "handler", + "method", + ctx.getRequest(), + ctx.getResponse(), + (err: any, result: any) => resolve(result), + "setActionItemMessage" as any + ) ); }); @@ -224,13 +228,15 @@ describe("AlterActions", () => { const instance = new actions.custom(info as any, ctx.getRequest(), ctx.getResponse()); const result = await new Promise((resolve) => { - instance.resolve( - "handler", - "method", - ctx.getRequest(), - ctx.getResponse(), - (err: any, result: any) => resolve(result), - "setActionItemMessage" as any + ctx.runInContext(() => + instance.resolve( + "handler", + "method", + ctx.getRequest(), + ctx.getResponse(), + (err: any, result: any) => resolve(result), + "setActionItemMessage" as any + ) ); }); expect(result).toEqual(undefined); @@ -276,15 +282,17 @@ describe("AlterActions", () => { const instance = new actions.custom(info as any, ctx.getRequest(), ctx.getResponse()); const result: any = await new Promise((resolve) => { - instance.resolve( - "handler", - "method", - ctx.getRequest(), - ctx.getResponse(), - (err: any, result: any) => { - return resolve(err); - }, - "setActionItemMessage" as any + ctx.runInContext(() => + instance.resolve( + "handler", + "method", + ctx.getRequest(), + ctx.getResponse(), + (err: any, result: any) => { + return resolve(err); + }, + "setActionItemMessage" as any + ) ); }); expect(result.message).toEqual("bad request"); @@ -333,7 +341,7 @@ describe("AlterActions", () => { actions = alterActions.transform(actions); const info: FormioActionInfo = await new Promise((resolve) => { - actions.custom.info(ctx.getRequest(), ctx.getResponse(), (err, info) => resolve(info)); + ctx.runInContext(() => actions.custom.info(ctx.getRequest(), ctx.getResponse(), (err, info) => resolve(info))); }); const settings: FormioComponent[] = await new Promise((resolve) => { @@ -343,13 +351,15 @@ describe("AlterActions", () => { const instance = new actions.custom(info as any, ctx.getRequest(), ctx.getResponse()); new Promise((resolve) => { - instance.resolve( - "handler", - "method", - ctx.getRequest(), - ctx.getResponse(), - (err: any, result: any) => resolve(result), - "setActionItemMessage" as any + ctx.runInContext(() => + instance.resolve( + "handler", + "method", + ctx.getRequest(), + ctx.getResponse(), + (err: any, result: any) => resolve(result), + "setActionItemMessage" as any + ) ); }); diff --git a/packages/third-parties/formio/src/components/AlterActions.ts b/packages/third-parties/formio/src/components/AlterActions.ts index 993d2e4ff97..bed137efe53 100644 --- a/packages/third-parties/formio/src/components/AlterActions.ts +++ b/packages/third-parties/formio/src/components/AlterActions.ts @@ -1,10 +1,10 @@ -import {Inject, InjectorService, Provider, runInContext} from "@tsed/di"; -import {EndpointMetadata} from "@tsed/schema"; +import {PlatformContext, setResponseHeaders} from "@tsed/common"; +import {AnyToPromise, AnyToPromiseStatus} from "@tsed/core"; +import {getContext, Inject, InjectorService, Provider} from "@tsed/di"; import {FormioActionInfo} from "@tsed/formio-types"; import {PlatformParams} from "@tsed/platform-params"; import {PlatformResponseFilter} from "@tsed/platform-response-filter"; -import {AnyToPromise, AnyToPromiseStatus} from "@tsed/core"; -import {PlatformContext, setResponseHeaders} from "@tsed/common"; +import {EndpointMetadata} from "@tsed/schema"; import {Alter} from "../decorators/alter"; import {AlterHook} from "../domain/AlterHook"; import {SetActionItemMessage} from "../domain/FormioAction"; @@ -65,7 +65,7 @@ export class AlterActions implements AlterHook { } protected createHandler(provider: Provider, propertyKey: string | symbol) { - const promisedHandler = this.params.compileHandler({ + const compiledHandler = this.params.compileHandler({ token: provider.token, propertyKey }); @@ -74,52 +74,59 @@ export class AlterActions implements AlterHook { action: any, handler: string, method: string, - req: {$ctx: PlatformContext}, + req: any, res: any, next: any, setActionItemMessage: SetActionItemMessage ) => { - const $ctx = req.$ctx; - $ctx.set("ACTION_CTX", {handler, method, setActionItemMessage, action}); - $ctx.endpoint = EndpointMetadata.get(provider.useClass, "resolve"); - - await runInContext($ctx, async () => { - try { - const resolver = new AnyToPromise(); - const handler = await promisedHandler; - const {state, data, status, headers} = await resolver.call(() => handler({$ctx})); - if (state === AnyToPromiseStatus.RESOLVED) { - if (status) { - $ctx.response.status(status); - } - - if (headers) { - $ctx.response.setHeaders(headers); - } + const $ctx = getContext(); - if (data !== undefined) { - $ctx.data = data; - - return await this.flush($ctx.data, $ctx); - } + if ($ctx) { + $ctx.set("ACTION_CTX", {handler, method, setActionItemMessage, action}); + $ctx.endpoint = EndpointMetadata.get(provider.useClass, "resolve"); + try { + if (await this.onRequest(compiledHandler, $ctx)) { next(); } } catch (er) { next(er); } - }); + } }; } + private async onRequest(handler: any, $ctx: PlatformContext) { + const resolver = new AnyToPromise(); + const {state, data, status, headers} = await resolver.call(() => handler({$ctx})); + + if (state === AnyToPromiseStatus.RESOLVED) { + if (status) { + $ctx.response.status(status); + } + + if (headers) { + $ctx.response.setHeaders(headers); + } + + if (data !== undefined) { + $ctx.data = data; + + return await this.flush($ctx.data, $ctx); + } + + return true; + } + } + private async flush(data: any, $ctx: PlatformContext) { const {response} = $ctx; if (!response.isDone()) { setResponseHeaders($ctx); - data = await this.responseFilter.serialize(data, $ctx); - data = await this.responseFilter.transform(data, $ctx); + data = await this.responseFilter.serialize(data, $ctx as any); + data = await this.responseFilter.transform(data, $ctx as any); response.body(data); } diff --git a/test/helper/createFakeHandlerContext.ts b/test/helper/createFakeHandlerContext.ts index 93b2fec5a64..2475b203b33 100644 --- a/test/helper/createFakeHandlerContext.ts +++ b/test/helper/createFakeHandlerContext.ts @@ -3,7 +3,5 @@ import {AnyToPromiseWithCtx, PlatformTest} from "@tsed/common"; export function createFakeHandlerContext() { const $ctx = PlatformTest.createRequestContext(); - return new AnyToPromiseWithCtx({ - $ctx - }); + return new AnyToPromiseWithCtx($ctx); } diff --git a/yarn.lock b/yarn.lock index 52dabc3537f..8ec8c07bb74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3106,12 +3106,12 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.4.tgz#d8c7b8db9226d2d7664553a0741ad7d0397ee503" integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg== -"@prisma/client@^3.10.0": - version "3.15.2" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.15.2.tgz#2181398147afc79bfe0d83c03a88dc45b49bd365" - integrity sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w== +"@prisma/client@^4.0.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.1.0.tgz#7c5341b2276c083821e432536ff97d8cc8f8b936" + integrity sha512-MvfPGAd42vHTiCYxwS6N+2U3F+ukoJ48D2QRnX1zSPJHBkh1CBtshl75daKzvVfgQwSouzSQeugKDej5di+E/g== dependencies: - "@prisma/engines-version" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" + "@prisma/engines-version" "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8" "@prisma/debug@3.15.2": version "3.15.2" @@ -3149,10 +3149,10 @@ strip-ansi "6.0.1" undici "5.5.1" -"@prisma/engines-version@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e": - version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#bf5e2373ca68ce7556b967cb4965a7095e93fe53" - integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w== +"@prisma/engines-version@4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8": + version "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8.tgz#ce00e6377126e491a8b1e0e2039c97e2924bd6d9" + integrity sha512-cRRJwpHFGFJZvtHbY3GZjMffNBEjjZk68ztn+S2hDgPCGB4H66IK26roK94GJxBodSehwRJ0wGyebC2GoIH1JQ== "@prisma/engines@3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11": version "3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11" @@ -3617,73 +3617,6 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== -"@tsed/common@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/common/-/common-6.120.0.tgz#67f68e7416f3202c711c4b9c0c76f7beb9bae61e" - integrity sha512-4hFOwJWRW5Iphqk+zVKhW14BJ+XGGCOiUYxCc7jLzL5J9Hi6kIJBcsMTQiUntwLAhNkIhOR0ySZOUiAsmJGjew== - dependencies: - "@tsed/components-scan" "6.120.0" - "@tsed/core" "6.120.0" - "@tsed/di" "6.120.0" - "@tsed/exceptions" "6.120.0" - "@tsed/json-mapper" "6.120.0" - "@tsed/logger" ">=6.2.0" - "@tsed/logger-file" ">=6.2.0" - "@tsed/perf" "6.120.0" - "@tsed/platform-cache" "6.120.0" - "@tsed/platform-exceptions" "6.120.0" - "@tsed/platform-log-middleware" "6.120.0" - "@tsed/platform-middlewares" "6.120.0" - "@tsed/platform-params" "6.120.0" - "@tsed/platform-response-filter" "6.120.0" - "@tsed/platform-views" "6.120.0" - "@tsed/schema" "6.120.0" - "@types/json-schema" "7.0.11" - "@types/on-finished" "2.3.1" - on-finished "2.4.1" - tslib "2.4.0" - uuid "8.3.2" - -"@tsed/components-scan@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/components-scan/-/components-scan-6.120.0.tgz#98c77ecd6abe0fc9dc1e9e6d49c7d00bfb335331" - integrity sha512-fILXxCp5FMLVe8/Lfg4fH/WIXTCGZKbqRdPRWeIt2H49J5+BunmSWslAnY8i/rm0wJNd5GB40CDLfmXGayBfFw== - dependencies: - globby "11.0.3" - normalize-path "3.0.0" - tslib "2.4.0" - -"@tsed/core@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/core/-/core-6.120.0.tgz#3e392f22c1a16777c03960fcaba2aba2a4281f64" - integrity sha512-N+NdAbsCWRUqSAIMdAQzy5p9zFIjuyYTB4eLwVYSCjAG2gWrhzUFziGbbooUg6/lWPPzJamYxWYYwORPX+TOgA== - dependencies: - reflect-metadata "^0.1.13" - tslib "2.4.0" - -"@tsed/di@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/di/-/di-6.120.0.tgz#cbdecea6b863831a8b5a4a1863efc6ae008e96a0" - integrity sha512-XkqvdXnpU4SuuszQAeAeIXe0zEt7O/oQ8MPM761/yqXhLYNOy3j5PY8FN1fjV2g/yzw7LGNS/WmgJBrNI6gQQQ== - dependencies: - tslib "2.4.0" - -"@tsed/exceptions@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/exceptions/-/exceptions-6.120.0.tgz#05045b497532c0074945c9fad66be1ea715a75c0" - integrity sha512-3Z/lvHvqEBMRHkh2Ti7oclieUYAMiILxZBSy15UlRodnku2RodJP9w7afdSt3M/ayFJW10FsNEkfy7J493QktQ== - dependencies: - change-case "4.1.2" - statuses ">=2.0.1" - tslib "2.4.0" - -"@tsed/json-mapper@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/json-mapper/-/json-mapper-6.120.0.tgz#3506d34359c67d6dcaf98c9b970c3ebbd45fa295" - integrity sha512-I8fUvoKQQmb92ceyyYM0iOWQV/c+0rBHtwbMQbqNu9SUcgvIkdtHC73LvLYghmxSC5H0IUSg/51WzIXIm0ANug== - dependencies: - tslib "2.4.0" - "@tsed/logger-file@>=6.2.0": version "6.2.0" resolved "https://registry.yarnpkg.com/@tsed/logger-file/-/logger-file-6.2.0.tgz#d0e56f5fc658b81f0b8123197530c987fb445978" @@ -3725,107 +3658,6 @@ read-package-json "2.1.2" semver "7.3.2" -"@tsed/openspec@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/openspec/-/openspec-6.120.0.tgz#ec2088d64470c506f090a211570f3f3d5320586a" - integrity sha512-uEaXdcIFJ2wEnZr6jDpwwaAEWfzLjdMrNLEXTVcsWR1l5tRBV45RBTCBnKx08Dwfo9A1dpe6eJZ4uym8dBlM/A== - -"@tsed/perf@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/perf/-/perf-6.120.0.tgz#062ed896893613ad5e4b4cc1cf27633ad04f1a91" - integrity sha512-Pff07N8C6ySEQiQlB0+jPoE/cMqXlCt0ILgDhtnrwE/eR5w45IhMpGLUuttD2lJ6QlKaJ7xtvLxxJWVR2Z7ZSw== - dependencies: - "@tsed/core" "6.120.0" - chalk "^4.1.0" - tslib "2.4.0" - -"@tsed/platform-cache@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/platform-cache/-/platform-cache-6.120.0.tgz#7b3229cabb928a6b000ad02fef7bf55f8f0e7f22" - integrity sha512-TtNLdoybCiSPfqSbnO7Gq/cuzSOck5taRj7UKlpMZUwFdJ1V6JcghvIbUKjdDT5yTgLCUpZ50Uz45hmt5Q0p+Q== - dependencies: - "@types/cache-manager" "^3.4.3" - cache-manager "^3.6.0" - micromatch "4.0.5" - tslib "2.4.0" - -"@tsed/platform-exceptions@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/platform-exceptions/-/platform-exceptions-6.120.0.tgz#dd777696791aad29f69c16d13ad6663e9a8fd76b" - integrity sha512-dy4yj1zhtUoZP8PxzLEF6Cc4cIBp3NX8bXdgRv3m8Uz2HOqbVvE0L/xVzskcbtmQNcvLfbiT6m4tFy+xmsh2ZA== - dependencies: - tslib "2.4.0" - -"@tsed/platform-express@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/platform-express/-/platform-express-6.120.0.tgz#b26362f3f7aece2506b588664791a88b0c6a5bd8" - integrity sha512-VEY0Vtb4BhyniaI6VxS5Lmjj4bU1ih6JRiOm9u6I8TXugZcU3S7+f2k9tT9hJUbSVpZOXr+2/cit9JA+ilGfFw== - dependencies: - express "^4.18.1" - multer "^1.4.5-lts.1" - tslib "2.4.0" - -"@tsed/platform-log-middleware@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/platform-log-middleware/-/platform-log-middleware-6.120.0.tgz#fd1d9501fc2aad70edfb52f844aa1a84e5b5fc19" - integrity sha512-niD9uTan29WJBWmW40hUOuoPUIecYNnTDk2Ocjhp7i+/2qTNR3M/FzUf/BjKxuQFRd8B/pzRgsbU2AxGL8sw3Q== - dependencies: - tslib "2.4.0" - -"@tsed/platform-middlewares@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/platform-middlewares/-/platform-middlewares-6.120.0.tgz#9c0ea5389416a4c603156a74ead317b81565eb4e" - integrity sha512-h8Qd4cX3uXH8HaQzJ7Le+1sT8CplC+OgxTscOAHE0Wynw/E33JT+e4jgKXuqUBXA9WeMdDLEn4Q2odkxRmHrkw== - dependencies: - tslib "2.4.0" - -"@tsed/platform-params@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/platform-params/-/platform-params-6.120.0.tgz#d1a34c2542520f6627eb18aa2ab51ab43cbede5b" - integrity sha512-9vL8xSMNAx8HD2OD/Inl+sASWPFEK069CtiPfvnua+hYrdRytalm5W7jvCXwGQkH6glur1y2cH6gstNhwksAcA== - dependencies: - tslib "2.4.0" - -"@tsed/platform-response-filter@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/platform-response-filter/-/platform-response-filter-6.120.0.tgz#811492704fd6a359936f23ce68ee85727dcc50fd" - integrity sha512-T84QH+n3T3d9UaYQQZNQZWa4k3r3kPshBb+5HjBVA0XgclcJzy0AJNgjfkOiolIL9/juOOn9IXQ9tOLA0ikFBw== - dependencies: - tslib "2.4.0" - -"@tsed/platform-views@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/platform-views/-/platform-views-6.120.0.tgz#1d388ddf814f2d58505e77aa50ebe7759c715964" - integrity sha512-3hRysFYccO2s1VQwCVr8Mu3bMLb9VvgRuH/5WzOuJAnvXGhudGZCUsMqaLkevoXkJ12K+avoFBrfx8qJmtitrg== - dependencies: - consolidate "^0.16.0" - ejs "^3.1.5" - tslib "2.4.0" - -"@tsed/schema@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/schema/-/schema-6.120.0.tgz#ed8337345ee3ae0444897ac2f32583782965eb29" - integrity sha512-Clv+0gLYlh1SL2RYvJED/zy7gJVge2RptinOZvqev3uU/dc5jBT1DMnL7AtHCYjxB3TR+4JEr15tPOl6M0SKpw== - dependencies: - "@tsed/openspec" "6.120.0" - change-case "^4.1.2" - fs-extra "^10.0.1" - json-schema "0.4.0" - micromatch "4.0.5" - statuses ">=2.0.1" - tslib "2.4.0" - -"@tsed/swagger@6.120.0": - version "6.120.0" - resolved "https://registry.yarnpkg.com/@tsed/swagger/-/swagger-6.120.0.tgz#a8642ba6b1af66b75bd125e20a6e6354ab5e102c" - integrity sha512-YrKZwVWm3uA988vZLBQuyrEwpm4jqHmQrolPOKSXPIsOVBunw66SSMkc9iZHzUkfevzE91bFdT2tf/8P55euPw== - dependencies: - "@tsed/openspec" "6.120.0" - fs-extra "10.0.1" - micromatch "4.0.5" - swagger-ui-dist "^4.5.2" - tslib "2.4.0" - "@tsed/ts-doc@4.0.14": version "4.0.14" resolved "https://registry.yarnpkg.com/@tsed/ts-doc/-/ts-doc-4.0.14.tgz#6f09a1b09962354c0a1e7847f475a3e35a191eed" @@ -8593,13 +8425,6 @@ consolidate@^0.15.1: dependencies: bluebird "^3.1.1" -consolidate@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" - integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ== - dependencies: - bluebird "^3.7.2" - constant-case@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-1.1.2.tgz#8ec2ca5ba343e00aa38dbf4e200fd5ac907efd63"