@tsed/platform-exceptions 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 +12 -11
- package/src/components/ErrorFilter.spec.ts +56 -0
- package/src/components/ErrorFilter.ts +44 -0
- package/src/components/ExceptionFilter.ts +15 -0
- package/src/components/MongooseErrorFilter.ts +12 -0
- package/src/components/StringErrorFilter.spec.ts +24 -0
- package/src/components/StringErrorFilter.ts +13 -0
- package/src/decorators/catch.ts +18 -0
- package/src/domain/ExceptionFiltersContainer.ts +18 -0
- package/src/domain/ExceptionSchema.spec.ts +217 -0
- package/src/domain/ExceptionSchema.ts +36 -0
- package/src/errors/ResourceNotFound.ts +11 -0
- package/src/index.ts +14 -0
- package/src/interfaces/ExceptionFilterMethods.ts +5 -0
- package/src/services/PlatformExceptions.spec.ts +197 -0
- package/src/services/PlatformExceptions.ts +53 -0
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.
|
|
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.
|
|
28
|
-
"@tsed/core": "8.0.
|
|
29
|
-
"@tsed/di": "8.0.
|
|
30
|
-
"@tsed/exceptions": "8.0.
|
|
31
|
-
"@tsed/schema": "8.0.
|
|
32
|
-
"@tsed/typescript": "8.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.
|
|
40
|
-
"@tsed/di": "8.0.
|
|
41
|
-
"@tsed/exceptions": "8.0.
|
|
42
|
-
"@tsed/schema": "8.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
|
+
});
|
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,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]);
|