Skip to content

Commit 4365d52

Browse files
committed
feat(exceptions): ValidationException
1 parent 0b31123 commit 4365d52

13 files changed

+207
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { ValidationError } from 'class-validator'
2+
3+
/**
4+
* @file Workspace Test Fixture - ValidationError[]
5+
* @module exceptions/tests/fixtures/VALIDATION_ERRORS
6+
*/
7+
8+
export default [
9+
{
10+
constraints: {
11+
length: '$property must be longer than or equal to 10 characters'
12+
},
13+
property: 'title',
14+
value: 'Hello'
15+
},
16+
{
17+
constraints: {
18+
contains: 'text must contain a hello string'
19+
},
20+
property: 'text',
21+
value: 'this is a great post about hell world'
22+
}
23+
] as ValidationError[]

packages/exceptions/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"@vates/toggle-scripts": "1.0.0",
122122
"@vercel/ncc": "0.31.1",
123123
"axios": "0.23.0",
124+
"class-validator": "0.13.1",
124125
"faker": "5.5.3",
125126
"firebase-admin": "10.0.0",
126127
"jest": "27.2.5",
@@ -136,6 +137,7 @@
136137
"@firebase/util": ">=1.4.0",
137138
"@types/node": ">=15.0.0",
138139
"axios": ">=0.23.0",
140+
"class-validator": ">=0.13.1",
139141
"firebase-admin": ">=10.0.0",
140142
"typescript": "4.5.0-beta"
141143
},
@@ -149,6 +151,9 @@
149151
"axios": {
150152
"optional": true
151153
},
154+
"class-validator": {
155+
"optional": true
156+
},
152157
"firebase-admin": {
153158
"optional": true
154159
},

packages/exceptions/src/dtos/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
*/
55

66
export type { ExceptionDataDTO } from './exception-data.dto'
7+
export type { ValidationExceptionDTO } from './validation-exception.dto'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import ExceptionCode from '@packages/exceptions/enums/exception-code.enum'
2+
import type { ValidationError } from 'class-validator'
3+
import type { ExceptionDataDTO } from './exception-data.dto'
4+
5+
/**
6+
* @file Data Transfer Objects - ValidationExceptionDTO
7+
* @module exceptions/dtos/ValidationExceptionDTO
8+
*/
9+
10+
/**
11+
* `ValidationExceptionData` data transfer object.
12+
*
13+
* @extends {ExceptionDataDTO}
14+
*/
15+
export interface ValidationExceptionDTO
16+
extends ExceptionDataDTO<ValidationError> {
17+
/**
18+
* HTTP error response status code.
19+
*
20+
* @default ExceptionCode.UNPROCESSABLE_ENTITY
21+
*/
22+
code?: ExceptionCode
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { ExceptionJSON } from '@packages/exceptions'
2+
import type { ValidationExceptionDTO } from '@packages/exceptions/dtos'
3+
import { ExceptionCode } from '@packages/exceptions/enums'
4+
import ERRORS from '@packages/exceptions/tests/fixtures/validation-errors.fixture'
5+
import type { Testcase } from '@tests/utils/types'
6+
import TestSubject from '../validation.exception'
7+
8+
/**
9+
* @file Unit Tests - ValidationException
10+
* @module exceptions/exceptions/tests/unit/ValidationException
11+
*/
12+
13+
describe('unit:exceptions/ValidationException', () => {
14+
type Case = Testcase<Pick<ExceptionJSON, 'code' | 'data' | 'message'>> & {
15+
do: string
16+
dto: Partial<ValidationExceptionDTO>
17+
}
18+
19+
const MESSAGE = `Model validation failure: [title,text]`
20+
21+
const cases: Case[] = [
22+
{
23+
do: 'create ValidationException',
24+
dto: { options: {} },
25+
expected: {
26+
code: ExceptionCode.UNPROCESSABLE_ENTITY,
27+
data: { isExceptionJSON: true, options: {} },
28+
message: MESSAGE
29+
}
30+
},
31+
{
32+
do: 'override default code',
33+
dto: { code: ExceptionCode.BAD_REQUEST },
34+
expected: {
35+
code: ExceptionCode.BAD_REQUEST,
36+
data: { isExceptionJSON: true },
37+
message: MESSAGE
38+
}
39+
},
40+
{
41+
do: 'override default message',
42+
dto: { id: 42, message: 'user id must be belong to existing user' },
43+
expected: {
44+
code: ExceptionCode.UNPROCESSABLE_ENTITY,
45+
data: { id: 42, isExceptionJSON: true },
46+
message: 'user id must be belong to existing user'
47+
}
48+
}
49+
]
50+
51+
it.each<Case>(cases)('should $do', testcase => {
52+
// Arrange
53+
const { dto, expected } = testcase
54+
55+
// Act
56+
const result = new TestSubject('Model', { ...dto, errors: ERRORS }).toJSON()
57+
58+
// Expect
59+
expect(result.code).toBe(expected.code)
60+
expect(result.data).toStrictEqual(expected.data)
61+
expect(result.message).toBe(expected.message)
62+
})
63+
})

packages/exceptions/src/exceptions/base.exception.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ import { DEM } from './constants.exceptions'
3434
*/
3535
// eslint-disable-next-line unicorn/custom-error-definition
3636
export default class Exception<T = any> extends AggregateError {
37+
/**
38+
* @property {ExceptionErrors<T>} errors - Aggregated errors
39+
*/
40+
override errors: ExceptionErrors<T>
41+
3742
/**
3843
* @property {ExceptionClassName} className - Associated CSS class name
3944
*/
@@ -45,15 +50,10 @@ export default class Exception<T = any> extends AggregateError {
4550
code: ExceptionCode
4651

4752
/**
48-
* @property {ExceptionData} data - Additional exception data
53+
* @property {ExceptionData} data - Custom error data
4954
*/
5055
data: ExceptionData
5156

52-
/**
53-
* @property {ExceptionErrors<T>} errors - Aggregated errors
54-
*/
55-
errors: ExceptionErrors<T>
56-
5757
/**
5858
* @property {ExceptionId} id - HTTP error response status code name
5959
*/
@@ -64,7 +64,7 @@ export default class Exception<T = any> extends AggregateError {
6464
*
6565
* @param {ExceptionCode} [code=500] - HTTP error response status code
6666
* @param {NullishString} [message=DEM] - Exception message
67-
* @param {ExceptionDataDTO<T>} [data={}] - Additional exception data
67+
* @param {ExceptionDataDTO<T>} [data={}] - Custom error data
6868
* @param {ExceptionErrors<T>} [data.errors] - Single error or group of errors
6969
* @param {string} [data.message] - Custom message. Overrides `message`
7070
* @param {string} [stack] - Error stack

packages/exceptions/src/exceptions/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55

66
export { default as Exception } from './base.exception'
77
export * from './constants.exceptions'
8+
export { default as ValidationException } from './validation.exception'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ValidationExceptionDTO } from '@packages/exceptions/dtos'
2+
import ExceptionCode from '@packages/exceptions/enums/exception-code.enum'
3+
import { isExceptionCode } from '@packages/exceptions/guards'
4+
import { ValidationExceptionErrors } from '@packages/exceptions/types'
5+
import type { ValidationError } from 'class-validator'
6+
import Exception from './base.exception'
7+
8+
/**
9+
* @file Exceptions - ValidationException
10+
* @module exceptions/exceptions/ValidationException
11+
*/
12+
13+
/**
14+
* Converts groups of [validation errors][1] into an exception.
15+
*
16+
* [1]: https://github.com/typestack/class-validator#validation-errors
17+
*
18+
* @extends {Exception}
19+
*/
20+
export default class ValidationException extends Exception<ValidationError> {
21+
/**
22+
* @property {ValidationExceptionErrors} errors - Aggregated validation errors
23+
*/
24+
declare errors: ValidationExceptionErrors
25+
26+
/**
27+
* Instantiates a new `ValidationException`.
28+
*
29+
* @param {string} model - Data model name
30+
* @param {ValidationExceptionDTO} [dto={}] - Custom error data
31+
* @param {ValidationExceptionErrors} [dto.errors] - Validation error(s)
32+
* @param {ExceptionCode} [dto.code=422] - HTTP error response status code
33+
* @param {string} [dto.message] - Custom message. Overrides preset message
34+
* @param {string} [stack] - Error stack
35+
*/
36+
constructor(
37+
readonly model: string,
38+
dto: ValidationExceptionDTO = {},
39+
stack?: string
40+
) {
41+
super(
42+
isExceptionCode(dto.code) ? dto.code : ExceptionCode.UNPROCESSABLE_ENTITY,
43+
null,
44+
dto,
45+
stack
46+
)
47+
48+
// Remove code from error data if ExceptionCode
49+
if (isExceptionCode(dto.code)) Reflect.deleteProperty(this.data, 'code')
50+
51+
// Set message if custom message wasn't provided
52+
if (!dto.message) {
53+
this.message = `${model} validation failure`
54+
55+
if (this.errors.length > 0) {
56+
this.message += `: [${this.errors.map(e => e.property)}]`
57+
}
58+
}
59+
}
60+
}

packages/exceptions/src/types/exception-data.type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
/**
7-
* Additional `Exception` data.
7+
* Custom `Exception` data.
88
*/
99
export type ExceptionData = {
1010
[x: string]: any

packages/exceptions/src/types/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
export type { EmptyString } from './empty-string.type'
77
export type { ExceptionData } from './exception-data.type'
88
export type { ExceptionErrors } from './exception-errors.type'
9+
export type { ValidationExceptionErrors } from './validation-exception-errors.type'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { ValidationError } from 'class-validator'
2+
import type { ExceptionErrors } from './exception-errors.type'
3+
4+
/**
5+
* @file Type Definitions - ValidationExceptionErrors
6+
* @module exceptions/types/ValidationExceptionErrors
7+
*/
8+
9+
/**
10+
* Aggregated `ValidationException` errors.
11+
*/
12+
export type ValidationExceptionErrors = ExceptionErrors<ValidationError>

tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"noErrorTruncation": true,
1515
"noFallthroughCasesInSwitch": true,
1616
"noImplicitAny": true,
17+
"noImplicitOverride": true,
1718
"noImplicitReturns": true,
1819
// Must be disabled to use type-only imports
1920
"noUnusedLocals": false,

yarn.lock

+9-11
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,7 @@ __metadata:
800800
"@vates/toggle-scripts": 1.0.0
801801
"@vercel/ncc": 0.31.1
802802
axios: 0.23.0
803+
class-validator: 0.13.1
803804
faker: 5.5.3
804805
firebase-admin: 10.0.0
805806
jest: 27.2.5
@@ -817,6 +818,7 @@ __metadata:
817818
"@firebase/util": ">=1.4.0"
818819
"@types/node": ">=15.0.0"
819820
axios: ">=0.23.0"
821+
class-validator: ">=0.13.1"
820822
firebase-admin: ">=10.0.0"
821823
typescript: 4.5.0-beta
822824
peerDependenciesMeta:
@@ -826,6 +828,8 @@ __metadata:
826828
optional: true
827829
axios:
828830
optional: true
831+
class-validator:
832+
optional: true
829833
firebase-admin:
830834
optional: true
831835
typescript:
@@ -921,6 +925,7 @@ __metadata:
921925
"@babel/core": 7.15.8
922926
"@commitlint/cli": 13.2.1
923927
"@commitlint/config-conventional": 13.2.0
928+
"@commitlint/types": 13.2.0
924929
"@flex-development/grease": 2.0.0
925930
"@flex-development/log": 4.0.1-dev.0
926931
"@flex-development/trext": 1.1.0
@@ -2101,10 +2106,10 @@ __metadata:
21012106
languageName: node
21022107
linkType: hard
21032108

2104-
"@types/node@npm:*, @types/node@npm:>= 8, @types/node@npm:>=15":
2105-
version: 16.10.3
2106-
resolution: "@types/node@npm:16.10.3"
2107-
checksum: 3fd429bce8a4acb497dcc62b536782a5e87ccf7cd91d64a78b263ae5d66cb72bf84be9eeeff6f84ae0567b065a40b267caa66d311f9e094990a5847bbd168a29
2109+
"@types/node@npm:*, @types/node@npm:>= 8, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:>=15":
2110+
version: 16.11.0
2111+
resolution: "@types/node@npm:16.11.0"
2112+
checksum: 194ae80ec72f664e15e03c33f116be96aa1e85b167a19e31003c53ddfc36dabd65744e9a76c1d46b7ce2e5981d2ccb8e84a85c3ec9ac89f1471daaa885bdcfd0
21082113
languageName: node
21092114
linkType: hard
21102115

@@ -2115,13 +2120,6 @@ __metadata:
21152120
languageName: node
21162121
linkType: hard
21172122

2118-
"@types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0":
2119-
version: 16.11.0
2120-
resolution: "@types/node@npm:16.11.0"
2121-
checksum: 194ae80ec72f664e15e03c33f116be96aa1e85b167a19e31003c53ddfc36dabd65744e9a76c1d46b7ce2e5981d2ccb8e84a85c3ec9ac89f1471daaa885bdcfd0
2122-
languageName: node
2123-
linkType: hard
2124-
21252123
"@types/normalize-package-data@npm:^2.4.0, @types/normalize-package-data@npm:^2.4.1":
21262124
version: 2.4.1
21272125
resolution: "@types/normalize-package-data@npm:2.4.1"

0 commit comments

Comments
 (0)