@ballistix.digital/exception-mapper 0.0.1 → 0.2.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 +16 -0
- package/README.md +238 -2
- package/dist/index.d.ts +9 -1
- package/dist/index.js +17 -0
- package/dist/module/exception/domain/model/exceptionModuleOptions.d.ts +10 -0
- package/dist/module/exception/domain/model/exceptionModuleOptions.js +4 -0
- package/dist/module/exception/exception.module.d.ts +7 -0
- package/dist/module/exception/exception.module.js +57 -0
- package/dist/module/exception/mapper/baseException.mapper.d.ts +6 -0
- package/dist/module/exception/mapper/baseException.mapper.js +23 -0
- package/dist/module/exception/mapper/databaseException.mapper.d.ts +10 -0
- package/dist/module/exception/mapper/databaseException.mapper.js +45 -0
- package/dist/module/exception/mapper/exceptionDto.mapper.d.ts +4 -0
- package/dist/module/exception/mapper/exceptionDto.mapper.js +31 -0
- package/dist/module/exception/mapper/exceptionMapper.interface.d.ts +5 -0
- package/dist/module/exception/mapper/exceptionMapper.interface.js +2 -0
- package/dist/module/exception/mapper/genericException.mapper.d.ts +6 -0
- package/dist/module/exception/mapper/genericException.mapper.js +25 -0
- package/dist/module/exception/mapper/validationException.mapper.d.ts +6 -0
- package/dist/module/exception/mapper/validationException.mapper.js +31 -0
- package/dist/module/exception/service/exceptionMapperRegistry.service.d.ts +14 -0
- package/dist/module/exception/service/exceptionMapperRegistry.service.js +41 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,3 +5,19 @@ All notable changes to this package are recorded here. The format follows
|
|
|
5
5
|
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
|
+
|
|
9
|
+
## 0.1.0 - 2026-09-07
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `ExceptionModule`, a global NestJS module with `forRoot` and `forRootAsync`.
|
|
14
|
+
- `ExceptionMapperRegistry`, which holds the mappers in order and turns any
|
|
15
|
+
thrown value into a coded `BaseException` or into the `ExceptionDto`
|
|
16
|
+
envelope.
|
|
17
|
+
- `ExceptionDtoMapper`, which builds the envelope from one exception and never
|
|
18
|
+
throws.
|
|
19
|
+
- The four mappers: `BaseExceptionMapper`, `ValidationExceptionMapper`,
|
|
20
|
+
`DatabaseExceptionMapper` and `GenericExceptionMapper`, plus the
|
|
21
|
+
`ExceptionMapper` interface a further mapper implements.
|
|
22
|
+
- `ExceptionModuleOptions.exposeErrorDetails`, which adds the raw database
|
|
23
|
+
driver message as a second detail of `DATABASE_ERROR`.
|
package/README.md
CHANGED
|
@@ -1,6 +1,242 @@
|
|
|
1
1
|
# @ballistix.digital/exception-mapper
|
|
2
2
|
|
|
3
3
|
A NestJS module that maps any thrown value to a coded `BaseException` and to
|
|
4
|
-
the `ExceptionDto` envelope, for HTTP filters and task processors.
|
|
4
|
+
the `ExceptionDto` envelope, for HTTP filters and task processors. A **mapper**
|
|
5
|
+
turns one kind of thrown value into an exception, and the **registry** holds
|
|
6
|
+
the mappers in order. The exception classes, the codes and the envelope live
|
|
7
|
+
in [`@ballistix.digital/exception-types`](../exception-types/README.md).
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
This file is the consumer guide. [docs/](../docs/index.md) describes how the
|
|
10
|
+
package is built.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @ballistix.digital/exception-mapper @ballistix.digital/exception-types
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The package needs Node 22.12 or later. Install these peer dependencies in the
|
|
19
|
+
application.
|
|
20
|
+
|
|
21
|
+
| Peer | Range |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `@ballistix.digital/exception-types` | the release each published manifest pins |
|
|
24
|
+
| `@nestjs/common` | `^11` |
|
|
25
|
+
| `class-validator` | `^0.15.1` |
|
|
26
|
+
| `typeorm` | `>=0.3.0 <2.0.0` |
|
|
27
|
+
| `reflect-metadata` | `^0.2.2` |
|
|
28
|
+
|
|
29
|
+
The application throws the exception classes of the types package, and this
|
|
30
|
+
package maps to the same classes. A publish of `exception-mapper` pins the
|
|
31
|
+
`exception-types` version of the same build.
|
|
32
|
+
|
|
33
|
+
## Register the module
|
|
34
|
+
|
|
35
|
+
Register the module once, at the root of the application. It is global, so a
|
|
36
|
+
service in any feature module injects `ExceptionMapperRegistry` without an
|
|
37
|
+
import.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Module } from '@nestjs/common';
|
|
41
|
+
import { ExceptionModule } from '@ballistix.digital/exception-mapper';
|
|
42
|
+
|
|
43
|
+
@Module({
|
|
44
|
+
imports: [ExceptionModule.forRoot()],
|
|
45
|
+
})
|
|
46
|
+
export class AppModule {}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`forRootAsync` takes the options from a factory, so a `ConfigService` can
|
|
50
|
+
decide them:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { Module } from '@nestjs/common';
|
|
54
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
55
|
+
import { ExceptionModule } from '@ballistix.digital/exception-mapper';
|
|
56
|
+
|
|
57
|
+
@Module({
|
|
58
|
+
imports: [
|
|
59
|
+
ExceptionModule.forRootAsync({
|
|
60
|
+
imports: [ConfigModule],
|
|
61
|
+
inject: [ConfigService],
|
|
62
|
+
useFactory: (config: ConfigService) => ({
|
|
63
|
+
exposeErrorDetails: config.get('EXPOSE_ERROR_DETAILS') === 'true',
|
|
64
|
+
}),
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
export class AppModule {}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The raw driver message of a `QueryFailedError` can carry SQL text and
|
|
72
|
+
constraint names. With `exposeErrorDetails` on, the database mapper adds that
|
|
73
|
+
message as a second entry of `errors`, and the `title` stays the generic
|
|
74
|
+
message. CAUTION: If an untrusted client reads the response, keep
|
|
75
|
+
`exposeErrorDetails` off. The option is off by default.
|
|
76
|
+
|
|
77
|
+
## From a thrown value to the envelope
|
|
78
|
+
|
|
79
|
+
The registry asks each mapper in one order, and the first match wins.
|
|
80
|
+
|
|
81
|
+
```mermaid
|
|
82
|
+
flowchart TD
|
|
83
|
+
thrown["a thrown value"] --> base{"BaseExceptionMapper<br/>supports?"}
|
|
84
|
+
base -- yes --> same["the same BaseException"]
|
|
85
|
+
base -- no --> validation{"ValidationExceptionMapper<br/>supports?"}
|
|
86
|
+
validation -- yes --> failed["ValidationFailedException<br/>400"]
|
|
87
|
+
validation -- no --> database{"DatabaseExceptionMapper<br/>supports?"}
|
|
88
|
+
database -- yes --> dbError["DatabaseErrorException<br/>400 for SQLSTATE class 22, 23, P0<br/>500 for the rest"]
|
|
89
|
+
database -- no --> generic["GenericExceptionMapper<br/>GenericErrorException<br/>the status of the value, else 500"]
|
|
90
|
+
same --> dtoMapper["ExceptionDtoMapper"]
|
|
91
|
+
failed --> dtoMapper
|
|
92
|
+
dbError --> dtoMapper
|
|
93
|
+
generic --> dtoMapper
|
|
94
|
+
dtoMapper --> envelope["the ExceptionDto envelope"]
|
|
95
|
+
envelope --> retry{"status >= 500?"}
|
|
96
|
+
retry -- yes --> again["a task processor repeats the attempt"]
|
|
97
|
+
retry -- no --> stop["a task processor fails the task"]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Look at the last branch: the status the mappers give is also the retry rule a
|
|
101
|
+
task processor reads.
|
|
102
|
+
|
|
103
|
+
The database mapper reads the first two characters of the Postgres SQLSTATE,
|
|
104
|
+
its class. Class 22 is a data exception, such as a malformed uuid. Class 23 is
|
|
105
|
+
an integrity constraint violation, such as a unique or foreign key breach.
|
|
106
|
+
Class P0 is a `RAISE EXCEPTION` from PL/pgSQL. All three mean the statement
|
|
107
|
+
itself was wrong, which is the fault of the request, so they give 400. A
|
|
108
|
+
deadlock or a connection failure gives 500.
|
|
109
|
+
|
|
110
|
+
`ValidationExceptionMapper` matches the `ValidationException` that
|
|
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
|
|
113
|
+
the list supports the value, so `map()` never returns `undefined`.
|
|
114
|
+
|
|
115
|
+
## Write your HTTP filter
|
|
116
|
+
|
|
117
|
+
The package ships no `@Catch()` filter. Write one in the application:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
|
|
121
|
+
import { ExceptionMapperRegistry } from '@ballistix.digital/exception-mapper';
|
|
122
|
+
import { Response } from 'express';
|
|
123
|
+
|
|
124
|
+
@Catch()
|
|
125
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
126
|
+
constructor(private readonly registry: ExceptionMapperRegistry) {}
|
|
127
|
+
|
|
128
|
+
public catch(exception: unknown, host: ArgumentsHost): void {
|
|
129
|
+
const response = host.switchToHttp().getResponse<Response>();
|
|
130
|
+
const envelope = this.registry.toExceptionDto(exception);
|
|
131
|
+
|
|
132
|
+
response.status(envelope.status).json(envelope);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The filter stays in the application, so a breakpoint on the raw thrown value
|
|
138
|
+
sits in application code. What the filter logs, and what request context it
|
|
139
|
+
adds, is the decision of the application. See
|
|
140
|
+
[ADR 0002](../docs/decisions/0002-filter-stays-in-the-app.md).
|
|
141
|
+
|
|
142
|
+
Register it as the global filter:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { APP_FILTER } from '@nestjs/core';
|
|
146
|
+
|
|
147
|
+
@Module({
|
|
148
|
+
imports: [ExceptionModule.forRoot()],
|
|
149
|
+
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }],
|
|
150
|
+
})
|
|
151
|
+
export class AppModule {}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Use it in a task processor
|
|
155
|
+
|
|
156
|
+
A task processor has no response to write. It stores the envelope on the row,
|
|
157
|
+
and the status decides whether another attempt can succeed:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
@Injectable()
|
|
161
|
+
export class ReportTaskProcessor {
|
|
162
|
+
constructor(
|
|
163
|
+
private readonly registry: ExceptionMapperRegistry,
|
|
164
|
+
private readonly repository: Repository<ReportTask>,
|
|
165
|
+
) {}
|
|
166
|
+
|
|
167
|
+
public async run(task: ReportTask): Promise<void> {
|
|
168
|
+
try {
|
|
169
|
+
await this.build(task);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const envelope = this.registry.toExceptionDto(error);
|
|
172
|
+
|
|
173
|
+
// A 5xx is a server fault, so the attempt can be repeated. A 4xx is the
|
|
174
|
+
// input's fault and repeats with the same result.
|
|
175
|
+
task.processingError = envelope;
|
|
176
|
+
task.status = envelope.status >= 500 ? TaskStatus.PENDING : TaskStatus.FAILED;
|
|
177
|
+
|
|
178
|
+
await this.repository.save(task);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The retry rule belongs to the application. This package only says which status
|
|
185
|
+
the thrown value earns.
|
|
186
|
+
|
|
187
|
+
## Test with the registry
|
|
188
|
+
|
|
189
|
+
The registry is a plain class. A unit test builds it by hand, with no NestJS
|
|
190
|
+
bootstrap:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const registry = new ExceptionMapperRegistry(
|
|
194
|
+
new BaseExceptionMapper(),
|
|
195
|
+
new ValidationExceptionMapper(),
|
|
196
|
+
new DatabaseExceptionMapper({ exposeErrorDetails: false }),
|
|
197
|
+
new GenericExceptionMapper(),
|
|
198
|
+
new ExceptionDtoMapper(),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(registry.toExceptionDto(new Error('boom'))).toMatchObject({
|
|
202
|
+
code: ExceptionCodeEnum.GENERIC_ERROR,
|
|
203
|
+
status: 500,
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`DatabaseExceptionMapper` is the only mapper with a constructor argument. Pass
|
|
208
|
+
`{}` when the test does not care about the driver message.
|
|
209
|
+
|
|
210
|
+
## API reference
|
|
211
|
+
|
|
212
|
+
Everything below comes from the package root.
|
|
213
|
+
|
|
214
|
+
| Export | What it does |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `ExceptionModule.forRoot(options?)` | Registers the module with options that are already known |
|
|
217
|
+
| `ExceptionModule.forRootAsync(options)` | The same, with `imports`, `inject` and a `useFactory` |
|
|
218
|
+
| `ExceptionModuleOptions` | `{ exposeErrorDetails?: boolean }` |
|
|
219
|
+
| `ExceptionModuleAsyncOptions` | `{ imports?, inject?, useFactory }` |
|
|
220
|
+
| `EXCEPTION_MODULE_OPTIONS` | The injection token of the resolved options |
|
|
221
|
+
| `ExceptionMapperRegistry` | Holds the mappers in order. `map(error)` gives the exception, `toExceptionDto(error)` gives the envelope |
|
|
222
|
+
| `ExceptionDtoMapper` | `map(exception)` turns one `BaseException` into the envelope |
|
|
223
|
+
| `ExceptionMapper<TError>` | The interface a mapper implements: `supports(error)` and `map(error)` |
|
|
224
|
+
| `BaseExceptionMapper` | Maps a `BaseException` to itself |
|
|
225
|
+
| `ValidationExceptionMapper` | Maps a `ValidationException` to `ValidationFailedException` |
|
|
226
|
+
| `DatabaseExceptionMapper` | Maps a TypeORM `QueryFailedError` to `DatabaseErrorException` |
|
|
227
|
+
| `GenericExceptionMapper` | Maps anything else to `GenericErrorException` |
|
|
228
|
+
|
|
229
|
+
## Guarantees
|
|
230
|
+
|
|
231
|
+
- `toExceptionDto()` never throws. A detail that fails its own validation
|
|
232
|
+
travels raw into the envelope instead of replacing the error the caller
|
|
233
|
+
handles.
|
|
234
|
+
- First match wins. The order of the mappers is a constructor argument, not a
|
|
235
|
+
search.
|
|
236
|
+
- The generic mapper is the fallback for a value no mapper in the list
|
|
237
|
+
supports, so `map()` always returns a `BaseException`.
|
|
238
|
+
- A validation rule with no `ValidationRuleEnum` member becomes `UNKNOWN`, so
|
|
239
|
+
a new class-validator rule cannot break the mapping.
|
|
240
|
+
- The same thrown value gives the same envelope on every path. An HTTP
|
|
241
|
+
response and a stored task error read alike.
|
|
242
|
+
- The package ships no filter, no logger and no HTTP dependency.
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { ExceptionModule } from './module/exception/exception.module';
|
|
2
|
+
export { EXCEPTION_MODULE_OPTIONS, ExceptionModuleAsyncOptions, ExceptionModuleOptions, } from './module/exception/domain/model/exceptionModuleOptions';
|
|
3
|
+
export { ExceptionMapperRegistry } from './module/exception/service/exceptionMapperRegistry.service';
|
|
4
|
+
export { ExceptionDtoMapper } from './module/exception/mapper/exceptionDto.mapper';
|
|
5
|
+
export { ExceptionMapper } from './module/exception/mapper/exceptionMapper.interface';
|
|
6
|
+
export { BaseExceptionMapper } from './module/exception/mapper/baseException.mapper';
|
|
7
|
+
export { ValidationExceptionMapper } from './module/exception/mapper/validationException.mapper';
|
|
8
|
+
export { DatabaseExceptionMapper } from './module/exception/mapper/databaseException.mapper';
|
|
9
|
+
export { GenericExceptionMapper } from './module/exception/mapper/genericException.mapper';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GenericExceptionMapper = exports.DatabaseExceptionMapper = exports.ValidationExceptionMapper = exports.BaseExceptionMapper = exports.ExceptionDtoMapper = exports.ExceptionMapperRegistry = exports.EXCEPTION_MODULE_OPTIONS = exports.ExceptionModule = void 0;
|
|
4
|
+
var exception_module_1 = require("./module/exception/exception.module");
|
|
5
|
+
Object.defineProperty(exports, "ExceptionModule", { enumerable: true, get: function () { return exception_module_1.ExceptionModule; } });
|
|
6
|
+
var exceptionModuleOptions_1 = require("./module/exception/domain/model/exceptionModuleOptions");
|
|
7
|
+
Object.defineProperty(exports, "EXCEPTION_MODULE_OPTIONS", { enumerable: true, get: function () { return exceptionModuleOptions_1.EXCEPTION_MODULE_OPTIONS; } });
|
|
8
|
+
var exceptionMapperRegistry_service_1 = require("./module/exception/service/exceptionMapperRegistry.service");
|
|
9
|
+
Object.defineProperty(exports, "ExceptionMapperRegistry", { enumerable: true, get: function () { return exceptionMapperRegistry_service_1.ExceptionMapperRegistry; } });
|
|
10
|
+
var exceptionDto_mapper_1 = require("./module/exception/mapper/exceptionDto.mapper");
|
|
11
|
+
Object.defineProperty(exports, "ExceptionDtoMapper", { enumerable: true, get: function () { return exceptionDto_mapper_1.ExceptionDtoMapper; } });
|
|
12
|
+
var baseException_mapper_1 = require("./module/exception/mapper/baseException.mapper");
|
|
13
|
+
Object.defineProperty(exports, "BaseExceptionMapper", { enumerable: true, get: function () { return baseException_mapper_1.BaseExceptionMapper; } });
|
|
14
|
+
var validationException_mapper_1 = require("./module/exception/mapper/validationException.mapper");
|
|
15
|
+
Object.defineProperty(exports, "ValidationExceptionMapper", { enumerable: true, get: function () { return validationException_mapper_1.ValidationExceptionMapper; } });
|
|
16
|
+
var databaseException_mapper_1 = require("./module/exception/mapper/databaseException.mapper");
|
|
17
|
+
Object.defineProperty(exports, "DatabaseExceptionMapper", { enumerable: true, get: function () { return databaseException_mapper_1.DatabaseExceptionMapper; } });
|
|
18
|
+
var genericException_mapper_1 = require("./module/exception/mapper/genericException.mapper");
|
|
19
|
+
Object.defineProperty(exports, "GenericExceptionMapper", { enumerable: true, get: function () { return genericException_mapper_1.GenericExceptionMapper; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
export interface ExceptionModuleOptions {
|
|
3
|
+
exposeErrorDetails?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface ExceptionModuleAsyncOptions {
|
|
6
|
+
imports?: ModuleMetadata['imports'];
|
|
7
|
+
inject?: any[];
|
|
8
|
+
useFactory: (...args: any[]) => ExceptionModuleOptions | Promise<ExceptionModuleOptions>;
|
|
9
|
+
}
|
|
10
|
+
export declare const EXCEPTION_MODULE_OPTIONS: unique symbol;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { ExceptionModuleAsyncOptions, ExceptionModuleOptions } from './domain/model/exceptionModuleOptions';
|
|
3
|
+
export declare class ExceptionModule {
|
|
4
|
+
static forRoot(options?: ExceptionModuleOptions): DynamicModule;
|
|
5
|
+
static forRootAsync(options: ExceptionModuleAsyncOptions): DynamicModule;
|
|
6
|
+
private static createMapperProviders;
|
|
7
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var ExceptionModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ExceptionModule = void 0;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const exceptionModuleOptions_1 = require("./domain/model/exceptionModuleOptions");
|
|
13
|
+
const baseException_mapper_1 = require("./mapper/baseException.mapper");
|
|
14
|
+
const validationException_mapper_1 = require("./mapper/validationException.mapper");
|
|
15
|
+
const databaseException_mapper_1 = require("./mapper/databaseException.mapper");
|
|
16
|
+
const genericException_mapper_1 = require("./mapper/genericException.mapper");
|
|
17
|
+
const exceptionDto_mapper_1 = require("./mapper/exceptionDto.mapper");
|
|
18
|
+
const exceptionMapperRegistry_service_1 = require("./service/exceptionMapperRegistry.service");
|
|
19
|
+
let ExceptionModule = ExceptionModule_1 = class ExceptionModule {
|
|
20
|
+
static forRoot(options = {}) {
|
|
21
|
+
return {
|
|
22
|
+
module: ExceptionModule_1,
|
|
23
|
+
providers: [{ provide: exceptionModuleOptions_1.EXCEPTION_MODULE_OPTIONS, useValue: options }, ...this.createMapperProviders()],
|
|
24
|
+
exports: [exceptionMapperRegistry_service_1.ExceptionMapperRegistry, exceptionDto_mapper_1.ExceptionDtoMapper],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
static forRootAsync(options) {
|
|
28
|
+
return {
|
|
29
|
+
module: ExceptionModule_1,
|
|
30
|
+
imports: options.imports ?? [],
|
|
31
|
+
providers: [
|
|
32
|
+
{
|
|
33
|
+
provide: exceptionModuleOptions_1.EXCEPTION_MODULE_OPTIONS,
|
|
34
|
+
useFactory: options.useFactory,
|
|
35
|
+
inject: options.inject ?? [],
|
|
36
|
+
},
|
|
37
|
+
...this.createMapperProviders(),
|
|
38
|
+
],
|
|
39
|
+
exports: [exceptionMapperRegistry_service_1.ExceptionMapperRegistry, exceptionDto_mapper_1.ExceptionDtoMapper],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
static createMapperProviders() {
|
|
43
|
+
return [
|
|
44
|
+
baseException_mapper_1.BaseExceptionMapper,
|
|
45
|
+
validationException_mapper_1.ValidationExceptionMapper,
|
|
46
|
+
databaseException_mapper_1.DatabaseExceptionMapper,
|
|
47
|
+
genericException_mapper_1.GenericExceptionMapper,
|
|
48
|
+
exceptionDto_mapper_1.ExceptionDtoMapper,
|
|
49
|
+
exceptionMapperRegistry_service_1.ExceptionMapperRegistry,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
exports.ExceptionModule = ExceptionModule;
|
|
54
|
+
exports.ExceptionModule = ExceptionModule = ExceptionModule_1 = __decorate([
|
|
55
|
+
(0, common_1.Global)(),
|
|
56
|
+
(0, common_1.Module)({})
|
|
57
|
+
], ExceptionModule);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BaseException } from '@ballistix.digital/exception-types';
|
|
2
|
+
import { ExceptionMapper } from './exceptionMapper.interface';
|
|
3
|
+
export declare class BaseExceptionMapper implements ExceptionMapper<BaseException<unknown>> {
|
|
4
|
+
supports(error: unknown): error is BaseException<unknown>;
|
|
5
|
+
map(error: BaseException<unknown>): BaseException<unknown>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.BaseExceptionMapper = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const exception_types_1 = require("@ballistix.digital/exception-types");
|
|
12
|
+
let BaseExceptionMapper = class BaseExceptionMapper {
|
|
13
|
+
supports(error) {
|
|
14
|
+
return error instanceof exception_types_1.BaseException;
|
|
15
|
+
}
|
|
16
|
+
map(error) {
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
exports.BaseExceptionMapper = BaseExceptionMapper;
|
|
21
|
+
exports.BaseExceptionMapper = BaseExceptionMapper = __decorate([
|
|
22
|
+
(0, common_1.Injectable)()
|
|
23
|
+
], BaseExceptionMapper);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { QueryFailedError } from 'typeorm';
|
|
2
|
+
import { DatabaseErrorException } from '@ballistix.digital/exception-types';
|
|
3
|
+
import { ExceptionModuleOptions } from '../domain/model/exceptionModuleOptions';
|
|
4
|
+
import { ExceptionMapper } from './exceptionMapper.interface';
|
|
5
|
+
export declare class DatabaseExceptionMapper implements ExceptionMapper<QueryFailedError> {
|
|
6
|
+
private readonly options;
|
|
7
|
+
constructor(options: ExceptionModuleOptions);
|
|
8
|
+
supports(error: unknown): error is QueryFailedError;
|
|
9
|
+
map(error: QueryFailedError): DatabaseErrorException;
|
|
10
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DatabaseExceptionMapper = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const typeorm_1 = require("typeorm");
|
|
18
|
+
const exception_types_1 = require("@ballistix.digital/exception-types");
|
|
19
|
+
const exceptionModuleOptions_1 = require("../domain/model/exceptionModuleOptions");
|
|
20
|
+
let DatabaseExceptionMapper = class DatabaseExceptionMapper {
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
}
|
|
24
|
+
supports(error) {
|
|
25
|
+
return error instanceof typeorm_1.QueryFailedError;
|
|
26
|
+
}
|
|
27
|
+
map(error) {
|
|
28
|
+
const driverMessage = this.options.exposeErrorDetails ? error.message : undefined;
|
|
29
|
+
const sqlStateClass = String(error['code'] ?? '').slice(0, 2);
|
|
30
|
+
switch (sqlStateClass) {
|
|
31
|
+
case '22':
|
|
32
|
+
case '23':
|
|
33
|
+
case 'P0':
|
|
34
|
+
return new exception_types_1.DatabaseErrorException(undefined, 400, driverMessage);
|
|
35
|
+
default:
|
|
36
|
+
return new exception_types_1.DatabaseErrorException(undefined, 500, driverMessage);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
exports.DatabaseExceptionMapper = DatabaseExceptionMapper;
|
|
41
|
+
exports.DatabaseExceptionMapper = DatabaseExceptionMapper = __decorate([
|
|
42
|
+
(0, common_1.Injectable)(),
|
|
43
|
+
__param(0, (0, common_1.Inject)(exceptionModuleOptions_1.EXCEPTION_MODULE_OPTIONS)),
|
|
44
|
+
__metadata("design:paramtypes", [Object])
|
|
45
|
+
], DatabaseExceptionMapper);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ExceptionDtoMapper = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const exception_types_1 = require("@ballistix.digital/exception-types");
|
|
12
|
+
let ExceptionDtoMapper = class ExceptionDtoMapper {
|
|
13
|
+
map(exception) {
|
|
14
|
+
const plain = {
|
|
15
|
+
title: exception.message,
|
|
16
|
+
status: exception.status,
|
|
17
|
+
code: exception.code,
|
|
18
|
+
errors: exception.errors,
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
return (0, exception_types_1.convertAndValidate)(exception_types_1.ExceptionDto, plain, true);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return plain;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
exports.ExceptionDtoMapper = ExceptionDtoMapper;
|
|
29
|
+
exports.ExceptionDtoMapper = ExceptionDtoMapper = __decorate([
|
|
30
|
+
(0, common_1.Injectable)()
|
|
31
|
+
], ExceptionDtoMapper);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { GenericErrorException } from '@ballistix.digital/exception-types';
|
|
2
|
+
import { ExceptionMapper } from './exceptionMapper.interface';
|
|
3
|
+
export declare class GenericExceptionMapper implements ExceptionMapper<unknown> {
|
|
4
|
+
supports(_error: unknown): _error is unknown;
|
|
5
|
+
map(error: unknown): GenericErrorException;
|
|
6
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.GenericExceptionMapper = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const exception_types_1 = require("@ballistix.digital/exception-types");
|
|
12
|
+
let GenericExceptionMapper = class GenericExceptionMapper {
|
|
13
|
+
supports(_error) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
map(error) {
|
|
17
|
+
const message = String(error?.message ?? error);
|
|
18
|
+
const status = Number(error?.status) || 500;
|
|
19
|
+
return new exception_types_1.GenericErrorException(message, status);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
exports.GenericExceptionMapper = GenericExceptionMapper;
|
|
23
|
+
exports.GenericExceptionMapper = GenericExceptionMapper = __decorate([
|
|
24
|
+
(0, common_1.Injectable)()
|
|
25
|
+
], GenericExceptionMapper);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ValidationException, ValidationFailedException } from '@ballistix.digital/exception-types';
|
|
2
|
+
import { ExceptionMapper } from './exceptionMapper.interface';
|
|
3
|
+
export declare class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
|
|
4
|
+
supports(error: unknown): error is ValidationException;
|
|
5
|
+
map(error: ValidationException): ValidationFailedException;
|
|
6
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ValidationExceptionMapper = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
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
|
+
let ValidationExceptionMapper = class ValidationExceptionMapper {
|
|
21
|
+
supports(error) {
|
|
22
|
+
return error instanceof exception_types_1.ValidationException;
|
|
23
|
+
}
|
|
24
|
+
map(error) {
|
|
25
|
+
return new exception_types_1.ValidationFailedException(error.getValidationErrors().flatMap(collectValidationDetails));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
exports.ValidationExceptionMapper = ValidationExceptionMapper;
|
|
29
|
+
exports.ValidationExceptionMapper = ValidationExceptionMapper = __decorate([
|
|
30
|
+
(0, common_1.Injectable)()
|
|
31
|
+
], ValidationExceptionMapper);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseException, ExceptionDto } from '@ballistix.digital/exception-types';
|
|
2
|
+
import { BaseExceptionMapper } from '../mapper/baseException.mapper';
|
|
3
|
+
import { ValidationExceptionMapper } from '../mapper/validationException.mapper';
|
|
4
|
+
import { DatabaseExceptionMapper } from '../mapper/databaseException.mapper';
|
|
5
|
+
import { GenericExceptionMapper } from '../mapper/genericException.mapper';
|
|
6
|
+
import { ExceptionDtoMapper } from '../mapper/exceptionDto.mapper';
|
|
7
|
+
export declare class ExceptionMapperRegistry {
|
|
8
|
+
private readonly genericExceptionMapper;
|
|
9
|
+
private readonly exceptionDtoMapper;
|
|
10
|
+
private readonly mappers;
|
|
11
|
+
constructor(baseExceptionMapper: BaseExceptionMapper, validationExceptionMapper: ValidationExceptionMapper, databaseExceptionMapper: DatabaseExceptionMapper, genericExceptionMapper: GenericExceptionMapper, exceptionDtoMapper: ExceptionDtoMapper);
|
|
12
|
+
map(error: unknown): BaseException<unknown>;
|
|
13
|
+
toExceptionDto(error: unknown): ExceptionDto;
|
|
14
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ExceptionMapperRegistry = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const baseException_mapper_1 = require("../mapper/baseException.mapper");
|
|
15
|
+
const validationException_mapper_1 = require("../mapper/validationException.mapper");
|
|
16
|
+
const databaseException_mapper_1 = require("../mapper/databaseException.mapper");
|
|
17
|
+
const genericException_mapper_1 = require("../mapper/genericException.mapper");
|
|
18
|
+
const exceptionDto_mapper_1 = require("../mapper/exceptionDto.mapper");
|
|
19
|
+
let ExceptionMapperRegistry = class ExceptionMapperRegistry {
|
|
20
|
+
constructor(baseExceptionMapper, validationExceptionMapper, databaseExceptionMapper, genericExceptionMapper, exceptionDtoMapper) {
|
|
21
|
+
this.genericExceptionMapper = genericExceptionMapper;
|
|
22
|
+
this.exceptionDtoMapper = exceptionDtoMapper;
|
|
23
|
+
this.mappers = [baseExceptionMapper, validationExceptionMapper, databaseExceptionMapper];
|
|
24
|
+
}
|
|
25
|
+
map(error) {
|
|
26
|
+
const mapper = this.mappers.find((candidate) => candidate.supports(error));
|
|
27
|
+
return (mapper ?? this.genericExceptionMapper).map(error);
|
|
28
|
+
}
|
|
29
|
+
toExceptionDto(error) {
|
|
30
|
+
return this.exceptionDtoMapper.map(this.map(error));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
exports.ExceptionMapperRegistry = ExceptionMapperRegistry;
|
|
34
|
+
exports.ExceptionMapperRegistry = ExceptionMapperRegistry = __decorate([
|
|
35
|
+
(0, common_1.Injectable)(),
|
|
36
|
+
__metadata("design:paramtypes", [baseException_mapper_1.BaseExceptionMapper,
|
|
37
|
+
validationException_mapper_1.ValidationExceptionMapper,
|
|
38
|
+
databaseException_mapper_1.DatabaseExceptionMapper,
|
|
39
|
+
genericException_mapper_1.GenericExceptionMapper,
|
|
40
|
+
exceptionDto_mapper_1.ExceptionDtoMapper])
|
|
41
|
+
], ExceptionMapperRegistry);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ballistix.digital/exception-mapper",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.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,8 +30,8 @@
|
|
|
30
30
|
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@ballistix.digital/exception-types": "^0.
|
|
34
|
-
"@nestjs/common": "^11",
|
|
33
|
+
"@ballistix.digital/exception-types": "^0.2.0",
|
|
34
|
+
"@nestjs/common": "^11 || ^12",
|
|
35
35
|
"class-validator": "^0.15.1",
|
|
36
36
|
"reflect-metadata": "^0.2.2",
|
|
37
37
|
"typeorm": ">=0.3.0 <2.0.0"
|