@bymax-one/nest-core 1.6.2 → 1.6.3
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 +56 -1
- package/README.md +51 -11
- package/dist/index.cjs +22 -4
- package/dist/index.mjs +22 -4
- package/dist/openapi/index.cjs +1 -1
- package/dist/openapi/index.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,60 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.6.3] - 2026-10-23
|
|
15
|
+
|
|
16
|
+
The error envelope's `path` carried the query string, and the documented
|
|
17
|
+
recipe for pairing with `@bymax-one/nest-logger` bound nothing.
|
|
18
|
+
|
|
19
|
+
**Apply to a derived backend:**
|
|
20
|
+
|
|
21
|
+
- **`path` changes in the response body.** An application that reads `path`
|
|
22
|
+
from the envelope now gets the request path alone. `/invoices/42?token=x`
|
|
23
|
+
and `http://host/invoices/42` both now read `/invoices/42`. The query string was never part of the documented
|
|
24
|
+
contract, and is where reset codes and tokens travel.
|
|
25
|
+
- **The nest-logger correlation recipe changes.** An application that
|
|
26
|
+
followed the previous README recipe bound
|
|
27
|
+
`BYMAX_CORRELATION_PROVIDER` to `LogContextService` with `useExisting`, and
|
|
28
|
+
got no correlation id at all. Replace it with the `useFactory` in the
|
|
29
|
+
README's "Integration with `@bymax-one/nest-logger`", and make sure
|
|
30
|
+
nest-logger's request-id middleware is mounted (`applyAccessLog(app)` or
|
|
31
|
+
`applyRequestIdMiddleware(consumer)`). Without it there is no id to read.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **The envelope `path` no longer carries the query string.** Both adapters
|
|
36
|
+
report the original URL, query included: Express returns `originalUrl`, and
|
|
37
|
+
Fastify the raw URL. The filter copied that into `path`, which is documented
|
|
38
|
+
as the request URL path. So a query-string secret reached the client and any
|
|
39
|
+
log that stores the envelope. An absolute-form target, as a proxy sends it,
|
|
40
|
+
also carried its scheme, authority and any credentials; only the path is
|
|
41
|
+
kept now. The `onUnexpectedError` context now receives the same path. The unexpected-error sink already reported the route
|
|
42
|
+
template and is unaffected.
|
|
43
|
+
- **The nest-logger integration recipe binds what it promises.** It imported
|
|
44
|
+
`NestLoggerModule`, which does not exist; the module is `BymaxLoggerModule`.
|
|
45
|
+
It also aliased `LogContextService` with `useExisting`, but that service
|
|
46
|
+
has no `getCorrelationId()`. The binding was inert, and silently so: the
|
|
47
|
+
filter treats a failing correlation read as "no id". The README and the
|
|
48
|
+
specification now adapt the `requestId` key with a factory; the README
|
|
49
|
+
binds `PinoUnexpectedErrorSink` (nest-logger 1.4.1) beside it. The snippet
|
|
50
|
+
passed the published-surface gate before only because that gate ignores
|
|
51
|
+
diagnostics from a peer the fixture does not install.
|
|
52
|
+
- **A thrown value that cannot be described no longer breaks the response.**
|
|
53
|
+
With `envelope.exposeInternals` on, a value with no prototype, or an `Error`
|
|
54
|
+
whose `stack` getter throws, threw out of the filter. The internals dump now
|
|
55
|
+
degrades to a fixed message, and the hooks still run.
|
|
56
|
+
- **The discard warning no longer claims a mismatch for identical entries.**
|
|
57
|
+
It now says a generated client will not match the library's own types only
|
|
58
|
+
where the two definitions differ.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **Served component order (since 1.6.1).** Since 1.6.1, a document's own
|
|
63
|
+
`components.schemas` and `components.parameters` entries are listed before
|
|
64
|
+
this package's catalogue and before contributed entries. The 1.6.1 entry did
|
|
65
|
+
not say so. Order in a components map carries no meaning in OpenAPI, but it
|
|
66
|
+
shows in Swagger UI's model list and in a stored `openapi.json` snapshot.
|
|
67
|
+
|
|
14
68
|
## [1.6.2] - 2026-10-22
|
|
15
69
|
|
|
16
70
|
An unexpected error became `500 BYMAX_INTERNAL_ERROR` and the application had no
|
|
@@ -1138,4 +1192,5 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
1138
1192
|
[1.6.0]: https://github.com/bymaxone/nest-core/compare/v1.5.3...v1.6.0
|
|
1139
1193
|
[1.6.1]: https://github.com/bymaxone/nest-core/compare/v1.6.0...v1.6.1
|
|
1140
1194
|
[1.6.2]: https://github.com/bymaxone/nest-core/compare/v1.6.1...v1.6.2
|
|
1141
|
-
[
|
|
1195
|
+
[1.6.3]: https://github.com/bymaxone/nest-core/compare/v1.6.2...v1.6.3
|
|
1196
|
+
[Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.6.3...HEAD
|
package/README.md
CHANGED
|
@@ -1546,27 +1546,67 @@ your samples and your logs.
|
|
|
1546
1546
|
|
|
1547
1547
|
## 🔗 Integration with `@bymax-one/nest-logger`
|
|
1548
1548
|
|
|
1549
|
-
Pairing this package with `@bymax-one/nest-logger`
|
|
1550
|
-
error responses
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1549
|
+
Pairing this package with `@bymax-one/nest-logger` gives correlated logs and
|
|
1550
|
+
error responses without either package importing the other. nest-logger's
|
|
1551
|
+
request-id middleware stores the id under the `requestId` key of
|
|
1552
|
+
`LogContextService`. That service is not itself an `ICorrelationIdProvider` —
|
|
1553
|
+
it has no `getCorrelationId()` — so adapt it with a factory. `useExisting` on
|
|
1554
|
+
it binds an object without that method, and the filter then reads no id and
|
|
1555
|
+
reports nothing. Bind both tokens from a `@Global()` module of your own so they
|
|
1556
|
+
are visible outside the module that declares them:
|
|
1554
1557
|
|
|
1555
1558
|
```typescript
|
|
1556
1559
|
import { Global, Module } from '@nestjs/common'
|
|
1557
|
-
import { BYMAX_CORRELATION_PROVIDER } from '@bymax-one/nest-core'
|
|
1558
|
-
import {
|
|
1560
|
+
import { BYMAX_CORRELATION_PROVIDER, BYMAX_UNEXPECTED_ERROR_SINK } from '@bymax-one/nest-core'
|
|
1561
|
+
import type { ICorrelationIdProvider } from '@bymax-one/nest-core'
|
|
1562
|
+
import {
|
|
1563
|
+
BymaxLoggerModule,
|
|
1564
|
+
LogContextService,
|
|
1565
|
+
PinoUnexpectedErrorSink
|
|
1566
|
+
} from '@bymax-one/nest-logger'
|
|
1559
1567
|
|
|
1560
1568
|
@Global()
|
|
1561
1569
|
@Module({
|
|
1562
|
-
imports: [
|
|
1563
|
-
providers: [
|
|
1564
|
-
|
|
1570
|
+
imports: [BymaxLoggerModule.forRoot({ service: { name: 'api', version: '1.0.0' } })],
|
|
1571
|
+
providers: [
|
|
1572
|
+
{
|
|
1573
|
+
provide: BYMAX_CORRELATION_PROVIDER,
|
|
1574
|
+
inject: [LogContextService],
|
|
1575
|
+
useFactory: (context: LogContextService): ICorrelationIdProvider => ({
|
|
1576
|
+
getCorrelationId: () => context.get<string>('requestId')
|
|
1577
|
+
})
|
|
1578
|
+
},
|
|
1579
|
+
{ provide: BYMAX_UNEXPECTED_ERROR_SINK, useClass: PinoUnexpectedErrorSink }
|
|
1580
|
+
],
|
|
1581
|
+
exports: [BYMAX_CORRELATION_PROVIDER, BYMAX_UNEXPECTED_ERROR_SINK]
|
|
1565
1582
|
})
|
|
1566
1583
|
export class ObservabilityModule {}
|
|
1567
1584
|
```
|
|
1568
1585
|
|
|
1569
|
-
|
|
1586
|
+
The id exists only once nest-logger's request-id middleware runs, and
|
|
1587
|
+
`BymaxLoggerModule` mounts none on its own. Mount it in `main.ts`, or call
|
|
1588
|
+
`applyRequestIdMiddleware(consumer)` from a module's `configure()`:
|
|
1589
|
+
|
|
1590
|
+
> **Express only.** nest-logger's request-id middleware supports
|
|
1591
|
+
> `@nestjs/platform-express`; the Fastify adapter is unsupported, so do not
|
|
1592
|
+
> rely on this correlation provider there.
|
|
1593
|
+
|
|
1594
|
+
```typescript
|
|
1595
|
+
import { NestFactory } from '@nestjs/core'
|
|
1596
|
+
import { applyAccessLog } from '@bymax-one/nest-logger'
|
|
1597
|
+
import { AppModule } from './app.module'
|
|
1598
|
+
|
|
1599
|
+
async function bootstrap(): Promise<void> {
|
|
1600
|
+
const app = await NestFactory.create(AppModule)
|
|
1601
|
+
applyAccessLog(app) // opens the request context that carries `requestId`
|
|
1602
|
+
await app.listen(3000)
|
|
1603
|
+
}
|
|
1604
|
+
void bootstrap()
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
Every error envelope now carries the same correlation id your logs do, and the
|
|
1608
|
+
cause of every collapsed `500` is logged with it (`PinoUnexpectedErrorSink`
|
|
1609
|
+
needs `@bymax-one/nest-logger` 1.4.1 or later). The
|
|
1570
1610
|
same `@Global()`-module pattern is how every pluggable token in this package
|
|
1571
1611
|
is overridden: `BYMAX_TIMING_SINK` and `BYMAX_HEALTH_INDICATORS` follow it
|
|
1572
1612
|
identically.
|
package/dist/index.cjs
CHANGED
|
@@ -359,6 +359,19 @@ function extractRequestInfo(context) {
|
|
|
359
359
|
var INTERNAL_ERROR_STATUS = 500;
|
|
360
360
|
var INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
361
361
|
var VALIDATION_FAILED_MESSAGE = "Validation failed";
|
|
362
|
+
var ABSOLUTE_FORM_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
363
|
+
function pathOf(url) {
|
|
364
|
+
let target = String(url);
|
|
365
|
+
const prefix = ABSOLUTE_FORM_PREFIX.exec(target);
|
|
366
|
+
if (prefix !== null) {
|
|
367
|
+
const afterScheme = target.slice(prefix[0].length);
|
|
368
|
+
const authorityEnd = afterScheme.search(/[/?#]/);
|
|
369
|
+
target = authorityEnd === -1 ? "" : afterScheme.slice(authorityEnd);
|
|
370
|
+
}
|
|
371
|
+
const pathEnd = target.search(/[?#]/);
|
|
372
|
+
const path = pathEnd === -1 ? target : target.slice(0, pathEnd);
|
|
373
|
+
return path === "" ? "/" : path;
|
|
374
|
+
}
|
|
362
375
|
function resolveErrorCarrier(response) {
|
|
363
376
|
if (typeof response !== "object" || response === null) {
|
|
364
377
|
return void 0;
|
|
@@ -420,11 +433,16 @@ function extractHttpMessage(response, exception, carrier) {
|
|
|
420
433
|
}
|
|
421
434
|
return exception.message;
|
|
422
435
|
}
|
|
436
|
+
var UNREPRESENTABLE_MESSAGE = "Unrepresentable thrown value";
|
|
423
437
|
function buildInternalDetails(exception) {
|
|
424
|
-
|
|
425
|
-
|
|
438
|
+
try {
|
|
439
|
+
if (exception instanceof Error) {
|
|
440
|
+
return exception.stack !== void 0 ? { message: exception.message, stack: exception.stack } : { message: exception.message };
|
|
441
|
+
}
|
|
442
|
+
return { message: String(exception) };
|
|
443
|
+
} catch {
|
|
444
|
+
return { message: UNREPRESENTABLE_MESSAGE };
|
|
426
445
|
}
|
|
427
|
-
return { message: String(exception) };
|
|
428
446
|
}
|
|
429
447
|
exports.BymaxExceptionFilter = class BymaxExceptionFilter {
|
|
430
448
|
/**
|
|
@@ -496,7 +514,7 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
|
|
|
496
514
|
const traceId = this.readAnnotation(() => this.traceContext.getTraceContext()?.traceId);
|
|
497
515
|
const context = {
|
|
498
516
|
method: String(httpAdapter.getRequestMethod(request)),
|
|
499
|
-
path:
|
|
517
|
+
path: pathOf(httpAdapter.getRequestUrl(request)),
|
|
500
518
|
...correlationId !== void 0 ? { correlationId } : {},
|
|
501
519
|
...traceId !== void 0 ? { traceId } : {}
|
|
502
520
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -357,6 +357,19 @@ function extractRequestInfo(context) {
|
|
|
357
357
|
var INTERNAL_ERROR_STATUS = 500;
|
|
358
358
|
var INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
359
359
|
var VALIDATION_FAILED_MESSAGE = "Validation failed";
|
|
360
|
+
var ABSOLUTE_FORM_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
361
|
+
function pathOf(url) {
|
|
362
|
+
let target = String(url);
|
|
363
|
+
const prefix = ABSOLUTE_FORM_PREFIX.exec(target);
|
|
364
|
+
if (prefix !== null) {
|
|
365
|
+
const afterScheme = target.slice(prefix[0].length);
|
|
366
|
+
const authorityEnd = afterScheme.search(/[/?#]/);
|
|
367
|
+
target = authorityEnd === -1 ? "" : afterScheme.slice(authorityEnd);
|
|
368
|
+
}
|
|
369
|
+
const pathEnd = target.search(/[?#]/);
|
|
370
|
+
const path = pathEnd === -1 ? target : target.slice(0, pathEnd);
|
|
371
|
+
return path === "" ? "/" : path;
|
|
372
|
+
}
|
|
360
373
|
function resolveErrorCarrier(response) {
|
|
361
374
|
if (typeof response !== "object" || response === null) {
|
|
362
375
|
return void 0;
|
|
@@ -418,11 +431,16 @@ function extractHttpMessage(response, exception, carrier) {
|
|
|
418
431
|
}
|
|
419
432
|
return exception.message;
|
|
420
433
|
}
|
|
434
|
+
var UNREPRESENTABLE_MESSAGE = "Unrepresentable thrown value";
|
|
421
435
|
function buildInternalDetails(exception) {
|
|
422
|
-
|
|
423
|
-
|
|
436
|
+
try {
|
|
437
|
+
if (exception instanceof Error) {
|
|
438
|
+
return exception.stack !== void 0 ? { message: exception.message, stack: exception.stack } : { message: exception.message };
|
|
439
|
+
}
|
|
440
|
+
return { message: String(exception) };
|
|
441
|
+
} catch {
|
|
442
|
+
return { message: UNREPRESENTABLE_MESSAGE };
|
|
424
443
|
}
|
|
425
|
-
return { message: String(exception) };
|
|
426
444
|
}
|
|
427
445
|
var BymaxExceptionFilter = class {
|
|
428
446
|
/**
|
|
@@ -494,7 +512,7 @@ var BymaxExceptionFilter = class {
|
|
|
494
512
|
const traceId = this.readAnnotation(() => this.traceContext.getTraceContext()?.traceId);
|
|
495
513
|
const context = {
|
|
496
514
|
method: String(httpAdapter.getRequestMethod(request)),
|
|
497
|
-
path:
|
|
515
|
+
path: pathOf(httpAdapter.getRequestUrl(request)),
|
|
498
516
|
...correlationId !== void 0 ? { correlationId } : {},
|
|
499
517
|
...traceId !== void 0 ? { traceId } : {}
|
|
500
518
|
};
|
package/dist/openapi/index.cjs
CHANGED
|
@@ -737,7 +737,7 @@ function warnDiscardedComponents(logger, discarded) {
|
|
|
737
737
|
const elided = discarded.length - MAX_WARNED_OPERATIONS;
|
|
738
738
|
const listed = discarded.slice(0, MAX_WARNED_OPERATIONS).map(({ contributor, member, name }) => `${contributor} -> ${member}.${name}`).join(", ");
|
|
739
739
|
logger.warn(
|
|
740
|
-
`the served OpenAPI document already defines ${discarded.length} component entry(ies) a library also contributes: ${listed}${elided > 0 ? `, and ${elided} more` : ""}. The contributed version is not served, so a client generated from this document will not match that library's own types. What holds the name may be the application's own declaration, a core schema under openapi.includeCoreSchemas, or a contributor that sorted earlier \u2014 read the document before deciding what to remove.`
|
|
740
|
+
`the served OpenAPI document already defines ${discarded.length} component entry(ies) a library also contributes: ${listed}${elided > 0 ? `, and ${elided} more` : ""}. The contributed version is not served, so where the two differ a client generated from this document will not match that library's own types. What holds the name may be the application's own declaration, a core schema under openapi.includeCoreSchemas, or a contributor that sorted earlier \u2014 read the document before deciding what to remove.`
|
|
741
741
|
);
|
|
742
742
|
}
|
|
743
743
|
function buildConfig(builder, options) {
|
package/dist/openapi/index.mjs
CHANGED
|
@@ -735,7 +735,7 @@ function warnDiscardedComponents(logger, discarded) {
|
|
|
735
735
|
const elided = discarded.length - MAX_WARNED_OPERATIONS;
|
|
736
736
|
const listed = discarded.slice(0, MAX_WARNED_OPERATIONS).map(({ contributor, member, name }) => `${contributor} -> ${member}.${name}`).join(", ");
|
|
737
737
|
logger.warn(
|
|
738
|
-
`the served OpenAPI document already defines ${discarded.length} component entry(ies) a library also contributes: ${listed}${elided > 0 ? `, and ${elided} more` : ""}. The contributed version is not served, so a client generated from this document will not match that library's own types. What holds the name may be the application's own declaration, a core schema under openapi.includeCoreSchemas, or a contributor that sorted earlier \u2014 read the document before deciding what to remove.`
|
|
738
|
+
`the served OpenAPI document already defines ${discarded.length} component entry(ies) a library also contributes: ${listed}${elided > 0 ? `, and ${elided} more` : ""}. The contributed version is not served, so where the two differ a client generated from this document will not match that library's own types. What holds the name may be the application's own declaration, a core schema under openapi.includeCoreSchemas, or a contributor that sorted earlier \u2014 read the document before deciding what to remove.`
|
|
739
739
|
);
|
|
740
740
|
}
|
|
741
741
|
function buildConfig(builder, options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-core",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "Zero-dependency NestJS 11 application foundation kit: error-envelope exception filter, request-timing interceptor, pagination helpers, health endpoints with indicator discovery, an optional Prometheus metrics endpoint with a contribution contract, OpenAPI documents in development, and OpenTelemetry trace correlation.",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|