From bb75e80ea6be333b63d81fb13830cbb413b86922 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 7 Dec 2024 14:40:29 +0100 Subject: [PATCH 1/2] feat(platform-fastify): add Fastify platform --- packages/platform/platform-fastify/.npmignore | 7 + packages/platform/platform-fastify/.swcrc | 21 + .../platform/platform-fastify/package.json | 122 ++++ packages/platform/platform-fastify/readme.md | 67 +++ .../src/components/PlatformFastify.spec.ts | 34 ++ .../src/components/PlatformFastify.ts | 361 +++++++++++ .../platform/platform-fastify/src/index.ts | 8 + .../src/interfaces/PlatformFastifySettings.ts | 16 + .../src/interfaces/interfaces.ts | 26 + .../services/PlatformFastifyRequest.spec.ts | 54 ++ .../src/services/PlatformFastifyRequest.ts | 59 ++ .../services/PlatformFastifyResponse.spec.ts | 114 ++++ .../src/services/PlatformFastifyResponse.ts | 100 ++++ .../platform-fastify/test/app/Server.ts | 47 ++ .../platform-fastify/test/app/index.ts | 47 ++ .../test/app/public/index.html | 5 + .../platform-fastify/test/app/views/view.ejs | 1 + .../platform-fastify/test/health.spec.ts | 51 ++ .../test/platform-fastify.spec.ts | 100 ++++ .../test/test-auth.integration.spec.ts | 345 +++++++++++ .../test/test-session.integration.spec.ts | 117 ++++ .../platform-fastify/tsconfig.esm.json | 26 + .../platform/platform-fastify/tsconfig.json | 40 ++ .../platform-fastify/tsconfig.spec.json | 35 ++ .../platform-fastify/vitest.config.mts | 21 + .../src/common/services/PlatformHandler.ts | 2 +- .../domain/PlatformHandlerMetadata.spec.ts | 6 + .../src/domain/PlatformHandlerMetadata.ts | 4 + .../platform-test-sdk/src/tests/testView.ts | 2 +- .../scalar/src/middlewares/indexMiddleware.ts | 5 +- .../src/middlewares/indexMiddleware.ts | 6 +- tsconfig.json | 3 + yarn.lock | 558 +++++++++++++++++- 33 files changed, 2389 insertions(+), 21 deletions(-) create mode 100644 packages/platform/platform-fastify/.npmignore create mode 100644 packages/platform/platform-fastify/.swcrc create mode 100644 packages/platform/platform-fastify/package.json create mode 100644 packages/platform/platform-fastify/readme.md create mode 100644 packages/platform/platform-fastify/src/components/PlatformFastify.spec.ts create mode 100644 packages/platform/platform-fastify/src/components/PlatformFastify.ts create mode 100644 packages/platform/platform-fastify/src/index.ts create mode 100644 packages/platform/platform-fastify/src/interfaces/PlatformFastifySettings.ts create mode 100644 packages/platform/platform-fastify/src/interfaces/interfaces.ts create mode 100644 packages/platform/platform-fastify/src/services/PlatformFastifyRequest.spec.ts create mode 100644 packages/platform/platform-fastify/src/services/PlatformFastifyRequest.ts create mode 100644 packages/platform/platform-fastify/src/services/PlatformFastifyResponse.spec.ts create mode 100644 packages/platform/platform-fastify/src/services/PlatformFastifyResponse.ts create mode 100644 packages/platform/platform-fastify/test/app/Server.ts create mode 100644 packages/platform/platform-fastify/test/app/index.ts create mode 100644 packages/platform/platform-fastify/test/app/public/index.html create mode 100644 packages/platform/platform-fastify/test/app/views/view.ejs create mode 100644 packages/platform/platform-fastify/test/health.spec.ts create mode 100644 packages/platform/platform-fastify/test/platform-fastify.spec.ts create mode 100644 packages/platform/platform-fastify/test/test-auth.integration.spec.ts create mode 100644 packages/platform/platform-fastify/test/test-session.integration.spec.ts create mode 100644 packages/platform/platform-fastify/tsconfig.esm.json create mode 100644 packages/platform/platform-fastify/tsconfig.json create mode 100644 packages/platform/platform-fastify/tsconfig.spec.json create mode 100644 packages/platform/platform-fastify/vitest.config.mts diff --git a/packages/platform/platform-fastify/.npmignore b/packages/platform/platform-fastify/.npmignore new file mode 100644 index 00000000000..0887e21d43e --- /dev/null +++ b/packages/platform/platform-fastify/.npmignore @@ -0,0 +1,7 @@ +test +coverage +tsconfig.json +tsconfig.*.json +__mock__ +*.spec.js +*.tsbuildinfo diff --git a/packages/platform/platform-fastify/.swcrc b/packages/platform/platform-fastify/.swcrc new file mode 100644 index 00000000000..42a1b2d3da0 --- /dev/null +++ b/packages/platform/platform-fastify/.swcrc @@ -0,0 +1,21 @@ +{ + "sourceMaps": false, + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "target": "es2022", + "externalHelpers": true, + "keepClassNames": true, + "transform": { + "useDefineForClassFields": false, + "legacyDecorator": true, + "decoratorMetadata": true + } + }, + "module": { + "type": "es6" + } +} diff --git a/packages/platform/platform-fastify/package.json b/packages/platform/platform-fastify/package.json new file mode 100644 index 00000000000..72e789f2787 --- /dev/null +++ b/packages/platform/platform-fastify/package.json @@ -0,0 +1,122 @@ +{ + "name": "@tsed/platform-fastify", + "type": "module", + "version": "8.2.0", + "description": "Fastify package for Ts.ED framework", + "source": "./src/index.ts", + "main": "./lib/esm/index.js", + "module": "./lib/esm/index.js", + "typings": "./lib/types/index.d.ts", + "exports": { + ".": { + "@tsed/source": "./src/index.ts", + "types": "./lib/types/index.d.ts", + "import": "./lib/esm/index.js", + "default": "./lib/esm/index.js" + } + }, + "scripts": { + "build": "yarn barrels && yarn build:ts", + "barrels": "barrels", + "start": "node --import @swc-node/register/esm-register test/app/index.ts", + "test": "vitest run -u", + "build:ts": "tsc --build tsconfig.json", + "test:ci": "vitest run --coverage.thresholds.autoUpdate=true" + }, + "keywords": [ + "Koa", + "TypeScript", + "typescript", + "Decorator", + "decorators", + "decorator", + "fastify", + "Controller", + "Inject", + "ioc", + "di", + "mvc", + "swagger", + "swagger ui", + "ES2015", + "ES6", + "server", + "rest", + "api", + "validation" + ], + "author": { + "name": "Romain Lenzotti" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/tsedio/tsed/issues" + }, + "homepage": "http://tsed.dev/", + "repository": { + "type": "git", + "url": "git+https://github.com/tsedio/tsed.git" + }, + "dependencies": { + "@fastify/middie": ">=9.0.2", + "@fastify/static": ">=8.0.4", + "content-disposition": ">=0.5.4", + "tslib": "2.6.1" + }, + "devDependencies": { + "@fastify/accepts": "5.0.2", + "@fastify/cookie": "11.0.1", + "@fastify/formbody": "8.0.1", + "@fastify/session": "11.0.1", + "@tsed/barrels": "workspace:*", + "@tsed/core": "workspace:*", + "@tsed/di": "workspace:*", + "@tsed/platform-http": "workspace:*", + "@tsed/platform-test-sdk": "workspace:*", + "@types/content-disposition": "^0.5.4", + "cross-env": "7.0.3", + "fastify": "5.2.1", + "fastify-raw-body": "5.0.0", + "ts-node": "10.9.2" + }, + "peerDependencies": { + "@fastify/accepts": ">=4.3.0", + "@fastify/cookie": ">=9.3.1", + "@fastify/formbody": ">=7.4.0", + "@tsed/core": ">=8.0.0", + "@tsed/di": ">=8.0.0", + "@tsed/json-mapper": ">=8.0.0", + "@tsed/logger": ">=7.0.0", + "@tsed/openspec": ">=8.0.0", + "@tsed/platform-http": ">=8.0.0", + "@tsed/schema": ">=8.0.0", + "fastify": ">=5.1.0", + "fastify-raw-body": ">=5.0.0" + }, + "peerDependenciesMeta": { + "@tsed/common": { + "optional": false + }, + "@tsed/core": { + "optional": false + }, + "@tsed/di": { + "optional": false + }, + "@tsed/json-mapper": { + "optional": false + }, + "@tsed/logger": { + "optional": false + }, + "@tsed/openspec": { + "optional": false + }, + "@tsed/schema": { + "optional": false + }, + "fastify": { + "optional": false + } + } +} diff --git a/packages/platform/platform-fastify/readme.md b/packages/platform/platform-fastify/readme.md new file mode 100644 index 00000000000..4b625918f06 --- /dev/null +++ b/packages/platform/platform-fastify/readme.md @@ -0,0 +1,67 @@ +

+ Ts.ED logo +

+ +
+

Platform Fastify

+ +[![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/getting-started/ + +## Installation + +Run npm command (or yarn): + +```bash +npm install --save @tsed/platform-fastify fastify +``` + +## 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-fastify/src/components/PlatformFastify.spec.ts b/packages/platform/platform-fastify/src/components/PlatformFastify.spec.ts new file mode 100644 index 00000000000..402d58e6342 --- /dev/null +++ b/packages/platform/platform-fastify/src/components/PlatformFastify.spec.ts @@ -0,0 +1,34 @@ +import {PlatformBuilder} from "@tsed/platform-http"; + +import {PlatformFastify} from "./PlatformFastify.js"; + +class Server {} + +describe("PlatformFastify", () => { + describe("create()", () => { + beforeEach(() => { + vi.spyOn(PlatformBuilder, "create").mockReturnValue({}); + }); + afterEach(() => vi.resetAllMocks()); + it("should create platform", () => { + PlatformFastify.create(Server, {}); + + expect(PlatformBuilder.create).toHaveBeenCalledWith(Server, { + adapter: PlatformFastify + }); + }); + }); + describe("bootstrap()", () => { + beforeEach(() => { + vi.spyOn(PlatformBuilder, "bootstrap").mockReturnValue({}); + }); + afterEach(() => vi.resetAllMocks()); + it("should create platform", async () => { + await PlatformFastify.bootstrap(Server, {}); + + expect(PlatformBuilder.bootstrap).toHaveBeenCalledWith(Server, { + adapter: PlatformFastify + }); + }); + }); +}); diff --git a/packages/platform/platform-fastify/src/components/PlatformFastify.ts b/packages/platform/platform-fastify/src/components/PlatformFastify.ts new file mode 100644 index 00000000000..8b82b07d93a --- /dev/null +++ b/packages/platform/platform-fastify/src/components/PlatformFastify.ts @@ -0,0 +1,361 @@ +import * as Http from "node:http"; +import * as Https from "node:https"; + +import fastifyMiddie from "@fastify/middie"; +import fastifyStatics from "@fastify/static"; +import {type Env, isFunction, isString, ReturnHostInfoFromPort, Type} from "@tsed/core"; +import {constant, inject, logger, runInContext} from "@tsed/di"; +import {NotFound} from "@tsed/exceptions"; +import {PlatformExceptions} from "@tsed/platform-exceptions"; +import { + adapter, + createContext, + createServer, + Platform, + PlatformAdapter, + PlatformApplication, + PlatformBuilder, + PlatformHandler, + PlatformMulterSettings, + PlatformRequest, + PlatformResponse, + PlatformStaticsOptions +} from "@tsed/platform-http"; +import {PlatformHandlerMetadata, PlatformHandlerType, PlatformLayer} from "@tsed/platform-router"; +import Fastify, {FastifyInstance, FastifyReply, FastifyRequest} from "fastify"; +import {IncomingMessage, ServerResponse} from "http"; + +import type {PlatformFastifyPluginLoadingOptions, PlatformFastifyPluginSettings} from "../interfaces/interfaces.js"; +import type {PlatformFastifySettings} from "../interfaces/PlatformFastifySettings.js"; +import {PlatformFastifyRequest} from "../services/PlatformFastifyRequest.js"; +import {PlatformFastifyResponse} from "../services/PlatformFastifyResponse.js"; + +declare global { + namespace TsED { + export interface Application extends FastifyInstance {} + } + + namespace TsED {} +} + +export class PlatformFastify extends PlatformAdapter { + readonly NAME = "fastify"; + + readonly providers = [ + { + token: PlatformHandler + }, + {token: PlatformApplication}, + {token: Platform}, + { + token: PlatformResponse, + useClass: PlatformFastifyResponse + }, + { + token: PlatformRequest, + useClass: PlatformFastifyRequest + } + ]; + private decorated: boolean = false; + + /** + * Create new serverless application. In this mode, the component scan are disabled. + * @param module + * @param settings + */ + static create(module: Type, settings: Partial = {}) { + return PlatformBuilder.create(module, { + ...settings, + adapter: PlatformFastify + }); + } + + /** + * Bootstrap a server application + * @param module + * @param settings + */ + static bootstrap(module: Type, settings: Partial = {}) { + return PlatformBuilder.bootstrap(module, { + ...settings, + adapter: PlatformFastify + }); + } + + mapLayers(layers: PlatformLayer[]) { + const {app} = this; + const rawApp: FastifyInstance = app.getApp(); + + layers.forEach((layer) => { + switch (layer.method) { + case "use": + if ((rawApp as any).use) { + (rawApp as any).use(...layer.getArgs()); + } + return; + case "statics": + this.statics(layer.path as string, layer.opts as any); + + // rawApp.register(); + return; + } + try { + rawApp.route({ + method: layer.method.toUpperCase() as any, + url: layer.path as any, + handler: this.compose(layer), + config: { + rawBody: layer.handlers.some((handler) => handler.opts?.paramsTypes?.RAW_BODY) + } + }); + } catch (er) { + logger().warn({ + error_name: er.code, + error_message: er.message + }); + } + }); + } + + mapHandler(handler: (...args: any[]) => any, metadata: PlatformHandlerMetadata) { + if (metadata.isRawMiddleware()) { + return handler; + } + + switch (metadata.type) { + case PlatformHandlerType.MIDDLEWARE: + return (request: IncomingMessage, _: ServerResponse, done: (err?: any) => void) => { + const {$ctx} = request; + + $ctx.next = done; + + return runInContext($ctx, () => handler($ctx)); + }; + + default: + return async (request: FastifyRequest, _: FastifyReply, done: (err?: any) => void) => { + const {$ctx} = request; + + $ctx.next = done; + + await runInContext($ctx, () => handler($ctx)); + + if (metadata.type === PlatformHandlerType.CTX_FN) { + done(); + } + }; + } + } + + async useContext() { + const {app} = this; + const invoke = createContext(); + const rawApp: FastifyInstance = app.getApp(); + + rawApp.addHook("onRequest", async (request, reply) => { + const $ctx = invoke({ + request: request as any, + response: reply as any + }); + + ($ctx.request.getReq() as any).$ctx = $ctx; + + await $ctx.start(); + }); + + rawApp.addHook("onResponse", async (request, reply) => { + const {$ctx} = request; + ($ctx.request.getReq() as any).$ctx = undefined; + await $ctx.finish(); + }); + + await rawApp.register(fastifyMiddie, { + hook: "onRequest" + }); + + const plugins = await this.resolvePlugins(); + + for (const plugin of plugins) { + // console.log("register", (plugin.use as any)[Symbol.for("plugin-meta")].name, plugin.options) + await rawApp.register(plugin.use as any, plugin.options!); + } + } + + createApp() { + const {app, ...props} = constant("fastify") || {}; + const httpPort = constant("httpPort"); + const httpOptions = constant("httpOptions"); + const httpsPort = constant("httpsPort"); + const httpsOptions = constant("httpsOptions"); + const opts = { + ...props, + ignoreTrailingSlash: true, + http: + httpPort !== false + ? { + ...httpOptions + } + : null, + https: + httpsPort !== false + ? { + ...httpsOptions + } + : null + }; + + const instance: FastifyInstance = app || Fastify(opts); + + instance.decorateRequest("$ctx", null as never); + instance.decorateReply("locals", null); + + return { + app: instance, + callback: () => { + return async (request: IncomingMessage, response: ServerResponse) => { + await instance.ready(); + instance.server.emit("request", request, response); + }; + } + }; + } + + afterLoadRoutes() { + const rawApp: FastifyInstance = this.app.getApp(); + + rawApp.setErrorHandler((error, request, reply) => { + const {$ctx} = request; + $ctx.error = $ctx.error || error; + + return inject(PlatformExceptions)?.catch(error, $ctx); + }); + + rawApp.setNotFoundHandler((request, reply) => { + const {$ctx} = request; + + return inject(PlatformExceptions)?.catch(new NotFound(`Resource "${request.originalUrl}" not found`), $ctx); + }); + + return Promise.resolve(); + } + + getServers(): any[] { + const httpsPort = constant("httpsPort"); + const httpPort = constant("httpPort"); + + const listen = (hostinfo: ReturnHostInfoFromPort) => + this.app.getApp().listen({ + host: hostinfo.address, + port: hostinfo.port + }); + + const server = () => this.app.getApp().server; + + return [ + createServer({ + port: httpsPort, + type: "https", + token: Https.Server, + server, + listen + }), + createServer({ + port: httpPort, + type: "http", + token: Http.Server, + server, + listen + }) + ]; + } + + bodyParser(type: string, opts: Record | undefined): any { + return null; + } + + multipart(options: PlatformMulterSettings): any { + logger().warn({ + event: "PLATFORM_UNSUPPORTED_FEATURE", + message: "Multipart method is not implemented yet", + options + }); + return null; + } + + statics(endpoint: string, options: PlatformStaticsOptions): any { + this.app.getApp().register(fastifyStatics, { + root: options.root, + prefix: endpoint, + decorateReply: !this.decorated + }); + this.decorated = true; + + return null; + } + + protected compose(layer: PlatformLayer) { + return (req: FastifyRequest, reply: FastifyReply) => { + return runInContext(req.$ctx, async () => { + const $ctx = req.$ctx; + $ctx.next = null; + + for (const metadata of layer.handlers) { + try { + if (!req.$ctx.isDone()) { + if ( + ($ctx.error && metadata.type === PlatformHandlerType.ERR_MIDDLEWARE) || + (!$ctx.error && metadata.type !== PlatformHandlerType.ERR_MIDDLEWARE) + ) { + await metadata.compiledHandler($ctx); + } + } + } catch (er) { + $ctx.error = er; + } + } + + if (req.$ctx.error) { + // TODO maybe we can use platform exception here? + throw req.$ctx.error; + } + }); + }; + } + + protected async resolvePlugins(): Promise { + let plugins = constant("plugins", []); + const env = constant("env"); + + const promises = plugins.map(async (plugin: PlatformFastifyPluginSettings): Promise => { + if (isFunction(plugin)) { + return { + env, + use: plugin + }; + } + + if (isString(plugin)) { + plugin = {env, use: plugin}; + } + + let {use} = plugin; + + if (isString(use)) { + const mod = await import(use); + use = mod.default || mod; + } + + return { + env, + ...plugin, + use + }; + }); + + plugins = await Promise.all(promises); + + return plugins.filter((middleware) => middleware.use).filter((middleware) => middleware.env === env); + } +} + +adapter(PlatformFastify); diff --git a/packages/platform/platform-fastify/src/index.ts b/packages/platform/platform-fastify/src/index.ts new file mode 100644 index 00000000000..5c00513b467 --- /dev/null +++ b/packages/platform/platform-fastify/src/index.ts @@ -0,0 +1,8 @@ +/** + * @file Automatically generated by @tsed/barrels. + */ +export * from "./components/PlatformFastify.js"; +export * from "./interfaces/interfaces.js"; +export * from "./interfaces/PlatformFastifySettings.js"; +export * from "./services/PlatformFastifyRequest.js"; +export * from "./services/PlatformFastifyResponse.js"; diff --git a/packages/platform/platform-fastify/src/interfaces/PlatformFastifySettings.ts b/packages/platform/platform-fastify/src/interfaces/PlatformFastifySettings.ts new file mode 100644 index 00000000000..4b38afee0de --- /dev/null +++ b/packages/platform/platform-fastify/src/interfaces/PlatformFastifySettings.ts @@ -0,0 +1,16 @@ +// import type {RouterOptions} from "@koa/router"; + +import type {FastifyInstance} from "fastify"; + +export interface PlatformFastifySettings { + app?: FastifyInstance; + // /** + // * Koa router options + // */ + // router?: RouterOptions; + // /** + // * Body parser options + // * @param opts + // */ + // bodyParser?: (opts?: Options) => Middleware | Options; +} diff --git a/packages/platform/platform-fastify/src/interfaces/interfaces.ts b/packages/platform/platform-fastify/src/interfaces/interfaces.ts new file mode 100644 index 00000000000..3117f96518d --- /dev/null +++ b/packages/platform/platform-fastify/src/interfaces/interfaces.ts @@ -0,0 +1,26 @@ +import type {Env} from "@tsed/core"; +import type {FastifyPluginOptions, FastifyRegisterOptions} from "fastify"; + +import type {PlatformFastifySettings} from "./PlatformFastifySettings.js"; + +export * from "./PlatformFastifySettings.js"; + +export type PlatformFastifyPluginTypes = ((opts?: any) => any) | /*Type |*/ string; +export type PlatformFastifyPluginLoadingOptions = { + env?: Env; + use: PlatformFastifyPluginTypes; + options?: FastifyRegisterOptions; +}; +export type PlatformFastifyPluginSettings = PlatformFastifyPluginTypes | PlatformFastifyPluginLoadingOptions | any; + +declare global { + namespace TsED { + export interface Configuration { + /** + * Configuration related to Fastify platform application. + */ + fastify: PlatformFastifySettings; + plugins: PlatformFastifyPluginSettings[]; + } + } +} diff --git a/packages/platform/platform-fastify/src/services/PlatformFastifyRequest.spec.ts b/packages/platform/platform-fastify/src/services/PlatformFastifyRequest.spec.ts new file mode 100644 index 00000000000..bc3273234d6 --- /dev/null +++ b/packages/platform/platform-fastify/src/services/PlatformFastifyRequest.spec.ts @@ -0,0 +1,54 @@ +import {PlatformTest} from "@tsed/platform-http/testing"; + +import {PlatformFastifyRequest} from "./PlatformFastifyRequest.js"; + +function createRequest() { + const request = PlatformTest.createRequest(); + const ctx = PlatformTest.createRequestContext({ + event: { + request + }, + RequestKlass: PlatformFastifyRequest + }); + + return {req: request, request: ctx.request}; +} + +describe("PlatformFastifyRequest", () => { + beforeEach(() => PlatformTest.create()); + afterEach(() => PlatformTest.reset()); + + it("should create a PlatformRequest instance", () => { + const {req, request} = createRequest(); + + expect(request.raw).toEqual(req); + }); + + describe("secure", () => { + it("should get secure info", () => { + const {req, request} = createRequest(); + req.protocol = "https"; + + expect(request.secure).toEqual(true); + }); + }); + + describe("host()", () => { + it("should return the host", () => { + const {req, request} = createRequest(); + + req.hostname = "host"; + + expect(request.host).toEqual("host"); + }); + }); + + describe("getReq()", () => { + it("should return nodejs request", () => { + const {req, request} = createRequest(); + req.raw = {}; + + expect(request.getReq()).toEqual({}); + }); + }); +}); diff --git a/packages/platform/platform-fastify/src/services/PlatformFastifyRequest.ts b/packages/platform/platform-fastify/src/services/PlatformFastifyRequest.ts new file mode 100644 index 00000000000..8d4b53ef3f5 --- /dev/null +++ b/packages/platform/platform-fastify/src/services/PlatformFastifyRequest.ts @@ -0,0 +1,59 @@ +import type {IncomingMessage} from "node:http"; + +import {type PlatformContext, PlatformRequest} from "@tsed/platform-http"; +import type {FastifyRequest} from "fastify"; + +declare module "fastify" { + export interface FastifyRequest { + $ctx: PlatformContext; + } +} + +declare module "http" { + export interface IncomingMessage { + $ctx: PlatformContext; + } +} + +declare global { + namespace TsED { + export interface Request extends FastifyRequest {} + } +} + +/** + * @platform + * @fastify + */ +export class PlatformFastifyRequest extends PlatformRequest { + get host(): string { + return this.raw.hostname; + } + + get secure(): boolean { + return this.protocol === "https"; + } + + /** + * Returns the HTTP request header specified by field. The match is case-insensitive. + * + * ```typescript + * request.get('Content-Type') // => "text/plain" + * ``` + * + * @param name + */ + get(name: string) { + return this.raw.raw.headers[name.toLowerCase()]; + } + + getReq(): IncomingMessage { + return this.raw.raw; + } + + accepts(mime?: string | string[]) { + const accepts = this.raw.accepts().type([].concat(mime as any)); + + return (accepts ? accepts : false) as any; + } +} diff --git a/packages/platform/platform-fastify/src/services/PlatformFastifyResponse.spec.ts b/packages/platform/platform-fastify/src/services/PlatformFastifyResponse.spec.ts new file mode 100644 index 00000000000..d2232c21353 --- /dev/null +++ b/packages/platform/platform-fastify/src/services/PlatformFastifyResponse.spec.ts @@ -0,0 +1,114 @@ +import {PlatformTest} from "@tsed/platform-http/testing"; + +import {PlatformFastifyResponse} from "./PlatformFastifyResponse.js"; + +function createResponse() { + const response = PlatformTest.createResponse(); + response.code = response.status; + response.type = response.contentType; + response.header = response.set; + + const ctx = PlatformTest.createRequestContext({ + event: { + response + }, + ResponseKlass: PlatformFastifyResponse + }); + + return {res: response, response: ctx.response}; +} + +describe("PlatformFastifyResponse", () => { + beforeEach(() => PlatformTest.create()); + afterEach(() => PlatformTest.reset()); + + it("should create a PlatformResponse instance", () => { + const {res, response} = createResponse(); + + expect(response.raw).toEqual(res); + }); + + describe("getRes()", () => { + it("return res", async () => { + const {res, response} = createResponse(); + res.raw = {}; + + const result = await response.getRes(); + + expect(result).toEqual(res.raw); + }); + }); + describe("statusCode", () => { + it("return statusCode", () => { + const {response} = createResponse(); + + response.status(302); + + expect(response.statusCode).toEqual(302); + }); + }); + describe("contentType()", () => { + it("should set contentType", () => { + const {res, response} = createResponse(); + + response.contentType("text/html"); + + expect(res.headers).toEqual({"content-type": "text/html", "x-request-id": "id"}); + }); + }); + describe("body()", () => { + it("should set body", () => { + const {res, response} = createResponse(); + + response.body("body"); + + expect(res.data).toEqual("body"); + }); + }); + + describe("stream()", () => { + it("should set body", () => { + const {res, response} = createResponse(); + + response.stream("body"); + + expect(res.data).toEqual("body"); + }); + }); + describe("location", () => { + it("should set header location", () => { + const {res, response} = createResponse(); + + response.location("https://location"); + + expect(res.headers).toEqual({ + location: "https://location", + "x-request-id": "id" + }); + }); + + it("should go back based on Referrer", async () => { + const {res, response} = createResponse(); + + response.request.raw.headers["referrer"] = "https://location/back"; + + await response.location("back"); + + expect(res.headers).toEqual({ + location: "https://location/back", + "x-request-id": "id" + }); + }); + + it("should go back based on default path", async () => { + const {res, response} = createResponse(); + + await response.location("back"); + + expect(res.headers).toEqual({ + location: "/", + "x-request-id": "id" + }); + }); + }); +}); diff --git a/packages/platform/platform-fastify/src/services/PlatformFastifyResponse.ts b/packages/platform/platform-fastify/src/services/PlatformFastifyResponse.ts new file mode 100644 index 00000000000..9534055bf1c --- /dev/null +++ b/packages/platform/platform-fastify/src/services/PlatformFastifyResponse.ts @@ -0,0 +1,100 @@ +import type {ServerResponse} from "node:http"; + +import {type PlatformContext, PlatformResponse} from "@tsed/platform-http"; +import contentDisposition from "content-disposition"; +import type {FastifyReply} from "fastify"; + +declare global { + namespace TsED { + export interface Response extends FastifyReply { + locals: Record; + } + } +} + +/** + * @platform + * @fastify + */ +export class PlatformFastifyResponse extends PlatformResponse { + constructor(readonly $ctx: PlatformContext) { + super($ctx); + this.raw.locals = {}; + } + + // set raw(raw: FastifyReply) { + // this.$ctx.event.response = raw; + // } + + /** + * Return the Node.js response object + */ + getRes(): ServerResponse { + return this.raw.raw; + } + + /** + * Sets the HTTP status for the response. + * + * @param status + */ + status(status: number) { + this.raw.code(status); + + return this; + } + + /** + * Set `Content-Type` response header with `type` through `mime.lookup()` + * when it does not contain "/", or set the Content-Type to `type` otherwise. + * + * Examples: + * + * res.type('.html'); + * res.type('html'); + * res.type('json'); + * res.type('application/json'); + * res.type('png'); + */ + contentType(contentType: string) { + this.raw.type(contentType); + + return this; + } + + /** + * Returns the HTTP response header specified by field. The match is case-insensitive. + * + * ```typescript + * response.get('Content-Type') // => "text/plain" + * ``` + * + * @param name + */ + get(name: string) { + return this.raw.getHeader(name); + } + + attachment(filename: string) { + this.setHeader("Content-Disposition", contentDisposition(filename)); + return this; + } + + setHeader(key: string, item: any) { + this.raw.header(key, this.formatHeader(key, item)); + + return this; + } + + stream(data: any) { + this.raw.send(data); + + return this; + } + + json(data: any) { + this.raw.send(data); + + return this; + } +} diff --git a/packages/platform/platform-fastify/test/app/Server.ts b/packages/platform/platform-fastify/test/app/Server.ts new file mode 100644 index 00000000000..11d5ac4ed99 --- /dev/null +++ b/packages/platform/platform-fastify/test/app/Server.ts @@ -0,0 +1,47 @@ +import "@tsed/ajv"; +import "@tsed/platform-log-request"; +import "@tsed/swagger"; + +import path from "node:path"; + +import type {FastifySessionOptions} from "@fastify/session"; +import {Configuration} from "@tsed/di"; + +export const rootDir = import.meta.dirname; + +@Configuration({ + port: 8083, + statics: { + "/": path.join(rootDir, "public") + }, + views: { + root: path.join(rootDir, "views"), + extensions: { + ejs: "ejs" + } + }, + logger: { + level: "info" + }, + plugins: [ + "@fastify/accepts", + "@fastify/cookie", + { + use: "@fastify/session", + options: { + secret: "a secret with minimum length of 32 characters", + saveUninitialized: true, + cookie: {secure: false} + } satisfies FastifySessionOptions + }, + { + use: "fastify-raw-body", + options: { + global: false, + runFirst: true + } + }, + "@fastify/formbody" + ] +}) +export class Server {} diff --git a/packages/platform/platform-fastify/test/app/index.ts b/packages/platform/platform-fastify/test/app/index.ts new file mode 100644 index 00000000000..5329201db71 --- /dev/null +++ b/packages/platform/platform-fastify/test/app/index.ts @@ -0,0 +1,47 @@ +import {Controller} from "@tsed/di"; +import {$log} from "@tsed/logger"; +import {PathParams} from "@tsed/platform-params"; +import {Get} from "@tsed/schema"; + +import {PlatformFastify} from "../../src/index.js"; +import {Server} from "./Server.js"; + +if (process.env.NODE_ENV !== "test") { + @Controller("/hello") + class HelloWorld { + @Get("/") + get() { + return {test: "Hello world"}; + } + + @Get("/:id") + getById(@PathParams("id") id: string) { + return {test: "Hello world", id}; + } + } + + async function bootstrap() { + try { + $log.debug("Start server..."); + const platform = await PlatformFastify.bootstrap(Server, { + disableComponentScan: true, + mount: { + "/rest": [HelloWorld] + }, + swagger: [ + { + path: "/doc" + } + ] + }); + + await platform.listen(); + $log.debug("Server initialized"); + } catch (er) { + console.error(er); + $log.error(er); + } + } + + bootstrap(); +} diff --git a/packages/platform/platform-fastify/test/app/public/index.html b/packages/platform/platform-fastify/test/app/public/index.html new file mode 100644 index 00000000000..4833594d6da --- /dev/null +++ b/packages/platform/platform-fastify/test/app/public/index.html @@ -0,0 +1,5 @@ + + +Hello + + diff --git a/packages/platform/platform-fastify/test/app/views/view.ejs b/packages/platform/platform-fastify/test/app/views/view.ejs new file mode 100644 index 00000000000..2bff378de1d --- /dev/null +++ b/packages/platform/platform-fastify/test/app/views/view.ejs @@ -0,0 +1 @@ +

Hello <%= world %> with <%= opts %> and ID <%= localID %>

diff --git a/packages/platform/platform-fastify/test/health.spec.ts b/packages/platform/platform-fastify/test/health.spec.ts new file mode 100644 index 00000000000..77fab89be52 --- /dev/null +++ b/packages/platform/platform-fastify/test/health.spec.ts @@ -0,0 +1,51 @@ +import "@tsed/ajv"; + +import {Controller} from "@tsed/di"; +import {PlatformTest} from "@tsed/platform-http/testing"; +import {PlatformTestSdk} from "@tsed/platform-test-sdk"; +import {Get} from "@tsed/schema"; +import SuperTest from "supertest"; + +import {PlatformFastify} from "../src/index.js"; +import {rootDir, Server} from "./app/Server.js"; + +const utils = PlatformTestSdk.create({ + rootDir, + adapter: PlatformFastify, + server: Server +}); + +@Controller("/health") +class TestHealth { + @Get("/1") + scenario1() { + return { + fastify: "hello world" + }; + } +} + +describe("Health", () => { + let request: SuperTest.Agent; + + beforeAll( + utils.bootstrap({ + mount: { + "/rest": [TestHealth] + } + }) + ); + afterAll(() => utils.reset()); + + describe("GET /rest/health", () => { + it("should return hello world", async () => { + const request = SuperTest(PlatformTest.callback()); + + const {body} = await request.get("/rest/health/1").expect(200); + + expect(body).toEqual({ + fastify: "hello world" + }); + }); + }); +}); diff --git a/packages/platform/platform-fastify/test/platform-fastify.spec.ts b/packages/platform/platform-fastify/test/platform-fastify.spec.ts new file mode 100644 index 00000000000..6e0f225e5b7 --- /dev/null +++ b/packages/platform/platform-fastify/test/platform-fastify.spec.ts @@ -0,0 +1,100 @@ +import {PlatformTestSdk} from "@tsed/platform-test-sdk"; + +import {PlatformFastify} from "../src/index.js"; +import {rootDir, Server} from "./app/Server.js"; + +const utils = PlatformTestSdk.create({ + rootDir, + adapter: PlatformFastify, + server: Server +}); + +describe("PlatformFastify integration", () => { + describe("Handlers", () => { + utils.test("handlers"); + }); + describe("Children controllers", () => { + utils.test("childrenControllers"); + }); + describe("Inheritance controllers", () => { + utils.test("inheritanceController"); + }); + describe("Response", () => { + utils.test("response"); + }); + // untestable with Fastify + describe.skip("Stream", () => { + utils.test("stream"); + }); + describe("Middlewares", () => { + utils.test("middlewares"); + }); + describe("Scope Request", () => { + utils.test("scopeRequest"); + }); + describe("Headers", () => { + utils.test("headers"); + }); + describe("AcceptMime", () => { + utils.test("acceptMime"); + }); + describe("HeaderParams", () => { + utils.test("headerParams"); + }); + describe("PathParams", () => { + utils.test("pathParams"); + }); + describe("QueryParams", () => { + utils.test("queryParams"); + }); + describe("BodyParams", () => { + utils.test("bodyParams"); + }); + describe("Cookies", () => { + utils.test("cookies"); + }); + describe.skip("Session", () => { + utils.test("session"); + }); + describe("Location", () => { + utils.test("location"); + }); + describe("Redirect", () => { + utils.test("redirect"); + }); + describe("Errors", () => { + utils.test("errors"); + }); + describe("ResponseFilters", () => { + utils.test("responseFilter"); + }); + describe("Routing", () => { + utils.test("routing"); + }); + describe("Locals", () => { + utils.test("locals"); + }); + describe("Auth", () => { + utils.test("auth"); + }); + describe("Module", () => { + utils.test("module"); + }); + + // EXTRA + describe("Plugin: View", () => { + utils.test("view"); + }); + describe("Plugin: Statics files", () => { + utils.test("statics"); + }); + describe.skip("Plugin: Multer", () => { + utils.test("multer"); + }); + describe.skip("Plugin: DeepQueryParams", () => { + utils.test("deepQueryParams"); + }); + describe("Plugin: Custom404", () => { + utils.test("custom404"); + }); +}); diff --git a/packages/platform/platform-fastify/test/test-auth.integration.spec.ts b/packages/platform/platform-fastify/test/test-auth.integration.spec.ts new file mode 100644 index 00000000000..b18f3880383 --- /dev/null +++ b/packages/platform/platform-fastify/test/test-auth.integration.spec.ts @@ -0,0 +1,345 @@ +import {useDecorators} from "@tsed/core"; +import {Controller, Inject, Injectable} from "@tsed/di"; +import {BadRequest, Forbidden, Unauthorized} from "@tsed/exceptions"; +import {Req, Res} from "@tsed/platform-http"; +import {PlatformTest} from "@tsed/platform-http/testing"; +import {Middleware, UseAuth} from "@tsed/platform-middlewares"; +import {Context} from "@tsed/platform-params"; +import {Get, In, Post, Returns, Security} from "@tsed/schema"; +import SuperTest from "supertest"; +import {afterAll, beforeAll, describe, expect, it} from "vitest"; + +import baseSpec from "../data/swagger.json"; +import {PlatformTestingSdkOpts} from "../interfaces/index.js"; + +@Injectable() +export class TokenService { + private _token: string = "EMPTY"; + + token(token?: string) { + if (token) { + this._token = token; + } + + return this._token; + } + + isValid(token: string | undefined) { + return String(token).match(/token/); + } +} + +@Middleware() +class OAuthMiddleware { + @Inject() + tokenService: TokenService; + + public use(@Context() ctx: Context) { + const {endpoint, request} = ctx; + + const options = endpoint.get(OAuthMiddleware) || {}; + + if (!request.get("authorization")) { + throw new Unauthorized("Unauthorized"); + } + + if (!this.tokenService.isValid(request.get("authorization"))) { + throw new BadRequest("Bad token format"); + } + + if (options && options.role === "admin" && request.get("authorization") !== "admin_token") { + throw new Forbidden("Forbidden"); + } + + ctx.getRequest().user = { + id: "id", + name: "name" + }; + } +} + +export function OAuth(options: any = {}): Function { + return useDecorators( + UseAuth(OAuthMiddleware, options), + Security("global_auth", ...(options.scopes || [])), + In("header").Type(String).Name("Authorization").Required(), + Returns(401).Description("Unauthorized"), + Returns(403).Description("Forbidden") + ); +} + +@Controller("/auth") +class TestAuthCtrl { + @Inject() + tokenService: TokenService; + + @Post("/authorize") + authorize() { + this.tokenService.token("access_token"); + + return { + access_token: this.tokenService.token() + }; + } + + @Get("/userinfo") + @OAuth() + token(@Req("user") user: any) { + return user; + } + + @Get("/admin") + @OAuth({role: "admin", scopes: ["admin"]}) + admin() { + return { + granted: true + }; + } + + @Post("/stepUp") + stepUp() { + this.tokenService.token("admin_token"); + + return { + access_token: this.tokenService.token() + }; + } +} + +export function testAuth(options: PlatformTestingSdkOpts) { + let request: SuperTest.Agent; + + beforeAll( + PlatformTest.bootstrap(options.server, { + ...options, + mount: { + "/rest": [TestAuthCtrl] + }, + swagger: [ + { + path: "/doc", + spec: baseSpec as any + } + ] + }) + ); + beforeAll(() => { + request = SuperTest.agent(PlatformTest.callback()); + }); + afterAll(PlatformTest.reset); + + describe("Scenario 1: Create token, test token and stepup token", () => { + it("should create a token, call /userinfo to get userinfo and try admin route", async () => { + await request.get("/rest/auth/userinfo").expect(401); + await request + .get("/rest/auth/userinfo") + .set({ + Authorization: "wrong" + }) + .expect(400); + + const {body} = await request.post("/rest/auth/authorize").expect(200); + + expect(body.access_token).toEqual("access_token"); + + const {body: userInfo} = await request + .get("/rest/auth/userinfo") + .set({ + Authorization: body.access_token + }) + .expect(200); + + expect(userInfo).toEqual({ + id: "id", + name: "name" + }); + + await request + .get("/rest/auth/admin") + .set({ + Authorization: body.access_token + }) + .expect(403); + + const { + body: {access_token} + } = await request.post("/rest/auth/stepUp").expect(200); + + expect(access_token).toEqual("admin_token"); + + const {body: result} = await request + .get("/rest/auth/admin") + .set({ + Authorization: access_token + }) + .expect(200); + + expect(result).toEqual({ + granted: true + }); + }); + }); + + describe("Scenario 2: GET /swagger.json", () => { + it("should generate the swagger.spec", async () => { + const {body: spec} = await request.get("/doc/swagger.json").expect(200); + + expect(spec).toEqual({ + openapi: "3.0.1", + info: { + version: "1.0.0", + title: "Swagger Title", + description: "Swagger description", + termsOfService: "http://swagger.io/terms/", + contact: {email: "apiteam@swagger.io"}, + license: {name: "Apache 2.0", url: "http://www.apache.org/licenses/LICENSE-2.0.html"} + }, + paths: { + "/rest/auth/authorize": { + post: { + operationId: "testAuthCtrlAuthorize", + responses: {"200": {description: "Success"}}, + parameters: [], + tags: ["TestAuthCtrl"] + } + }, + "/rest/auth/userinfo": { + get: { + operationId: "testAuthCtrlToken", + responses: { + "401": { + content: {"application/json": {schema: {$ref: "#/components/schemas/Unauthorized"}}}, + description: "Unauthorized" + }, + "403": { + content: {"application/json": {schema: {$ref: "#/components/schemas/Forbidden"}}}, + description: "Forbidden" + } + }, + security: [{global_auth: []}], + parameters: [ + { + name: "Authorization", + required: true, + in: "header", + schema: {type: "string"} + } + ], + tags: ["TestAuthCtrl"] + } + }, + "/rest/auth/admin": { + get: { + operationId: "testAuthCtrlAdmin", + responses: { + "401": { + content: {"application/json": {schema: {$ref: "#/components/schemas/Unauthorized"}}}, + description: "Unauthorized" + }, + "403": { + content: {"application/json": {schema: {$ref: "#/components/schemas/Forbidden"}}}, + description: "Forbidden" + } + }, + security: [{global_auth: ["admin"]}], + parameters: [ + { + name: "Authorization", + required: true, + in: "header", + schema: {type: "string"} + } + ], + tags: ["TestAuthCtrl"] + } + }, + "/rest/auth/stepUp": { + post: { + operationId: "testAuthCtrlStepUp", + responses: {"200": {description: "Success"}}, + parameters: [], + tags: ["TestAuthCtrl"] + } + } + }, + components: { + securitySchemes: { + global_auth: { + type: "oauth2", + flows: { + implicit: { + authorizationUrl: "http://petstore.swagger.io/oauth/dialog", + scopes: {admin: "Admin access"} + } + } + } + }, + schemas: { + Unauthorized: { + type: "object", + properties: { + name: { + type: "string", + description: "The error name", + minLength: 1, + example: "UNAUTHORIZED", + default: "UNAUTHORIZED" + }, + message: {type: "string", description: "An error message", minLength: 1}, + status: { + type: "number", + description: "The status code of the exception", + example: 401, + default: 401 + }, + errors: { + type: "array", + items: {$ref: "#/components/schemas/GenericError"}, + description: "A list of related errors" + }, + stack: {type: "string", description: "The stack trace (only in development mode)"} + }, + required: ["name", "message", "status"] + }, + GenericError: { + type: "object", + properties: { + name: {type: "string", description: "The error name", minLength: 1}, + message: {type: "string", description: "An error message", minLength: 1} + }, + additionalProperties: true, + required: ["name", "message"] + }, + Forbidden: { + type: "object", + properties: { + name: { + type: "string", + description: "The error name", + minLength: 1, + example: "FORBIDDEN", + default: "FORBIDDEN" + }, + message: {type: "string", description: "An error message", minLength: 1}, + status: { + type: "number", + description: "The status code of the exception", + example: 403, + default: 403 + }, + errors: { + type: "array", + items: {$ref: "#/components/schemas/GenericError"}, + description: "A list of related errors" + }, + stack: {type: "string", description: "The stack trace (only in development mode)"} + }, + required: ["name", "message", "status"] + } + } + }, + tags: [{name: "TestAuthCtrl"}] + }); + }); + }); +} diff --git a/packages/platform/platform-fastify/test/test-session.integration.spec.ts b/packages/platform/platform-fastify/test/test-session.integration.spec.ts new file mode 100644 index 00000000000..f66418b8b1e --- /dev/null +++ b/packages/platform/platform-fastify/test/test-session.integration.spec.ts @@ -0,0 +1,117 @@ +import {Constant, Controller} from "@tsed/di"; +import {NotFound} from "@tsed/exceptions"; +import {Req} from "@tsed/platform-http"; +import {PlatformTest} from "@tsed/platform-http/testing"; +import {BodyParams, Session} from "@tsed/platform-params"; +import {PlatformTestSdk} from "@tsed/platform-test-sdk"; +import {Allow, Email, Get, Ignore, MinLength, Post, Property, Required, Returns} from "@tsed/schema"; +import SuperTest from "supertest"; +import {promisify} from "util"; +import {afterAll, beforeAll, describe, expect, it} from "vitest"; + +import {PlatformFastify} from "../src/index.js"; +import {rootDir, Server} from "./app/Server.js"; + +export class UserCreation { + @Property() + name: string; + + @Required() + @Email() + @Allow(null) + email: string; + + @Required() + @MinLength(6) + @Allow(null) + password: string; +} + +export class User extends UserCreation { + @Ignore() + declare password: string; +} + +@Controller("/session") +export class SessionCtrl { + @Constant("PLATFORM_NAME") + platformName: string; + + @Post("/connect") + @Returns(200, User) + connect(@Session() session: any, @BodyParams() user: UserCreation) { + session.user = user; + + return Promise.resolve(user); + } + + @Get("/userinfo") + @Returns(200, User) + userInfo(@Session("user") user: User): Promise { + if (!user) { + return Promise.reject(new NotFound("User not found")); + } + + return Promise.resolve(user); + } + + @Get("/logout") + @Returns(204) + logout(@Req() req: any) { + switch (this.platformName) { + case "koa": + return Promise.resolve((req.ctx.session = null)); + case "fastify": + case "express": + delete req.session.user; + + return promisify(req.session.destroy.bind(req.session))(); + } + + return Promise.resolve(); + } +} + +const utils = PlatformTestSdk.create({ + rootDir, + adapter: PlatformFastify, + server: Server +}); + +describe("Fastify: sesssion", () => { + beforeAll( + utils.bootstrap({ + logger: { + level: "info" + }, + mount: { + "/rest": [SessionCtrl] + } + }) + ); + afterAll(utils.reset); + + describe("Scenario1: POST /rest/session/connected", () => { + it("should keep connected user in session and destroy session", async () => { + const request = SuperTest.agent(PlatformTest.callback()); + await request.get("/rest/session/userinfo").expect(404); + + await request.post("/rest/session/connect").send({ + name: "name", + email: "test@test.fr", + password: "password1234" + }); + + // @ts-ignore + const response = await request.get("/rest/session/userinfo").expect(200); + + expect(response.body).toEqual({ + email: "test@test.fr", + name: "name" + }); + + await request.get("/rest/session/logout").expect(204); + await request.get("/rest/session/userinfo").expect(404); + }); + }); +}); diff --git a/packages/platform/platform-fastify/tsconfig.esm.json b/packages/platform/platform-fastify/tsconfig.esm.json new file mode 100644 index 00000000000..8954049da4a --- /dev/null +++ b/packages/platform/platform-fastify/tsconfig.esm.json @@ -0,0 +1,26 @@ +{ + "extends": "@tsed/typescript/tsconfig.node.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "./lib/esm", + "declarationDir": "./lib/types", + "declaration": true, + "composite": true, + "noEmit": false + }, + "include": ["src/**/*.ts", "src/**/*.json"], + "exclude": [ + "node_modules", + "test", + "lib", + "benchmark", + "coverage", + "spec", + "**/*.benchmark.ts", + "**/*.spec.ts", + "keys", + "**/__mock__/**", + "webpack.config.js" + ] +} diff --git a/packages/platform/platform-fastify/tsconfig.json b/packages/platform/platform-fastify/tsconfig.json new file mode 100644 index 00000000000..657e21b551d --- /dev/null +++ b/packages/platform/platform-fastify/tsconfig.json @@ -0,0 +1,40 @@ +{ + "extends": "@tsed/typescript/tsconfig.node.json", + "compilerOptions": { + "baseUrl": ".", + "noEmit": true + }, + "include": [], + "references": [ + { + "path": "../platform-http/tsconfig.json" + }, + { + "path": "../../core/tsconfig.json" + }, + { + "path": "../../di/tsconfig.json" + }, + { + "path": "../../specs/json-mapper/tsconfig.json" + }, + { + "path": "../../specs/openspec/tsconfig.json" + }, + { + "path": "../platform-views/tsconfig.json" + }, + { + "path": "../../specs/schema/tsconfig.json" + }, + { + "path": "../platform-test-sdk/tsconfig.json" + }, + { + "path": "./tsconfig.esm.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/platform/platform-fastify/tsconfig.spec.json b/packages/platform/platform-fastify/tsconfig.spec.json new file mode 100644 index 00000000000..fab48ebe865 --- /dev/null +++ b/packages/platform/platform-fastify/tsconfig.spec.json @@ -0,0 +1,35 @@ +{ + "extends": "@tsed/typescript/tsconfig.node.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "../..", + "declaration": false, + "composite": false, + "noEmit": true, + "paths": { + "@tsed/openspec": ["../../specs/openspec/src/index.ts"], + "@tsed/schema": ["../../specs/schema/src/index.ts"], + "@tsed/di": ["../../di/src/index.ts"], + "@tsed/exceptions": ["../../specs/exceptions/src/index.ts"], + "@tsed/json-mapper": ["../../specs/json-mapper/src/index.ts"], + "@tsed/platform-exceptions": ["../platform-exceptions/src/index.ts"], + "@tsed/platform-middlewares": ["../platform-middlewares/src/index.ts"], + "@tsed/platform-params": ["../platform-params/src/index.ts"], + "@tsed/platform-log-middleware": ["../platform-log-middleware/src/index.ts"], + "@tsed/platform-response-filter": ["../platform-response-filter/src/index.ts"], + "@tsed/platform-router": ["../platform-router/src/index.ts"], + "@tsed/platform-views": ["../platform-views/src/index.ts"], + "@tsed/normalize-path": ["../../utils/normalize-path/src/index.ts"], + "@tsed/components-scan": ["../../third-parties/components-scan/src/index.ts"], + "@tsed/ajv": ["../../specs/ajv/src/index.ts"], + "@tsed/platform-cache": ["../platform-cache/src/index.ts"], + "@tsed/swagger": ["../../specs/swagger/src/index.ts"], + "@tsed/platform-test-sdk": ["../platform-test-sdk/src/index.ts"], + "@tsed/platform-express": ["../platform-express/src/index.ts"], + "@tsed/platform-koa": ["../platform-koa/src/index.ts"] + }, + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src/**/*.spec.ts", "test/**/*.spec.ts", "vitest.config.mts"], + "exclude": ["node_modules", "lib", "benchmark", "coverage"] +} diff --git a/packages/platform/platform-fastify/vitest.config.mts b/packages/platform/platform-fastify/vitest.config.mts new file mode 100644 index 00000000000..d33c0c74734 --- /dev/null +++ b/packages/platform/platform-fastify/vitest.config.mts @@ -0,0 +1,21 @@ +// @ts-ignore +import {presets} from "@tsed/vitest/presets"; +import {defineConfig} from "vitest/config"; + +export default defineConfig( + { + ...presets, + test: { + ...presets.test, + coverage: { + ...presets.test.coverage, + thresholds: { + statements: 99.15, + branches: 95.6, + functions: 100, + lines: 99.15 + } + } + } + } +); \ No newline at end of file diff --git a/packages/platform/platform-http/src/common/services/PlatformHandler.ts b/packages/platform/platform-http/src/common/services/PlatformHandler.ts index a292165ca15..a8e851d2457 100644 --- a/packages/platform/platform-http/src/common/services/PlatformHandler.ts +++ b/packages/platform/platform-http/src/common/services/PlatformHandler.ts @@ -42,7 +42,7 @@ export class PlatformHandler { }) .on("alterHandler", (handlerMetadata: PlatformHandlerMetadata) => { const handler = handlerMetadata.isInjectable() ? this.createHandler(handlerMetadata) : handlerMetadata.handler; - + handlerMetadata.compiledHandler = handler; return this.platformApplication.adapter.mapHandler(handler, handlerMetadata); }); } diff --git a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts index 8af3f0c591b..cd251e7a3c9 100644 --- a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts +++ b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts @@ -34,6 +34,7 @@ describe("PlatformHandlerMetadata", () => { expect(handlerMetadata.hasNextFunction).toEqual(false); expect(handlerMetadata.hasErrorParam).toEqual(false); expect(handlerMetadata.toString()).toEqual(""); + expect(handlerMetadata.isRawMiddleware()).toEqual(false); }); }); describe("from function", () => { @@ -50,6 +51,7 @@ describe("PlatformHandlerMetadata", () => { expect(handlerMetadata.hasNextFunction).toEqual(true); expect(handlerMetadata.hasErrorParam).toEqual(false); expect(handlerMetadata.toString()).toEqual("handler"); + expect(handlerMetadata.isRawMiddleware()).toEqual(true); }); }); describe("from function err", () => { @@ -67,6 +69,7 @@ describe("PlatformHandlerMetadata", () => { expect(handlerMetadata.hasErrorParam).toEqual(true); expect(handlerMetadata.propertyKey).toBeUndefined(); expect(handlerMetadata.toString()).toEqual("handler"); + expect(handlerMetadata.isRawMiddleware()).toEqual(true); }); }); describe("from function without nextFn", () => { @@ -85,6 +88,7 @@ describe("PlatformHandlerMetadata", () => { expect(handlerMetadata.hasErrorParam).toEqual(false); expect(handlerMetadata.propertyKey).toBeUndefined(); expect(handlerMetadata.toString()).toEqual("handler"); + expect(handlerMetadata.isRawMiddleware()).toEqual(true); }); }); describe("from endpoint/middleware with injection", () => { @@ -117,6 +121,7 @@ describe("PlatformHandlerMetadata", () => { expect(handlerMetadata.getParams()[0].paramType).toEqual("REQUEST"); expect(handlerMetadata.getParams()[1].paramType).toEqual("NEXT_FN"); + expect(handlerMetadata.isRawMiddleware()).toEqual(false); }); }); @@ -144,6 +149,7 @@ describe("PlatformHandlerMetadata", () => { expect(handlerMetadata.hasErrorParam).toEqual(true); expect(handlerMetadata.propertyKey).toEqual("use"); expect(handlerMetadata.toString()).toEqual("Test.use"); + expect(handlerMetadata.isRawMiddleware()).toEqual(false); }); }); }); diff --git a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts index 7fb0ea50f19..a44ba195bec 100644 --- a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts +++ b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts @@ -139,6 +139,10 @@ export class PlatformHandlerMetadata { return this.type === PlatformHandlerType.RESPONSE_FN; } + public isRawMiddleware() { + return !this.isInjectable() && (this.type === PlatformHandlerType.RAW_FN || this.type === PlatformHandlerType.RAW_ERR_FN); + } + toString() { return [nameOf(this.target), this.propertyKey].filter(Boolean).join("."); } diff --git a/packages/platform/platform-test-sdk/src/tests/testView.ts b/packages/platform/platform-test-sdk/src/tests/testView.ts index e0c3f7b3905..bbc55010f9d 100644 --- a/packages/platform/platform-test-sdk/src/tests/testView.ts +++ b/packages/platform/platform-test-sdk/src/tests/testView.ts @@ -58,7 +58,7 @@ export function testView(options: PlatformTestingSdkOpts) { it("should render a view", async () => { const response = await request.get("/rest/views/scenario-1").expect(200); - expect(response.headers["content-type"]).toEqual("text/html; charset=utf-8"); + expect(response.headers["content-type"]).toContain("text/html"); expect(response.text).toEqual(`

Hello world with opts and ID local-10909

${EOL}`); }); }); diff --git a/packages/specs/scalar/src/middlewares/indexMiddleware.ts b/packages/specs/scalar/src/middlewares/indexMiddleware.ts index 9ff8f1554d3..89ea31738eb 100644 --- a/packages/specs/scalar/src/middlewares/indexMiddleware.ts +++ b/packages/specs/scalar/src/middlewares/indexMiddleware.ts @@ -1,5 +1,4 @@ -import {basename} from "node:path"; -import {join} from "node:path"; +import {basename, join} from "node:path"; import {context} from "@tsed/di"; @@ -18,7 +17,7 @@ export function indexMiddleware(viewPath: string, conf: ScalarSettings) { ...options }; - ctx.response.body( + ctx.response.contentType("text/html").body( await ctx.response.render(viewPath, { cssPath: join(path, basename(cssPath)), cdn, diff --git a/packages/specs/swagger/src/middlewares/indexMiddleware.ts b/packages/specs/swagger/src/middlewares/indexMiddleware.ts index 25fb6072a52..e284792832a 100644 --- a/packages/specs/swagger/src/middlewares/indexMiddleware.ts +++ b/packages/specs/swagger/src/middlewares/indexMiddleware.ts @@ -1,8 +1,6 @@ -import {basename} from "node:path"; -import {join} from "node:path"; +import {basename, join} from "node:path"; import {context} from "@tsed/di"; -import {PlatformContext} from "@tsed/platform-http"; import {SwaggerSettings} from "../interfaces/SwaggerSettings.js"; @@ -16,7 +14,7 @@ export function indexMiddleware(viewPath: string, conf: SwaggerSettings & {urls: const ctx = context(); const {path, options = {}, fileName, showExplorer, cssPath, jsPath, urls} = conf; - ctx.response.body( + ctx.response.contentType("text/html").body( await ctx.response.render(viewPath, { spec: {}, url: `${path}/${fileName}`, diff --git a/tsconfig.json b/tsconfig.json index 518e326ab43..a9883ed82d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -98,6 +98,9 @@ { "path": "./packages/platform/platform-express/tsconfig.json" }, + { + "path": "./packages/platform/platform-koa/tsconfig.json" + }, { "path": "./packages/graphql/typegraphql/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index 905c7310cc5..a3b35511f03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3474,6 +3474,34 @@ __metadata: languageName: node linkType: hard +"@fastify/accept-negotiator@npm:^2.0.0": + version: 2.0.1 + resolution: "@fastify/accept-negotiator@npm:2.0.1" + checksum: 10/7a2db0bb9fd1e4b261f68333ea5150385dc0edb4af01e231bed1c3f98ff5a143b32997d77a6403e0d941f8610bb48bfaa3dc4d2fb0aa7da47678cee280157eb5 + languageName: node + linkType: hard + +"@fastify/accepts@npm:5.0.2": + version: 5.0.2 + resolution: "@fastify/accepts@npm:5.0.2" + dependencies: + accepts: "npm:^1.3.8" + fastify-plugin: "npm:^5.0.0" + checksum: 10/33021c4e1394b059b0f34b035db2dc4951d87c36fd42c5e661c5aedddfdb23124fed13cb805489ba5bec2567c5c96a779f113b51fa9606a4f24abd6e66b1370d + languageName: node + linkType: hard + +"@fastify/ajv-compiler@npm:^4.0.0": + version: 4.0.2 + resolution: "@fastify/ajv-compiler@npm:4.0.2" + dependencies: + ajv: "npm:^8.12.0" + ajv-formats: "npm:^3.0.1" + fast-uri: "npm:^3.0.0" + checksum: 10/adc3f9a99418e4495af64099a2b137bf5960909191287568f2ddc48c4bffe70e140f17453ab7f4ac021bf78ddb6fc14db8a1d3a3fe7ee6e7a6d321c8e2c232f8 + languageName: node + linkType: hard + "@fastify/busboy@npm:^2.0.0": version: 2.1.1 resolution: "@fastify/busboy@npm:2.1.1" @@ -3481,6 +3509,117 @@ __metadata: languageName: node linkType: hard +"@fastify/cookie@npm:11.0.1": + version: 11.0.1 + resolution: "@fastify/cookie@npm:11.0.1" + dependencies: + cookie: "npm:^1.0.0" + fastify-plugin: "npm:^5.0.0" + checksum: 10/6a0f2e2e4b2d87893c8f4827da541c9b1b2dfa30679342629e04ce46cdd829fba20162c0679b03c7d228dfa512ae44fcebd7f75870530d34113e41de8fe1ae09 + languageName: node + linkType: hard + +"@fastify/error@npm:^4.0.0": + version: 4.0.0 + resolution: "@fastify/error@npm:4.0.0" + checksum: 10/9afdb1262fbd57e1c922cd7b3326c45d2ca018456030669dcad76feda60df894a3c25b9d019cd050ffc3686a0de938799be826328ab2d74257941788c7f642e7 + languageName: node + linkType: hard + +"@fastify/fast-json-stringify-compiler@npm:^5.0.0": + version: 5.0.2 + resolution: "@fastify/fast-json-stringify-compiler@npm:5.0.2" + dependencies: + fast-json-stringify: "npm:^6.0.0" + checksum: 10/597b5efce6af8bd47679e8c6d537688211f38fd3be1c1b3c8874fff497ec9b09f4b89b33e7e508049f11daa2403930c09abbd59454df13aadffbd74b84772976 + languageName: node + linkType: hard + +"@fastify/formbody@npm:8.0.1": + version: 8.0.1 + resolution: "@fastify/formbody@npm:8.0.1" + dependencies: + fast-querystring: "npm:^1.1.2" + fastify-plugin: "npm:^5.0.0" + checksum: 10/823c53d7e1ebf59bda1aa967ed5a979607c98bb962505ccfbe3d4dc05a78186a9b113c65f5cff1c9835f7c53222aabeea851d72c0e86fa242d010a4a601205f4 + languageName: node + linkType: hard + +"@fastify/forwarded@npm:^3.0.0": + version: 3.0.0 + resolution: "@fastify/forwarded@npm:3.0.0" + checksum: 10/5da4417fa6a4c39f40cb10aa6f6fe0c970d24974ad9e729512ba381c7b1b768c924a3deadc60eb3a02d756086746aa9b12a5afb4032c16ca7a7ca5e3c2830155 + languageName: node + linkType: hard + +"@fastify/merge-json-schemas@npm:^0.2.0": + version: 0.2.1 + resolution: "@fastify/merge-json-schemas@npm:0.2.1" + dependencies: + dequal: "npm:^2.0.3" + checksum: 10/10ce2bc689f279375974cc61d0779a6672dcc80d62120ebcbf07f821b6f07e6149cd17c196f20d4457b07458407bdbb277a27a42ec3ff322e7a5bbb378e41fda + languageName: node + linkType: hard + +"@fastify/middie@npm:>=9.0.2": + version: 9.0.3 + resolution: "@fastify/middie@npm:9.0.3" + dependencies: + "@fastify/error": "npm:^4.0.0" + fastify-plugin: "npm:^5.0.0" + path-to-regexp: "npm:^8.1.0" + reusify: "npm:^1.0.4" + checksum: 10/d1e8cd97cd5a86992d12c98d0b7a621d3e92f44cfab8b76808d6410258861e142ec1ea97bdd9a67ae4a6fecc63db7b2e0fa646737c4976c2829415bb64c64b93 + languageName: node + linkType: hard + +"@fastify/proxy-addr@npm:^5.0.0": + version: 5.0.0 + resolution: "@fastify/proxy-addr@npm:5.0.0" + dependencies: + "@fastify/forwarded": "npm:^3.0.0" + ipaddr.js: "npm:^2.1.0" + checksum: 10/7aa26049de8aed5c2eefb58e1d66bc354509036e539a5bf56e684eb03bc6dd2aa862192b09ca2dd14891dad1e96f6ff07b248be2e68cefd7578f0c1452b35f64 + languageName: node + linkType: hard + +"@fastify/send@npm:^3.2.0": + version: 3.3.1 + resolution: "@fastify/send@npm:3.3.1" + dependencies: + "@lukeed/ms": "npm:^2.0.2" + escape-html: "npm:~1.0.3" + fast-decode-uri-component: "npm:^1.0.1" + http-errors: "npm:^2.0.0" + mime: "npm:^3" + checksum: 10/c0a0956d77a92df0f91889f5ab944306be76fe1f25e77312b0789ccc5b24aba14e63456f50a49849d94f1d7873b779953dc881ec0207145f5905c64f7eb12aac + languageName: node + linkType: hard + +"@fastify/session@npm:11.0.1": + version: 11.0.1 + resolution: "@fastify/session@npm:11.0.1" + dependencies: + fastify-plugin: "npm:^4.5.1" + safe-stable-stringify: "npm:^2.4.3" + checksum: 10/7e760bb83a69ee60c13173be6053fa77fd4e08d4d5ce547f11f2cbe7252f796567a9981465a28d07e82d8dc743740d2b08e66863258c8157461c382ab3739520 + languageName: node + linkType: hard + +"@fastify/static@npm:>=8.0.4": + version: 8.0.4 + resolution: "@fastify/static@npm:8.0.4" + dependencies: + "@fastify/accept-negotiator": "npm:^2.0.0" + "@fastify/send": "npm:^3.2.0" + content-disposition: "npm:^0.5.4" + fastify-plugin: "npm:^5.0.0" + fastq: "npm:^1.17.1" + glob: "npm:^11.0.0" + checksum: 10/84e333e39997c808b39b9df4f56d3282fe4c03626fd363b9912e4fc0c65cef5a67cd659cf98fe5031e7f96a13c6341267c4f3c31b126f4a2f2ffd2a3de35e948 + languageName: node + linkType: hard + "@formio/bootstrap3@npm:2.12.4-rc.1": version: 2.12.4-rc.1 resolution: "@formio/bootstrap3@npm:2.12.4-rc.1" @@ -4081,6 +4220,13 @@ __metadata: languageName: node linkType: hard +"@lukeed/ms@npm:^2.0.2": + version: 2.0.2 + resolution: "@lukeed/ms@npm:2.0.2" + checksum: 10/6ae47ed3ebc857ffc0283cfe46129947209c770d0974eb86626138b6c194a760d08863ec593ec75a645aec133b3237b37af500739b030293e4d9a81130f4e2ae + languageName: node + linkType: hard + "@mikro-orm/core@npm:6.3.12": version: 6.3.12 resolution: "@mikro-orm/core@npm:6.3.12" @@ -7782,6 +7928,61 @@ __metadata: languageName: unknown linkType: soft +"@tsed/platform-fastify@workspace:packages/platform/platform-fastify": + version: 0.0.0-use.local + resolution: "@tsed/platform-fastify@workspace:packages/platform/platform-fastify" + dependencies: + "@fastify/accepts": "npm:5.0.2" + "@fastify/cookie": "npm:11.0.1" + "@fastify/formbody": "npm:8.0.1" + "@fastify/middie": "npm:>=9.0.2" + "@fastify/session": "npm:11.0.1" + "@fastify/static": "npm:>=8.0.4" + "@tsed/barrels": "workspace:*" + "@tsed/core": "workspace:*" + "@tsed/di": "workspace:*" + "@tsed/platform-http": "workspace:*" + "@tsed/platform-test-sdk": "workspace:*" + "@types/content-disposition": "npm:^0.5.4" + content-disposition: "npm:>=0.5.4" + cross-env: "npm:7.0.3" + fastify: "npm:5.2.1" + fastify-raw-body: "npm:5.0.0" + ts-node: "npm:10.9.2" + tslib: "npm:2.6.1" + peerDependencies: + "@fastify/accepts": ">=4.3.0" + "@fastify/cookie": ">=9.3.1" + "@fastify/formbody": ">=7.4.0" + "@tsed/core": ">=8.0.0" + "@tsed/di": ">=8.0.0" + "@tsed/json-mapper": ">=8.0.0" + "@tsed/logger": ">=7.0.0" + "@tsed/openspec": ">=8.0.0" + "@tsed/platform-http": ">=8.0.0" + "@tsed/schema": ">=8.0.0" + fastify: ">=5.1.0" + fastify-raw-body: ">=5.0.0" + peerDependenciesMeta: + "@tsed/common": + optional: false + "@tsed/core": + optional: false + "@tsed/di": + optional: false + "@tsed/json-mapper": + optional: false + "@tsed/logger": + optional: false + "@tsed/openspec": + optional: false + "@tsed/schema": + optional: false + fastify: + optional: false + languageName: unknown + linkType: soft + "@tsed/platform-http@workspace:*, @tsed/platform-http@workspace:packages/platform/platform-http": version: 0.0.0-use.local resolution: "@tsed/platform-http@workspace:packages/platform/platform-http" @@ -8954,6 +9155,13 @@ __metadata: languageName: node linkType: hard +"@types/content-disposition@npm:^0.5.4": + version: 0.5.8 + resolution: "@types/content-disposition@npm:0.5.8" + checksum: 10/eeea868fb510ae7a32aa2d7de680fba79d59001f3e758a334621e10bc0a6496d3a42bb79243a5e53b9c63cb524522853ccc144fe1ab160c4247d37cdb81146c4 + languageName: node + linkType: hard + "@types/conventional-commits-parser@npm:^5.0.0": version: 5.0.0 resolution: "@types/conventional-commits-parser@npm:5.0.0" @@ -10496,6 +10704,13 @@ __metadata: languageName: node linkType: hard +"abstract-logging@npm:^2.0.1": + version: 2.0.1 + resolution: "abstract-logging@npm:2.0.1" + checksum: 10/6967d15e5abbafd17f56eaf30ba8278c99333586fa4f7935fd80e93cfdc006c37fcc819c5d63ee373a12e6cb2d0417f7c3c6b9e42b957a25af9937d26749415e + languageName: node + linkType: hard + "accepts@npm:^1.3.5, accepts@npm:^1.3.8, accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -10679,6 +10894,20 @@ __metadata: languageName: node linkType: hard +"ajv-formats@npm:^3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" + dependencies: + ajv: "npm:^8.0.0" + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10/5679b9f9ced9d0213a202a37f3aa91efcffe59a6de1a6e3da5c873344d3c161820a1f11cc29899661fee36271fd2895dd3851b6461c902a752ad661d1c1e8722 + languageName: node + linkType: hard + "ajv-keywords@npm:^3.5.2": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" @@ -10699,7 +10928,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:8.17.1, ajv@npm:^8.17.1, ajv@npm:^8.9.0": +"ajv@npm:8.17.1, ajv@npm:^8.12.0, ajv@npm:^8.17.1, ajv@npm:^8.9.0": version: 8.17.1 resolution: "ajv@npm:8.17.1" dependencies: @@ -11504,6 +11733,13 @@ __metadata: languageName: node linkType: hard +"atomic-sleep@npm:^1.0.0": + version: 1.0.0 + resolution: "atomic-sleep@npm:1.0.0" + checksum: 10/3ab6d2cf46b31394b4607e935ec5c1c3c4f60f3e30f0913d35ea74b51b3585e84f590d09e58067f11762eec71c87d25314ce859030983dc0e4397eed21daa12e + languageName: node + linkType: hard + "atpl@npm:0.9.3": version: 0.9.3 resolution: "atpl@npm:0.9.3" @@ -11525,6 +11761,16 @@ __metadata: languageName: node linkType: hard +"avvio@npm:^9.0.0": + version: 9.1.0 + resolution: "avvio@npm:9.1.0" + dependencies: + "@fastify/error": "npm:^4.0.0" + fastq: "npm:^1.17.1" + checksum: 10/4bc7c0ac1b9e3a814db4bc5b89fd6a38b082614677d9a4e2d2b9c11bc830deac81dd0e5bdae4ba31b1e165c19de9f2772564fd3b840b3bfa5048f757bb6a4eda + languageName: node + linkType: hard + "axios@npm:*": version: 0.27.2 resolution: "axios@npm:0.27.2" @@ -13368,7 +13614,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:0.5.4, content-disposition@npm:~0.5.2": +"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.4, content-disposition@npm:~0.5.2": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -13377,6 +13623,15 @@ __metadata: languageName: node linkType: hard +"content-disposition@npm:>=0.5.4": + version: 1.0.0 + resolution: "content-disposition@npm:1.0.0" + dependencies: + safe-buffer: "npm:5.2.1" + checksum: 10/0dcc1a2d7874526b0072df3011b134857b49d97a3bc135bb464a299525d4972de6f5f464fd64da6c4d8406d26a1ffb976f62afaffef7723b1021a44498d10e08 + languageName: node + linkType: hard + "content-type@npm:^1.0.4, content-type@npm:~1.0.4": version: 1.0.4 resolution: "content-type@npm:1.0.4" @@ -13618,6 +13873,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^1.0.0, cookie@npm:^1.0.1": + version: 1.0.2 + resolution: "cookie@npm:1.0.2" + checksum: 10/f5817cdc84d8977761b12549eba29435e675e65c7fef172bc31737788cd8adc83796bf8abe6d950554e7987325ad2d9ac2971c5bd8ff0c4f81c145f82e4ab1be + languageName: node + linkType: hard + "cookie@npm:~0.4.1": version: 0.4.2 resolution: "cookie@npm:0.4.2" @@ -14362,6 +14624,13 @@ __metadata: languageName: node linkType: hard +"dequal@npm:^2.0.3": + version: 2.0.3 + resolution: "dequal@npm:2.0.3" + checksum: 10/6ff05a7561f33603df87c45e389c9ac0a95e3c056be3da1a0c4702149e3a7f6fe5ffbb294478687ba51a9e95f3a60e8b6b9005993acd79c292c7d15f71964b6b + languageName: node + linkType: hard + "destroy@npm:1.2.0, destroy@npm:^1.0.4": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -16097,6 +16366,20 @@ __metadata: languageName: node linkType: hard +"fast-json-stringify@npm:^6.0.0": + version: 6.0.1 + resolution: "fast-json-stringify@npm:6.0.1" + dependencies: + "@fastify/merge-json-schemas": "npm:^0.2.0" + ajv: "npm:^8.12.0" + ajv-formats: "npm:^3.0.1" + fast-uri: "npm:^3.0.0" + json-schema-ref-resolver: "npm:^2.0.0" + rfdc: "npm:^1.2.0" + checksum: 10/e8b0802df228a4f76a87daf7be47f05d071fa1dd827fcf1a43b9e92e5e1cc3b40ed6b999793d84d1c69c67e06bead0ac438ec1e0b0a1705bd9c53014bf5dfa87 + languageName: node + linkType: hard + "fast-levenshtein@npm:^2.0.6": version: 2.0.6 resolution: "fast-levenshtein@npm:2.0.6" @@ -16104,7 +16387,7 @@ __metadata: languageName: node linkType: hard -"fast-querystring@npm:^1.0.0": +"fast-querystring@npm:^1.0.0, fast-querystring@npm:^1.1.2": version: 1.1.2 resolution: "fast-querystring@npm:1.1.2" dependencies: @@ -16113,6 +16396,13 @@ __metadata: languageName: node linkType: hard +"fast-redact@npm:^3.1.1": + version: 3.5.0 + resolution: "fast-redact@npm:3.5.0" + checksum: 10/24b27e2023bd5a62f908d97a753b1adb8d89206b260f97727728e00b693197dea2fc2aa3711147a385d0ec6e713569fd533df37a4ef947e08cb65af3019c7ad5 + languageName: node + linkType: hard + "fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" @@ -16120,6 +16410,13 @@ __metadata: languageName: node linkType: hard +"fast-uri@npm:^3.0.0": + version: 3.0.6 + resolution: "fast-uri@npm:3.0.6" + checksum: 10/43c87cd03926b072a241590e49eca0e2dfe1d347ddffd4b15307613b42b8eacce00a315cf3c7374736b5f343f27e27ec88726260eb03a758336d507d6fbaba0a + languageName: node + linkType: hard + "fast-uri@npm:^3.0.1": version: 3.0.1 resolution: "fast-uri@npm:3.0.1" @@ -16159,6 +16456,63 @@ __metadata: languageName: node linkType: hard +"fastify-plugin@npm:^4.5.1": + version: 4.5.1 + resolution: "fastify-plugin@npm:4.5.1" + checksum: 10/7c6d777ada0f01c8a1166a2a669cccfd6074c7764121f07cce997745f198227a271c7a317aaf0da273b329f24307f0eba3f093d872d29b839b33deb525bbafe2 + languageName: node + linkType: hard + +"fastify-plugin@npm:^5.0.0": + version: 5.0.1 + resolution: "fastify-plugin@npm:5.0.1" + checksum: 10/76f6960558239d1ead520ecfb9dbb9b0435a63376d9d48bed0861609a909bf1958cb097745bb1a5485592f2c6d1438941e7481203c86b0e74d2bc34f09e8ed3e + languageName: node + linkType: hard + +"fastify-raw-body@npm:5.0.0": + version: 5.0.0 + resolution: "fastify-raw-body@npm:5.0.0" + dependencies: + fastify-plugin: "npm:^5.0.0" + raw-body: "npm:^3.0.0" + secure-json-parse: "npm:^2.4.0" + checksum: 10/561501a7d1b4af9e8f74c921f94173e6913dfabe0255a3ba41632d47a50ea61fd639c7d94999d4a2076240ff7eca7843de040eff8313138f7bc3cddc61eadf08 + languageName: node + linkType: hard + +"fastify@npm:5.2.1": + version: 5.2.1 + resolution: "fastify@npm:5.2.1" + dependencies: + "@fastify/ajv-compiler": "npm:^4.0.0" + "@fastify/error": "npm:^4.0.0" + "@fastify/fast-json-stringify-compiler": "npm:^5.0.0" + "@fastify/proxy-addr": "npm:^5.0.0" + abstract-logging: "npm:^2.0.1" + avvio: "npm:^9.0.0" + fast-json-stringify: "npm:^6.0.0" + find-my-way: "npm:^9.0.0" + light-my-request: "npm:^6.0.0" + pino: "npm:^9.0.0" + process-warning: "npm:^4.0.0" + rfdc: "npm:^1.3.1" + secure-json-parse: "npm:^3.0.1" + semver: "npm:^7.6.0" + toad-cache: "npm:^3.7.0" + checksum: 10/ac785822186ecac6183dd81659b8315c0567eca3c768e2acf89a9389cc6babe9dada0d886965129617d3c25cbd70c83975362a79000f8c5aab77229b3d84d464 + languageName: node + linkType: hard + +"fastq@npm:^1.17.1": + version: 1.18.0 + resolution: "fastq@npm:1.18.0" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10/c5b501333dc8f5d188d828ea162aad03ff5a81aed185b6d4a5078aaeae0a42babc34296d7af13ebce86401cccd93c9b7b3cbf61280821c5f20af233378b42fbb + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.13.0 resolution: "fastq@npm:1.13.0" @@ -16322,7 +16676,7 @@ __metadata: languageName: node linkType: hard -"find-my-way@npm:^9.1.0": +"find-my-way@npm:^9.0.0, find-my-way@npm:^9.1.0": version: 9.1.0 resolution: "find-my-way@npm:9.1.0" dependencies: @@ -18273,7 +18627,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -18669,6 +19023,13 @@ __metadata: languageName: node linkType: hard +"ipaddr.js@npm:^2.1.0": + version: 2.2.0 + resolution: "ipaddr.js@npm:2.2.0" + checksum: 10/9e1cdd9110b3bca5d910ab70d7fb1933e9c485d9b92cb14ef39f30c412ba3fe02a553921bf696efc7149cc653453c48ccf173adb996ec27d925f1f340f872986 + languageName: node + linkType: hard + "is-accessor-descriptor@npm:^0.1.6": version: 0.1.6 resolution: "is-accessor-descriptor@npm:0.1.6" @@ -19715,6 +20076,15 @@ __metadata: languageName: node linkType: hard +"json-schema-ref-resolver@npm:^2.0.0": + version: 2.0.1 + resolution: "json-schema-ref-resolver@npm:2.0.1" + dependencies: + dequal: "npm:^2.0.3" + checksum: 10/dca0c2eac8ef813d20cfe7a277d8a885fbacccb3bade199fd288b2d3af6ebfbc4cf8cd0ccdd478152e30519cbc5876fbc558834d8f95a7522daa5153cdfc1302 + languageName: node + linkType: hard + "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -20449,6 +20819,17 @@ __metadata: languageName: node linkType: hard +"light-my-request@npm:^6.0.0": + version: 6.5.1 + resolution: "light-my-request@npm:6.5.1" + dependencies: + cookie: "npm:^1.0.1" + process-warning: "npm:^4.0.0" + set-cookie-parser: "npm:^2.6.0" + checksum: 10/15a5ebebf868546ecad3bc5ba23923669761e3d4938cf0d640ead05f1878a8245d36e3432ebdcd2e7754674476c3c909e16c519d0abb3867f9f6a328256861a4 + languageName: node + linkType: hard + "lilconfig@npm:~3.1.2": version: 3.1.2 resolution: "lilconfig@npm:3.1.2" @@ -21817,6 +22198,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^3": + version: 3.0.0 + resolution: "mime@npm:3.0.0" + bin: + mime: cli.js + checksum: 10/b2d31580deb58be89adaa1877cbbf152b7604b980fd7ef8f08b9e96bfedf7d605d9c23a8ba62aa12c8580b910cd7c1d27b7331d0f40f7a14e17d5a0bbec3b49f + languageName: node + linkType: hard + "mime@npm:^4.0.0": version: 4.0.1 resolution: "mime@npm:4.0.1" @@ -23565,6 +23955,13 @@ __metadata: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 10/f7b4b7200026a08f6e4a17ba6d72e6c5cbb41789ed9cf7deaf9d9e322872c7dc5a7898549a894651ee0ee9ae635d34a678115bf8acdfba8ebd2ba2af688b563c + languageName: node + linkType: hard + "on-finished@npm:2.4.1, on-finished@npm:^2.3.0": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -24411,6 +24808,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^8.1.0": + version: 8.2.0 + resolution: "path-to-regexp@npm:8.2.0" + checksum: 10/23378276a172b8ba5f5fb824475d1818ca5ccee7bbdb4674701616470f23a14e536c1db11da9c9e6d82b82c556a817bbf4eee6e41b9ed20090ef9427cbb38e13 + languageName: node + linkType: hard + "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -24541,6 +24945,43 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^2.0.0": + version: 2.0.0 + resolution: "pino-abstract-transport@npm:2.0.0" + dependencies: + split2: "npm:^4.0.0" + checksum: 10/e5699ecb06c7121055978e988e5cecea5b6892fc2589c64f1f86df5e7386bbbfd2ada268839e911b021c6b3123428aed7c6be3ac7940eee139556c75324c7e83 + languageName: node + linkType: hard + +"pino-std-serializers@npm:^7.0.0": + version: 7.0.0 + resolution: "pino-std-serializers@npm:7.0.0" + checksum: 10/884e08f65aa5463d820521ead3779d4472c78fc434d8582afb66f9dcb8d8c7119c69524b68106cb8caf92c0487be7794cf50e5b9c0383ae65b24bf2a03480951 + languageName: node + linkType: hard + +"pino@npm:^9.0.0": + version: 9.6.0 + resolution: "pino@npm:9.6.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + fast-redact: "npm:^3.1.1" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^4.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10/0a36125718dc2350bbaff243e4856108a80805dc1b305da1e246460cd22396d11a8b3a78b39b0b270cce4fb8ae6aa6e584f5387f6c2ee47348aae5db49d919e6 + languageName: node + linkType: hard + "pirates@npm:^4.0.6": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -24751,6 +25192,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^4.0.0": + version: 4.0.1 + resolution: "process-warning@npm:4.0.1" + checksum: 10/8b0ec9129845215c1e4a72f3a66082e3aa76f81e265374de6c70f2213f4516856316ed88338b8520e9274dab947d6b3750684b448f45148f57757f365e96793f + languageName: node + linkType: hard + "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -25153,6 +25601,13 @@ __metadata: languageName: node linkType: hard +"quick-format-unescaped@npm:^4.0.3": + version: 4.0.4 + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: 10/591eca457509a99368b623db05248c1193aa3cedafc9a077d7acab09495db1231017ba3ad1b5386e5633271edd0a03b312d8640a59ee585b8516a42e15438aa7 + languageName: node + linkType: hard + "quick-lru@npm:^4.0.1": version: 4.0.1 resolution: "quick-lru@npm:4.0.1" @@ -25255,6 +25710,18 @@ __metadata: languageName: node linkType: hard +"raw-body@npm:^3.0.0": + version: 3.0.0 + resolution: "raw-body@npm:3.0.0" + dependencies: + bytes: "npm:3.1.2" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.6.3" + unpipe: "npm:1.0.0" + checksum: 10/2443429bbb2f9ae5c50d3d2a6c342533dfbde6b3173740b70fa0302b30914ff400c6d31a46b3ceacbe7d0925dc07d4413928278b494b04a65736fc17ca33e30c + languageName: node + linkType: hard + "razor-tmpl@npm:^1.3.1": version: 1.3.1 resolution: "razor-tmpl@npm:1.3.1" @@ -25530,6 +25997,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 10/ddf44ee76301c774e9c9f2826da8a3c5c9f8fc87310f4a364e803ef003aa1a43c378b4323051ced212097fff1af459070f4499338b36a7469df1d4f7e8c0ba4c + languageName: node + linkType: hard + "rechoir@npm:^0.8.0": version: 0.8.0 resolution: "rechoir@npm:0.8.0" @@ -26019,6 +26493,13 @@ __metadata: languageName: node linkType: hard +"rfdc@npm:^1.2.0, rfdc@npm:^1.4.1": + version: 1.4.1 + resolution: "rfdc@npm:1.4.1" + checksum: 10/2f3d11d3d8929b4bfeefc9acb03aae90f971401de0add5ae6c5e38fec14f0405e6a4aad8fdb76344bfdd20c5193110e3750cbbd28ba86d73729d222b6cf4a729 + languageName: node + linkType: hard + "rfdc@npm:^1.3.1": version: 1.3.1 resolution: "rfdc@npm:1.3.1" @@ -26026,13 +26507,6 @@ __metadata: languageName: node linkType: hard -"rfdc@npm:^1.4.1": - version: 1.4.1 - resolution: "rfdc@npm:1.4.1" - checksum: 10/2f3d11d3d8929b4bfeefc9acb03aae90f971401de0add5ae6c5e38fec14f0405e6a4aad8fdb76344bfdd20c5193110e3750cbbd28ba86d73729d222b6cf4a729 - languageName: node - linkType: hard - "right-align@npm:^0.1.1": version: 0.1.3 resolution: "right-align@npm:0.1.3" @@ -26310,6 +26784,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.3.1, safe-stable-stringify@npm:^2.4.3": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -26361,6 +26842,20 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^2.4.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: 10/974386587060b6fc5b1ac06481b2f9dbbb0d63c860cc73dc7533f27835fdb67b0ef08762dbfef25625c15bc0a0c366899e00076cb0d556af06b71e22f1dede4c + languageName: node + linkType: hard + +"secure-json-parse@npm:^3.0.1": + version: 3.0.2 + resolution: "secure-json-parse@npm:3.0.2" + checksum: 10/a83e2067eef8f5c25b0614f261593721d5cb56c7b739197cb64b7f1fcc6944b632aac4cbc8817cf5976b891a7d6eee8bd07e1aabe082eabf424922f112a7a356 + languageName: node + linkType: hard + "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0" @@ -26586,6 +27081,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.6.0": + version: 2.7.1 + resolution: "set-cookie-parser@npm:2.7.1" + checksum: 10/c92b1130032693342bca13ea1b1bc93967ab37deec4387fcd8c2a843c0ef2fd9a9f3df25aea5bb3976cd05a91c2cf4632dd6164d6e1814208fb7d7e14edd42b4 + languageName: node + linkType: hard + "set-function-length@npm:^1.1.1": version: 1.2.0 resolution: "set-function-length@npm:1.2.0" @@ -27055,6 +27557,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.2.0 + resolution: "sonic-boom@npm:4.2.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10/385ef7fb5ea5976c1d2a1fef0b6df8df6b7caba8696d2d67f689d60c05e3ea2d536752ce7e1c69b9fad844635f1036d07c446f8e8149f5c6a80e0040a455b310 + languageName: node + linkType: hard + "sort-keys@npm:^2.0.0": version: 2.0.0 resolution: "sort-keys@npm:2.0.0" @@ -28316,6 +28827,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10/ea2d816c4f6077a7062fac5414a88e82977f807c82ee330938fb9691fe11883bb03f078551c0518bb649c239e47ba113d44014fcbb5db42c5abd5996f35e4213 + languageName: node + linkType: hard + "through2@npm:^2.0.0, through2@npm:~2.0.0": version: 2.0.5 resolution: "through2@npm:2.0.5" @@ -28510,6 +29030,13 @@ __metadata: languageName: node linkType: hard +"toad-cache@npm:^3.7.0": + version: 3.7.0 + resolution: "toad-cache@npm:3.7.0" + checksum: 10/cdc62aacc047e94eab21697943e117bbb1938168a03e5e85fdba28ab6ea66f4796ff16b219019a64d2115048378f9dd1f4e62c78c1f1d4961d0b3d23f9a9374d + languageName: node + linkType: hard + "toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" @@ -28713,6 +29240,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.6.1": + version: 2.6.1 + resolution: "tslib@npm:2.6.1" + checksum: 10/5cf1aa7ea4ca7ee9b8aa3d80eb7ee86634b307fbefcb948a831c2b13728e21e156ef7fb9edcbe21f05c08f65e4cf4480587086f31133491ba1a49c9e0b28fc75 + languageName: node + linkType: hard + "tslib@npm:2.6.2, tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" From dc8812a6b57d2829e42e71dd1a63d992ef0af90c Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 13 Dec 2024 02:40:41 +0100 Subject: [PATCH 2/2] fix(platform-response-filter): improve the response type resolution --- .../PlatformContentTypeResolver.spec.ts | 35 +++++++++++++++++-- .../services/PlatformContentTypeResolver.ts | 24 +++++++------ .../src/services/__mock__/response.txt | 1 + 3 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 packages/platform/platform-response-filter/src/services/__mock__/response.txt diff --git a/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.spec.ts b/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.spec.ts index c183865fe18..a57507803e7 100644 --- a/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.spec.ts +++ b/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.spec.ts @@ -1,5 +1,6 @@ import {PlatformTest} from "@tsed/platform-http/testing"; import {EndpointMetadata, Get, Returns, View} from "@tsed/schema"; +import {createReadStream} from "fs"; import {PLATFORM_CONTENT_TYPE_RESOLVER} from "./PlatformContentTypeResolver.js"; import {PLATFORM_CONTENT_TYPES_CONTAINER} from "./PlatformContentTypesContainer.js"; @@ -30,7 +31,7 @@ describe("PlatformContentTypeResolver", () => { beforeEach(() => PlatformTest.create()); afterEach(() => PlatformTest.reset()); - it("should return the content type (undefined)", async () => { + it("should return the content type (undefined | Buffer)", async () => { class TestController { @Get("/") get() {} @@ -40,10 +41,40 @@ describe("PlatformContentTypeResolver", () => { ctx.endpoint = EndpointMetadata.get(TestController, "get"); - const result = await contentTypeResolver(data, ctx); + const result = await contentTypeResolver(Buffer.from("data"), ctx); + + expect(result).toEqual(undefined); + }); + it("should return the content type (undefined | Stream)", async () => { + class TestController { + @Get("/") + get() {} + } + + const stream = createReadStream(`${import.meta.dirname}/__mock__/response.txt`); + + const {contentTypeResolver, ctx, data} = await getTestFixture(); + + ctx.endpoint = EndpointMetadata.get(TestController, "get"); + + const result = await contentTypeResolver(stream, ctx); expect(result).toEqual(undefined); }); + it("should return the content type (object - empty resolve content type - application/json)", async () => { + class TestController { + @Get("/") + get() {} + } + + const {contentTypeResolver, ctx, data} = await getTestFixture(); + + ctx.endpoint = EndpointMetadata.get(TestController, "get"); + + const result = await contentTypeResolver(data, ctx); + + expect(result).toEqual("application/json"); + }); it("should return the content type (object - application/json)", async () => { class TestController { @Get("/") diff --git a/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.ts b/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.ts index 82ee1bdb14c..0fb81e64fd6 100644 --- a/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.ts +++ b/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.ts @@ -1,29 +1,33 @@ -import {isObject} from "@tsed/core"; +import {isObject, isStream} from "@tsed/core"; import {type BaseContext, inject, injectable} from "@tsed/di"; import {ANY_CONTENT_TYPE} from "../constants/ANY_CONTENT_TYPE.js"; import {PLATFORM_CONTENT_TYPES_CONTAINER} from "./PlatformContentTypesContainer.js"; /** + * Determine the content type of the response based on the data and the context. + * @param {unknown} data + * @param {BaseContext} ctx * @ignore */ -export function getContentType(data: any, ctx: BaseContext) { - const {endpoint, response} = ctx; - const {operation} = endpoint; +export function getContentType(data: unknown, ctx: BaseContext) { + const { + endpoint, + endpoint: {operation}, + response + } = ctx; - const contentType = response.getContentType() || operation.getContentTypeOf(response.statusCode) || ""; + const contentType = response.getContentType() || operation.getContentTypeOf(response.statusCode); if (contentType && contentType !== ANY_CONTENT_TYPE) { - if (contentType === "application/json" && isObject(data)) { - return "application/json"; - } - - return contentType; + return contentType === "application/json" && isObject(data) ? "application/json" : contentType; } if (endpoint.view) { return "text/html"; } + + return isObject(data) && !isStream(data) && !Buffer.isBuffer(data) ? "application/json" : contentType; } /** diff --git a/packages/platform/platform-response-filter/src/services/__mock__/response.txt b/packages/platform/platform-response-filter/src/services/__mock__/response.txt new file mode 100644 index 00000000000..9daeafb9864 --- /dev/null +++ b/packages/platform/platform-response-filter/src/services/__mock__/response.txt @@ -0,0 +1 @@ +test