@biorate/nestjs-tools 2.5.0 → 2.6.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
CHANGED
|
@@ -8,7 +8,7 @@ Production‑grade middleware, guards, interceptors, filters, decorators, and HT
|
|
|
8
8
|
- **Response time** — Prometheus histogram + `x-response-time` header.
|
|
9
9
|
- **HTTP proxy** — Prometheus‑instrumented `http-proxy-middleware` wrapper.
|
|
10
10
|
- **Tracing** — OpenTelemetry span attributes for incoming requests.
|
|
11
|
-
- **Exception filter** — structured JSON error responses for HTTP
|
|
11
|
+
- **Exception filter** — structured JSON error responses for HTTP and WebSocket transports (RPC is unsupported).
|
|
12
12
|
- **Auth** — `@AuthBasic()` decorator + `AuthGuardProvider`.
|
|
13
13
|
- **Roles** — `@Roles()` decorator + `RolesGuardProvider`.
|
|
14
14
|
- **CORS** — config‑driven origin validation.
|
|
@@ -25,14 +25,14 @@ Peer dependencies: `@nestjs/common`, `@nestjs/core`, `@nestjs/swagger`, `class-v
|
|
|
25
25
|
|
|
26
26
|
## Quick start
|
|
27
27
|
|
|
28
|
+
`main.ts`:
|
|
29
|
+
|
|
28
30
|
```ts
|
|
29
31
|
import { NestFactory } from '@nestjs/core';
|
|
30
32
|
import { AppModule } from './app.module';
|
|
31
33
|
import {
|
|
32
34
|
AllExceptionsFilter,
|
|
33
|
-
|
|
34
|
-
RequestCountMiddleware,
|
|
35
|
-
ResponseTimeMiddleware,
|
|
35
|
+
RoutesInterceptor,
|
|
36
36
|
TracingInterceptor,
|
|
37
37
|
} from '@biorate/nestjs-tools';
|
|
38
38
|
|
|
@@ -40,17 +40,38 @@ async function bootstrap() {
|
|
|
40
40
|
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
|
41
41
|
|
|
42
42
|
app.useGlobalFilters(new AllExceptionsFilter());
|
|
43
|
-
app.useGlobalInterceptors(new TracingInterceptor());
|
|
44
|
-
|
|
45
|
-
// Middleware
|
|
46
|
-
app.use(BootstrapProvider.create());
|
|
47
|
-
app.use(RequestCountMiddleware.create());
|
|
48
|
-
app.use(ResponseTimeMiddleware.create());
|
|
43
|
+
app.useGlobalInterceptors(new RoutesInterceptor(), new TracingInterceptor());
|
|
49
44
|
|
|
50
45
|
await app.listen(3000);
|
|
51
46
|
}
|
|
47
|
+
|
|
48
|
+
bootstrap();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`app.module.ts`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
|
55
|
+
import {
|
|
56
|
+
RequestCountMiddleware,
|
|
57
|
+
ResponseTimeMiddleware,
|
|
58
|
+
} from '@biorate/nestjs-tools';
|
|
59
|
+
|
|
60
|
+
@Module({ imports: [...] })
|
|
61
|
+
export class AppModule implements NestModule {
|
|
62
|
+
public configure(consumer: MiddlewareConsumer) {
|
|
63
|
+
consumer
|
|
64
|
+
.apply(RequestCountMiddleware, ResponseTimeMiddleware)
|
|
65
|
+
.forRoutes('*');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
52
68
|
```
|
|
53
69
|
|
|
70
|
+
> Middleware and interceptor classes use `@inject(Types.Config)` from the global
|
|
71
|
+
> `@biorate/inversion` container — bootstrap the app via the `@biorate/launcher`
|
|
72
|
+
> lifecycle with `Types.Config` bound, otherwise requests will fail with
|
|
73
|
+
> `InversionInjectionIsUndefinedError`.
|
|
74
|
+
|
|
54
75
|
## Middlewares
|
|
55
76
|
|
|
56
77
|
### `RequestCountMiddleware`
|
|
@@ -59,8 +80,6 @@ Prometheus counter `http_server_requests_seconds_count` with labels `method`, `u
|
|
|
59
80
|
|
|
60
81
|
```ts
|
|
61
82
|
consumer.apply(RequestCountMiddleware).forRoutes('*');
|
|
62
|
-
// or via BootstrapProvider
|
|
63
|
-
app.use(RequestCountMiddleware.create());
|
|
64
83
|
```
|
|
65
84
|
|
|
66
85
|
Uses `RoutesInterceptor` to resolve the actual route path (e.g. `/api/users/:id` instead of `/api/users/123`).
|
|
@@ -72,7 +91,7 @@ Config option: `app.middleware.RequestCountMiddleware.log-base-url` (boolean, de
|
|
|
72
91
|
Prometheus histogram `http_server_requests_seconds` + `x-response-time` header.
|
|
73
92
|
|
|
74
93
|
```ts
|
|
75
|
-
|
|
94
|
+
consumer.apply(ResponseTimeMiddleware).forRoutes('*');
|
|
76
95
|
```
|
|
77
96
|
|
|
78
97
|
Config options:
|
|
@@ -125,7 +144,7 @@ Catches all exceptions and returns a structured JSON response:
|
|
|
125
144
|
}
|
|
126
145
|
```
|
|
127
146
|
|
|
128
|
-
Supports HTTP
|
|
147
|
+
Supports HTTP and WebSocket transports; RPC is unsupported and throws `UnsupportedProtocolError`. Maps `meta.status` from `@biorate/errors` to HTTP status codes.
|
|
129
148
|
|
|
130
149
|
```ts
|
|
131
150
|
app.useGlobalFilters(new AllExceptionsFilter());
|
|
@@ -181,7 +200,7 @@ Granular control over which span attributes are written:
|
|
|
181
200
|
|
|
182
201
|
Resolves the full route path (controller + handler) and stores it in a `WeakMap<Request, { path }>` for use by `RequestCountMiddleware` and `ResponseTimeMiddleware`.
|
|
183
202
|
|
|
184
|
-
|
|
203
|
+
Not applied automatically: when using the count/time middlewares, register it globally via `app.useGlobalInterceptors(new RoutesInterceptor())` — otherwise they won't see the resolved route path and metrics fall back to `/`.
|
|
185
204
|
|
|
186
205
|
## Providers (Guards)
|
|
187
206
|
|
|
@@ -233,14 +252,6 @@ export class CustomRolesGuard extends RolesGuardProvider {
|
|
|
233
252
|
}
|
|
234
253
|
```
|
|
235
254
|
|
|
236
|
-
### `BootstrapProvider`
|
|
237
|
-
|
|
238
|
-
Convenience middleware that initializes the `@biorate/inversion` container lifecycle:
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
app.use(BootstrapProvider.create());
|
|
242
|
-
```
|
|
243
|
-
|
|
244
255
|
## Decorators
|
|
245
256
|
|
|
246
257
|
| Decorator | Composition |
|
|
@@ -260,15 +271,18 @@ In non‑debug environments, validates the origin against `ALLOWED_ORIGIN` confi
|
|
|
260
271
|
|
|
261
272
|
## HTTP controllers
|
|
262
273
|
|
|
274
|
+
> The `MetricsController`, `ProbeController`, and `LocalesController` in `src/http/controllers` are marked `@deprecated`. Their hexagonal replacements live in `src/app/infrastructure/controllers/` and are **not** exported individually at the package top level — `src/app/infrastructure/index.ts` re-exports them only as the `controllers` namespace (`export * as controllers from './controllers'`), so reach them as `import { controllers } from '@biorate/nestjs-tools'` (e.g. `controllers.MetricsController`).
|
|
275
|
+
|
|
263
276
|
### `MetricsController`
|
|
264
277
|
|
|
265
278
|
Exposes `GET /metrics` with Prometheus metrics from the shared registry.
|
|
266
279
|
|
|
267
280
|
### `ProbeController`
|
|
268
281
|
|
|
269
|
-
| Route
|
|
270
|
-
|
|
|
271
|
-
| `GET /probe` |
|
|
282
|
+
| Route | Purpose |
|
|
283
|
+
| ---------------------- | --------------- |
|
|
284
|
+
| `GET /probe/readiness` | Readiness probe |
|
|
285
|
+
| `GET /probe/healthz` | Healthz probe |
|
|
272
286
|
|
|
273
287
|
### `LocalesController`
|
|
274
288
|
|
|
@@ -296,14 +310,12 @@ src/app/
|
|
|
296
310
|
└──────────┬───────────────────────────────────────────┘
|
|
297
311
|
│
|
|
298
312
|
┌──────────▼───────────────────────────────────────────┐
|
|
299
|
-
│
|
|
313
|
+
│ RoutesInterceptor (path resolution) │
|
|
300
314
|
├──────────────────────────────────────────────────────┤
|
|
301
315
|
│ RequestCountMiddleware (counter) │
|
|
302
316
|
├──────────────────────────────────────────────────────┤
|
|
303
317
|
│ ResponseTimeMiddleware (histogram + header) │
|
|
304
318
|
├──────────────────────────────────────────────────────┤
|
|
305
|
-
│ RoutesInterceptor (path resolution) │
|
|
306
|
-
├──────────────────────────────────────────────────────┤
|
|
307
319
|
│ TracingInterceptor (OpenTelemetry span attrs) │
|
|
308
320
|
├──────────────────────────────────────────────────────┤
|
|
309
321
|
│ AllExceptionsFilter (structured error JSON) │
|
|
@@ -12,46 +12,44 @@ const common_1 = require("@nestjs/common");
|
|
|
12
12
|
const errors_1 = require("../errors");
|
|
13
13
|
let AllExceptionsFilter = class AllExceptionsFilter {
|
|
14
14
|
catch(exception, host) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
this.type = host.getType();
|
|
16
|
+
this.exception = exception;
|
|
17
|
+
this.host = host;
|
|
18
|
+
const method = `_${this.type}`;
|
|
17
19
|
if (!this[method])
|
|
18
|
-
throw new errors_1.UnsupportedProtocolError(
|
|
19
|
-
this[method](
|
|
20
|
+
throw new errors_1.UnsupportedProtocolError(this.type);
|
|
21
|
+
this[method]();
|
|
20
22
|
}
|
|
21
|
-
_http(
|
|
22
|
-
const ctx = host.switchToHttp();
|
|
23
|
+
_http() {
|
|
24
|
+
const ctx = this.host.switchToHttp();
|
|
23
25
|
const response = ctx.getResponse();
|
|
24
26
|
const request = ctx.getRequest();
|
|
25
|
-
const meta = exception?.meta;
|
|
26
|
-
const status = exception instanceof common_1.HttpException
|
|
27
|
-
? exception.getStatus()
|
|
27
|
+
const meta = this.exception?.meta;
|
|
28
|
+
const status = this.exception instanceof common_1.HttpException
|
|
29
|
+
? this.exception.getStatus()
|
|
28
30
|
: meta?.status ?? common_1.HttpStatus.INTERNAL_SERVER_ERROR;
|
|
29
|
-
this.log(exception);
|
|
30
|
-
response.status(status).json({
|
|
31
|
+
this.log(this.exception);
|
|
32
|
+
response.status(status).json(this.format({
|
|
31
33
|
status,
|
|
32
|
-
code: this.code(exception),
|
|
33
|
-
hint: this.hint(exception),
|
|
34
34
|
path: request.url,
|
|
35
35
|
meta: meta?.meta,
|
|
36
|
-
});
|
|
36
|
+
}));
|
|
37
37
|
}
|
|
38
|
-
_ws(
|
|
39
|
-
const ctx = host.switchToWs();
|
|
38
|
+
_ws() {
|
|
39
|
+
const ctx = this.host.switchToWs();
|
|
40
40
|
const socket = ctx.getClient();
|
|
41
|
-
const meta = exception?.meta;
|
|
41
|
+
const meta = this.exception?.meta;
|
|
42
42
|
const status = meta?.status ?? common_1.HttpStatus.INTERNAL_SERVER_ERROR;
|
|
43
|
-
this.log(exception);
|
|
43
|
+
this.log(this.exception);
|
|
44
44
|
socket.send(JSON.stringify({
|
|
45
45
|
event: 'error',
|
|
46
|
-
data: {
|
|
46
|
+
data: this.format({
|
|
47
47
|
status,
|
|
48
|
-
code: this.code(exception),
|
|
49
|
-
hint: this.hint(exception),
|
|
50
48
|
meta: meta?.meta,
|
|
51
|
-
},
|
|
49
|
+
}),
|
|
52
50
|
}));
|
|
53
51
|
}
|
|
54
|
-
_rpc(
|
|
52
|
+
_rpc() {
|
|
55
53
|
throw new errors_1.UnsupportedProtocolError('rpc');
|
|
56
54
|
}
|
|
57
55
|
code(exception) {
|
|
@@ -60,6 +58,13 @@ let AllExceptionsFilter = class AllExceptionsFilter {
|
|
|
60
58
|
hint(exception) {
|
|
61
59
|
return (exception?.getResponse?.()?.message ?? exception.message);
|
|
62
60
|
}
|
|
61
|
+
format(data) {
|
|
62
|
+
return {
|
|
63
|
+
code: this.code(this.exception),
|
|
64
|
+
hint: this.hint(this.exception),
|
|
65
|
+
...data,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
63
68
|
log(exception) {
|
|
64
69
|
if (exception instanceof axios_1.AxiosError)
|
|
65
70
|
console.error(new errors_1.AxiosRequestError(exception?.response?.data?.statusCode ?? exception.status, exception?.response?.data?.error ?? exception.code, exception?.response?.data?.message ?? exception.message).stack);
|
|
@@ -9,46 +9,44 @@ import { Catch, HttpException, HttpStatus, } from '@nestjs/common';
|
|
|
9
9
|
import { AxiosRequestError, UnsupportedProtocolError } from '../errors.js';
|
|
10
10
|
let AllExceptionsFilter = class AllExceptionsFilter {
|
|
11
11
|
catch(exception, host) {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
this.type = host.getType();
|
|
13
|
+
this.exception = exception;
|
|
14
|
+
this.host = host;
|
|
15
|
+
const method = `_${this.type}`;
|
|
14
16
|
if (!this[method])
|
|
15
|
-
throw new UnsupportedProtocolError(
|
|
16
|
-
this[method](
|
|
17
|
+
throw new UnsupportedProtocolError(this.type);
|
|
18
|
+
this[method]();
|
|
17
19
|
}
|
|
18
|
-
_http(
|
|
19
|
-
const ctx = host.switchToHttp();
|
|
20
|
+
_http() {
|
|
21
|
+
const ctx = this.host.switchToHttp();
|
|
20
22
|
const response = ctx.getResponse();
|
|
21
23
|
const request = ctx.getRequest();
|
|
22
|
-
const meta = exception?.meta;
|
|
23
|
-
const status = exception instanceof HttpException
|
|
24
|
-
? exception.getStatus()
|
|
24
|
+
const meta = this.exception?.meta;
|
|
25
|
+
const status = this.exception instanceof HttpException
|
|
26
|
+
? this.exception.getStatus()
|
|
25
27
|
: meta?.status ?? HttpStatus.INTERNAL_SERVER_ERROR;
|
|
26
|
-
this.log(exception);
|
|
27
|
-
response.status(status).json({
|
|
28
|
+
this.log(this.exception);
|
|
29
|
+
response.status(status).json(this.format({
|
|
28
30
|
status,
|
|
29
|
-
code: this.code(exception),
|
|
30
|
-
hint: this.hint(exception),
|
|
31
31
|
path: request.url,
|
|
32
32
|
meta: meta?.meta,
|
|
33
|
-
});
|
|
33
|
+
}));
|
|
34
34
|
}
|
|
35
|
-
_ws(
|
|
36
|
-
const ctx = host.switchToWs();
|
|
35
|
+
_ws() {
|
|
36
|
+
const ctx = this.host.switchToWs();
|
|
37
37
|
const socket = ctx.getClient();
|
|
38
|
-
const meta = exception?.meta;
|
|
38
|
+
const meta = this.exception?.meta;
|
|
39
39
|
const status = meta?.status ?? HttpStatus.INTERNAL_SERVER_ERROR;
|
|
40
|
-
this.log(exception);
|
|
40
|
+
this.log(this.exception);
|
|
41
41
|
socket.send(JSON.stringify({
|
|
42
42
|
event: 'error',
|
|
43
|
-
data: {
|
|
43
|
+
data: this.format({
|
|
44
44
|
status,
|
|
45
|
-
code: this.code(exception),
|
|
46
|
-
hint: this.hint(exception),
|
|
47
45
|
meta: meta?.meta,
|
|
48
|
-
},
|
|
46
|
+
}),
|
|
49
47
|
}));
|
|
50
48
|
}
|
|
51
|
-
_rpc(
|
|
49
|
+
_rpc() {
|
|
52
50
|
throw new UnsupportedProtocolError('rpc');
|
|
53
51
|
}
|
|
54
52
|
code(exception) {
|
|
@@ -57,6 +55,13 @@ let AllExceptionsFilter = class AllExceptionsFilter {
|
|
|
57
55
|
hint(exception) {
|
|
58
56
|
return (exception?.getResponse?.()?.message ?? exception.message);
|
|
59
57
|
}
|
|
58
|
+
format(data) {
|
|
59
|
+
return {
|
|
60
|
+
code: this.code(this.exception),
|
|
61
|
+
hint: this.hint(this.exception),
|
|
62
|
+
...data,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
60
65
|
log(exception) {
|
|
61
66
|
if (exception instanceof AxiosError)
|
|
62
67
|
console.error(new AxiosRequestError(exception?.response?.data?.statusCode ?? exception.status, exception?.response?.data?.error ?? exception.code, exception?.response?.data?.message ?? exception.message).stack);
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { ExceptionFilter, ArgumentsHost } from '@nestjs/common';
|
|
1
|
+
import { ExceptionFilter, ArgumentsHost, ContextType } from '@nestjs/common';
|
|
2
2
|
export declare class AllExceptionsFilter implements ExceptionFilter {
|
|
3
|
+
protected type: ContextType;
|
|
4
|
+
protected exception: Error;
|
|
5
|
+
protected host: ArgumentsHost;
|
|
3
6
|
catch(exception: Error, host: ArgumentsHost): void;
|
|
4
7
|
private _http;
|
|
5
8
|
private _ws;
|
|
6
9
|
private _rpc;
|
|
7
10
|
private code;
|
|
8
11
|
private hint;
|
|
12
|
+
protected format(data: Record<string, unknown>): {
|
|
13
|
+
code: string;
|
|
14
|
+
hint: string | [string];
|
|
15
|
+
};
|
|
9
16
|
protected log(exception: Error): void;
|
|
10
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biorate/nestjs-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "NestJS tools",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"keywords": [],
|
|
46
46
|
"author": "llevkin",
|
|
47
47
|
"license": "MIT",
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "ba34f02a95796fee72c9dc822e31eaa740e09b4a",
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@nestjs/common": ">=10.4.15",
|
|
51
51
|
"@nestjs/core": ">=10.4.15",
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"ws": ">=8.18.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@biorate/axios": "3.4.
|
|
63
|
-
"@biorate/config": "3.2.
|
|
62
|
+
"@biorate/axios": "3.4.1",
|
|
63
|
+
"@biorate/config": "3.2.4",
|
|
64
64
|
"@biorate/errors": "3.1.2",
|
|
65
|
-
"@biorate/inversion": "3.1.
|
|
66
|
-
"@biorate/opentelemetry": "2.4.
|
|
67
|
-
"@biorate/prometheus": "3.1.
|
|
68
|
-
"@biorate/tools": "3.1.
|
|
65
|
+
"@biorate/inversion": "3.1.4",
|
|
66
|
+
"@biorate/opentelemetry": "2.4.3",
|
|
67
|
+
"@biorate/prometheus": "3.1.4",
|
|
68
|
+
"@biorate/tools": "3.1.3",
|
|
69
69
|
"http-proxy": "1.18.1",
|
|
70
70
|
"http-proxy-middleware": "2.0.6",
|
|
71
71
|
"json-stringify-safe": "5.0.1",
|