@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. [docs/](../docs/index.md) describes how the
10
- package is built.
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. `GenericExceptionMapper` is the fallback the registry uses when no mapper in
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](../docs/decisions/0002-filter-stays-in-the-app.md).
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
- return new exception_types_1.ValidationFailedException(error.getValidationErrors().flatMap(collectValidationDetails));
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.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.3.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",