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 68df38b
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 156 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
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));

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
Loading

0 comments on commit 68df38b

Please sign in to comment.