Skip to content

Commit

Permalink
refactor(platform-response-filter): add PLATFORM_CONTENT_TYPES_CONTAI…
Browse files Browse the repository at this point in the history
…NER and PLATFORM_CONTENT_TYPE_RESOLVER
  • Loading branch information
Romakita committed Dec 7, 2024
1 parent 7d5df7b commit ee5d013
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ANY_CONTENT_TYPE = "*/*";
4 changes: 3 additions & 1 deletion packages/platform/platform-response-filter/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
@@ -1,112 +1,116 @@
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>(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)
@View("view.html")
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");
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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>(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();
Original file line number Diff line number Diff line change
@@ -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<Type<ResponseFilterMethods>[]>("responseFilters", []);
const containers: Map<ResponseFilterKey, TokenProvider> = 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<ResponseFilterMethods>(token);
}
}
};
}

export type PLATFORM_CONTENT_TYPES_CONTAINER = ReturnType<typeof factory>;
export const PLATFORM_CONTENT_TYPES_CONTAINER = injectable(Symbol.for("PLATFORM_CONTENT_TYPES_CONTAINER")).factory(factory).token();
Original file line number Diff line number Diff line change
@@ -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<ResponseFilterKey, TokenProvider> = new Map();
protected responseFilters = constant<Type<ResponseFilterMethods>[]>("responseFilters", []);
protected additionalProperties = constant<boolean>("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>(PLATFORM_CONTENT_TYPES_CONTAINER);
protected contentTypeResolver = inject<PLATFORM_CONTENT_TYPE_RESOLVER>(PLATFORM_CONTENT_TYPE_RESOLVER);

/**
* Call filters to transform data
Expand All @@ -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);
Expand Down Expand Up @@ -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<ResponseFilterMethods>(token);
}
}

private 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.

0 comments on commit ee5d013

Please sign in to comment.