@bymax-one/nest-core 1.5.2 → 1.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/CHANGELOG.md +225 -1
- package/README.md +225 -14
- package/dist/health/index.d.cts +1 -0
- package/dist/health/index.d.ts +1 -0
- package/dist/health.transition-B_IgXJTz.d.cts +119 -0
- package/dist/health.transition-B_IgXJTz.d.ts +119 -0
- package/dist/index.cjs +218 -27
- package/dist/index.d.cts +29 -11
- package/dist/index.d.ts +29 -11
- package/dist/index.mjs +218 -28
- package/dist/openapi/index.cjs +41 -17
- package/dist/openapi/index.mjs +41 -17
- package/dist/pagination/index.cjs +11 -1
- package/dist/pagination/index.d.cts +32 -3
- package/dist/pagination/index.d.ts +32 -3
- package/dist/pagination/index.mjs +11 -1
- package/package.json +1 -1
package/dist/openapi/index.mjs
CHANGED
|
@@ -324,7 +324,7 @@ var CORE_PARAMETERS = {
|
|
|
324
324
|
}
|
|
325
325
|
};
|
|
326
326
|
|
|
327
|
-
// src/openapi/openapi.
|
|
327
|
+
// src/openapi/openapi.shape.ts
|
|
328
328
|
var OPERATION_METHODS = [
|
|
329
329
|
"get",
|
|
330
330
|
"post",
|
|
@@ -335,21 +335,26 @@ var OPERATION_METHODS = [
|
|
|
335
335
|
"options",
|
|
336
336
|
"trace"
|
|
337
337
|
];
|
|
338
|
-
var METRICS_SCHEME_NAME = "BymaxMetricsAuth";
|
|
339
|
-
var ERROR_ENVELOPE_SCHEMA = "BymaxErrorEnvelope";
|
|
340
|
-
var HEALTH_RESPONSE_SCHEMA = "BymaxHealthResponse";
|
|
341
338
|
function asRecord(value) {
|
|
342
339
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
343
340
|
return {};
|
|
344
341
|
}
|
|
345
342
|
return value;
|
|
346
343
|
}
|
|
347
|
-
function mergeAbsent(existing, additions) {
|
|
348
|
-
return { ...additions, ...existing };
|
|
349
|
-
}
|
|
350
344
|
function operationsOf(item) {
|
|
351
345
|
return Object.entries(asRecord(item)).filter(([key]) => OPERATION_METHODS.includes(key));
|
|
352
346
|
}
|
|
347
|
+
function operationKey(method, path) {
|
|
348
|
+
return `${method.toUpperCase()} ${path}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/openapi/openapi.document.ts
|
|
352
|
+
var METRICS_SCHEME_NAME = "BymaxMetricsAuth";
|
|
353
|
+
var ERROR_ENVELOPE_SCHEMA = "BymaxErrorEnvelope";
|
|
354
|
+
var HEALTH_RESPONSE_SCHEMA = "BymaxHealthResponse";
|
|
355
|
+
function mergeAbsent(existing, additions) {
|
|
356
|
+
return { ...additions, ...existing };
|
|
357
|
+
}
|
|
353
358
|
function mergeResponses(existing, additions) {
|
|
354
359
|
const merged = new Map(Object.entries(existing));
|
|
355
360
|
for (const [status, value] of Object.entries(additions)) {
|
|
@@ -375,21 +380,24 @@ function withoutDisabledRoutes(paths, options, routes) {
|
|
|
375
380
|
});
|
|
376
381
|
return Object.fromEntries(kept.filter(([, item]) => item !== void 0));
|
|
377
382
|
}
|
|
378
|
-
function
|
|
383
|
+
function inheritsRequirement(document, openapi) {
|
|
384
|
+
if (document.security === void 0) {
|
|
385
|
+
return openapi.security.length > 0;
|
|
386
|
+
}
|
|
387
|
+
return Array.isArray(document.security) && document.security.length > 0;
|
|
388
|
+
}
|
|
389
|
+
function ownRouteSecurity(path, method, options, routes, inherits) {
|
|
379
390
|
if (method !== "get") {
|
|
380
391
|
return void 0;
|
|
381
392
|
}
|
|
382
393
|
if (options.metrics.authToken !== void 0 && routes.isMetrics(path)) {
|
|
383
394
|
return [{ [METRICS_SCHEME_NAME]: [] }];
|
|
384
395
|
}
|
|
385
|
-
if (
|
|
396
|
+
if (inherits && (routes.isHealth(path) || routes.isMetrics(path))) {
|
|
386
397
|
return [];
|
|
387
398
|
}
|
|
388
399
|
return void 0;
|
|
389
400
|
}
|
|
390
|
-
function operationKey(method, path) {
|
|
391
|
-
return `${method.toUpperCase()} ${path}`;
|
|
392
|
-
}
|
|
393
401
|
function coreResponses(path, options, routes) {
|
|
394
402
|
const responses = {};
|
|
395
403
|
if (options.envelope.enabled) {
|
|
@@ -423,7 +431,7 @@ function mergeFragment(operation, fragment) {
|
|
|
423
431
|
}
|
|
424
432
|
return merged;
|
|
425
433
|
}
|
|
426
|
-
function augmentOperation(operation, path, method, options, routes, contributions) {
|
|
434
|
+
function augmentOperation(operation, path, method, options, routes, contributions, inherits) {
|
|
427
435
|
const declaredByDocument = operation["security"] !== void 0;
|
|
428
436
|
let result = { ...operation };
|
|
429
437
|
for (const fragment of fragmentsFor(result["operationId"], contributions)) {
|
|
@@ -432,7 +440,7 @@ function augmentOperation(operation, path, method, options, routes, contribution
|
|
|
432
440
|
if (!declaredByDocument) {
|
|
433
441
|
const override = options.openapi.operationSecurity[operationKey(method, path)];
|
|
434
442
|
const describedByLibrary = result["security"] !== void 0;
|
|
435
|
-
const security = override ?? (describedByLibrary ? void 0 : ownRouteSecurity(path, method, options, routes));
|
|
443
|
+
const security = override ?? (describedByLibrary ? void 0 : ownRouteSecurity(path, method, options, routes, inherits));
|
|
436
444
|
if (security !== void 0) {
|
|
437
445
|
result["security"] = security;
|
|
438
446
|
}
|
|
@@ -507,12 +515,20 @@ function assertOverridesMatch(paths, openapi) {
|
|
|
507
515
|
`[BymaxCoreModule] openapi.operationSecurity addresses ${unmatched.length} operation(s) that the document does not contain: ${unmatched.join(", ")}. Keys are "<METHOD> <path>" with the path exactly as documented, including any global prefix. The document contains: ${documented.length === 0 ? "(none)" : documented.join(", ")}.`
|
|
508
516
|
);
|
|
509
517
|
}
|
|
510
|
-
function augmentPaths(paths, options, routes, contributions) {
|
|
518
|
+
function augmentPaths(paths, options, routes, contributions, inherits) {
|
|
511
519
|
return Object.fromEntries(
|
|
512
520
|
Object.entries(paths).map(([path, item]) => {
|
|
513
521
|
const augmented = operationsOf(item).map(([method, operation]) => [
|
|
514
522
|
method,
|
|
515
|
-
augmentOperation(
|
|
523
|
+
augmentOperation(
|
|
524
|
+
asRecord(operation),
|
|
525
|
+
path,
|
|
526
|
+
method,
|
|
527
|
+
options,
|
|
528
|
+
routes,
|
|
529
|
+
contributions,
|
|
530
|
+
inherits
|
|
531
|
+
)
|
|
516
532
|
]);
|
|
517
533
|
return [path, { ...asRecord(item), ...Object.fromEntries(augmented) }];
|
|
518
534
|
})
|
|
@@ -550,7 +566,15 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
550
566
|
const routes = indexOwnRoutes(options, pathPrefixes);
|
|
551
567
|
const served = withoutDisabledRoutes(asRecord(document.paths), options, routes);
|
|
552
568
|
assertOverridesMatch(served, openapi);
|
|
553
|
-
const paths = document.paths === void 0 ? {} : {
|
|
569
|
+
const paths = document.paths === void 0 ? {} : {
|
|
570
|
+
paths: augmentPaths(
|
|
571
|
+
served,
|
|
572
|
+
options,
|
|
573
|
+
routes,
|
|
574
|
+
contributions,
|
|
575
|
+
inheritsRequirement(document, openapi)
|
|
576
|
+
)
|
|
577
|
+
};
|
|
554
578
|
const security = openapi.security.length > 0 && document.security === void 0 ? { security: openapi.security } : {};
|
|
555
579
|
return { ...document, components: Object.fromEntries(merged), ...paths, ...security };
|
|
556
580
|
}
|
|
@@ -19,6 +19,12 @@ function coercePositiveInt(value, fallback) {
|
|
|
19
19
|
function clampPageToLimit(page, limit) {
|
|
20
20
|
return Math.min(page, Math.floor(Number.MAX_SAFE_INTEGER / limit) + 1);
|
|
21
21
|
}
|
|
22
|
+
function clampPageToOffset(page, limit, maxOffset) {
|
|
23
|
+
if (maxOffset === void 0 || !Number.isSafeInteger(maxOffset) || maxOffset < 0) {
|
|
24
|
+
return page;
|
|
25
|
+
}
|
|
26
|
+
return Math.min(page, Math.floor(maxOffset / limit) + 1);
|
|
27
|
+
}
|
|
22
28
|
function clampLimit(rawLimit, options) {
|
|
23
29
|
const defaultLimit = coercePositiveInt(options?.defaultLimit, DEFAULT_LIMIT);
|
|
24
30
|
const maxLimit = coercePositiveInt(options?.maxLimit, DEFAULT_MAX_LIMIT);
|
|
@@ -29,7 +35,11 @@ function clampLimit(rawLimit, options) {
|
|
|
29
35
|
function normalizePageQuery(raw, options) {
|
|
30
36
|
const limit = clampLimit(raw.limit, options);
|
|
31
37
|
return {
|
|
32
|
-
page:
|
|
38
|
+
page: clampPageToOffset(
|
|
39
|
+
clampPageToLimit(coercePositiveInt(raw.page, MINIMUM), limit),
|
|
40
|
+
limit,
|
|
41
|
+
options?.maxOffset
|
|
42
|
+
),
|
|
33
43
|
limit
|
|
34
44
|
};
|
|
35
45
|
}
|
|
@@ -51,14 +51,43 @@ interface PageResult<T> {
|
|
|
51
51
|
* Options are per-call and never retained between calls.
|
|
52
52
|
*
|
|
53
53
|
* @param raw - The untrusted page and limit values from the request.
|
|
54
|
-
* @param options - Per-call `defaultLimit` (default `20`)
|
|
55
|
-
*
|
|
54
|
+
* @param options - Per-call `defaultLimit` (default `20`), `maxLimit` (default
|
|
55
|
+
* `100`) and `maxOffset` (absent by default) overrides. `maxLimit` bounds how
|
|
56
|
+
* many rows a request reads; `maxOffset` bounds how far in it starts, which is
|
|
57
|
+
* the half an offset-paginated database pays for.
|
|
56
58
|
* @returns A clamped, safe query ready to hand to a repository.
|
|
57
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* Options for {@link normalizePageQuery}: the shared limit bounds, plus the one
|
|
62
|
+
* that only means anything to offset pagination.
|
|
63
|
+
*
|
|
64
|
+
* Declared here rather than beside the shared bounds so it cannot reach the
|
|
65
|
+
* cursor normalizer, which takes {@link PaginationLimitOptions} and has no
|
|
66
|
+
* offset to bound. An option that type-checks on a function that ignores it is
|
|
67
|
+
* worse than a missing one — it reads as configured and does nothing.
|
|
68
|
+
*/
|
|
69
|
+
interface PageQueryOptions extends PaginationLimitOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Hard cap applied to the repository offset the query drives,
|
|
72
|
+
* `(page - 1) * limit`. Absent by default, which bounds nothing beyond
|
|
73
|
+
* arithmetic safety.
|
|
74
|
+
*
|
|
75
|
+
* `maxLimit` bounds how many rows a request reads; this bounds how far in it
|
|
76
|
+
* starts, which is the half that costs on an offset-paginated database — a
|
|
77
|
+
* `SELECT … OFFSET 20000000000` is a twenty-byte request that scans a table.
|
|
78
|
+
* Set it wherever the page index reaches SQL and the dataset has a knowable
|
|
79
|
+
* ceiling. There is deliberately no default: legitimate deep paging exists,
|
|
80
|
+
* and a silent cap would change the rows a working query returns.
|
|
81
|
+
*
|
|
82
|
+
* `0` is meaningful and means "the first page only". Any other value that is
|
|
83
|
+
* not a non-negative safe integer is read as absent.
|
|
84
|
+
*/
|
|
85
|
+
maxOffset?: number;
|
|
86
|
+
}
|
|
58
87
|
declare function normalizePageQuery(raw: {
|
|
59
88
|
page?: unknown;
|
|
60
89
|
limit?: unknown;
|
|
61
|
-
}, options?:
|
|
90
|
+
}, options?: PageQueryOptions): PageQuery;
|
|
62
91
|
/**
|
|
63
92
|
* Assemble a {@link PageResult} from a page of items and the total count.
|
|
64
93
|
*
|
|
@@ -51,14 +51,43 @@ interface PageResult<T> {
|
|
|
51
51
|
* Options are per-call and never retained between calls.
|
|
52
52
|
*
|
|
53
53
|
* @param raw - The untrusted page and limit values from the request.
|
|
54
|
-
* @param options - Per-call `defaultLimit` (default `20`)
|
|
55
|
-
*
|
|
54
|
+
* @param options - Per-call `defaultLimit` (default `20`), `maxLimit` (default
|
|
55
|
+
* `100`) and `maxOffset` (absent by default) overrides. `maxLimit` bounds how
|
|
56
|
+
* many rows a request reads; `maxOffset` bounds how far in it starts, which is
|
|
57
|
+
* the half an offset-paginated database pays for.
|
|
56
58
|
* @returns A clamped, safe query ready to hand to a repository.
|
|
57
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* Options for {@link normalizePageQuery}: the shared limit bounds, plus the one
|
|
62
|
+
* that only means anything to offset pagination.
|
|
63
|
+
*
|
|
64
|
+
* Declared here rather than beside the shared bounds so it cannot reach the
|
|
65
|
+
* cursor normalizer, which takes {@link PaginationLimitOptions} and has no
|
|
66
|
+
* offset to bound. An option that type-checks on a function that ignores it is
|
|
67
|
+
* worse than a missing one — it reads as configured and does nothing.
|
|
68
|
+
*/
|
|
69
|
+
interface PageQueryOptions extends PaginationLimitOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Hard cap applied to the repository offset the query drives,
|
|
72
|
+
* `(page - 1) * limit`. Absent by default, which bounds nothing beyond
|
|
73
|
+
* arithmetic safety.
|
|
74
|
+
*
|
|
75
|
+
* `maxLimit` bounds how many rows a request reads; this bounds how far in it
|
|
76
|
+
* starts, which is the half that costs on an offset-paginated database — a
|
|
77
|
+
* `SELECT … OFFSET 20000000000` is a twenty-byte request that scans a table.
|
|
78
|
+
* Set it wherever the page index reaches SQL and the dataset has a knowable
|
|
79
|
+
* ceiling. There is deliberately no default: legitimate deep paging exists,
|
|
80
|
+
* and a silent cap would change the rows a working query returns.
|
|
81
|
+
*
|
|
82
|
+
* `0` is meaningful and means "the first page only". Any other value that is
|
|
83
|
+
* not a non-negative safe integer is read as absent.
|
|
84
|
+
*/
|
|
85
|
+
maxOffset?: number;
|
|
86
|
+
}
|
|
58
87
|
declare function normalizePageQuery(raw: {
|
|
59
88
|
page?: unknown;
|
|
60
89
|
limit?: unknown;
|
|
61
|
-
}, options?:
|
|
90
|
+
}, options?: PageQueryOptions): PageQuery;
|
|
62
91
|
/**
|
|
63
92
|
* Assemble a {@link PageResult} from a page of items and the total count.
|
|
64
93
|
*
|
|
@@ -17,6 +17,12 @@ function coercePositiveInt(value, fallback) {
|
|
|
17
17
|
function clampPageToLimit(page, limit) {
|
|
18
18
|
return Math.min(page, Math.floor(Number.MAX_SAFE_INTEGER / limit) + 1);
|
|
19
19
|
}
|
|
20
|
+
function clampPageToOffset(page, limit, maxOffset) {
|
|
21
|
+
if (maxOffset === void 0 || !Number.isSafeInteger(maxOffset) || maxOffset < 0) {
|
|
22
|
+
return page;
|
|
23
|
+
}
|
|
24
|
+
return Math.min(page, Math.floor(maxOffset / limit) + 1);
|
|
25
|
+
}
|
|
20
26
|
function clampLimit(rawLimit, options) {
|
|
21
27
|
const defaultLimit = coercePositiveInt(options?.defaultLimit, DEFAULT_LIMIT);
|
|
22
28
|
const maxLimit = coercePositiveInt(options?.maxLimit, DEFAULT_MAX_LIMIT);
|
|
@@ -27,7 +33,11 @@ function clampLimit(rawLimit, options) {
|
|
|
27
33
|
function normalizePageQuery(raw, options) {
|
|
28
34
|
const limit = clampLimit(raw.limit, options);
|
|
29
35
|
return {
|
|
30
|
-
page:
|
|
36
|
+
page: clampPageToOffset(
|
|
37
|
+
clampPageToLimit(coercePositiveInt(raw.page, MINIMUM), limit),
|
|
38
|
+
limit,
|
|
39
|
+
options?.maxOffset
|
|
40
|
+
),
|
|
31
41
|
limit
|
|
32
42
|
};
|
|
33
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
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",
|