@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.
@@ -324,7 +324,7 @@ var CORE_PARAMETERS = {
324
324
  }
325
325
  };
326
326
 
327
- // src/openapi/openapi.document.ts
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 ownRouteSecurity(path, method, options, routes) {
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 (options.openapi.security.length > 0 && routes.isHealth(path)) {
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(asRecord(operation), path, method, options, routes, contributions)
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 ? {} : { paths: augmentPaths(served, options, routes, contributions) };
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: clampPageToLimit(coercePositiveInt(raw.page, MINIMUM), limit),
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`) and `maxLimit`
55
- * (default `100`) overrides.
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?: PaginationLimitOptions): PageQuery;
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`) and `maxLimit`
55
- * (default `100`) overrides.
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?: PaginationLimitOptions): PageQuery;
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: clampPageToLimit(coercePositiveInt(raw.page, MINIMUM), limit),
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.5.2",
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",