@platecms/delta-errors 0.7.0

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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # errors
2
+
3
+ This package contains all error related implementations for both frontend and backend.
4
+
5
+ - [Features](#features)
6
+ - [Base classes and types](#base-classes-and-types)
7
+ - [Error classes](#error-classes)
8
+
9
+ ## Features
10
+
11
+ This package contains errors and their context types that can be returned as API responses.
12
+ For example, the `InvalidPrnError` is not in here, because it should be returned as a `BadRequestError` if it is thrown.
13
+
14
+ Furthermore, these are all called "errors". The difference between errors and exceptions is specific to languages (or even frameworks):
15
+
16
+ - TypeScript/JavaScript uses errors exclusively. NestJS has exceptions to return as responses.
17
+ - Python uses exceptions exclusively.
18
+
19
+ ### Base classes and types
20
+
21
+ - `PlateError` - Abstract base class for all Plate errors. Extends the standard `Error` class and provides context and error code functionality.
22
+ - `ErrorCode` - Enum containing all available error codes for identifying different types of errors.
23
+ - `ResourceErrorContext` - Interface defining the context structure for resource-related errors, including action type and resource type information.
24
+
25
+ ### Error classes
26
+
27
+ - `BadRequestError` - Thrown when the request is malformed or contains invalid parameters.
28
+ - `ForbiddenError` - Thrown when the user is authenticated but does not have sufficient permissions to access a resource.
29
+ - `InternalServerError` - Thrown when an unexpected error occurs on the server side.
30
+ - `InvalidCastError` - Thrown when a CAST is invalid or malformed.
31
+ - `InvalidPrnError` - Thrown when a PRN is invalid or malformed.
32
+ - `NotFoundError` - Thrown when a requested resource cannot be found.
33
+ - `NotImplementedError` - Thrown when attempting to use functionality that has not been implemented.
34
+ - `UnauthorizedError` - Thrown when authentication is required but not provided or is invalid.
35
+ - `UnprocessableContentError` - Thrown when the request content is semantically incorrect.
36
+ - `ValidationFailedError` - Thrown when request validation fails.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@platecms/delta-errors",
3
+ "version": "0.7.0",
4
+ "description": "Error classes and their data for the Delta platform.",
5
+ "license": "UNLICENSED",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://bitbucket.org/startmetplate/delta.git"
12
+ },
13
+ "homepage": "https://bitbucket.org/startmetplate/delta/src/dev/packages/errors",
14
+ "main": "./src/index.js",
15
+ "types": "./src/index.d.ts",
16
+ "files": [
17
+ "src/**/*"
18
+ ],
19
+ "peerDependencies": {
20
+ "@platecms/delta-types": "0.7.0",
21
+ "tslib": "2.8.1",
22
+ "class-transformer": "0.5.1",
23
+ "reflect-metadata": "0.2.2",
24
+ "class-validator": "0.14.2"
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from "./lib/errors/bad-request.error";
2
+ export * from "./lib/errors/class-validator.error";
3
+ export * from "./lib/errors/forbidden.error";
4
+ export * from "./lib/errors/internal-server.error";
5
+ export * from "./lib/errors/not-found.error";
6
+ export * from "./lib/errors/not-implemented.error";
7
+ export * from "./lib/errors/unauthorized.error";
8
+ export * from "./lib/errors/unprocessable-content.error";
9
+ export * from "./lib/errors/validation-failed.error";
10
+ export * from "./lib/helpers/to-class-validator-error.helper";
11
+ export type * from "./lib/types/base-error-context.type";
12
+ export type * from "./lib/types/base-validation-error-data.interface";
13
+ export * from "./lib/types/error-code.enum";
14
+ export * from "./lib/types/plate.error";
15
+ export type * from "./lib/types/resource-access-error-context.type";
@@ -0,0 +1,14 @@
1
+ import { BaseErrorContext } from "../types/base-error-context.type";
2
+ import { ErrorCode } from "../types/error-code.enum";
3
+ import { PlateError } from "../types/plate.error";
4
+
5
+ /**
6
+ * A request is malformed.
7
+ */
8
+ export class BadRequestError extends PlateError {
9
+ public override readonly code = ErrorCode.BAD_REQUEST;
10
+
11
+ public constructor(message: string, context: BaseErrorContext = {}) {
12
+ super(message, context);
13
+ }
14
+ }
@@ -0,0 +1,26 @@
1
+ import { BaseValidationErrorData } from "../types/base-validation-error-data.interface";
2
+ import { ErrorCode } from "../types/error-code.enum";
3
+ import { PlateError } from "../types/plate.error";
4
+ import { ValidationErrorContext } from "../types/validation-error-context.interface";
5
+
6
+ interface ClassValidatorErrorData extends BaseValidationErrorData {
7
+ /**
8
+ * The constraint that failed.
9
+ */
10
+ constraint: string;
11
+ }
12
+
13
+ /**
14
+ * The request failed validation due to class-validator.
15
+ * @remarks This error can be thrown when using class-validator to validate an object.
16
+ * @remarks This error can be removed once we replace class-validator with our own validation framework.
17
+ */
18
+ class ClassValidatorError extends PlateError<ValidationErrorContext<ClassValidatorErrorData>> {
19
+ public override readonly code = ErrorCode.VALIDATION_FAILED;
20
+
21
+ public constructor(context: ValidationErrorContext<ClassValidatorErrorData>) {
22
+ super("Validation failed!", context);
23
+ }
24
+ }
25
+
26
+ export { ClassValidatorError, type ClassValidatorErrorData };
@@ -0,0 +1,18 @@
1
+ import { ErrorCode } from "../types/error-code.enum";
2
+ import { PlateError } from "../types/plate.error";
3
+ import { ResourceAccessErrorContext } from "../types/resource-access-error-context.type";
4
+
5
+ /**
6
+ * The client does not have access to the requested resource.
7
+ * @remarks This corresponds with status 403 in the HTTP definition.
8
+ */
9
+ export class ForbiddenError extends PlateError<ResourceAccessErrorContext> {
10
+ public override readonly code = ErrorCode.FORBIDDEN;
11
+
12
+ public constructor(context: ResourceAccessErrorContext) {
13
+ super(
14
+ `The client does not have the permission to perform '${context.action}' on '${context.resourceType}'.`,
15
+ context,
16
+ );
17
+ }
18
+ }
@@ -0,0 +1,22 @@
1
+ import { BaseErrorContext } from "../types/base-error-context.type";
2
+ import { ErrorCode } from "../types/error-code.enum";
3
+ import { PlateError } from "../types/plate.error";
4
+
5
+ export interface InternalServerErrorContext extends BaseErrorContext {
6
+ /**
7
+ * The error that caused this internal server error.
8
+ */
9
+ readonly originalError: Error;
10
+ }
11
+
12
+ /**
13
+ * An unexpected error occurred on the server.
14
+ * @remarks This corresponds with status 500 in the HTTP definition.
15
+ */
16
+ export class InternalServerError extends PlateError<InternalServerErrorContext> {
17
+ public override readonly code = ErrorCode.INTERNAL_SERVER_ERROR;
18
+
19
+ public constructor(context: InternalServerErrorContext) {
20
+ super("Something unexpected went wrong!", context);
21
+ }
22
+ }
@@ -0,0 +1,24 @@
1
+ import { BaseErrorContext } from "../types/base-error-context.type";
2
+ import { ErrorCode } from "../types/error-code.enum";
3
+ import { PlateError } from "../types/plate.error";
4
+
5
+ export interface NotFoundErrorContext extends BaseErrorContext {
6
+ resourceType: string;
7
+
8
+ /**
9
+ * The value that was used to retrieve the resource. This can be a PRN, email, path, slug, etc.
10
+ */
11
+ identifier: string;
12
+ }
13
+
14
+ /**
15
+ * A requested resource cannot be found.
16
+ * @remarks This corresponds with status 404 in the HTTP definition.
17
+ */
18
+ export class NotFoundError extends PlateError<NotFoundErrorContext> {
19
+ public override readonly code = ErrorCode.NOT_FOUND;
20
+
21
+ public constructor(context: NotFoundErrorContext) {
22
+ super(`${context.resourceType} with PRN '${context.identifier}' could not be found.`, context);
23
+ }
24
+ }
@@ -0,0 +1,14 @@
1
+ import { ErrorCode } from "../types/error-code.enum";
2
+ import { PlateError } from "../types/plate.error";
3
+
4
+ /**
5
+ * The requested functionality has not been implemented.
6
+ * @remarks This corresponds with status 501 in the HTTP definition.
7
+ */
8
+ export class NotImplementedError extends PlateError<null> {
9
+ public override readonly code = ErrorCode.NOT_IMPLEMENTED;
10
+
11
+ public constructor() {
12
+ super("The functionality is not yet implemented.", null);
13
+ }
14
+ }
@@ -0,0 +1,15 @@
1
+ import { BaseErrorContext } from "../types/base-error-context.type";
2
+ import { ErrorCode } from "../types/error-code.enum";
3
+ import { PlateError } from "../types/plate.error";
4
+
5
+ /**
6
+ * The client is not authenticated.
7
+ * @remarks This corresponds with status 401 in the HTTP definition.
8
+ */
9
+ export class UnauthorizedError extends PlateError<BaseErrorContext | null> {
10
+ public override readonly code = ErrorCode.UNAUTHORIZED;
11
+
12
+ public constructor(context: BaseErrorContext | null = null) {
13
+ super("The client is not authenticated.", context);
14
+ }
15
+ }
@@ -0,0 +1,27 @@
1
+ import { BaseErrorContext } from "../types/base-error-context.type";
2
+ import { ErrorCode } from "../types/error-code.enum";
3
+ import { PlateError } from "../types/plate.error";
4
+
5
+ export interface UnprocessableContentErrorContext extends BaseErrorContext {
6
+ /**
7
+ * The resource type that was attempted to be accessed.
8
+ */
9
+ resourceType: string;
10
+
11
+ /**
12
+ * The reason why the request is unprocessable.
13
+ */
14
+ reason: string;
15
+ }
16
+
17
+ /**
18
+ * A valid request was submitted but unable to be followed due to semantic errors.
19
+ * @remarks This is used as a fallback code, as most such errors will be returned as a more specific exception like ValidationFailedException.
20
+ */
21
+ export class UnprocessableContentError extends PlateError<UnprocessableContentErrorContext> {
22
+ public override readonly code = ErrorCode.UNPROCESSABLE_CONTENT;
23
+
24
+ public constructor(message: string, context: UnprocessableContentErrorContext) {
25
+ super(message, context);
26
+ }
27
+ }
@@ -0,0 +1,41 @@
1
+ import { BaseValidationErrorData } from "../types/base-validation-error-data.interface";
2
+ import { ErrorCode } from "../types/error-code.enum";
3
+ import { PlateError } from "../types/plate.error";
4
+ import { ValidationErrorContext } from "../types/validation-error-context.interface";
5
+
6
+ interface ValidationFailedErrorData extends BaseValidationErrorData {
7
+ /**
8
+ * The rule that failed.
9
+ */
10
+ validationRule: {
11
+ /**
12
+ * The type of the rule that failed.
13
+ * @example Count, StringFormat, etc.
14
+ */
15
+ ruleType: string;
16
+
17
+ /**
18
+ * Only set if the validation was a dynamically created validation rule.
19
+ */
20
+ prn: string | undefined;
21
+
22
+ /**
23
+ * Additional settings of the validation rule, e.g. min, max, regex.
24
+ */
25
+ settings: object;
26
+ };
27
+ }
28
+
29
+ /**
30
+ * The request failed validation.
31
+ * @remarks This error can be thrown for either hardcoded validations (like a required field for an input DTO) or dynamic validation rules (like in the internal validation system).
32
+ */
33
+ class ValidationFailedError extends PlateError<ValidationErrorContext<ValidationFailedErrorData>> {
34
+ public override readonly code = ErrorCode.VALIDATION_FAILED;
35
+
36
+ public constructor(context: ValidationErrorContext<ValidationFailedErrorData>) {
37
+ super("Validation failed.", context);
38
+ }
39
+ }
40
+
41
+ export { ValidationFailedError, type ValidationFailedErrorData };
@@ -0,0 +1,361 @@
1
+ import {
2
+ IsArray,
3
+ IsEmail,
4
+ IsNotEmpty,
5
+ IsNumber,
6
+ Matches,
7
+ Max,
8
+ Min,
9
+ MinLength,
10
+ ValidateNested,
11
+ validate,
12
+ } from "class-validator";
13
+ import { ClassValidatorError } from "../errors/class-validator.error";
14
+ import { toClassValidatorError } from "./to-class-validator-error.helper";
15
+
16
+ // Test classes to create realistic validation errors
17
+ class UserRegistrationDto {
18
+ @IsEmail({}, { message: "email must be an email" })
19
+ public email!: string;
20
+
21
+ @MinLength(8, { message: "password must be longer than or equal to 8 characters" })
22
+ @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/u, {
23
+ message: "password must match /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/ regular expression",
24
+ })
25
+ public password!: string;
26
+
27
+ @IsNotEmpty({ message: "firstName should not be empty" })
28
+ public firstName!: string;
29
+
30
+ @IsNotEmpty({ message: "lastName should not be empty" })
31
+ public lastName!: string;
32
+
33
+ @IsNumber({}, { message: "age must be a number" })
34
+ @Min(18, { message: "age must not be less than 18" })
35
+ @Max(100, { message: "age must not be greater than 100" })
36
+ public age!: number;
37
+ }
38
+
39
+ class CountryDto {
40
+ @IsNotEmpty({ message: "code should not be empty" })
41
+ public code!: string;
42
+
43
+ @IsNotEmpty({ message: "name should not be empty" })
44
+ public name!: string;
45
+ }
46
+
47
+ class NestedAddressDto {
48
+ @IsNotEmpty({ message: "street should not be empty" })
49
+ public street!: string;
50
+
51
+ @IsNotEmpty({ message: "city should not be empty" })
52
+ public city!: string;
53
+
54
+ @IsNotEmpty({ message: "zipCode should not be empty" })
55
+ public zipCode!: string;
56
+
57
+ @ValidateNested()
58
+ public country!: CountryDto;
59
+ }
60
+
61
+ class UserWithAddressDto {
62
+ @IsEmail({}, { message: "email must be an email" })
63
+ public email!: string;
64
+
65
+ @ValidateNested()
66
+ public address!: NestedAddressDto;
67
+ }
68
+
69
+ class ArrayItemDto {
70
+ @IsNotEmpty({ message: "item should not be empty" })
71
+ public item!: string;
72
+ }
73
+
74
+ class ArrayContainerDto {
75
+ @IsArray({ message: "items must be an array" })
76
+ @ValidateNested({ each: true })
77
+ public items!: ArrayItemDto[];
78
+ }
79
+
80
+ describe("toClassValidatorError", () => {
81
+ it("handles empty validation errors array", () => {
82
+ // Act
83
+ const result = toClassValidatorError([]);
84
+
85
+ // Assert
86
+ expect(result).toBeInstanceOf(ClassValidatorError);
87
+ expect(result.message).toBe("Validation failed!");
88
+ expect(result.context.validationErrors).toEqual([]);
89
+ expect(result.code).toBe("VALIDATION_FAILED");
90
+ });
91
+
92
+ it("handles single validation error", async () => {
93
+ // Arrange
94
+ const userDto = new UserRegistrationDto();
95
+ userDto.email = "invalid-email";
96
+ userDto.password = "ValidPass123";
97
+ userDto.firstName = "Crazy";
98
+ userDto.lastName = "Frog";
99
+ userDto.age = 25;
100
+ const validationErrors = await validate(userDto);
101
+
102
+ // Act
103
+ const result = toClassValidatorError(validationErrors);
104
+
105
+ // Assert
106
+ expect(result).toBeInstanceOf(ClassValidatorError);
107
+ expect(result.message).toBe("Validation failed!");
108
+ expect(result.context.validationErrors).toHaveLength(1);
109
+ expect(result.context.validationErrors[0]).toEqual({
110
+ path: ["email"],
111
+ message: "email must be an email",
112
+ constraint: "isEmail",
113
+ provided: "invalid-email",
114
+ });
115
+ });
116
+
117
+ it("handles multiple top-level validation errors", async () => {
118
+ // Arrange
119
+ const userDto = new UserRegistrationDto();
120
+ userDto.email = "invalid-email";
121
+ userDto.password = "ValidPass123";
122
+ userDto.firstName = "Crazy";
123
+ userDto.lastName = "Frog";
124
+ userDto.age = 150; // Above maximum age
125
+ const validationErrors = await validate(userDto);
126
+
127
+ // Act
128
+ const result = toClassValidatorError(validationErrors);
129
+
130
+ // Assert
131
+ expect(result.context.validationErrors).toHaveLength(2); // email + age
132
+ expect(result.context.validationErrors[0]).toEqual({
133
+ path: ["email"],
134
+ message: "email must be an email",
135
+ constraint: "isEmail",
136
+ provided: "invalid-email",
137
+ });
138
+ expect(result.context.validationErrors[1]).toEqual({
139
+ path: ["age"],
140
+ message: "age must not be greater than 100",
141
+ constraint: "max",
142
+ provided: 150,
143
+ });
144
+ });
145
+
146
+ it("handles multiple constraints on single property", async () => {
147
+ // Arrange
148
+ const userDto = new UserRegistrationDto();
149
+ userDto.email = "valid@email.com";
150
+ userDto.password = "123"; // Invalid: too short and doesn't match pattern
151
+ userDto.firstName = "Crazy";
152
+ userDto.lastName = "Frog";
153
+ userDto.age = 25;
154
+ const validationErrors = await validate(userDto);
155
+
156
+ // Act
157
+ const result = toClassValidatorError(validationErrors);
158
+
159
+ // Assert
160
+ expect(result.context.validationErrors).toHaveLength(2);
161
+ const passwordErrors = result.context.validationErrors.filter((error) => error.path.includes("password"));
162
+ expect(passwordErrors).toHaveLength(2);
163
+ expect(passwordErrors).toEqual(
164
+ expect.arrayContaining([
165
+ {
166
+ path: ["password"],
167
+ message: "password must be longer than or equal to 8 characters",
168
+ constraint: "minLength",
169
+ provided: "123",
170
+ },
171
+ {
172
+ path: ["password"],
173
+ message: "password must match /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/ regular expression",
174
+ constraint: "matches",
175
+ provided: "123",
176
+ },
177
+ ]),
178
+ );
179
+ });
180
+
181
+ it("handles nested validation errors", async () => {
182
+ // Arrange
183
+ const userWithAddressDto = Object.assign(new UserWithAddressDto(), {
184
+ email: "invalid-email",
185
+ address: Object.assign(new NestedAddressDto(), {
186
+ street: "",
187
+ city: "",
188
+ zipCode: "",
189
+ country: Object.assign(new CountryDto(), {
190
+ code: "",
191
+ name: "",
192
+ }),
193
+ }),
194
+ });
195
+ const validationErrors = await validate(userWithAddressDto);
196
+
197
+ // Act
198
+ const result = toClassValidatorError(validationErrors);
199
+
200
+ // Assert
201
+ expect(result.context.validationErrors.length).toBeGreaterThan(0);
202
+ expect(result.context.validationErrors).toEqual(
203
+ expect.arrayContaining([
204
+ {
205
+ path: ["email"],
206
+ message: "email must be an email",
207
+ constraint: "isEmail",
208
+ provided: "invalid-email",
209
+ },
210
+ {
211
+ path: ["address", "street"],
212
+ message: "street should not be empty",
213
+ constraint: "isNotEmpty",
214
+ provided: "",
215
+ },
216
+ {
217
+ path: ["address", "city"],
218
+ message: "city should not be empty",
219
+ constraint: "isNotEmpty",
220
+ provided: "",
221
+ },
222
+ {
223
+ path: ["address", "zipCode"],
224
+ message: "zipCode should not be empty",
225
+ constraint: "isNotEmpty",
226
+ provided: "",
227
+ },
228
+ {
229
+ path: ["address", "country", "code"],
230
+ message: "code should not be empty",
231
+ constraint: "isNotEmpty",
232
+ provided: "",
233
+ },
234
+ {
235
+ path: ["address", "country", "name"],
236
+ message: "name should not be empty",
237
+ constraint: "isNotEmpty",
238
+ provided: "",
239
+ },
240
+ ]),
241
+ );
242
+ });
243
+
244
+ it("handles array validation errors", async () => {
245
+ // Arrange
246
+ const arrayDto = new ArrayContainerDto();
247
+ arrayDto.items = "not-an-array" as unknown as ArrayItemDto[];
248
+ const validationErrors = await validate(arrayDto);
249
+
250
+ // Act
251
+ const result = toClassValidatorError(validationErrors);
252
+
253
+ // Assert
254
+ expect(result.context.validationErrors).toEqual(
255
+ expect.arrayContaining([
256
+ {
257
+ path: ["items"],
258
+ message: "items must be an array",
259
+ constraint: "isArray",
260
+ provided: "not-an-array",
261
+ },
262
+ {
263
+ path: ["items"],
264
+ message: "each value in nested property items must be either object or array",
265
+ constraint: "nestedValidation",
266
+ provided: "not-an-array",
267
+ },
268
+ ]),
269
+ );
270
+ });
271
+
272
+ it("handles nested array validation errors", async () => {
273
+ // Arrange
274
+ const arrayDto = new ArrayContainerDto();
275
+ arrayDto.items = [
276
+ Object.assign(new ArrayItemDto(), { item: "" }), // Empty item
277
+ Object.assign(new ArrayItemDto(), { item: "valid" }),
278
+ Object.assign(new ArrayItemDto(), { item: "" }), // Empty item
279
+ ];
280
+ const validationErrors = await validate(arrayDto);
281
+
282
+ // Act
283
+ const result = toClassValidatorError(validationErrors);
284
+
285
+ // Assert
286
+ expect(result.context.validationErrors).toHaveLength(2);
287
+ expect(result.context.validationErrors).toEqual(
288
+ expect.arrayContaining([
289
+ {
290
+ path: ["items", "0", "item"],
291
+ message: "item should not be empty",
292
+ constraint: "isNotEmpty",
293
+ provided: "",
294
+ },
295
+ {
296
+ path: ["items", "2", "item"],
297
+ message: "item should not be empty",
298
+ constraint: "isNotEmpty",
299
+ provided: "",
300
+ },
301
+ ]),
302
+ );
303
+ });
304
+
305
+ it("handles errors with multiple constraints and data types", async () => {
306
+ // Arrange
307
+ const userDto = new UserRegistrationDto();
308
+ userDto.email = "not-an-email";
309
+ userDto.password = "weak";
310
+ userDto.firstName = "";
311
+ userDto.lastName = "";
312
+ userDto.age = 15; // Below minimum age
313
+ const validationErrors = await validate(userDto);
314
+
315
+ // Act
316
+ const result = toClassValidatorError(validationErrors);
317
+
318
+ // Assert
319
+ expect(result.context.validationErrors).toHaveLength(6); // email + password(2) + firstName + lastName + age
320
+ expect(result.context.validationErrors).toEqual(
321
+ expect.arrayContaining([
322
+ {
323
+ path: ["email"],
324
+ message: "email must be an email",
325
+ constraint: "isEmail",
326
+ provided: "not-an-email",
327
+ },
328
+ {
329
+ path: ["password"],
330
+ message: "password must be longer than or equal to 8 characters",
331
+ constraint: "minLength",
332
+ provided: "weak",
333
+ },
334
+ {
335
+ path: ["password"],
336
+ message: "password must match /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/ regular expression",
337
+ constraint: "matches",
338
+ provided: "weak",
339
+ },
340
+ {
341
+ path: ["firstName"],
342
+ message: "firstName should not be empty",
343
+ constraint: "isNotEmpty",
344
+ provided: "",
345
+ },
346
+ {
347
+ path: ["lastName"],
348
+ message: "lastName should not be empty",
349
+ constraint: "isNotEmpty",
350
+ provided: "",
351
+ },
352
+ {
353
+ path: ["age"],
354
+ message: "age must not be less than 18",
355
+ constraint: "min",
356
+ provided: 15,
357
+ },
358
+ ]),
359
+ );
360
+ });
361
+ });
@@ -0,0 +1,41 @@
1
+ import { ValidationError } from "class-validator";
2
+ import { ClassValidatorError, ClassValidatorErrorData } from "../errors/class-validator.error";
3
+
4
+ /**
5
+ * Flattens class-validator errors into a list of ClassValidatorErrorData.
6
+ * @param errors - The class-validator errors.
7
+ * @returns A list of ClassValidatorErrorData.
8
+ */
9
+ function toClassValidatorErrorData(errors: ValidationError[]): ClassValidatorErrorData[] {
10
+ const issues: ClassValidatorErrorData[] = [];
11
+
12
+ function visit(error: ValidationError, path: string[] = []): void {
13
+ const currentPath = [...path, error.property].filter(Boolean);
14
+
15
+ const constraints = error.constraints ?? {};
16
+ for (const [constraint, message] of Object.entries(constraints)) {
17
+ issues.push({
18
+ path: currentPath,
19
+ message,
20
+ constraint,
21
+ provided: error.value,
22
+ });
23
+ }
24
+
25
+ (error.children ?? []).forEach((child) => visit(child, currentPath));
26
+ }
27
+
28
+ errors.forEach((error) => visit(error));
29
+ return issues;
30
+ }
31
+
32
+ /**
33
+ * Converts class-validator errors to a ClassValidatorError.
34
+ * @param errors - The class-validator errors.
35
+ * @returns A ClassValidatorError.
36
+ */
37
+ function toClassValidatorError(errors: ValidationError[]): ClassValidatorError {
38
+ return new ClassValidatorError({ validationErrors: toClassValidatorErrorData(errors) });
39
+ }
40
+
41
+ export { toClassValidatorError };
@@ -0,0 +1,6 @@
1
+ export interface BaseErrorContext {
2
+ /**
3
+ * The operation that was performed when the error occurred.
4
+ */
5
+ operation?: string;
6
+ }
@@ -0,0 +1,16 @@
1
+ export interface BaseValidationErrorData {
2
+ /**
3
+ * Location of the error in the object.
4
+ */
5
+ path: string[];
6
+
7
+ /**
8
+ * Human-readable error description, only to be used by developers.
9
+ */
10
+ message: string;
11
+
12
+ /**
13
+ * The value that failed validation.
14
+ */
15
+ provided: unknown;
16
+ }
@@ -0,0 +1,12 @@
1
+ export const enum ErrorCode {
2
+ INVALID_PRN = "INVALID_PRN",
3
+ INVALID_CAST = "INVALID_CAST",
4
+ NOT_FOUND = "NOT_FOUND",
5
+ BAD_REQUEST = "BAD_REQUEST",
6
+ UNPROCESSABLE_CONTENT = "UNPROCESSABLE_CONTENT",
7
+ VALIDATION_FAILED = "VALIDATION_FAILED",
8
+ UNAUTHORIZED = "UNAUTHORIZED",
9
+ FORBIDDEN = "FORBIDDEN",
10
+ NOT_IMPLEMENTED = "NOT_IMPLEMENTED",
11
+ INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
12
+ }
@@ -0,0 +1,26 @@
1
+ import { BaseErrorContext } from "./base-error-context.type";
2
+ import { ErrorCode } from "./error-code.enum";
3
+
4
+ /**
5
+ * A base class for all Plate errors.
6
+ */
7
+ export abstract class PlateError<
8
+ TContext extends BaseErrorContext | BaseErrorContext[] | null = BaseErrorContext,
9
+ > extends Error {
10
+ /**
11
+ * The context of the error.
12
+ * @remarks This is used to provide additional information about the error. The data included depends on the error type.
13
+ */
14
+ public readonly context: TContext;
15
+
16
+ /**
17
+ * The error code.
18
+ * @remarks This is used to identify the type of error that occurred.
19
+ */
20
+ public abstract readonly code: ErrorCode;
21
+
22
+ public constructor(message: string, context: TContext) {
23
+ super(message);
24
+ this.context = context;
25
+ }
26
+ }
@@ -0,0 +1,17 @@
1
+ import { ActionType } from "@platecms/delta-types";
2
+ import { BaseErrorContext } from "./base-error-context.type";
3
+
4
+ /**
5
+ * The context of a resource error.
6
+ */
7
+ export interface ResourceAccessErrorContext extends BaseErrorContext {
8
+ /**
9
+ * The action that was attempted.
10
+ */
11
+ action: ActionType;
12
+
13
+ /**
14
+ * The resource type that was attempted to be accessed.
15
+ */
16
+ resourceType: string;
17
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseValidationErrorData } from "./base-validation-error-data.interface";
2
+ import { BaseErrorContext } from "./base-error-context.type";
3
+
4
+ export interface ValidationErrorContext<TValidationErrorData extends BaseValidationErrorData> extends BaseErrorContext {
5
+ validationErrors: TValidationErrorData[];
6
+ }