diff --git a/packages/platform/platform-response-filter/src/constants/ANY_CONTENT_TYPE.ts b/packages/platform/platform-response-filter/src/constants/ANY_CONTENT_TYPE.ts new file mode 100644 index 00000000000..3163198c351 --- /dev/null +++ b/packages/platform/platform-response-filter/src/constants/ANY_CONTENT_TYPE.ts @@ -0,0 +1 @@ +export const ANY_CONTENT_TYPE = "*/*"; diff --git a/packages/platform/platform-response-filter/src/index.ts b/packages/platform/platform-response-filter/src/index.ts index 68aad9ad560..d97d48c4c33 100644 --- a/packages/platform/platform-response-filter/src/index.ts +++ b/packages/platform/platform-response-filter/src/index.ts @@ -1,10 +1,12 @@ /** * @file Automatically generated by @tsed/barrels. */ +export * from "./constants/ANY_CONTENT_TYPE.js"; export * from "./decorators/responseFilter.js"; export * from "./domain/ResponseFiltersContainer.js"; export * from "./errors/TemplateRenderError.js"; export * from "./interfaces/ResponseFilterMethods.js"; +export * from "./services/PlatformContentTypeResolver.js"; +export * from "./services/PlatformContentTypesContainer.js"; export * from "./services/PlatformResponseFilter.js"; -export * from "./utils/getContentType.js"; export * from "./utils/renderView.js"; diff --git a/packages/platform/platform-response-filter/src/utils/getContentType.spec.ts b/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.spec.ts similarity index 55% rename from packages/platform/platform-response-filter/src/utils/getContentType.spec.ts rename to packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.spec.ts index 0e8255aa575..c183865fe18 100644 --- a/packages/platform/platform-response-filter/src/utils/getContentType.spec.ts +++ b/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.spec.ts @@ -1,94 +1,102 @@ import {PlatformTest} from "@tsed/platform-http/testing"; import {EndpointMetadata, Get, Returns, View} from "@tsed/schema"; -import {getContentType} from "./getContentType.js"; +import {PLATFORM_CONTENT_TYPE_RESOLVER} from "./PlatformContentTypeResolver.js"; +import {PLATFORM_CONTENT_TYPES_CONTAINER} from "./PlatformContentTypesContainer.js"; + +async function getTestFixture(contentTypes = ["application/json", "text/html"]) { + const contentTypeResolver = await PlatformTest.invoke(PLATFORM_CONTENT_TYPE_RESOLVER, [ + { + token: PLATFORM_CONTENT_TYPES_CONTAINER, + use: { + contentTypes + } + } + ]); + + const data = { + test: "test" + }; + + const ctx = PlatformTest.createRequestContext(); + return { + data, + ctx, + contentTypeResolver + }; +} -describe("getContentType", () => { +describe("PlatformContentTypeResolver", () => { beforeEach(() => PlatformTest.create()); afterEach(() => PlatformTest.reset()); - it("should return the content type (undefined)", () => { + it("should return the content type (undefined)", async () => { class TestController { @Get("/") get() {} } - const ctx = PlatformTest.createRequestContext(); + const {contentTypeResolver, ctx, data} = await getTestFixture(); + ctx.endpoint = EndpointMetadata.get(TestController, "get"); - const result = getContentType( - { - test: "test" - }, - ctx - ); + const result = await contentTypeResolver(data, ctx); expect(result).toEqual(undefined); }); - it("should return the content type (object - application/json)", () => { + it("should return the content type (object - application/json)", async () => { class TestController { @Get("/") @(Returns(200).ContentType("application/json")) get() {} } - const ctx = PlatformTest.createRequestContext(); + const {contentTypeResolver, ctx, data} = await getTestFixture(); + ctx.endpoint = EndpointMetadata.get(TestController, "get"); ctx.response.getRes().statusCode = 200; + vi.spyOn(ctx.response, "getContentType").mockReturnValue("application/json"); - const result = getContentType( - { - test: "test" - }, - ctx - ); + const result = contentTypeResolver(data, ctx); expect(result).toEqual("application/json"); }); - it("should return the content type (string - application/json)", () => { + it("should return the content type (string - application/json)", async () => { class TestController { @Get("/") @(Returns(200).ContentType("application/json")) get() {} } - const ctx = PlatformTest.createRequestContext(); + const {contentTypeResolver, ctx, data} = await getTestFixture(); + ctx.endpoint = EndpointMetadata.get(TestController, "get"); ctx.response.getRes().statusCode = 200; vi.spyOn(ctx.response, "getContentType").mockReturnValue("application/json"); - const result = getContentType( - { - test: "test" - }, - ctx - ); + const result = contentTypeResolver(data, ctx); expect(result).toEqual("application/json"); }); - it("should return the content type (string - text/html)", () => { + it("should return the content type (string - text/html)", async () => { class TestController { @Get("/") @(Returns(200).ContentType("text/html")) get() {} } - const ctx = PlatformTest.createRequestContext(); + const {contentTypeResolver, ctx, data} = await getTestFixture(); + ctx.endpoint = EndpointMetadata.get(TestController, "get"); ctx.response.getRes().statusCode = 200; vi.spyOn(ctx.response, "getContentType").mockReturnValue("text/html"); - const result = getContentType( - { - test: "test" - }, - ctx - ); + const result = contentTypeResolver(data, ctx); expect(result).toEqual("text/html"); }); - it("should return the content type (string - view)", () => { + it("should return the content type (string - view)", async () => { class TestController { @Get("/") @Returns(200) @@ -96,17 +104,13 @@ describe("getContentType", () => { get() {} } - const ctx = PlatformTest.createRequestContext(); + const {contentTypeResolver, ctx, data} = await getTestFixture(); + ctx.endpoint = EndpointMetadata.get(TestController, "get"); ctx.response.getRes().statusCode = 200; ctx.view = "true"; - const result = getContentType( - { - test: "test" - }, - ctx - ); + const result = contentTypeResolver(data, ctx); expect(result).toEqual("text/html"); }); diff --git a/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.ts b/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.ts new file mode 100644 index 00000000000..82ee1bdb14c --- /dev/null +++ b/packages/platform/platform-response-filter/src/services/PlatformContentTypeResolver.ts @@ -0,0 +1,54 @@ +import {isObject} 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"; + +/** + * @ignore + */ +export function getContentType(data: any, ctx: BaseContext) { + const {endpoint, response} = ctx; + const {operation} = endpoint; + + 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; + } + + if (endpoint.view) { + return "text/html"; + } +} + +/** + * @ignore + */ +function resolver(data: any, ctx: BaseContext) { + const contentType = getContentType(data, ctx); + + if (ctx.request.get("Accept")) { + const {contentTypes} = inject(PLATFORM_CONTENT_TYPES_CONTAINER); + + const bestContentType = ctx.request.accepts([contentType].concat(contentTypes).filter(Boolean)); + + if (bestContentType) { + return [].concat(bestContentType as any).filter((type) => type !== "*/*")[0]; + } + } + + return contentType; +} + +/** + * @ignore + */ +export type PLATFORM_CONTENT_TYPE_RESOLVER = typeof resolver; +export const PLATFORM_CONTENT_TYPE_RESOLVER = injectable(Symbol.for("PLATFORM_CONTENT_TYPE_RESOLVER")) + .factory(() => resolver) + .token(); diff --git a/packages/platform/platform-response-filter/src/services/PlatformContentTypesContainer.ts b/packages/platform/platform-response-filter/src/services/PlatformContentTypesContainer.ts new file mode 100644 index 00000000000..287347d3f07 --- /dev/null +++ b/packages/platform/platform-response-filter/src/services/PlatformContentTypesContainer.ts @@ -0,0 +1,31 @@ +import type {Type} from "@tsed/core"; +import {constant, inject, injectable, type TokenProvider} from "@tsed/di"; + +import {ANY_CONTENT_TYPE} from "../constants/ANY_CONTENT_TYPE.js"; +import {ResponseFilterKey, ResponseFiltersContainer} from "../domain/ResponseFiltersContainer.js"; +import type {ResponseFilterMethods} from "../interfaces/ResponseFilterMethods.js"; + +function factory() { + const responseFilters = constant[]>("responseFilters", []); + const containers: Map = new Map(); + + ResponseFiltersContainer.forEach((token, type) => { + if (responseFilters.includes(token)) { + containers.set(type, token); + } + }); + + return { + contentTypes: [...containers.keys()], + resolve(bestContentType: string) { + const token = containers.get(bestContentType) || containers.get(ANY_CONTENT_TYPE); + + if (token) { + return inject(token); + } + } + }; +} + +export type PLATFORM_CONTENT_TYPES_CONTAINER = ReturnType; +export const PLATFORM_CONTENT_TYPES_CONTAINER = injectable(Symbol.for("PLATFORM_CONTENT_TYPES_CONTAINER")).factory(factory).token(); diff --git a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts index 979a6f7ccab..6aa7c78e9be 100644 --- a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts +++ b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts @@ -1,45 +1,18 @@ -import {isSerializable, Type} from "@tsed/core"; -import {BaseContext, constant, inject, injectable, TokenProvider} from "@tsed/di"; +import {isSerializable} from "@tsed/core"; +import {BaseContext, constant, inject, injectable} from "@tsed/di"; import {serialize} from "@tsed/json-mapper"; -import {ResponseFilterKey, ResponseFiltersContainer} from "../domain/ResponseFiltersContainer.js"; -import {ResponseFilterMethods} from "../interfaces/ResponseFilterMethods.js"; -import {ANY_CONTENT_TYPE, getContentType} from "../utils/getContentType.js"; import {renderView} from "../utils/renderView.js"; +import {PLATFORM_CONTENT_TYPE_RESOLVER} from "./PlatformContentTypeResolver.js"; +import {PLATFORM_CONTENT_TYPES_CONTAINER} from "./PlatformContentTypesContainer.js"; /** * @platform */ export class PlatformResponseFilter { - protected types: Map = new Map(); - protected responseFilters = constant[]>("responseFilters", []); protected additionalProperties = constant("additionalProperties"); - - constructor() { - ResponseFiltersContainer.forEach((token, type) => { - if (this.responseFilters.includes(token)) { - this.types.set(type, token); - } - }); - } - - get contentTypes(): ResponseFilterKey[] { - return [...this.types.keys()]; - } - - getBestContentType(data: any, ctx: BaseContext) { - const contentType = getContentType(data, ctx); - - if (ctx.request.get("Accept")) { - const bestContentType = ctx.request.accepts([contentType].concat(this.contentTypes).filter(Boolean)); - - if (bestContentType) { - return [].concat(bestContentType as any).filter((type) => type !== "*/*")[0]; - } - } - - return contentType; - } + protected container = inject(PLATFORM_CONTENT_TYPES_CONTAINER); + protected contentTypeResolver = inject(PLATFORM_CONTENT_TYPE_RESOLVER); /** * Call filters to transform data @@ -50,11 +23,11 @@ export class PlatformResponseFilter { const {response} = ctx; if (ctx.endpoint?.operation) { - const bestContentType = this.getBestContentType(data, ctx); + const bestContentType = this.contentTypeResolver(data, ctx); bestContentType && response.contentType(bestContentType); - const resolved = this.resolve(bestContentType); + const resolved = this.container.resolve(bestContentType); if (resolved) { return resolved.transform(data, ctx); @@ -92,14 +65,6 @@ export class PlatformResponseFilter { return data; } - private resolve(bestContentType: string) { - const token = this.types.get(bestContentType) || this.types.get(ANY_CONTENT_TYPE); - - if (token) { - return inject(token); - } - } - private getIncludes(ctx: BaseContext) { if (ctx.request.query.includes) { return [].concat(ctx.request.query.includes).flatMap((include: string) => include.split(",")); diff --git a/packages/platform/platform-response-filter/src/utils/getContentType.ts b/packages/platform/platform-response-filter/src/utils/getContentType.ts deleted file mode 100644 index 573e666a273..00000000000 --- a/packages/platform/platform-response-filter/src/utils/getContentType.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {isObject} from "@tsed/core"; -import type {BaseContext} from "@tsed/di"; - -export const ANY_CONTENT_TYPE = "*/*"; - -/** - * @ignore - */ -export function getContentType(data: any, ctx: BaseContext) { - const {endpoint, response} = ctx; - const {operation} = endpoint; - - 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; - } - - if (endpoint.view) { - return "text/html"; - } -}