Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(platform-response-filter): expose only .transform method to facilitate OverrideProvider usage #2948

Merged
merged 1 commit into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {JsonMethodStore} from "@tsed/schema";

export function inspectOperationsPaths(endpoint: JsonMethodStore) {
return [...endpoint.operationPaths.values()].map(({method, path}) => ({
method,
path
}));
}
20 changes: 11 additions & 9 deletions packages/platform/platform-http/test/integration/route.spec.ts
Original file line number Diff line number Diff line change
@@ -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)", () => {
Expand All @@ -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: "/"
Expand All @@ -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: "/"
Expand All @@ -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: "/"
Expand All @@ -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: "/"
Expand All @@ -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: "/"
Expand All @@ -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: "/"
Expand All @@ -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: "/"
Expand All @@ -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: "/"
Expand All @@ -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: "/"
Expand Down
1 change: 0 additions & 1 deletion packages/platform/platform-response-filter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -164,7 +164,6 @@ describe("PlatformResponseFilter", () => {
});
});
});

describe("when filter list is not given", () => {
beforeEach(() =>
PlatformTest.create({
Expand Down Expand Up @@ -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>(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"});
});
Expand All @@ -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});
Expand All @@ -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: []});
Expand All @@ -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, {
Expand All @@ -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");
});
Expand All @@ -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>(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>(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));
Romakita marked this conversation as resolved.
Show resolved Hide resolved

expect(actualError.message).toEqual("Template rendering error: Test.test()\nError: parser error");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -15,57 +18,82 @@ export class PlatformResponseFilter {
protected contentTypeResolver = inject<PLATFORM_CONTENT_TYPE_RESOLVER>(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<unknown> {
const {endpoint} = $ctx;

if (ctx.endpoint?.operation) {
const bestContentType = this.contentTypeResolver(data, ctx);
if (endpoint) {
if (endpoint.view) {
data = await this.renderView(data, $ctx);
Romakita marked this conversation as resolved.
Show resolved Hide resolved
} 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;
}

/**
* Serialize data before calling filters
* @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(","));
}
Expand Down
Loading
Loading