diff --git a/packages/platform/platform-http/src/common/services/PlatformHandler.ts b/packages/platform/platform-http/src/common/services/PlatformHandler.ts index 34f33918a33..a292165ca15 100644 --- a/packages/platform/platform-http/src/common/services/PlatformHandler.ts +++ b/packages/platform/platform-http/src/common/services/PlatformHandler.ts @@ -124,8 +124,7 @@ export class PlatformHandler { const {response} = $ctx; if (!$ctx.isDone()) { - let data = await this.responseFilter.serialize($ctx.data, $ctx as any); - data = await this.responseFilter.transform(data, $ctx as any); + const data = await this.responseFilter.transform($ctx.data, $ctx as any); response.body(data); } }); diff --git a/packages/platform/platform-http/test/integration/__fixtures__/inspectOperationsPaths.ts b/packages/platform/platform-http/test/integration/__fixtures__/inspectOperationsPaths.ts new file mode 100644 index 00000000000..dd27c6f27d8 --- /dev/null +++ b/packages/platform/platform-http/test/integration/__fixtures__/inspectOperationsPaths.ts @@ -0,0 +1,8 @@ +import type {JsonMethodStore} from "@tsed/schema"; + +export function inspectOperationsPaths(endpoint: JsonMethodStore) { + return [...endpoint.operationPaths.values()].map(({method, path}) => ({ + method, + path + })); +} diff --git a/packages/platform/platform-http/test/integration/route.spec.ts b/packages/platform/platform-http/test/integration/route.spec.ts index 2b057259519..455cb0459d3 100644 --- a/packages/platform/platform-http/test/integration/route.spec.ts +++ b/packages/platform/platform-http/test/integration/route.spec.ts @@ -1,5 +1,7 @@ import {All, Delete, EndpointMetadata, Get, Head, OperationVerbs, Options, Patch, Post, Put} from "@tsed/schema"; +import {inspectOperationsPaths} from "./__fixtures__/inspectOperationsPaths.js"; + describe("Route decorators", () => { describe("All", () => { it("should register route and middleware (1)", () => { @@ -12,7 +14,7 @@ describe("Route decorators", () => { const endpoint = EndpointMetadata.get(Test, "test"); // THEN - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.ALL, path: "/" @@ -33,7 +35,7 @@ describe("Route decorators", () => { const endpoint = EndpointMetadata.get(Test, "test"); // THEN - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.GET, path: "/" @@ -53,7 +55,7 @@ describe("Route decorators", () => { const endpoint = EndpointMetadata.get(Test, "test"); // THEN - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.GET, path: "/" @@ -75,7 +77,7 @@ describe("Route decorators", () => { const endpoint = EndpointMetadata.get(Test, "test"); // THEN - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.POST, path: "/" @@ -96,7 +98,7 @@ describe("Route decorators", () => { const endpoint = EndpointMetadata.get(Test, "test"); // THEN - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.PUT, path: "/" @@ -117,7 +119,7 @@ describe("Route decorators", () => { const endpoint = EndpointMetadata.get(Test, "test"); // THEN - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.DELETE, path: "/" @@ -138,7 +140,7 @@ describe("Route decorators", () => { const endpoint = EndpointMetadata.get(Test, "test"); // THEN - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.HEAD, path: "/" @@ -159,7 +161,7 @@ describe("Route decorators", () => { const endpoint = EndpointMetadata.get(Test, "test"); // THEN - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.PATCH, path: "/" @@ -180,7 +182,7 @@ describe("Route decorators", () => { const endpoint = EndpointMetadata.get(Test, "test"); // THEN - expect([...endpoint.operationPaths.values()]).toEqual([ + expect(inspectOperationsPaths(endpoint)).toEqual([ { method: OperationVerbs.OPTIONS, path: "/" diff --git a/packages/platform/platform-response-filter/src/index.ts b/packages/platform/platform-response-filter/src/index.ts index d97d48c4c33..afcc1791f64 100644 --- a/packages/platform/platform-response-filter/src/index.ts +++ b/packages/platform/platform-response-filter/src/index.ts @@ -9,4 +9,3 @@ export * from "./interfaces/ResponseFilterMethods.js"; export * from "./services/PlatformContentTypeResolver.js"; export * from "./services/PlatformContentTypesContainer.js"; export * from "./services/PlatformResponseFilter.js"; -export * from "./utils/renderView.js"; diff --git a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.spec.ts b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.spec.ts index 35ce0b813ba..f5e52685690 100644 --- a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.spec.ts +++ b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.spec.ts @@ -1,7 +1,7 @@ import {catchAsyncError} from "@tsed/core"; import {PlatformTest} from "@tsed/platform-http/testing"; import {Context} from "@tsed/platform-params"; -import {EndpointMetadata, Get, Returns, View} from "@tsed/schema"; +import {EndpointMetadata, Get, Ignore, Property, Returns, View} from "@tsed/schema"; import {ResponseFilter} from "../decorators/responseFilter.js"; import {ResponseFilterMethods} from "../interfaces/ResponseFilterMethods.js"; @@ -29,7 +29,7 @@ class AllFilter implements ResponseFilterMethods { } describe("PlatformResponseFilter", () => { - describe("transform()", () => { + describe("transform() with registered filters", () => { describe("when filter list is given", () => { beforeEach(() => PlatformTest.create({ @@ -164,7 +164,6 @@ describe("PlatformResponseFilter", () => { }); }); }); - describe("when filter list is not given", () => { beforeEach(() => PlatformTest.create({ @@ -228,18 +227,14 @@ describe("PlatformResponseFilter", () => { }); }); }); - describe("serialize()", () => { - beforeEach(() => - PlatformTest.create({ - responseFilters: [CustomJsonFilter, AllFilter, ApplicationJsonFilter] - }) - ); + describe("transform() without registered filters", () => { + beforeEach(() => PlatformTest.create()); afterEach(() => PlatformTest.reset()); it("should transform value", async () => { const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); const ctx = PlatformTest.createRequestContext(); - const result = await platformResponseFilter.serialize({test: "test"}, ctx); + const result = await platformResponseFilter.transform({test: "test"}, ctx); expect(result).toEqual({test: "test"}); }); @@ -255,7 +250,7 @@ describe("PlatformResponseFilter", () => { vi.spyOn(ctx.endpoint, "getResponseOptions"); - const result = await platformResponseFilter.serialize({test: "test"}, ctx); + const result = await platformResponseFilter.transform({test: "test"}, ctx); expect(result).toEqual({test: "test"}); expect(ctx.endpoint.getResponseOptions).toHaveBeenCalledWith(200, {includes: undefined}); @@ -274,7 +269,7 @@ describe("PlatformResponseFilter", () => { ctx.request.query.includes = []; - const result = await platformResponseFilter.serialize({test: "test"}, ctx); + const result = await platformResponseFilter.transform({test: "test"}, ctx); expect(result).toEqual({test: "test"}); expect(ctx.endpoint.getResponseOptions).toHaveBeenCalledWith(200, {includes: []}); @@ -293,7 +288,7 @@ describe("PlatformResponseFilter", () => { ctx.request.query.includes = ["test,test2"]; - const result = await platformResponseFilter.serialize({test: "test"}, ctx); + const result = await platformResponseFilter.transform({test: "test"}, ctx); expect(result).toEqual({test: "test"}); expect(ctx.endpoint.getResponseOptions).toHaveBeenCalledWith(200, { @@ -312,7 +307,7 @@ describe("PlatformResponseFilter", () => { ctx.endpoint = EndpointMetadata.get(Test, "test"); vi.spyOn(ctx.response, "render").mockResolvedValue("template"); - const result = await platformResponseFilter.serialize({test: "test"}, ctx); + const result = await platformResponseFilter.transform({test: "test"}, ctx); expect(result).toEqual("template"); }); @@ -328,9 +323,62 @@ describe("PlatformResponseFilter", () => { ctx.endpoint = EndpointMetadata.get(Test, "test"); vi.spyOn(ctx.response, "render").mockRejectedValue(new Error("parsing error")); - const result = await catchAsyncError(() => platformResponseFilter.serialize({test: "test"}, ctx)); + const result = await catchAsyncError(() => platformResponseFilter.transform({test: "test"}, ctx)); expect(result?.message).toEqual("Template rendering error: Test.test()\nError: parsing error"); }); + + it("should render content", async () => { + class Model { + @Property() + data: string; + + @Ignore() + test: string; + } + + class Test { + @Get("/") + @View("view", {options: "options"}) + @Returns(200, Model) + test() {} + } + + const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); + const ctx = PlatformTest.createRequestContext(); + + ctx.endpoint = EndpointMetadata.get(Test, "test"); + + vi.spyOn(ctx.response, "render").mockResolvedValue("HTML"); + + ctx.data = {data: "data"}; + + await platformResponseFilter.transform(ctx.data, ctx); + + expect(ctx.response.render).toHaveBeenCalledWith("view", { + $ctx: ctx, + data: "data", + options: "options" + }); + }); + it("should render content and throw an error", async () => { + class Test { + @Get("/") + @View("view", {options: "options"}) + test() {} + } + + const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); + const ctx = PlatformTest.createRequestContext(); + ctx.endpoint = EndpointMetadata.get(Test, "test"); + + vi.spyOn(ctx.response, "render").mockRejectedValue(new Error("parser error")); + + ctx.data = {data: "data"}; + + let actualError: any = await catchAsyncError(() => platformResponseFilter.transform(ctx.data, ctx)); + + expect(actualError.message).toEqual("Template rendering error: Test.test()\nError: parser error"); + }); }); }); diff --git a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts index 6aa7c78e9be..512a59768bd 100644 --- a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts +++ b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts @@ -2,11 +2,14 @@ import {isSerializable} from "@tsed/core"; import {BaseContext, constant, inject, injectable} from "@tsed/di"; import {serialize} from "@tsed/json-mapper"; -import {renderView} from "../utils/renderView.js"; +import {TemplateRenderError} from "../errors/TemplateRenderError.js"; import {PLATFORM_CONTENT_TYPE_RESOLVER} from "./PlatformContentTypeResolver.js"; import {PLATFORM_CONTENT_TYPES_CONTAINER} from "./PlatformContentTypesContainer.js"; /** + * PlatformResponseFilter is responsible for transforming the response data + * to the appropriate format based on the endpoint metadata and context. + * * @platform */ export class PlatformResponseFilter { @@ -15,26 +18,39 @@ export class PlatformResponseFilter { protected contentTypeResolver = inject(PLATFORM_CONTENT_TYPE_RESOLVER); /** - * Call filters to transform data - * @param data - * @param ctx + * Transform the data to the right format. + * @param data The data to transform. + * @param $ctx The context. */ - transform(data: unknown, ctx: BaseContext) { - const {response} = ctx; + async transform(data: unknown, $ctx: BaseContext): Promise { + const {endpoint} = $ctx; - if (ctx.endpoint?.operation) { - const bestContentType = this.contentTypeResolver(data, ctx); + if (endpoint) { + if (endpoint.view) { + data = await this.renderView(data, $ctx); + } else if (isSerializable(data)) { + data = await this.serialize(data, $ctx); + } + } - bestContentType && response.contentType(bestContentType); + return this.resolve(data, $ctx); + } - const resolved = this.container.resolve(bestContentType); + /** + * Render the view with the given data. + * @param data The data to render. + * @param $ctx The context. + * @protected + */ + protected async renderView(data: unknown, $ctx: BaseContext) { + const {response, endpoint} = $ctx; + try { + const {path, options} = endpoint.view; - if (resolved) { - return resolved.transform(data, ctx); - } + return await response.render(path, {...options, ...(data as object), $ctx}); + } catch (err) { + throw new TemplateRenderError(endpoint.targetName, endpoint.propertyKey, err); } - - return data; } /** @@ -42,30 +58,42 @@ export class PlatformResponseFilter { * @param data * @param ctx */ - async serialize(data: unknown, ctx: BaseContext) { + protected async serialize(data: unknown, ctx: BaseContext) { const {response, endpoint} = ctx; - if (endpoint) { - if (endpoint.view) { - data = await renderView(data, ctx); - } else if (isSerializable(data)) { - const responseOpts = endpoint.getResponseOptions(response.statusCode, { - includes: this.getIncludes(ctx) - }); - - data = serialize(data, { - useAlias: true, - additionalProperties: this.additionalProperties, - ...responseOpts, - endpoint: true - }); + const responseOpts = endpoint.getResponseOptions(response.statusCode, { + includes: this.getIncludes(ctx) + }); + + data = serialize(data, { + useAlias: true, + additionalProperties: this.additionalProperties, + ...responseOpts, + endpoint: true + }); + + return data; + } + + protected resolve(data: any, ctx: BaseContext) { + const {response} = ctx; + + if (ctx.endpoint?.operation) { + const bestContentType = this.contentTypeResolver(data, ctx); + + bestContentType && response.contentType(bestContentType); + + const resolved = this.container.resolve(bestContentType); + + if (resolved) { + return resolved.transform(data, ctx); } } return data; } - private getIncludes(ctx: BaseContext) { + protected 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/renderView.spec.ts b/packages/platform/platform-response-filter/src/utils/renderView.spec.ts deleted file mode 100644 index 02ed555cd06..00000000000 --- a/packages/platform/platform-response-filter/src/utils/renderView.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {PlatformTest} from "@tsed/platform-http/testing"; -import {EndpointMetadata, Get, Ignore, Property, Returns, View} from "@tsed/schema"; - -import {renderView} from "./renderView.js"; - -describe("renderView", () => { - beforeEach(() => PlatformTest.create()); - afterEach(() => PlatformTest.reset()); - - it("should render content", async () => { - class Model { - @Property() - data: string; - - @Ignore() - test: string; - } - - class Test { - @Get("/") - @View("view", {options: "options"}) - @Returns(200, Model) - test() {} - } - - const ctx = PlatformTest.createRequestContext(); - ctx.endpoint = EndpointMetadata.get(Test, "test"); - - vi.spyOn(ctx.response, "render").mockResolvedValue("HTML"); - - ctx.data = {data: "data"}; - - await renderView(ctx.data, ctx); - - expect(ctx.response.render).toHaveBeenCalledWith("view", { - $ctx: ctx, - data: "data", - options: "options" - }); - }); - it("should throw an error", async () => { - class Test { - @Get("/") - @View("view", {options: "options"}) - test() {} - } - - const ctx = PlatformTest.createRequestContext(); - ctx.endpoint = EndpointMetadata.get(Test, "test"); - - vi.spyOn(ctx.response, "render").mockRejectedValue(new Error("parser error")); - - ctx.data = {data: "data"}; - - let actualError: any; - try { - await renderView(ctx.data, ctx); - } catch (er) { - actualError = er; - } - - expect(actualError.message).toEqual("Template rendering error: Test.test()\nError: parser error"); - }); -}); diff --git a/packages/platform/platform-response-filter/src/utils/renderView.ts b/packages/platform/platform-response-filter/src/utils/renderView.ts deleted file mode 100644 index 280d463c326..00000000000 --- a/packages/platform/platform-response-filter/src/utils/renderView.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {BaseContext} from "@tsed/di"; - -import {TemplateRenderError} from "../errors/TemplateRenderError.js"; - -export async function renderView(data: any, $ctx: BaseContext) { - const {response, endpoint} = $ctx; - try { - const {data} = $ctx; - const {path, options} = endpoint.view; - - return await response.render(path, {...options, ...data, $ctx}); - } catch (err) { - throw new TemplateRenderError(endpoint.targetName, endpoint.propertyKey, err); - } -} diff --git a/packages/third-parties/formio/src/components/AlterActions.ts b/packages/third-parties/formio/src/components/AlterActions.ts index 27dd7aff6e4..217c93c9a0e 100644 --- a/packages/third-parties/formio/src/components/AlterActions.ts +++ b/packages/third-parties/formio/src/components/AlterActions.ts @@ -126,7 +126,6 @@ export class AlterActions implements AlterHook { if (!$ctx.isDone()) { setResponseHeaders($ctx); - data = await this.responseFilter.serialize(data, $ctx as any); data = await this.responseFilter.transform(data, $ctx as any); response.body(data); diff --git a/packages/third-parties/formio/vitest.config.mts b/packages/third-parties/formio/vitest.config.mts index b01f2296164..40b02954f85 100644 --- a/packages/third-parties/formio/vitest.config.mts +++ b/packages/third-parties/formio/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 95.77, + statements: 95.76, branches: 96.66, functions: 96.85, - lines: 95.77 + lines: 95.76 } } } } -); \ No newline at end of file +); diff --git a/packages/third-parties/sse/src/domain/EventStreamContext.ts b/packages/third-parties/sse/src/domain/EventStreamContext.ts index 6c000472d73..78956eff0a1 100644 --- a/packages/third-parties/sse/src/domain/EventStreamContext.ts +++ b/packages/third-parties/sse/src/domain/EventStreamContext.ts @@ -95,19 +95,14 @@ export class EventStreamContext { return this; } - this.responseFilter - .serialize(data, $ctx as any) - .then((data: unknown) => { - return this.responseFilter.transform(data, $ctx as any); - }) - .then((data: unknown) => { - data = JSON.stringify(data); + this.responseFilter.transform(data, $ctx as any).then((data: unknown) => { + data = JSON.stringify(data); - this.write({ - event: event || "", - data: data as string - }); + this.write({ + event: event || "", + data: data as string }); + }); return this; } diff --git a/packages/third-parties/sse/vitest.config.mts b/packages/third-parties/sse/vitest.config.mts index 73cb4db6220..9bf80fce52b 100644 --- a/packages/third-parties/sse/vitest.config.mts +++ b/packages/third-parties/sse/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 52.28, - branches: 75, + statements: 50.67, + branches: 73.91, functions: 64.28, - lines: 52.28 + lines: 50.67 } } } } -); \ No newline at end of file +);