@tsed/platform-exceptions 8.0.0 → 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 CHANGED
@@ -2,13 +2,14 @@
2
2
  "name": "@tsed/platform-exceptions",
3
3
  "description": "Platform exceptions module for Ts.ED Framework",
4
4
  "type": "module",
5
- "version": "8.0.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"
@@ -24,22 +25,22 @@
24
25
  "tslib": "2.7.0"
25
26
  },
26
27
  "devDependencies": {
27
- "@tsed/barrels": "8.0.0",
28
- "@tsed/core": "8.0.0",
29
- "@tsed/di": "8.0.0",
30
- "@tsed/exceptions": "8.0.0",
31
- "@tsed/schema": "8.0.0",
32
- "@tsed/typescript": "8.0.0",
28
+ "@tsed/barrels": "8.0.2",
29
+ "@tsed/core": "8.0.2",
30
+ "@tsed/di": "8.0.2",
31
+ "@tsed/exceptions": "8.0.2",
32
+ "@tsed/schema": "8.0.2",
33
+ "@tsed/typescript": "8.0.2",
33
34
  "ajv": "^8.17.1",
34
35
  "eslint": "9.12.0",
35
36
  "typescript": "5.4.5",
36
37
  "vitest": "2.1.2"
37
38
  },
38
39
  "peerDependencies": {
39
- "@tsed/core": "8.0.0",
40
- "@tsed/di": "8.0.0",
41
- "@tsed/exceptions": "8.0.0",
42
- "@tsed/schema": "8.0.0"
40
+ "@tsed/core": "8.0.2",
41
+ "@tsed/di": "8.0.2",
42
+ "@tsed/exceptions": "8.0.2",
43
+ "@tsed/schema": "8.0.2"
43
44
  },
44
45
  "peerDependenciesMeta": {
45
46
  "@tsed/core": {
@@ -0,0 +1,56 @@
1
+ import {ErrorFilter} from "./ErrorFilter.js";
2
+
3
+ describe("ErrorFilter", () => {
4
+ it("should map error if error.errors is an object", () => {
5
+ const error = {
6
+ errors: {
7
+ "ingredients.0.product": {
8
+ stringValue:
9
+ "\"{\n id: '5e9a0c1d5575346932e090f0',\n label: 'Artichauts',\n shelf: 'produce',\n minimumQuantity: 0,\n users: []\n}\"",
10
+ kind: "ObjectId",
11
+ value: {
12
+ id: "5e9a0c1d5575346932e090f0",
13
+ label: "Artichauts",
14
+ shelf: "produce",
15
+ minimumQuantity: 0,
16
+ users: []
17
+ },
18
+ path: "product",
19
+ reason: {}
20
+ }
21
+ },
22
+ _message: "Recipe validation failed",
23
+ name: "ValidationError",
24
+ message:
25
+ "Recipe validation failed: ingredients.0.product: Cast to ObjectId failed for value \"{\n id: '5e9a0c1d5575346932e090f0',\n label: 'Artichauts',\n shelf: 'produce',\n minimumQuantity: 0,\n users: []\n}\" at path \"product\""
26
+ };
27
+ const errorFilterInstance = new ErrorFilter();
28
+ expect(errorFilterInstance.mapError(error).errors).toEqual([error.errors]);
29
+ });
30
+ it("should map error if has error.code", () => {
31
+ const error = {
32
+ errors: {
33
+ "ingredients.0.product": {
34
+ stringValue:
35
+ "\"{\n id: '5e9a0c1d5575346932e090f0',\n label: 'Artichauts',\n shelf: 'produce',\n minimumQuantity: 0,\n users: []\n}\"",
36
+ kind: "ObjectId",
37
+ value: {
38
+ id: "5e9a0c1d5575346932e090f0",
39
+ label: "Artichauts",
40
+ shelf: "produce",
41
+ minimumQuantity: 0,
42
+ users: []
43
+ },
44
+ path: "product",
45
+ reason: {}
46
+ }
47
+ },
48
+ _message: "Recipe validation failed",
49
+ code: "ValidationError",
50
+ message:
51
+ "Recipe validation failed: ingredients.0.product: Cast to ObjectId failed for value \"{\n id: '5e9a0c1d5575346932e090f0',\n label: 'Artichauts',\n shelf: 'produce',\n minimumQuantity: 0,\n users: []\n}\" at path \"product\""
52
+ };
53
+ const errorFilterInstance = new ErrorFilter();
54
+ expect(errorFilterInstance.mapError(error).errors).toEqual([error.errors]);
55
+ });
56
+ });
@@ -0,0 +1,44 @@
1
+ import {Env} from "@tsed/core";
2
+ import {BaseContext} from "@tsed/di";
3
+
4
+ import {Catch} from "../decorators/catch.js";
5
+ import type {ExceptionFilterMethods} from "../interfaces/ExceptionFilterMethods.js";
6
+
7
+ @Catch(Error)
8
+ export class ErrorFilter implements ExceptionFilterMethods {
9
+ catch(error: any, ctx: BaseContext): void {
10
+ const {response, logger, env} = ctx;
11
+ const err = this.mapError(error, env);
12
+
13
+ response
14
+ .setHeaders(this.getHeaders(error))
15
+ .status(err.status)
16
+ .contentType("application/json")
17
+ .body(env === Env.PROD ? "InternalServerError" : err);
18
+ }
19
+
20
+ mapError(error: any, env?: Env) {
21
+ return {
22
+ name: error.origin?.name || error.name || error.code,
23
+ message: error.message,
24
+ status: error.status || error.statusCode || 500,
25
+ errors: this.getErrors(error),
26
+ stack: env === Env.DEV ? error.stack : undefined
27
+ };
28
+ }
29
+
30
+ protected getErrors(error: any) {
31
+ return [error, error.origin].filter(Boolean).reduce((errs, {errors}: any) => {
32
+ return errs.concat(errors).filter(Boolean);
33
+ }, []);
34
+ }
35
+
36
+ protected getHeaders(error: any) {
37
+ return [error, error.origin].filter(Boolean).reduce((obj, {headers}: any) => {
38
+ return {
39
+ ...obj,
40
+ ...(headers || {})
41
+ };
42
+ }, {});
43
+ }
44
+ }
@@ -0,0 +1,15 @@
1
+ import {BaseContext} from "@tsed/di";
2
+ import {Exception} from "@tsed/exceptions";
3
+
4
+ import {Catch} from "../decorators/catch.js";
5
+ import {ErrorFilter} from "./ErrorFilter.js";
6
+
7
+ @Catch(Exception)
8
+ export class ExceptionFilter extends ErrorFilter {
9
+ catch(error: Exception, ctx: BaseContext) {
10
+ const {response, env} = ctx;
11
+ const err = this.mapError(error, env);
12
+
13
+ response.setHeaders(this.getHeaders(error)).contentType("application/json").status(error.status).body(err);
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ import {BaseContext} from "@tsed/di";
2
+ import {BadRequest} from "@tsed/exceptions";
3
+
4
+ import {Catch} from "../decorators/catch.js";
5
+ import {ErrorFilter} from "./ErrorFilter.js";
6
+
7
+ @Catch("MongooseError", "MongoError")
8
+ export class MongooseErrorFilter extends ErrorFilter {
9
+ catch(error: Error, ctx: BaseContext) {
10
+ return super.catch(new BadRequest(error.message, error), ctx);
11
+ }
12
+ }
@@ -0,0 +1,24 @@
1
+ import {PlatformTest} from "@tsed/platform-http/testing";
2
+
3
+ import {StringErrorFilter} from "./StringErrorFilter.js";
4
+
5
+ describe("StringErrorFilter", () => {
6
+ beforeEach(() => PlatformTest.create());
7
+ afterEach(() => PlatformTest.reset());
8
+ it("should map message", () => {
9
+ const instance = new StringErrorFilter();
10
+ const ctx = PlatformTest.createRequestContext();
11
+
12
+ instance.catch("message", ctx);
13
+
14
+ expect(ctx.response.getBody()).toEqual("message");
15
+ });
16
+ it("should map undefined message", () => {
17
+ const instance = new StringErrorFilter();
18
+ const ctx = PlatformTest.createRequestContext();
19
+
20
+ instance.catch(undefined as any, ctx);
21
+
22
+ expect(ctx.response.getBody()).toEqual("");
23
+ });
24
+ });
@@ -0,0 +1,13 @@
1
+ import {BaseContext} from "@tsed/di";
2
+
3
+ import {Catch} from "../decorators/catch.js";
4
+ import type {ExceptionFilterMethods} from "../interfaces/ExceptionFilterMethods.js";
5
+
6
+ const toHTML = (message = "") => message.replace(/\n/gi, "<br />");
7
+
8
+ @Catch(String)
9
+ export class StringErrorFilter implements ExceptionFilterMethods {
10
+ catch(error: string, ctx: BaseContext): void {
11
+ ctx.response.status(404).body(toHTML(error));
12
+ }
13
+ }
@@ -0,0 +1,18 @@
1
+ import {Type} from "@tsed/core";
2
+ import {injectable, registerProvider} from "@tsed/di";
3
+
4
+ import {registerExceptionType} from "../domain/ExceptionFiltersContainer.js";
5
+
6
+ /**
7
+ * Register a new class to handle an specific exception.
8
+ * @decorator
9
+ * @param types
10
+ */
11
+ export function Catch(...types: (Type<Error | any> | string)[]) {
12
+ return (target: any) => {
13
+ types.forEach((type) => {
14
+ registerExceptionType(type, target as any);
15
+ });
16
+ injectable(target).class(target);
17
+ };
18
+ }
@@ -0,0 +1,18 @@
1
+ import {Type} from "@tsed/core";
2
+
3
+ import type {ExceptionFilterMethods} from "../interfaces/ExceptionFilterMethods.js";
4
+ /**
5
+ * @ignore
6
+ */
7
+ export type ExceptionFilterKey = Type<any> | Symbol | string;
8
+ /**
9
+ * @ignore
10
+ */
11
+ // tslint:disable-next-line:variable-name
12
+ export const ExceptionFiltersContainer = new Map<ExceptionFilterKey, Type<ExceptionFilterMethods>>();
13
+ /**
14
+ * @ignore
15
+ */
16
+ export function registerExceptionType(type: ExceptionFilterKey, token: Type<ExceptionFilterMethods>) {
17
+ ExceptionFiltersContainer.set(type, token);
18
+ }
@@ -0,0 +1,217 @@
1
+ import "./ExceptionSchema";
2
+
3
+ import SwaggerParser from "@apidevtools/swagger-parser";
4
+ import {BadRequest, Exception} from "@tsed/exceptions";
5
+ import {getJsonSchema, getSpec, OperationPath, Path, Returns, SpecTypes} from "@tsed/schema";
6
+ import {Ajv} from "ajv";
7
+ import fs from "fs-extra";
8
+
9
+ const rootDir = import.meta.dirname; // automatically replaced by import.meta.dirname on build
10
+
11
+ function getAjv() {
12
+ return new Ajv({
13
+ verbose: false,
14
+ coerceTypes: true
15
+ });
16
+ }
17
+
18
+ const validateSpec = async (spec: any) => {
19
+ const file = rootDir + "/spec.json";
20
+ spec = {
21
+ ...spec
22
+ };
23
+ try {
24
+ spec.openapi = "3.0.1";
25
+ spec.info = {
26
+ title: "Title",
27
+ description: "Description",
28
+ termsOfService: "termsOfService",
29
+ contact: {
30
+ email: "apiteam@swagger.io"
31
+ },
32
+ license: {
33
+ name: "Apache 2.0",
34
+ url: "http://www.apache.org/licenses/LICENSE-2.0.html"
35
+ },
36
+ version: "1.0.0"
37
+ };
38
+
39
+ fs.writeJsonSync(file, spec, {encoding: "utf8"});
40
+ await SwaggerParser.validate(file);
41
+ fs.unlinkSync(file);
42
+
43
+ return true;
44
+ } catch (er) {
45
+ console.error(er);
46
+ // unlinkSync(file);
47
+
48
+ return er;
49
+ }
50
+ };
51
+
52
+ describe("ExceptionSchema", () => {
53
+ it("should generate the right json schema", () => {
54
+ const schema = getJsonSchema(Exception);
55
+
56
+ expect(schema).toEqual({
57
+ definitions: {
58
+ GenericError: {
59
+ additionalProperties: true,
60
+ properties: {
61
+ message: {
62
+ description: "An error message",
63
+ minLength: 1,
64
+ type: "string"
65
+ },
66
+ name: {
67
+ description: "The error name",
68
+ minLength: 1,
69
+ type: "string"
70
+ }
71
+ },
72
+ required: ["name", "message"],
73
+ type: "object"
74
+ }
75
+ },
76
+ properties: {
77
+ errors: {
78
+ description: "A list of related errors",
79
+ items: {
80
+ $ref: "#/definitions/GenericError"
81
+ },
82
+ type: "array"
83
+ },
84
+ message: {
85
+ description: "An error message",
86
+ minLength: 1,
87
+ type: "string"
88
+ },
89
+ name: {
90
+ description: "The error name",
91
+ minLength: 1,
92
+ type: "string"
93
+ },
94
+ stack: {
95
+ description: "The stack trace (only in development mode)",
96
+ type: "string"
97
+ },
98
+ status: {
99
+ description: "The status code of the exception",
100
+ type: "number"
101
+ }
102
+ },
103
+ required: ["name", "message", "status"],
104
+ type: "object"
105
+ });
106
+
107
+ const ajv = getAjv();
108
+ ajv.validate(schema, {status: 400});
109
+ expect(ajv.errors).toEqual([
110
+ {
111
+ instancePath: "",
112
+ keyword: "required",
113
+ message: "must have required property 'name'",
114
+ params: {
115
+ missingProperty: "name"
116
+ },
117
+ schemaPath: "#/required"
118
+ }
119
+ ]);
120
+ });
121
+
122
+ it("should generate the right spec - OS3", async () => {
123
+ @Path("/")
124
+ class MyController {
125
+ @OperationPath("GET", "/")
126
+ @(Returns(400, BadRequest).Description("BadRequest"))
127
+ get() {}
128
+ }
129
+
130
+ const spec = getSpec(MyController, {specType: SpecTypes.OPENAPI});
131
+ expect(spec).toEqual({
132
+ components: {
133
+ schemas: {
134
+ GenericError: {
135
+ additionalProperties: true,
136
+ properties: {
137
+ message: {
138
+ description: "An error message",
139
+ minLength: 1,
140
+ type: "string"
141
+ },
142
+ name: {
143
+ description: "The error name",
144
+ minLength: 1,
145
+ type: "string"
146
+ }
147
+ },
148
+ required: ["name", "message"],
149
+ type: "object"
150
+ },
151
+ BadRequest: {
152
+ properties: {
153
+ errors: {
154
+ description: "A list of related errors",
155
+ items: {
156
+ $ref: "#/components/schemas/GenericError"
157
+ },
158
+ type: "array"
159
+ },
160
+ message: {
161
+ description: "An error message",
162
+ minLength: 1,
163
+ type: "string"
164
+ },
165
+ name: {
166
+ default: "BAD_REQUEST",
167
+ description: "The error name",
168
+ example: "BAD_REQUEST",
169
+ minLength: 1,
170
+ type: "string"
171
+ },
172
+ stack: {
173
+ description: "The stack trace (only in development mode)",
174
+ type: "string"
175
+ },
176
+ status: {
177
+ default: 400,
178
+ description: "The status code of the exception",
179
+ example: 400,
180
+ type: "number"
181
+ }
182
+ },
183
+ required: ["name", "message", "status"],
184
+ type: "object"
185
+ }
186
+ }
187
+ },
188
+ paths: {
189
+ "/": {
190
+ get: {
191
+ operationId: "myControllerGet",
192
+ parameters: [],
193
+ responses: {
194
+ "400": {
195
+ content: {
196
+ "application/json": {
197
+ schema: {
198
+ $ref: "#/components/schemas/BadRequest"
199
+ }
200
+ }
201
+ },
202
+ description: "BadRequest"
203
+ }
204
+ },
205
+ tags: ["MyController"]
206
+ }
207
+ }
208
+ },
209
+ tags: [
210
+ {
211
+ name: "MyController"
212
+ }
213
+ ]
214
+ });
215
+ expect(await validateSpec(spec)).toEqual(true);
216
+ });
217
+ });
@@ -0,0 +1,36 @@
1
+ import * as Exceptions from "@tsed/exceptions";
2
+ import {Exception} from "@tsed/exceptions";
3
+ import {array, defineStatusModel, from, getStatusConstant, number, object, string} from "@tsed/schema";
4
+
5
+ /**
6
+ * @ignore
7
+ */
8
+ const ErrorSchema = object({
9
+ name: string().required().description("The error name"),
10
+ message: string().required().description("An error message")
11
+ })
12
+ .label("GenericError")
13
+ .unknown();
14
+
15
+ from(Exception).properties({
16
+ name: string().required().description("The error name"),
17
+ message: string().required().description("An error message"),
18
+ status: number().required().description("The status code of the exception"),
19
+ errors: array().items(ErrorSchema).description("A list of related errors"),
20
+ stack: string().description("The stack trace (only in development mode)")
21
+ });
22
+
23
+ // Auto load models for all Exceptions
24
+ Object.values(Exceptions).forEach((target: any) => {
25
+ if (target !== Exception && target.STATUS) {
26
+ if (target.STATUS > 302) {
27
+ const name = getStatusConstant(target.STATUS);
28
+ from(target).properties({
29
+ name: string().required().example(name).default(name).description("The error name"),
30
+ status: number().required().example(target.STATUS).default(target.STATUS).description("The status code of the exception")
31
+ });
32
+
33
+ defineStatusModel(target.STATUS, target);
34
+ }
35
+ }
36
+ });
@@ -0,0 +1,11 @@
1
+ import {NotFound} from "@tsed/exceptions";
2
+
3
+ export class ResourceNotFound extends NotFound {
4
+ readonly url: string;
5
+
6
+ constructor(url: string) {
7
+ super(`Resource "${url}" not found`);
8
+
9
+ this.url = url;
10
+ }
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @file Automatically generated by @tsed/barrels.
3
+ */
4
+ export * from "./components/ErrorFilter.js";
5
+ export * from "./components/ExceptionFilter.js";
6
+ export * from "./components/MongooseErrorFilter.js";
7
+ export * from "./components/StringErrorFilter.js";
8
+ export * from "./decorators/catch.js";
9
+ export * from "./domain/ExceptionFiltersContainer.js";
10
+ export * from "./domain/ExceptionSchema.js";
11
+ export * from "./errors/ResourceNotFound.js";
12
+ export * from "./interfaces/ExceptionFilterMethods.js";
13
+ export * from "./services/PlatformExceptions.js";
14
+ export {PlatformExceptions as default} from "./services/PlatformExceptions.js";
@@ -0,0 +1,5 @@
1
+ import {DIContext} from "@tsed/di";
2
+
3
+ export interface ExceptionFilterMethods<T = unknown> {
4
+ catch(error: T, ctx: DIContext): void;
5
+ }
@@ -0,0 +1,197 @@
1
+ import {Env} from "@tsed/core";
2
+ import {BadRequest} from "@tsed/exceptions";
3
+ import {PlatformTest} from "@tsed/platform-http/testing";
4
+ import {ValidationError} from "@tsed/platform-params";
5
+
6
+ import {PlatformExceptions} from "./PlatformExceptions.js";
7
+
8
+ describe("PlatformExceptions", () => {
9
+ describe("Env.TEST", () => {
10
+ beforeEach(() => PlatformTest.create());
11
+ afterEach(() => PlatformTest.reset());
12
+ beforeEach(() => {
13
+ vi.spyOn(PlatformTest.injector.logger, "error").mockReturnValue(undefined);
14
+ });
15
+
16
+ it("should map string error", () => {
17
+ const platformExceptions = PlatformTest.get<PlatformExceptions>(PlatformExceptions);
18
+
19
+ const ctx = PlatformTest.createRequestContext();
20
+ vi.spyOn(ctx.response, "body").mockReturnThis();
21
+ vi.spyOn(ctx.response, "setHeaders").mockReturnThis();
22
+ vi.spyOn(ctx.response, "status").mockReturnThis();
23
+ vi.spyOn(ctx.response, "contentType").mockReturnThis();
24
+
25
+ const error = "MyError";
26
+
27
+ platformExceptions.catch(error, ctx);
28
+
29
+ expect(ctx.response.body).toHaveBeenCalledWith("MyError");
30
+ });
31
+ it("should map exception", () => {
32
+ const middleware = PlatformTest.get<PlatformExceptions>(PlatformExceptions);
33
+
34
+ const ctx = PlatformTest.createRequestContext();
35
+ vi.spyOn(ctx.response, "body").mockReturnThis();
36
+ vi.spyOn(ctx.response, "setHeaders").mockReturnThis();
37
+ vi.spyOn(ctx.response, "status").mockReturnThis();
38
+ vi.spyOn(ctx.response, "contentType").mockReturnThis();
39
+
40
+ const origin = new ValidationError("wrong ID", [
41
+ {
42
+ path: "id",
43
+ error: "format"
44
+ }
45
+ ]);
46
+
47
+ const error = new BadRequest("Bad request on ID", origin);
48
+ error.headers = {
49
+ "x-path": "id"
50
+ };
51
+
52
+ middleware.catch(error, ctx);
53
+
54
+ expect(ctx.response.setHeaders).toHaveBeenCalledWith({"x-path": "id"});
55
+ expect(ctx.response.body).toHaveBeenCalledWith({
56
+ errors: [
57
+ {
58
+ error: "format",
59
+ path: "id"
60
+ }
61
+ ],
62
+ message: "Bad request on ID, innerException: wrong ID",
63
+ name: "VALIDATION_ERROR",
64
+ status: 400,
65
+ stack: undefined
66
+ });
67
+ });
68
+ it("should map error", () => {
69
+ const middleware = PlatformTest.get<PlatformExceptions>(PlatformExceptions);
70
+
71
+ const ctx = PlatformTest.createRequestContext();
72
+ vi.spyOn(ctx.response, "body").mockReturnThis();
73
+ vi.spyOn(ctx.response, "setHeaders").mockReturnThis();
74
+ vi.spyOn(ctx.response, "status").mockReturnThis();
75
+ vi.spyOn(ctx.response, "contentType").mockReturnThis();
76
+
77
+ class Custom extends Error {}
78
+
79
+ const error = new Custom("My message");
80
+
81
+ middleware.catch(error, ctx);
82
+
83
+ expect(ctx.response.setHeaders).toHaveBeenCalledWith({});
84
+ expect(ctx.response.body).toHaveBeenCalledWith({
85
+ errors: [],
86
+ message: "My message",
87
+ name: "Error",
88
+ status: 500,
89
+ stack: undefined
90
+ });
91
+ });
92
+ it("should map error from given name", () => {
93
+ const middleware = PlatformTest.get<PlatformExceptions>(PlatformExceptions);
94
+
95
+ const ctx = PlatformTest.createRequestContext();
96
+ vi.spyOn(ctx.response, "body").mockReturnThis();
97
+ vi.spyOn(ctx.response, "setHeaders").mockReturnThis();
98
+ vi.spyOn(ctx.response, "status").mockReturnThis();
99
+ vi.spyOn(ctx.response, "contentType").mockReturnThis();
100
+
101
+ class MongooseError extends Error {}
102
+
103
+ const error = new MongooseError("My message");
104
+
105
+ middleware.catch(error, ctx);
106
+
107
+ expect(ctx.response.setHeaders).toHaveBeenCalledWith({});
108
+ expect(ctx.response.body).toHaveBeenCalledWith({
109
+ errors: [],
110
+ message: "My message, innerException: My message",
111
+ name: "Error",
112
+ stack: undefined,
113
+ status: 400
114
+ });
115
+ });
116
+ it("should map raw error", () => {
117
+ const platformExceptions = PlatformTest.get<PlatformExceptions>(PlatformExceptions);
118
+
119
+ const ctx = PlatformTest.createRequestContext();
120
+ vi.spyOn(ctx.response, "body").mockReturnThis();
121
+ vi.spyOn(ctx.response, "setHeaders").mockReturnThis();
122
+ vi.spyOn(ctx.response, "status").mockReturnThis();
123
+ vi.spyOn(ctx.response, "contentType").mockReturnThis();
124
+
125
+ const error = {
126
+ status: 413,
127
+ name: "PayloadTooLargeError",
128
+ message: "request entity too large"
129
+ };
130
+
131
+ platformExceptions.catch(error, ctx);
132
+
133
+ expect(ctx.response.body).toHaveBeenCalledWith({
134
+ errors: [],
135
+ message: "request entity too large",
136
+ name: "PayloadTooLargeError",
137
+ stack: undefined,
138
+ status: 413
139
+ });
140
+ });
141
+ });
142
+
143
+ describe("Env.PROD", () => {
144
+ beforeEach(() =>
145
+ PlatformTest.create({
146
+ env: Env.PROD
147
+ })
148
+ );
149
+ afterEach(() => PlatformTest.reset());
150
+ it("should map error return internal error in prod profile", () => {
151
+ const middleware = PlatformTest.get<PlatformExceptions>(PlatformExceptions);
152
+
153
+ const ctx = PlatformTest.createRequestContext();
154
+ vi.spyOn(ctx.response, "body").mockReturnThis();
155
+ vi.spyOn(ctx.response, "setHeaders").mockReturnThis();
156
+ vi.spyOn(ctx.response, "status").mockReturnThis();
157
+ vi.spyOn(ctx.response, "contentType").mockReturnThis();
158
+
159
+ const error = new Error("My message");
160
+
161
+ middleware.catch(error, ctx);
162
+
163
+ expect(ctx.response.setHeaders).toHaveBeenCalledWith({});
164
+ expect(ctx.response.body).toHaveBeenCalledWith("InternalServerError");
165
+ });
166
+ });
167
+ describe("resourceNotFound()", () => {
168
+ beforeEach(() =>
169
+ PlatformTest.create({
170
+ env: Env.PROD
171
+ })
172
+ );
173
+ afterEach(() => PlatformTest.reset());
174
+ it("should map error return internal error in prod profile", () => {
175
+ const middleware = PlatformTest.get<PlatformExceptions>(PlatformExceptions);
176
+
177
+ const ctx = PlatformTest.createRequestContext();
178
+ ctx.request.raw.url = "url";
179
+
180
+ vi.spyOn(ctx.response, "body").mockReturnThis();
181
+ vi.spyOn(ctx.response, "setHeaders").mockReturnThis();
182
+ vi.spyOn(ctx.response, "status").mockReturnThis();
183
+ vi.spyOn(ctx.response, "contentType").mockReturnThis();
184
+
185
+ middleware.resourceNotFound(ctx);
186
+
187
+ expect(ctx.response.setHeaders).toHaveBeenCalledWith({});
188
+ expect(ctx.response.body).toHaveBeenCalledWith({
189
+ errors: [],
190
+ message: 'Resource "url" not found',
191
+ name: "NOT_FOUND",
192
+ stack: undefined,
193
+ status: 404
194
+ });
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,53 @@
1
+ import {ancestorsOf, classOf, nameOf} from "@tsed/core";
2
+ import {DIContext, inject, injectable, type TokenProvider} from "@tsed/di";
3
+
4
+ import {ErrorFilter} from "../components/ErrorFilter.js";
5
+ import {ExceptionFilter} from "../components/ExceptionFilter.js";
6
+ import {MongooseErrorFilter} from "../components/MongooseErrorFilter.js";
7
+ import {StringErrorFilter} from "../components/StringErrorFilter.js";
8
+ import {ExceptionFilterKey, ExceptionFiltersContainer} from "../domain/ExceptionFiltersContainer.js";
9
+ import {ResourceNotFound} from "../errors/ResourceNotFound.js";
10
+
11
+ /**
12
+ * Catch all errors and return the json error with the right status code when it's possible.
13
+ *
14
+ * @platform
15
+ */
16
+ export class PlatformExceptions {
17
+ types: Map<ExceptionFilterKey, TokenProvider> = new Map();
18
+
19
+ constructor() {
20
+ ExceptionFiltersContainer.forEach((token, type) => {
21
+ this.types.set(type, token);
22
+ });
23
+ }
24
+
25
+ catch(error: unknown, ctx: DIContext) {
26
+ return this.resolve(error, ctx).catch(error, ctx);
27
+ }
28
+
29
+ resourceNotFound(ctx: DIContext) {
30
+ return this.catch(new ResourceNotFound(ctx.request.url), ctx);
31
+ }
32
+
33
+ protected resolve(error: any, ctx: DIContext) {
34
+ const name = nameOf(classOf(error));
35
+
36
+ if (name && this.types.has(name)) {
37
+ return inject(this.types.get(name)!);
38
+ }
39
+
40
+ const target = ancestorsOf(error)
41
+ .reverse()
42
+ .find((target) => this.types.has(target));
43
+
44
+ if (target) {
45
+ return inject(this.types.get(target)!);
46
+ }
47
+
48
+ // default
49
+ return inject(this.types.get(Error)!);
50
+ }
51
+ }
52
+
53
+ injectable(PlatformExceptions).imports([ErrorFilter, ExceptionFilter, MongooseErrorFilter, StringErrorFilter]);