Skip to content

Commit

Permalink
feat(platform-response-filter): expose only .transform method to faci…
Browse files Browse the repository at this point in the history
…litate OverrideProvider usage
  • Loading branch information
Romakita committed Dec 31, 2024
1 parent 6e7a023 commit a0e668e
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 143 deletions.
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
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 transform", () => {
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 transform", () => {
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));

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);
} 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

This file was deleted.

15 changes: 0 additions & 15 deletions packages/platform/platform-response-filter/src/utils/renderView.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit a0e668e

Please sign in to comment.