@tsed/platform-response-filter 8.0.1 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -13
- package/src/decorators/responseFilter.ts +17 -0
- package/src/domain/ResponseFiltersContainer.ts +18 -0
- package/src/errors/TemplateRenderError.spec.ts +9 -0
- package/src/errors/TemplateRenderError.ts +21 -0
- package/src/index.ts +10 -0
- package/src/interfaces/ResponseFilterMethods.ts +17 -0
- package/src/services/PlatformResponseFilter.spec.ts +336 -0
- package/src/services/PlatformResponseFilter.ts +112 -0
- package/src/utils/getContentType.spec.ts +113 -0
- package/src/utils/getContentType.ts +26 -0
- package/src/utils/renderView.spec.ts +64 -0
- package/src/utils/renderView.ts +15 -0
package/package.json
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
"name": "@tsed/platform-response-filter",
|
|
3
3
|
"description": "Platform response filter module for Ts.ED Framework",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.0.
|
|
5
|
+
"version": "8.0.2",
|
|
6
6
|
"source": "./src/index.ts",
|
|
7
7
|
"main": "./lib/esm/index.js",
|
|
8
8
|
"module": "./lib/esm/index.js",
|
|
9
9
|
"typings": "./lib/types/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
+
"@tsed/source": "./src/index.ts",
|
|
12
13
|
"types": "./lib/types/index.d.ts",
|
|
13
14
|
"import": "./lib/esm/index.js",
|
|
14
15
|
"default": "./lib/esm/index.js"
|
|
@@ -25,23 +26,23 @@
|
|
|
25
26
|
"tslib": "2.7.0"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
|
-
"@tsed/barrels": "8.0.
|
|
29
|
-
"@tsed/core": "8.0.
|
|
30
|
-
"@tsed/di": "8.0.
|
|
31
|
-
"@tsed/exceptions": "8.0.
|
|
32
|
-
"@tsed/json-mapper": "8.0.
|
|
33
|
-
"@tsed/schema": "8.0.
|
|
34
|
-
"@tsed/typescript": "8.0.
|
|
29
|
+
"@tsed/barrels": "8.0.2",
|
|
30
|
+
"@tsed/core": "8.0.2",
|
|
31
|
+
"@tsed/di": "8.0.2",
|
|
32
|
+
"@tsed/exceptions": "8.0.2",
|
|
33
|
+
"@tsed/json-mapper": "8.0.2",
|
|
34
|
+
"@tsed/schema": "8.0.2",
|
|
35
|
+
"@tsed/typescript": "8.0.2",
|
|
35
36
|
"eslint": "9.12.0",
|
|
36
37
|
"typescript": "5.4.5",
|
|
37
38
|
"vitest": "2.1.2"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
|
-
"@tsed/core": "8.0.
|
|
41
|
-
"@tsed/di": "8.0.
|
|
42
|
-
"@tsed/exceptions": "8.0.
|
|
43
|
-
"@tsed/json-mapper": "8.0.
|
|
44
|
-
"@tsed/schema": "8.0.
|
|
41
|
+
"@tsed/core": "8.0.2",
|
|
42
|
+
"@tsed/di": "8.0.2",
|
|
43
|
+
"@tsed/exceptions": "8.0.2",
|
|
44
|
+
"@tsed/json-mapper": "8.0.2",
|
|
45
|
+
"@tsed/schema": "8.0.2"
|
|
45
46
|
},
|
|
46
47
|
"peerDependenciesMeta": {
|
|
47
48
|
"@tsed/core": {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {injectable} from "@tsed/di";
|
|
2
|
+
|
|
3
|
+
import {registerResponseFilter, ResponseFilterKey} from "../domain/ResponseFiltersContainer.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Register a response filter service.
|
|
7
|
+
* @param contentTypes
|
|
8
|
+
* @decorator
|
|
9
|
+
*/
|
|
10
|
+
export function ResponseFilter(...contentTypes: ResponseFilterKey[]): ClassDecorator {
|
|
11
|
+
return (target: any) => {
|
|
12
|
+
contentTypes.forEach((contentType) => {
|
|
13
|
+
registerResponseFilter(contentType, target as any);
|
|
14
|
+
});
|
|
15
|
+
injectable(target).class(target);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {Type} from "@tsed/core";
|
|
2
|
+
|
|
3
|
+
import {ResponseFilterMethods} from "../interfaces/ResponseFilterMethods.js";
|
|
4
|
+
/**
|
|
5
|
+
* @ignore
|
|
6
|
+
*/
|
|
7
|
+
export type ResponseFilterKey = "*/*" | "application/json" | "text/html" | "plain/text" | string;
|
|
8
|
+
/**
|
|
9
|
+
* @ignore
|
|
10
|
+
*/
|
|
11
|
+
// tslint:disable-next-line:variable-name
|
|
12
|
+
export const ResponseFiltersContainer = new Map<ResponseFilterKey, Type<ResponseFilterMethods>>();
|
|
13
|
+
/**
|
|
14
|
+
* @ignore
|
|
15
|
+
*/
|
|
16
|
+
export function registerResponseFilter(type: ResponseFilterKey, token: Type<ResponseFilterMethods>) {
|
|
17
|
+
ResponseFiltersContainer.set(type, token);
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {TemplateRenderError} from "./TemplateRenderError.js";
|
|
2
|
+
|
|
3
|
+
describe("TemplateRenderingError", () => {
|
|
4
|
+
it("should have a message", () => {
|
|
5
|
+
const errorInstance = new TemplateRenderError(class Target {}, "method", new Error("test"));
|
|
6
|
+
expect(errorInstance.message).toEqual("Template rendering error: Target.method()\nError: test");
|
|
7
|
+
expect(errorInstance.name).toEqual("TEMPLATE_RENDER_ERROR");
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {nameOf, Type} from "@tsed/core";
|
|
2
|
+
import {InternalServerError} from "@tsed/exceptions";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @private
|
|
6
|
+
*/
|
|
7
|
+
export class TemplateRenderError extends InternalServerError {
|
|
8
|
+
name = "TEMPLATE_RENDER_ERROR";
|
|
9
|
+
|
|
10
|
+
constructor(target: Type<any> | string, method: string | symbol, err: Error) {
|
|
11
|
+
super(TemplateRenderError.buildMessage(target, method, err));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
static buildMessage(target: Type<any> | string, method: string | symbol, err: Error) {
|
|
19
|
+
return `Template rendering error: ${nameOf(target)}.${String(method)}()\n` + err;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Automatically generated by @tsed/barrels.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./decorators/responseFilter.js";
|
|
5
|
+
export * from "./domain/ResponseFiltersContainer.js";
|
|
6
|
+
export * from "./errors/TemplateRenderError.js";
|
|
7
|
+
export * from "./interfaces/ResponseFilterMethods.js";
|
|
8
|
+
export * from "./services/PlatformResponseFilter.js";
|
|
9
|
+
export * from "./utils/getContentType.js";
|
|
10
|
+
export * from "./utils/renderView.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {Type} from "@tsed/core";
|
|
2
|
+
import type {BaseContext} from "@tsed/di";
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
namespace TsED {
|
|
6
|
+
interface Configuration {
|
|
7
|
+
/**
|
|
8
|
+
* A list of response filters must be called before returning a response to the consumer. See more on [Response filters](/docs/response-filter.md).
|
|
9
|
+
*/
|
|
10
|
+
responseFilters: Type<ResponseFilterMethods>[];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ResponseFilterMethods<T = unknown> {
|
|
16
|
+
transform(data: T, ctx: BaseContext): any;
|
|
17
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import {catchAsyncError} from "@tsed/core";
|
|
2
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
3
|
+
import {Context} from "@tsed/platform-params";
|
|
4
|
+
import {EndpointMetadata, Get, Returns, View} from "@tsed/schema";
|
|
5
|
+
|
|
6
|
+
import {ResponseFilter} from "../decorators/responseFilter.js";
|
|
7
|
+
import {ResponseFilterMethods} from "../interfaces/ResponseFilterMethods.js";
|
|
8
|
+
import {PlatformResponseFilter} from "./PlatformResponseFilter.js";
|
|
9
|
+
|
|
10
|
+
@ResponseFilter("custom/json")
|
|
11
|
+
class CustomJsonFilter implements ResponseFilterMethods {
|
|
12
|
+
transform(data: unknown, ctx: Context) {
|
|
13
|
+
return {data};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@ResponseFilter("application/json")
|
|
18
|
+
class ApplicationJsonFilter implements ResponseFilterMethods {
|
|
19
|
+
transform(data: unknown, ctx: Context) {
|
|
20
|
+
return {data, "content-type": "application/json"};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@ResponseFilter("*/*")
|
|
25
|
+
class AllFilter implements ResponseFilterMethods {
|
|
26
|
+
transform(data: unknown, ctx: Context) {
|
|
27
|
+
return {data, "content-type": "*/*"};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("PlatformResponseFilter", () => {
|
|
32
|
+
describe("transform()", () => {
|
|
33
|
+
describe("when filter list is given", () => {
|
|
34
|
+
beforeEach(() =>
|
|
35
|
+
PlatformTest.create({
|
|
36
|
+
responseFilters: [CustomJsonFilter, AllFilter, ApplicationJsonFilter]
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
afterEach(() => PlatformTest.reset());
|
|
40
|
+
|
|
41
|
+
it("should transform data for custom/json", async () => {
|
|
42
|
+
class Test {
|
|
43
|
+
@Get("/")
|
|
44
|
+
test() {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
48
|
+
|
|
49
|
+
const ctx = PlatformTest.createRequestContext();
|
|
50
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
51
|
+
const data = {text: "test"};
|
|
52
|
+
|
|
53
|
+
vi.spyOn(ctx.response, "contentType").mockReturnThis();
|
|
54
|
+
vi.spyOn(ctx.response, "get").mockReturnValue(undefined);
|
|
55
|
+
vi.spyOn(ctx.request, "get").mockReturnValue("custom/json");
|
|
56
|
+
vi.spyOn(ctx.request, "accepts").mockReturnValue(["custom/json"]);
|
|
57
|
+
|
|
58
|
+
const result = await platformResponseFilter.transform(data, ctx);
|
|
59
|
+
|
|
60
|
+
expect(result).toEqual({
|
|
61
|
+
data: {
|
|
62
|
+
text: "test"
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it("should transform data for application/json", async () => {
|
|
67
|
+
class Test {
|
|
68
|
+
@Get("/")
|
|
69
|
+
test() {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
73
|
+
|
|
74
|
+
const ctx = PlatformTest.createRequestContext();
|
|
75
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
76
|
+
const data = {text: "test"};
|
|
77
|
+
|
|
78
|
+
vi.spyOn(ctx.response, "contentType").mockReturnThis();
|
|
79
|
+
vi.spyOn(ctx.response, "get").mockReturnValue(undefined);
|
|
80
|
+
vi.spyOn(ctx.request, "get").mockReturnValue("application/json");
|
|
81
|
+
vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]);
|
|
82
|
+
|
|
83
|
+
const result = await platformResponseFilter.transform(data, ctx);
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual({
|
|
86
|
+
"content-type": "application/json",
|
|
87
|
+
data: {
|
|
88
|
+
text: "test"
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it("should return data without transformation", async () => {
|
|
93
|
+
class Test {
|
|
94
|
+
@Get("/")
|
|
95
|
+
test() {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
99
|
+
|
|
100
|
+
const ctx = PlatformTest.createRequestContext();
|
|
101
|
+
const data = {text: "test"};
|
|
102
|
+
|
|
103
|
+
vi.spyOn(ctx.response, "contentType").mockReturnThis();
|
|
104
|
+
vi.spyOn(ctx.response, "get").mockReturnValue(undefined);
|
|
105
|
+
vi.spyOn(ctx.request, "get").mockReturnValue("application/json");
|
|
106
|
+
vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]);
|
|
107
|
+
|
|
108
|
+
const result = await platformResponseFilter.transform(data, ctx);
|
|
109
|
+
|
|
110
|
+
expect(result).toEqual({
|
|
111
|
+
text: "test"
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
it("should get content-type set from response", async () => {
|
|
115
|
+
class Test {
|
|
116
|
+
@Get("/")
|
|
117
|
+
test() {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
121
|
+
|
|
122
|
+
const ctx = PlatformTest.createRequestContext();
|
|
123
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
124
|
+
const data = {text: "test"};
|
|
125
|
+
|
|
126
|
+
vi.spyOn(ctx.response, "contentType").mockReturnThis();
|
|
127
|
+
vi.spyOn(ctx.response, "get").mockReturnValue("text/json; charset: utf-8");
|
|
128
|
+
vi.spyOn(ctx.request, "get").mockReturnValue("application/json");
|
|
129
|
+
vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]);
|
|
130
|
+
|
|
131
|
+
const result = await platformResponseFilter.transform(data, ctx);
|
|
132
|
+
|
|
133
|
+
expect(result).toEqual({
|
|
134
|
+
"content-type": "application/json",
|
|
135
|
+
data: {
|
|
136
|
+
text: "test"
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
it("should transform data for any content type", async () => {
|
|
141
|
+
class Test {
|
|
142
|
+
@Get("/")
|
|
143
|
+
test() {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
147
|
+
|
|
148
|
+
const ctx = PlatformTest.createRequestContext();
|
|
149
|
+
const data = {text: "test"};
|
|
150
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
151
|
+
|
|
152
|
+
vi.spyOn(ctx.response, "contentType").mockReturnThis();
|
|
153
|
+
vi.spyOn(ctx.response, "get").mockReturnValue(undefined);
|
|
154
|
+
vi.spyOn(ctx.request, "get").mockReturnValue("*/*");
|
|
155
|
+
vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]);
|
|
156
|
+
|
|
157
|
+
const result = await platformResponseFilter.transform(data, ctx);
|
|
158
|
+
|
|
159
|
+
expect(result).toEqual({
|
|
160
|
+
"content-type": "application/json",
|
|
161
|
+
data: {
|
|
162
|
+
text: "test"
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("when filter list is not given", () => {
|
|
169
|
+
beforeEach(() =>
|
|
170
|
+
PlatformTest.create({
|
|
171
|
+
responseFilters: [AllFilter]
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
afterEach(() => PlatformTest.reset());
|
|
175
|
+
it("should transform data for default content-type from metadata", async () => {
|
|
176
|
+
class Test {
|
|
177
|
+
@Get("/")
|
|
178
|
+
@(Returns(200).ContentType("application/json"))
|
|
179
|
+
test() {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
183
|
+
|
|
184
|
+
const ctx = PlatformTest.createRequestContext();
|
|
185
|
+
const data = {text: "test"};
|
|
186
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
187
|
+
|
|
188
|
+
vi.spyOn(ctx.response, "contentType").mockReturnThis();
|
|
189
|
+
vi.spyOn(ctx.response, "get").mockReturnValue(undefined);
|
|
190
|
+
vi.spyOn(ctx.request, "get").mockReturnValue(undefined);
|
|
191
|
+
vi.spyOn(ctx.request, "accepts").mockReturnValue(false);
|
|
192
|
+
|
|
193
|
+
const result = await platformResponseFilter.transform(data, ctx);
|
|
194
|
+
|
|
195
|
+
expect(result).toEqual({
|
|
196
|
+
"content-type": "*/*",
|
|
197
|
+
data: {
|
|
198
|
+
text: "test"
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it("should transform data for default content-type from metadata with any response filter", async () => {
|
|
203
|
+
class Test {
|
|
204
|
+
@Get("/")
|
|
205
|
+
@(Returns(200).ContentType("application/json"))
|
|
206
|
+
test() {}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
210
|
+
|
|
211
|
+
const ctx = PlatformTest.createRequestContext();
|
|
212
|
+
const data = {text: "test"};
|
|
213
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
214
|
+
|
|
215
|
+
vi.spyOn(ctx.response, "contentType").mockReturnThis();
|
|
216
|
+
vi.spyOn(ctx.response, "get").mockReturnValue(undefined);
|
|
217
|
+
vi.spyOn(ctx.request, "get").mockReturnValue(undefined);
|
|
218
|
+
vi.spyOn(ctx.request, "accepts").mockReturnValue(false);
|
|
219
|
+
|
|
220
|
+
const result = await platformResponseFilter.transform(data, ctx);
|
|
221
|
+
|
|
222
|
+
expect(result).toEqual({
|
|
223
|
+
"content-type": "*/*",
|
|
224
|
+
data: {
|
|
225
|
+
text: "test"
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
describe("serialize()", () => {
|
|
232
|
+
beforeEach(() =>
|
|
233
|
+
PlatformTest.create({
|
|
234
|
+
responseFilters: [CustomJsonFilter, AllFilter, ApplicationJsonFilter]
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
afterEach(() => PlatformTest.reset());
|
|
238
|
+
it("should transform value", async () => {
|
|
239
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
240
|
+
const ctx = PlatformTest.createRequestContext();
|
|
241
|
+
|
|
242
|
+
const result = await platformResponseFilter.serialize({test: "test"}, ctx);
|
|
243
|
+
|
|
244
|
+
expect(result).toEqual({test: "test"});
|
|
245
|
+
});
|
|
246
|
+
it("should transform value (endpoint)", async () => {
|
|
247
|
+
class Test {
|
|
248
|
+
@Get("/")
|
|
249
|
+
test() {}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
253
|
+
const ctx = PlatformTest.createRequestContext();
|
|
254
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
255
|
+
|
|
256
|
+
vi.spyOn(ctx.endpoint, "getResponseOptions");
|
|
257
|
+
|
|
258
|
+
const result = await platformResponseFilter.serialize({test: "test"}, ctx);
|
|
259
|
+
|
|
260
|
+
expect(result).toEqual({test: "test"});
|
|
261
|
+
expect(ctx.endpoint.getResponseOptions).toHaveBeenCalledWith(200, {includes: undefined});
|
|
262
|
+
});
|
|
263
|
+
it("should transform value (endpoint + includes)", async () => {
|
|
264
|
+
class Test {
|
|
265
|
+
@Get("/")
|
|
266
|
+
test() {}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
270
|
+
const ctx = PlatformTest.createRequestContext();
|
|
271
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
272
|
+
|
|
273
|
+
vi.spyOn(ctx.endpoint, "getResponseOptions");
|
|
274
|
+
|
|
275
|
+
ctx.request.query.includes = [];
|
|
276
|
+
|
|
277
|
+
const result = await platformResponseFilter.serialize({test: "test"}, ctx);
|
|
278
|
+
|
|
279
|
+
expect(result).toEqual({test: "test"});
|
|
280
|
+
expect(ctx.endpoint.getResponseOptions).toHaveBeenCalledWith(200, {includes: []});
|
|
281
|
+
});
|
|
282
|
+
it("should transform value (endpoint + includes with ,)", async () => {
|
|
283
|
+
class Test {
|
|
284
|
+
@Get("/")
|
|
285
|
+
test() {}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
289
|
+
const ctx = PlatformTest.createRequestContext();
|
|
290
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
291
|
+
|
|
292
|
+
vi.spyOn(ctx.endpoint, "getResponseOptions");
|
|
293
|
+
|
|
294
|
+
ctx.request.query.includes = ["test,test2"];
|
|
295
|
+
|
|
296
|
+
const result = await platformResponseFilter.serialize({test: "test"}, ctx);
|
|
297
|
+
|
|
298
|
+
expect(result).toEqual({test: "test"});
|
|
299
|
+
expect(ctx.endpoint.getResponseOptions).toHaveBeenCalledWith(200, {
|
|
300
|
+
includes: ["test", "test2"]
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
it("should transform value (endpoint - view)", async () => {
|
|
304
|
+
class Test {
|
|
305
|
+
@Get("/")
|
|
306
|
+
@View("test.pug")
|
|
307
|
+
test() {}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
311
|
+
const ctx = PlatformTest.createRequestContext();
|
|
312
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
313
|
+
vi.spyOn(ctx.response, "render").mockResolvedValue("template");
|
|
314
|
+
|
|
315
|
+
const result = await platformResponseFilter.serialize({test: "test"}, ctx);
|
|
316
|
+
|
|
317
|
+
expect(result).toEqual("template");
|
|
318
|
+
});
|
|
319
|
+
it("should transform value (endpoint - view - error)", async () => {
|
|
320
|
+
class Test {
|
|
321
|
+
@Get("/")
|
|
322
|
+
@View("test.pug")
|
|
323
|
+
test() {}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const platformResponseFilter = PlatformTest.get<PlatformResponseFilter>(PlatformResponseFilter);
|
|
327
|
+
const ctx = PlatformTest.createRequestContext();
|
|
328
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
329
|
+
vi.spyOn(ctx.response, "render").mockRejectedValue(new Error("parsing error"));
|
|
330
|
+
|
|
331
|
+
const result = await catchAsyncError(() => platformResponseFilter.serialize({test: "test"}, ctx));
|
|
332
|
+
|
|
333
|
+
expect(result?.message).toEqual("Template rendering error: Test.test()\nError: parsing error");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {isSerializable, Type} from "@tsed/core";
|
|
2
|
+
import {BaseContext, constant, inject, injectable, TokenProvider} from "@tsed/di";
|
|
3
|
+
import {serialize} from "@tsed/json-mapper";
|
|
4
|
+
|
|
5
|
+
import {ResponseFilterKey, ResponseFiltersContainer} from "../domain/ResponseFiltersContainer.js";
|
|
6
|
+
import {ResponseFilterMethods} from "../interfaces/ResponseFilterMethods.js";
|
|
7
|
+
import {ANY_CONTENT_TYPE, getContentType} from "../utils/getContentType.js";
|
|
8
|
+
import {renderView} from "../utils/renderView.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @platform
|
|
12
|
+
*/
|
|
13
|
+
export class PlatformResponseFilter {
|
|
14
|
+
protected types: Map<ResponseFilterKey, TokenProvider> = new Map();
|
|
15
|
+
protected responseFilters = constant<Type<ResponseFilterMethods>[]>("responseFilters", []);
|
|
16
|
+
protected additionalProperties = constant<boolean>("additionalProperties");
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
ResponseFiltersContainer.forEach((token, type) => {
|
|
20
|
+
if (this.responseFilters.includes(token)) {
|
|
21
|
+
this.types.set(type, token);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get contentTypes(): ResponseFilterKey[] {
|
|
27
|
+
return [...this.types.keys()];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getBestContentType(data: any, ctx: BaseContext) {
|
|
31
|
+
const contentType = getContentType(data, ctx);
|
|
32
|
+
|
|
33
|
+
if (ctx.request.get("Accept")) {
|
|
34
|
+
const bestContentType = ctx.request.accepts([contentType].concat(this.contentTypes).filter(Boolean));
|
|
35
|
+
|
|
36
|
+
if (bestContentType) {
|
|
37
|
+
return [].concat(bestContentType as any).filter((type) => type !== "*/*")[0];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return contentType;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Call filters to transform data
|
|
46
|
+
* @param data
|
|
47
|
+
* @param ctx
|
|
48
|
+
*/
|
|
49
|
+
transform(data: unknown, ctx: BaseContext) {
|
|
50
|
+
const {response} = ctx;
|
|
51
|
+
|
|
52
|
+
if (ctx.endpoint?.operation) {
|
|
53
|
+
const bestContentType = this.getBestContentType(data, ctx);
|
|
54
|
+
|
|
55
|
+
bestContentType && response.contentType(bestContentType);
|
|
56
|
+
|
|
57
|
+
const resolved = this.resolve(bestContentType);
|
|
58
|
+
|
|
59
|
+
if (resolved) {
|
|
60
|
+
return resolved.transform(data, ctx);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Serialize data before calling filters
|
|
69
|
+
* @param data
|
|
70
|
+
* @param ctx
|
|
71
|
+
*/
|
|
72
|
+
async serialize(data: unknown, ctx: BaseContext) {
|
|
73
|
+
const {response, endpoint} = ctx;
|
|
74
|
+
|
|
75
|
+
if (endpoint) {
|
|
76
|
+
if (endpoint.view) {
|
|
77
|
+
data = await renderView(data, ctx);
|
|
78
|
+
} else if (isSerializable(data)) {
|
|
79
|
+
const responseOpts = endpoint.getResponseOptions(response.statusCode, {
|
|
80
|
+
includes: this.getIncludes(ctx)
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
data = serialize(data, {
|
|
84
|
+
useAlias: true,
|
|
85
|
+
additionalProperties: this.additionalProperties,
|
|
86
|
+
...responseOpts,
|
|
87
|
+
endpoint: true
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private resolve(bestContentType: string) {
|
|
96
|
+
const token = this.types.get(bestContentType) || this.types.get(ANY_CONTENT_TYPE);
|
|
97
|
+
|
|
98
|
+
if (token) {
|
|
99
|
+
return inject<ResponseFilterMethods>(token);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private getIncludes(ctx: BaseContext) {
|
|
104
|
+
if (ctx.request.query.includes) {
|
|
105
|
+
return [].concat(ctx.request.query.includes).flatMap((include: string) => include.split(","));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
injectable(PlatformResponseFilter);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
2
|
+
import {EndpointMetadata, Get, Returns, View} from "@tsed/schema";
|
|
3
|
+
|
|
4
|
+
import {getContentType} from "./getContentType.js";
|
|
5
|
+
|
|
6
|
+
describe("getContentType", () => {
|
|
7
|
+
beforeEach(() => PlatformTest.create());
|
|
8
|
+
afterEach(() => PlatformTest.reset());
|
|
9
|
+
|
|
10
|
+
it("should return the content type (undefined)", () => {
|
|
11
|
+
class TestController {
|
|
12
|
+
@Get("/")
|
|
13
|
+
get() {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ctx = PlatformTest.createRequestContext();
|
|
17
|
+
ctx.endpoint = EndpointMetadata.get(TestController, "get");
|
|
18
|
+
|
|
19
|
+
const result = getContentType(
|
|
20
|
+
{
|
|
21
|
+
test: "test"
|
|
22
|
+
},
|
|
23
|
+
ctx
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(result).toEqual(undefined);
|
|
27
|
+
});
|
|
28
|
+
it("should return the content type (object - application/json)", () => {
|
|
29
|
+
class TestController {
|
|
30
|
+
@Get("/")
|
|
31
|
+
@(Returns(200).ContentType("application/json"))
|
|
32
|
+
get() {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ctx = PlatformTest.createRequestContext();
|
|
36
|
+
ctx.endpoint = EndpointMetadata.get(TestController, "get");
|
|
37
|
+
ctx.response.getRes().statusCode = 200;
|
|
38
|
+
vi.spyOn(ctx.response, "getContentType").mockReturnValue("application/json");
|
|
39
|
+
|
|
40
|
+
const result = getContentType(
|
|
41
|
+
{
|
|
42
|
+
test: "test"
|
|
43
|
+
},
|
|
44
|
+
ctx
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(result).toEqual("application/json");
|
|
48
|
+
});
|
|
49
|
+
it("should return the content type (string - application/json)", () => {
|
|
50
|
+
class TestController {
|
|
51
|
+
@Get("/")
|
|
52
|
+
@(Returns(200).ContentType("application/json"))
|
|
53
|
+
get() {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ctx = PlatformTest.createRequestContext();
|
|
57
|
+
ctx.endpoint = EndpointMetadata.get(TestController, "get");
|
|
58
|
+
ctx.response.getRes().statusCode = 200;
|
|
59
|
+
vi.spyOn(ctx.response, "getContentType").mockReturnValue("application/json");
|
|
60
|
+
|
|
61
|
+
const result = getContentType(
|
|
62
|
+
{
|
|
63
|
+
test: "test"
|
|
64
|
+
},
|
|
65
|
+
ctx
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual("application/json");
|
|
69
|
+
});
|
|
70
|
+
it("should return the content type (string - text/html)", () => {
|
|
71
|
+
class TestController {
|
|
72
|
+
@Get("/")
|
|
73
|
+
@(Returns(200).ContentType("text/html"))
|
|
74
|
+
get() {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ctx = PlatformTest.createRequestContext();
|
|
78
|
+
ctx.endpoint = EndpointMetadata.get(TestController, "get");
|
|
79
|
+
ctx.response.getRes().statusCode = 200;
|
|
80
|
+
vi.spyOn(ctx.response, "getContentType").mockReturnValue("text/html");
|
|
81
|
+
|
|
82
|
+
const result = getContentType(
|
|
83
|
+
{
|
|
84
|
+
test: "test"
|
|
85
|
+
},
|
|
86
|
+
ctx
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(result).toEqual("text/html");
|
|
90
|
+
});
|
|
91
|
+
it("should return the content type (string - view)", () => {
|
|
92
|
+
class TestController {
|
|
93
|
+
@Get("/")
|
|
94
|
+
@Returns(200)
|
|
95
|
+
@View("view.html")
|
|
96
|
+
get() {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ctx = PlatformTest.createRequestContext();
|
|
100
|
+
ctx.endpoint = EndpointMetadata.get(TestController, "get");
|
|
101
|
+
ctx.response.getRes().statusCode = 200;
|
|
102
|
+
ctx.view = "true";
|
|
103
|
+
|
|
104
|
+
const result = getContentType(
|
|
105
|
+
{
|
|
106
|
+
test: "test"
|
|
107
|
+
},
|
|
108
|
+
ctx
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual("text/html");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {isObject} from "@tsed/core";
|
|
2
|
+
import type {BaseContext} from "@tsed/di";
|
|
3
|
+
|
|
4
|
+
export const ANY_CONTENT_TYPE = "*/*";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @ignore
|
|
8
|
+
*/
|
|
9
|
+
export function getContentType(data: any, ctx: BaseContext) {
|
|
10
|
+
const {endpoint, response} = ctx;
|
|
11
|
+
const {operation} = endpoint;
|
|
12
|
+
|
|
13
|
+
const contentType = response.getContentType() || operation.getContentTypeOf(response.statusCode) || "";
|
|
14
|
+
|
|
15
|
+
if (contentType && contentType !== ANY_CONTENT_TYPE) {
|
|
16
|
+
if (contentType === "application/json" && isObject(data)) {
|
|
17
|
+
return "application/json";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return contentType;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (endpoint.view) {
|
|
24
|
+
return "text/html";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
2
|
+
import {EndpointMetadata, Get, Ignore, Property, Returns, View} from "@tsed/schema";
|
|
3
|
+
|
|
4
|
+
import {renderView} from "./renderView.js";
|
|
5
|
+
|
|
6
|
+
describe("renderView", () => {
|
|
7
|
+
beforeEach(() => PlatformTest.create());
|
|
8
|
+
afterEach(() => PlatformTest.reset());
|
|
9
|
+
|
|
10
|
+
it("should render content", async () => {
|
|
11
|
+
class Model {
|
|
12
|
+
@Property()
|
|
13
|
+
data: string;
|
|
14
|
+
|
|
15
|
+
@Ignore()
|
|
16
|
+
test: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class Test {
|
|
20
|
+
@Get("/")
|
|
21
|
+
@View("view", {options: "options"})
|
|
22
|
+
@Returns(200, Model)
|
|
23
|
+
test() {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ctx = PlatformTest.createRequestContext();
|
|
27
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
28
|
+
|
|
29
|
+
vi.spyOn(ctx.response, "render").mockResolvedValue("HTML");
|
|
30
|
+
|
|
31
|
+
ctx.data = {data: "data"};
|
|
32
|
+
|
|
33
|
+
await renderView(ctx.data, ctx);
|
|
34
|
+
|
|
35
|
+
expect(ctx.response.render).toHaveBeenCalledWith("view", {
|
|
36
|
+
$ctx: ctx,
|
|
37
|
+
data: "data",
|
|
38
|
+
options: "options"
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it("should throw an error", async () => {
|
|
42
|
+
class Test {
|
|
43
|
+
@Get("/")
|
|
44
|
+
@View("view", {options: "options"})
|
|
45
|
+
test() {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ctx = PlatformTest.createRequestContext();
|
|
49
|
+
ctx.endpoint = EndpointMetadata.get(Test, "test");
|
|
50
|
+
|
|
51
|
+
vi.spyOn(ctx.response, "render").mockRejectedValue(new Error("parser error"));
|
|
52
|
+
|
|
53
|
+
ctx.data = {data: "data"};
|
|
54
|
+
|
|
55
|
+
let actualError: any;
|
|
56
|
+
try {
|
|
57
|
+
await renderView(ctx.data, ctx);
|
|
58
|
+
} catch (er) {
|
|
59
|
+
actualError = er;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
expect(actualError.message).toEqual("Template rendering error: Test.test()\nError: parser error");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {BaseContext} from "@tsed/di";
|
|
2
|
+
|
|
3
|
+
import {TemplateRenderError} from "../errors/TemplateRenderError.js";
|
|
4
|
+
|
|
5
|
+
export async function renderView(data: any, $ctx: BaseContext) {
|
|
6
|
+
const {response, endpoint} = $ctx;
|
|
7
|
+
try {
|
|
8
|
+
const {data} = $ctx;
|
|
9
|
+
const {path, options} = endpoint.view;
|
|
10
|
+
|
|
11
|
+
return await response.render(path, {...options, ...data, $ctx});
|
|
12
|
+
} catch (err) {
|
|
13
|
+
throw new TemplateRenderError(endpoint.targetName, endpoint.propertyKey, err);
|
|
14
|
+
}
|
|
15
|
+
}
|