@ballistix.digital/exception-mapper 0.3.0 → 0.4.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/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `ValidationExceptionMapper` fills the `args` of a detail from the rule's own
|
|
12
|
+
constraint values when the decorator sets no `context`: `{ max: 24 }` for
|
|
13
|
+
`@Max(24)`, `{ pattern, modifiers }` for `@Matches`, `{ values }` for
|
|
14
|
+
`@IsIn` and `@IsEnum`, and a positional `{ constraints }` for any other rule
|
|
15
|
+
with values. A rule without values still carries no `args`. The mapper reads
|
|
16
|
+
the class-validator metadata of a class once per mapped failure.
|
|
17
|
+
- `ExceptionDtoMapper` copies the `type` of an exception into the envelope
|
|
18
|
+
when the exception carries one.
|
|
19
|
+
|
|
9
20
|
## 0.1.0 - 2026-09-07
|
|
10
21
|
|
|
11
22
|
### Added
|
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@ turns one kind of thrown value into an exception, and the **registry** holds
|
|
|
6
6
|
the mappers in order. The exception classes, the codes and the envelope live
|
|
7
7
|
in [`@ballistix.digital/exception-types`](../exception-types/README.md).
|
|
8
8
|
|
|
9
|
-
This file is the consumer guide.
|
|
10
|
-
|
|
9
|
+
This file is the consumer guide. The pages that describe how the package is
|
|
10
|
+
built live in the [Ballistix wiki](https://github.com/layeredprints/ballistix-okf-wiki/blob/main/general/libraries/exception-mapper/).
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -109,7 +109,27 @@ deadlock or a connection failure gives 500.
|
|
|
109
109
|
|
|
110
110
|
`ValidationExceptionMapper` matches the `ValidationException` that
|
|
111
111
|
`convertAndValidate` throws, and gives one detail for each field and rule that
|
|
112
|
-
failed. `
|
|
112
|
+
failed. The `args` of a detail come from the decorator's explicit `context`
|
|
113
|
+
when it sets one, else from the rule's own constraint values, read from the
|
|
114
|
+
class-validator metadata of the validated class. So `@Max(24)` on `months`
|
|
115
|
+
gives `{ inputField: 'months', rule: 'MAX', args: { max: 24 } }` with no
|
|
116
|
+
change to the DTO.
|
|
117
|
+
|
|
118
|
+
| Rule | args |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `min`, `max`, `minLength`, `maxLength`, `arrayMinSize`, `arrayMaxSize`, `minDate`, `maxDate` | `{ min }` or `{ max }` |
|
|
121
|
+
| `isLength`, `isByteLength` | `{ min, max }` |
|
|
122
|
+
| `matches` | `{ pattern, modifiers }`, the source and flags of the RegExp |
|
|
123
|
+
| `isIn`, `isNotIn`, `isEnum` | `{ values }` |
|
|
124
|
+
| `isDivisibleBy` | `{ divisor }` |
|
|
125
|
+
| `contains`, `notContains` | `{ seed }` |
|
|
126
|
+
| `equals`, `notEquals` | `{ comparison }` |
|
|
127
|
+
| any other rule with constraint values | `{ constraints: [...] }`, positional |
|
|
128
|
+
| a rule without constraint values (`isString`, `isNotEmpty`) | no `args` |
|
|
129
|
+
|
|
130
|
+
The values are JSON-safe: a RegExp becomes its source and flags, a Date its
|
|
131
|
+
ISO string, and a function or class object is left out. An error without a
|
|
132
|
+
`target` keeps the contexts only. `GenericExceptionMapper` is the fallback the registry uses when no mapper in
|
|
113
133
|
the list supports the value, so `map()` never returns `undefined`.
|
|
114
134
|
|
|
115
135
|
## Write your HTTP filter
|
|
@@ -137,7 +157,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
|
|
137
157
|
The filter stays in the application, so a breakpoint on the raw thrown value
|
|
138
158
|
sits in application code. What the filter logs, and what request context it
|
|
139
159
|
adds, is the decision of the application. See
|
|
140
|
-
[ADR 0002](
|
|
160
|
+
[ADR 0002](https://github.com/layeredprints/ballistix-okf-wiki/blob/main/general/libraries/exception-mapper/decisions/0002-filter-stays-in-the-app.md).
|
|
141
161
|
|
|
142
162
|
Register it as the global filter:
|
|
143
163
|
|
|
@@ -17,6 +17,9 @@ let ExceptionDtoMapper = class ExceptionDtoMapper {
|
|
|
17
17
|
code: exception.code,
|
|
18
18
|
errors: exception.errors,
|
|
19
19
|
};
|
|
20
|
+
if (exception.type !== undefined) {
|
|
21
|
+
plain.type = exception.type;
|
|
22
|
+
}
|
|
20
23
|
try {
|
|
21
24
|
return (0, exception_types_1.convertAndValidate)(exception_types_1.ExceptionDto, plain, true);
|
|
22
25
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { ValidationException, ValidationFailedException } from '@ballistix.digital/exception-types';
|
|
2
2
|
import { ExceptionMapper } from './exceptionMapper.interface';
|
|
3
3
|
export declare class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
|
|
4
|
+
private static readonly argsByRuleName;
|
|
5
|
+
private readonly metadataByClass;
|
|
4
6
|
supports(error: unknown): error is ValidationException;
|
|
5
7
|
map(error: ValidationException): ValidationFailedException;
|
|
8
|
+
private toDetails;
|
|
9
|
+
private argsFromMetadata;
|
|
10
|
+
private metadataOf;
|
|
11
|
+
private argsFromConstraints;
|
|
12
|
+
private toJsonValue;
|
|
13
|
+
private static patternArgs;
|
|
6
14
|
}
|
|
@@ -5,27 +5,108 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
};
|
|
8
|
+
var ValidationExceptionMapper_1;
|
|
8
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
10
|
exports.ValidationExceptionMapper = void 0;
|
|
10
11
|
const common_1 = require("@nestjs/common");
|
|
12
|
+
const class_validator_1 = require("class-validator");
|
|
11
13
|
const exception_types_1 = require("@ballistix.digital/exception-types");
|
|
12
|
-
const collectValidationDetails = (error) => {
|
|
13
|
-
const ownDetails = Object.keys(error.constraints ?? {}).map((ruleName) => ({
|
|
14
|
-
inputField: error.property,
|
|
15
|
-
rule: exception_types_1.validationRuleByName[ruleName] ?? exception_types_1.ValidationRuleEnum.UNKNOWN,
|
|
16
|
-
args: error.contexts?.[ruleName],
|
|
17
|
-
}));
|
|
18
|
-
return ownDetails.concat((error.children ?? []).flatMap(collectValidationDetails));
|
|
19
|
-
};
|
|
20
14
|
let ValidationExceptionMapper = class ValidationExceptionMapper {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.metadataByClass = new Map();
|
|
17
|
+
}
|
|
18
|
+
static { ValidationExceptionMapper_1 = this; }
|
|
19
|
+
static { this.argsByRuleName = {
|
|
20
|
+
[class_validator_1.MIN]: ([min]) => ({ min }),
|
|
21
|
+
[class_validator_1.MAX]: ([max]) => ({ max }),
|
|
22
|
+
[class_validator_1.MIN_LENGTH]: ([min]) => ({ min }),
|
|
23
|
+
[class_validator_1.MAX_LENGTH]: ([max]) => ({ max }),
|
|
24
|
+
[class_validator_1.ARRAY_MIN_SIZE]: ([min]) => ({ min }),
|
|
25
|
+
[class_validator_1.ARRAY_MAX_SIZE]: ([max]) => ({ max }),
|
|
26
|
+
[class_validator_1.MIN_DATE]: ([min]) => ({ min }),
|
|
27
|
+
[class_validator_1.MAX_DATE]: ([max]) => ({ max }),
|
|
28
|
+
[class_validator_1.IS_LENGTH]: ([min, max]) => ({ min, max }),
|
|
29
|
+
[class_validator_1.IS_BYTE_LENGTH]: ([min, max]) => ({ min, max }),
|
|
30
|
+
[class_validator_1.MATCHES]: (constraints) => ValidationExceptionMapper_1.patternArgs(constraints),
|
|
31
|
+
[class_validator_1.IS_IN]: ([values]) => ({ values }),
|
|
32
|
+
[class_validator_1.IS_NOT_IN]: ([values]) => ({ values }),
|
|
33
|
+
[class_validator_1.IS_ENUM]: ([, values]) => ({ values }),
|
|
34
|
+
[class_validator_1.IS_DIVISIBLE_BY]: ([divisor]) => ({ divisor }),
|
|
35
|
+
[class_validator_1.CONTAINS]: ([seed]) => ({ seed }),
|
|
36
|
+
[class_validator_1.NOT_CONTAINS]: ([seed]) => ({ seed }),
|
|
37
|
+
[class_validator_1.EQUALS]: ([comparison]) => ({ comparison }),
|
|
38
|
+
[class_validator_1.NOT_EQUALS]: ([comparison]) => ({ comparison }),
|
|
39
|
+
}; }
|
|
21
40
|
supports(error) {
|
|
22
41
|
return error instanceof exception_types_1.ValidationException;
|
|
23
42
|
}
|
|
24
43
|
map(error) {
|
|
25
|
-
|
|
44
|
+
this.metadataByClass.clear();
|
|
45
|
+
return new exception_types_1.ValidationFailedException(error.getValidationErrors().flatMap((root) => this.toDetails(root)));
|
|
46
|
+
}
|
|
47
|
+
toDetails(error) {
|
|
48
|
+
const ownDetails = Object.keys(error.constraints ?? {}).map((ruleName) => {
|
|
49
|
+
const args = error.contexts?.[ruleName] ?? this.argsFromMetadata(error, ruleName);
|
|
50
|
+
return {
|
|
51
|
+
inputField: error.property,
|
|
52
|
+
rule: exception_types_1.validationRuleByName[ruleName] ?? exception_types_1.ValidationRuleEnum.UNKNOWN,
|
|
53
|
+
...(args === undefined ? {} : { args }),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
return ownDetails.concat((error.children ?? []).flatMap((child) => this.toDetails(child)));
|
|
57
|
+
}
|
|
58
|
+
argsFromMetadata(error, ruleName) {
|
|
59
|
+
try {
|
|
60
|
+
const rule = this.metadataOf(error).find((candidate) => candidate.propertyName === error.property && candidate.name === ruleName);
|
|
61
|
+
const constraints = rule?.constraints ?? [];
|
|
62
|
+
return constraints.length === 0 ? undefined : this.argsFromConstraints(ruleName, constraints);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
metadataOf(error) {
|
|
69
|
+
const target = error.target?.constructor;
|
|
70
|
+
if (typeof target !== 'function') {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const cached = this.metadataByClass.get(target);
|
|
74
|
+
if (cached !== undefined) {
|
|
75
|
+
return cached;
|
|
76
|
+
}
|
|
77
|
+
const metadata = (0, class_validator_1.getMetadataStorage)().getTargetValidationMetadatas(target, undefined, false, false);
|
|
78
|
+
this.metadataByClass.set(target, metadata);
|
|
79
|
+
return metadata;
|
|
80
|
+
}
|
|
81
|
+
argsFromConstraints(ruleName, constraints) {
|
|
82
|
+
const named = ValidationExceptionMapper_1.argsByRuleName[ruleName];
|
|
83
|
+
const raw = named ? named(constraints) : { constraints };
|
|
84
|
+
const entries = Object.entries(raw ?? {})
|
|
85
|
+
.map(([key, value]) => [key, this.toJsonValue(value)])
|
|
86
|
+
.filter(([, value]) => value !== undefined);
|
|
87
|
+
const empty = entries.every(([, value]) => Array.isArray(value) && value.length === 0);
|
|
88
|
+
return empty ? undefined : Object.fromEntries(entries);
|
|
89
|
+
}
|
|
90
|
+
toJsonValue(value) {
|
|
91
|
+
if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
if (value instanceof RegExp) {
|
|
95
|
+
return ValidationExceptionMapper_1.patternArgs([value]);
|
|
96
|
+
}
|
|
97
|
+
if (value instanceof Date) {
|
|
98
|
+
return value.toISOString();
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value.map((entry) => this.toJsonValue(entry)).filter((entry) => entry !== undefined);
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
static patternArgs([regex, modifiers]) {
|
|
106
|
+
return regex instanceof RegExp ? { pattern: regex.source, modifiers: regex.flags } : { pattern: regex, modifiers };
|
|
26
107
|
}
|
|
27
108
|
};
|
|
28
109
|
exports.ValidationExceptionMapper = ValidationExceptionMapper;
|
|
29
|
-
exports.ValidationExceptionMapper = ValidationExceptionMapper = __decorate([
|
|
110
|
+
exports.ValidationExceptionMapper = ValidationExceptionMapper = ValidationExceptionMapper_1 = __decorate([
|
|
30
111
|
(0, common_1.Injectable)()
|
|
31
112
|
], ValidationExceptionMapper);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ballistix.digital/exception-mapper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A NestJS module that maps any thrown value to a coded BaseException and to the ExceptionDto envelope, for HTTP filters and task processors.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@ballistix.digital/exception-types": "^0.
|
|
33
|
+
"@ballistix.digital/exception-types": "^0.4.0",
|
|
34
34
|
"@nestjs/common": "^11 || ^12",
|
|
35
35
|
"class-validator": "^0.15.1",
|
|
36
36
|
"reflect-metadata": "^0.2.2",
|