@dudousxd/nestjs-filter 1.24.0 → 1.28.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/dist/runner.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { type Type } from '@nestjs/common';
2
2
  import { ModuleRef } from '@nestjs/core';
3
- import type { FilterAdapter } from './adapter/adapter.js';
3
+ import type { FieldExtent, FilterAdapter } from './adapter/adapter.js';
4
4
  import type { ContextAccessor } from './context-accessor.js';
5
- import type { ComputedSource, CursorPage, EntityDescription, FilterContext, FilterModuleOptions, GroupByCountResult, SortItem } from './types.js';
5
+ import type { ComputedSource, CursorPage, EntityDescription, FieldHistogram, FilterContext, FilterModuleOptions, GroupByCountResult, SortItem } from './types.js';
6
6
  /**
7
7
  * One resolved entry of the computed-field registry: the dev-provided SQL
8
8
  * `source` plus the runtime flags carried by the declaration (the inline
@@ -163,6 +163,7 @@ export declare class FilterRunner {
163
163
  private applyProjectedComputed;
164
164
  apply<F extends object, Q>(FilterClass: Type<F>, input: unknown, qb: Q, context?: FilterContext, internal?: {
165
165
  native?: boolean;
166
+ distinctOrder?: boolean | undefined;
166
167
  }): Promise<Q>;
167
168
  private resolveFilter;
168
169
  private runSetup;
@@ -280,6 +281,15 @@ export declare class FilterRunner {
280
281
  applyDynamic<Q>(entity: Type<unknown>, input: unknown, qb: Q, context?: FilterContext, internal?: {
281
282
  skipSortAndPagination?: boolean;
282
283
  native?: boolean;
284
+ distinctOrder?: boolean | undefined;
285
+ /**
286
+ * Marks `paginate.size` as server-authored, lifting the module-level
287
+ * `maxPageSize` ceiling for this call only. See
288
+ * {@link FilterRunner.resolvePageSize}. Dynamic mode has no filter class
289
+ * and no route decorator, so — like `distinctOrder` — the per-call flag
290
+ * is the only way in.
291
+ */
292
+ trustedPageSize?: boolean;
283
293
  }): Promise<Q>;
284
294
  /**
285
295
  * Runs a dynamic query and **executes** it, returning the page of rows plus
@@ -293,10 +303,19 @@ export declare class FilterRunner {
293
303
  *
294
304
  * Requires an adapter implementing `getResultAndCount` (and `populate` for
295
305
  * to-many includes). `applyDynamic` is unchanged; this is additive.
306
+ *
307
+ * `opts.trustedPageSize` declares that `paginate.size` was written by the
308
+ * server, not received from a client, and lifts the module-level
309
+ * `maxPageSize` ceiling for this call — see
310
+ * {@link FilterRunner.resolvePageSize}. This is the entry point exports and
311
+ * batch jobs use, so it is where the escape hatch is needed; without it they
312
+ * abandon `paginate` for hand-built `limit`/`offset` and lose everything
313
+ * else this method does.
296
314
  */
297
315
  findAndCount<E>(entity: Type<E>, input: unknown, opts?: {
298
316
  qb?: unknown;
299
317
  context?: FilterContext;
318
+ trustedPageSize?: boolean;
300
319
  }): Promise<{
301
320
  rows: E[];
302
321
  total: number;
@@ -359,6 +378,269 @@ export declare class FilterRunner {
359
378
  * group-by-count rather than emitting a divide-by-zero or nonsensical width.
360
379
  */
361
380
  private parseGroupByCount;
381
+ /**
382
+ * **Field extent**: the `MIN`/`MAX` of the requested field(s) over the rows
383
+ * the active `where`/`search` select — what a range control (numeric slider,
384
+ * date-range calendar) needs before it can place its endpoints. Reads the
385
+ * `extent` structured key (`{ extent: ['price', 'createdAt'] }`, or the
386
+ * comma-separated string a GET route carries, parsed exactly like `distinct`).
387
+ *
388
+ * Unlike {@link groupByCount} this is NOT terminal — it measures the same
389
+ * filtered set the rows come from, so a route answers with rows AND extent
390
+ * from two builders. Sort/pagination/distinct/select are not part of the
391
+ * question and are not applied (`skipSortAndPagination`).
392
+ *
393
+ * **Why this lives in the runner and not in the route.** Reading
394
+ * `@Body('extent')` and handing it straight to `adapter.fieldExtent` bypasses
395
+ * the filter class's field governance: `static distinct` narrowing, the
396
+ * entity-metadata check that rejects a bare relation or an unknown
397
+ * identifier, and the alias remapping every other key gets. That is not an
398
+ * injection hole — the adapter resolves names through ORM metadata — but it
399
+ * IS a surface leak: a caller could measure a column the filter class
400
+ * deliberately does not expose. Routing every requested field through
401
+ * {@link validateDistinct} here closes it, and it is also the only place
402
+ * that can tell a computed alias from a typo (see below).
403
+ *
404
+ * **Allowlist.** The same one `distinct` uses: the filter class's static
405
+ * `distinct` list when `opts.filterClass` declares one, else the entity's
406
+ * columns via adapter metadata. `distinct` and `extent` answer the same
407
+ * question about a column — what values can this control offer — so a class
408
+ * that has already narrowed which columns a control may read must narrow
409
+ * this too, or `extent` becomes the way around that narrowing.
410
+ *
411
+ * **Disallowed/unknown fields are DROPPED**, and the request still answers
412
+ * for the fields that survived — matching `distinct` (which drops an invalid
413
+ * projected field rather than failing the query) rather than `groupByCount`
414
+ * (which always rejects, because there the field IS the whole query). Under
415
+ * the ambient `throwOnInvalid` policy the drop becomes a
416
+ * `BadRequestException`, again as `distinct`. A dropped field is simply
417
+ * absent from the result, which the `fieldExtent` contract already defines as
418
+ * "not measured" — the caller cannot tell it apart from a field the adapter
419
+ * could not resolve, and does not need to.
420
+ *
421
+ * **Computed members** route as `{ alias, source }` rather than a bare name,
422
+ * mirroring {@link groupByCount}'s grouping field: the adapter measures the
423
+ * dev-provided expression instead of resolving a column that does not exist.
424
+ * The registry comes from `opts.filterClass` when given, else from
425
+ * `@Filterable` metadata on the entity itself. Computed aliases bypass column
426
+ * validation — dev-declared, never client input — exactly like computed
427
+ * sort/distinct.
428
+ *
429
+ * Requires an adapter implementing the optional `fieldExtent` method; when
430
+ * absent, a clear error is thrown rather than a silent empty answer, which a
431
+ * range control would render as a collapsed (0, 0) span.
432
+ *
433
+ * @returns One entry per measured field keyed by field name (or computed
434
+ * alias). `{}` when the request named no extent fields at all.
435
+ */
436
+ fieldExtent<E>(entity: Type<E>, input: unknown, opts?: {
437
+ qb?: unknown;
438
+ context?: FilterContext;
439
+ filterClass?: Type<object>;
440
+ }): Promise<Record<string, FieldExtent>>;
441
+ /**
442
+ * Resolves ONE requested field to the shape an adapter measurement takes: the
443
+ * `{ alias, source }` pair of a computed member, or the validated column
444
+ * name. `null` when it is neither.
445
+ *
446
+ * Shared by {@link fieldExtent} and {@link fieldHistogram} so both obey the
447
+ * same allowlist, and so the one judgement nothing outside the runner can
448
+ * make — computed alias, or typo? both are strings no column matches — is
449
+ * made once. What the two callers differ on is only what `null` MEANS (a
450
+ * dropped field there, a rejected request here), which is why that decision
451
+ * stays with them.
452
+ *
453
+ * Validation runs with `throwOnInvalid: false` unconditionally: the caller
454
+ * words its own rejection for the key the client actually sent, rather than
455
+ * surfacing `Invalid distinct field` for a request that never said `distinct`.
456
+ */
457
+ private resolveMeasurableField;
458
+ /**
459
+ * A fresh builder carrying the request's WHERE/search and nothing else — the
460
+ * scope every measurement in this file asks its question over.
461
+ *
462
+ * `skipSortAndPagination` is the load-bearing part: an aggregate over a
463
+ * LIMITed builder answers for a page and looks exactly like an answer for the
464
+ * set, and an ORDER BY on a column an aggregate SELECT no longer projects is
465
+ * MySQL error 3065.
466
+ */
467
+ private buildFilteredQb;
468
+ /**
469
+ * The static `distinct` allowlist declared on a filter class, if any — the
470
+ * one {@link applyProjection} gates the DISTINCT projection with, reused by
471
+ * {@link fieldExtent} so both reads of a column obey the same narrowing.
472
+ *
473
+ * Read defensively rather than through a cast: `static distinct` is a plain
474
+ * class property no type checks, so a class can carry anything under that
475
+ * name. A non-array (or a list holding non-strings) yields `undefined`/the
476
+ * string entries, which degrades to entity-metadata validation instead of
477
+ * silently comparing field names against garbage and refusing everything.
478
+ */
479
+ private resolveDistinctAllowlist;
480
+ /**
481
+ * **Field histogram**: one numeric field's extent AND its bucketed
482
+ * distribution over the same filtered set — the two halves of a faceted range
483
+ * control, from one request. Reads the `histogram` structured key
484
+ * (`{ histogram: { field: 'price', buckets: 20 } }`).
485
+ *
486
+ * **Why this is a method and not two calls in a route.** The halves already
487
+ * exist — {@link fieldExtent} places a slider's endpoints,
488
+ * {@link FilterAdapter.groupByCount}'s bucketed variant draws the
489
+ * distribution behind them — but they are circular for the caller: the
490
+ * bucketed variant needs a WIDTH, and a width that is not derived from the
491
+ * data is either arbitrary (a hardcoded 1000 that yields two bars on one
492
+ * filter and four hundred on the next) or requires the extent the caller is
493
+ * asking for in the same breath. Nobody can break that cycle from outside:
494
+ * you must measure, then divide. So the runner measures, then divides.
495
+ *
496
+ * **Two round trips, and it cannot be one.** The width is a function of the
497
+ * first query's OUTPUT, so the second query's text does not exist until the
498
+ * first has returned. Folding them into one statement means either a
499
+ * correlated `(SELECT MAX(col)) - (SELECT MIN(col))` inside the bucket
500
+ * expression — the same scan twice, once per row-group, to avoid a round trip
501
+ * — or window functions the adapter contract does not have. Two plain
502
+ * aggregate queries over an indexable column is the cheaper shape, and it
503
+ * keeps this a composition of capabilities adapters already implement.
504
+ *
505
+ * **Not on the adapter contract, deliberately.** Every optional method added
506
+ * to `FilterAdapter` is a cost each adapter author pays forever, and this one
507
+ * would buy nothing: it is arithmetic between two existing calls, identical
508
+ * for every ORM. An adapter implementing `fieldExtent` and `groupByCount`
509
+ * gets this for free, and one implementing neither is told which is missing.
510
+ *
511
+ * **No `opts.qb`**, unlike its neighbours. Two passes need two builders — the
512
+ * first is consumed by an aggregate SELECT — and a single caller-supplied
513
+ * builder can only serve one of them. Silently creating a fresh builder for
514
+ * the second pass would drop whatever pre-scoping the caller put on theirs,
515
+ * so the distribution would describe a WIDER set than the extent: a chart
516
+ * with bars outside its own axis, and nothing to make it obvious.
517
+ *
518
+ * Field governance is {@link fieldExtent}'s, through the same
519
+ * {@link resolveMeasurableField}: alias remapping, the filter class's static
520
+ * `distinct` allowlist (else entity metadata), and computed members routed as
521
+ * `{ alias, source }`. The one divergence is what an invalid field means —
522
+ * rejected here, as in {@link groupByCount}, because the field IS the query
523
+ * and there is no partial answer to fall back to.
524
+ *
525
+ * **Dates are refused, not bucketed.** `fieldExtent` supports DATE columns on
526
+ * purpose, but bucketing is `FLOOR(value / width)`, and a date divided by a
527
+ * number is nonsense that no database announces: MySQL coerces the column to
528
+ * `20240131` and buckets THAT, which produces plausible-looking bars over an
529
+ * axis that skips two thirds of every year. See {@link assertBucketable}.
530
+ *
531
+ * @returns `{ min, max, bucketWidth, buckets }` — null ends and an empty
532
+ * bucket list when no row in scope carries a value.
533
+ */
534
+ fieldHistogram<E>(entity: Type<E>, input: unknown, opts?: {
535
+ context?: FilterContext;
536
+ filterClass?: Type<object>;
537
+ }): Promise<FieldHistogram>;
538
+ /**
539
+ * Parses the raw `histogram` block into a canonical `{ field, buckets }`.
540
+ * `null` when no usable `field` is present — the caller rejects, since a
541
+ * histogram of nothing has no meaningful empty answer.
542
+ *
543
+ * `buckets` accepts a numeric string as well as a number: on a GET route
544
+ * `?histogram[buckets]=20` arrives as text, and rejecting it there while
545
+ * accepting `20` from a POST body would make the two transports disagree
546
+ * about the same request. Anything unusable (absent, zero, negative, NaN,
547
+ * an object) degrades to the default rather than 400ing: it is a rendering
548
+ * hint, and a request that says nothing about bar count still has an answer.
549
+ */
550
+ private parseHistogram;
551
+ /**
552
+ * Refuses a field whose column type cannot survive `FLOOR(value / width)`,
553
+ * BEFORE either query runs.
554
+ *
555
+ * A DATE column is the case this exists for. `fieldExtent` supports dates
556
+ * deliberately (a calendar sizes itself from one), so `extent` and
557
+ * `histogram` accept the same field names right up to this point — and the
558
+ * failure mode without the check is not an error but a wrong answer: MySQL
559
+ * coerces a date to `20240131` before dividing, so the query succeeds and
560
+ * returns bars over an axis where two thirds of every year does not exist.
561
+ *
562
+ * Only ROOT-column metadata can answer this, so anything it cannot type — a
563
+ * relation path, a JSON sub-path, a computed source — passes here and is
564
+ * caught by {@link toBucketableNumber} once the extent comes back with actual
565
+ * values. Refusing everything untypeable instead would reject `author.age`,
566
+ * which `where`, `sort` and `distinct` all accept.
567
+ */
568
+ private assertBucketable;
569
+ /**
570
+ * Coerces one measured extent end to the number the width arithmetic needs,
571
+ * or `null` for "no row carries a value".
572
+ *
573
+ * Not a `typeof value === 'number'` check, in either direction:
574
+ *
575
+ * - a DECIMAL column hydrates to a STRING on mysql2 and pg, so the strict
576
+ * check would refuse the most ordinary histogram there is — a price;
577
+ * - `Number(new Date())` is a finite epoch, so a bare numeric coercion would
578
+ * wave a date extent straight through into `FLOOR(ms / width)`. Dates are
579
+ * therefore tested for FIRST, by identity, not by what they coerce to.
580
+ *
581
+ * This is the net under {@link assertBucketable}, which only sees root-column
582
+ * metadata: a computed source, a relation path or a JSON sub-path is typed by
583
+ * nothing until its value arrives here.
584
+ */
585
+ private toBucketableNumber;
586
+ /**
587
+ * Derives a bucket width from the measured span and the desired bar count,
588
+ * snapped to the nearest 1/2/5 × 10ⁿ step.
589
+ *
590
+ * The raw `span / desired` is the arithmetically correct width and the wrong
591
+ * answer for a control. Two reasons, both visible to a user:
592
+ *
593
+ * - buckets are anchored at multiples of the width (`FLOOR(col / w) * w`,
594
+ * which the adapter capability defines and which is what lets the grouping
595
+ * be one expression), so an ugly width means ugly edges: a span of
596
+ * 499–128000 over 10 gives 12750.1, and axis labels at 12750.1, 25500.2,
597
+ * 38250.3;
598
+ * - a raw width changes on every row inserted, so the bars re-partition and
599
+ * visibly jump whenever the filtered set shifts slightly. A snapped width
600
+ * holds still across a range of spans, which is the hysteresis a facet
601
+ * that redraws on every keystroke needs.
602
+ *
603
+ * Snapped to the NEAREST step in log space (the √2 / √10 / √50 thresholds),
604
+ * not upward: rounding up turns a span of 101 over 10 buckets into a width of
605
+ * 20 and six bars, which is a worse lie about the request than eleven bars.
606
+ * The count therefore lands within about √2 of `desired` in either direction
607
+ * — `buckets` is a target, and the returned `bucketWidth` is authoritative.
608
+ *
609
+ * `span` is strictly positive here: the `min === max` case never reaches this.
610
+ */
611
+ private niceBucketWidth;
612
+ /**
613
+ * Turns the adapter's sparse `{ value, count }` groups into the contiguous
614
+ * ascending bucket list a chart draws.
615
+ *
616
+ * Three things the raw groups get wrong for this purpose:
617
+ *
618
+ * - **Nulls.** A row whose column is null groups under `FLOOR(NULL / w)`,
619
+ * which is NULL — and `Number(null)` is 0, so passing the groups through
620
+ * unfiltered plants a phantom bar at zero holding every null row.
621
+ * - **Order.** `GROUP BY` has no defined output order. A histogram is a
622
+ * sequence, and bars drawn in the order MySQL happened to hash them are
623
+ * not a distribution.
624
+ * - **Gaps.** Empty buckets produce no group at all, so a sparse list renders
625
+ * as evenly spaced bars that lie about where the data sits. They are
626
+ * filled with zero-count entries, which is bounded work: the bucket count
627
+ * is `span / width`, and the width came from a clamped desired count.
628
+ *
629
+ * Groups are matched to buckets by INDEX rather than by comparing the
630
+ * returned `value` to a computed edge: for a width like 0.1 the database's
631
+ * `FLOOR(x / 0.1) * 0.1` and this code's `i * 0.1` differ in the last bits,
632
+ * and an equality match would silently drop those buckets to zero.
633
+ */
634
+ private assembleBuckets;
635
+ /**
636
+ * Rounds a bucket edge to the decimal precision its width implies. Every edge
637
+ * is an exact multiple of a 1/2/5 × 10ⁿ width, so this cannot move one onto a
638
+ * different bucket — it only sheds the binary-float residue that otherwise
639
+ * labels an axis `0.30000000000000004`. Skipped entirely for widths outside
640
+ * `toFixed`'s useful range, where rounding would destroy information rather
641
+ * than tidy it.
642
+ */
643
+ private snapEdge;
362
644
  /**
363
645
  * Runs a dynamic query with **keyset (cursor) pagination** and executes it,
364
646
  * returning a stable, non-overlapping page plus opaque forward/backward
@@ -377,10 +659,18 @@ export declare class FilterRunner {
377
659
  * Requires an adapter implementing `getResult`, `getPrimaryKey`,
378
660
  * `applyKeysetPagination` and `applyKeysetOrderAndLimit`. Additive — does not
379
661
  * change `apply`/`applyDynamic`/`findAndCount`.
662
+ *
663
+ * `opts.trustedPageSize` lifts the module-level `maxPageSize` ceiling off
664
+ * `first`/`last` for this call — same meaning as on {@link findAndCount},
665
+ * and it belongs here for the same reason: keyset paging is the pagination a
666
+ * long server-side walk SHOULD use (it is stable under concurrent writes),
667
+ * so capping it would leave the recommended export path as the one that
668
+ * cannot opt out.
380
669
  */
381
670
  findPage<E>(entity: Type<E>, input: unknown, opts?: {
382
671
  qb?: unknown;
383
672
  context?: FilterContext;
673
+ trustedPageSize?: boolean;
384
674
  }): Promise<CursorPage<E>>;
385
675
  /** Flips every keyset column's direction (for backward cursor paging). */
386
676
  private reverseKeyset;
@@ -393,8 +683,37 @@ export declare class FilterRunner {
393
683
  * Drops `where` column-filter clauses whose field is not a known scalar
394
684
  * column, relation, or dotted relation path on the entity — so a client
395
685
  * filter on an absent column (e.g. a base-scope `baseId` on a base-less
396
- * table) is silently ignored instead of crashing the ORM. Recurses AND/OR.
397
- * No-op (pass-through) when the adapter exposes no metadata.
686
+ * table) is dropped instead of crashing the ORM. Recurses AND/OR.
687
+ *
688
+ * The grammar check (`validateColumnFilters` → `isValidFieldPath`) only
689
+ * proves the field name is SQL-SAFE, not that it EXISTS: `ghostColumn`
690
+ * satisfies the pattern, sails through the operator allowlist, and blows up
691
+ * inside the ORM. That surfaces as a 500 for the consumer, and as a mid-run
692
+ * failure for a background job — both worse than not applying a constraint
693
+ * the entity cannot express in the first place.
694
+ *
695
+ * **Policy knob: `throwOnInvalid`, not `onUnknownKey`.** Three reasons, in
696
+ * order of weight: (1) `FilterModuleOptions.throwOnInvalid` is already
697
+ * DOCUMENTED as covering "unknown `where` columns" — static mode was simply
698
+ * never wired to it; (2) `applyDynamic` already routes this same function
699
+ * through `throwOnInvalid`, so choosing the other knob would recreate, in a
700
+ * new place, the very static-vs-dynamic asymmetry this fix closes; (3)
701
+ * `throwOnInvalid` is overridable per-`@Filterable`, while `onUnknownKey` is
702
+ * module-global — and whether a stray `where` column is a client bug or a
703
+ * tolerated legacy payload is a per-endpoint judgement. `onUnknownKey` keeps
704
+ * its own scope: keys of the STRUCTURED filter object, which are dispatch
705
+ * targets (`@FilterFor` / auto-field / relation), not column references.
706
+ *
707
+ * Default (`throwOnInvalid: false`) is drop-with-a-warning rather than a
708
+ * 400: it is the change that stops the crash without turning requests that
709
+ * work today into errors, and the warning names the field so the drop is
710
+ * observable rather than silent. Mirrors
711
+ * {@link pruneBlacklistedColumnFilters}, which made the same call.
712
+ *
713
+ * Falls back to accept-all (with a warning) when the adapter exposes no
714
+ * metadata — the same graceful degradation `resolveAutoFields` uses, so an
715
+ * adapter that cannot introspect keeps its pre-fix behavior instead of
716
+ * having every `where` clause dropped.
398
717
  */
399
718
  private pruneUnknownColumnFilters;
400
719
  /**
@@ -504,13 +823,46 @@ export declare class FilterRunner {
504
823
  * option. Returns undefined when neither is set.
505
824
  */
506
825
  private resolveDefaultSort;
826
+ /**
827
+ * Resolves the effective `distinctOrder`: the ROUTE's own
828
+ * `@ApplyFilter({ distinctOrder })` wins over the filter class's
829
+ * `@Filterable({ distinctOrder })`, and absent both it is off.
830
+ *
831
+ * There is deliberately no module-level knob. Whether a `SELECT DISTINCT`
832
+ * wants an ORDER BY is a property of the endpoint reading it — one filter
833
+ * class typically serves a rows route that orders itself and a distinct route
834
+ * that does not — so an app-wide switch would be answering a question at the
835
+ * wrong altitude, and silently, for queries whose cost it cannot see.
836
+ *
837
+ * Off unless asked for, for the same reason: ordering a projection the caller
838
+ * never asked to order is a clause this library would be inventing, and on a
839
+ * large distinct with no index on the projected column that clause is a
840
+ * filesort nobody signed up for. See {@link FilterableOptions.distinctOrder}.
841
+ */
842
+ private resolveDistinctOrder;
507
843
  private handleUnknownKey;
844
+ /**
845
+ * Narrows an already-structured sort element. The `direction` check is the
846
+ * strict part: an unrecognised direction is dropped rather than coerced,
847
+ * because guessing `asc` for a client that asked for something else silently
848
+ * returns the wrong page of a paginated result.
849
+ */
850
+ private isSortItem;
851
+ /**
852
+ * Parses one `"-field"` / `"field"` token into a {@link SortItem}, or
853
+ * `undefined` when there is no field left after trimming (`""`, `" "`,
854
+ * a bare `"-"`). Shared by both string shapes so a token means the same
855
+ * thing whether it arrived inside `"a,-b"` or as `["a", "-b"]`.
856
+ */
857
+ private parseSortToken;
508
858
  /**
509
859
  * Parses raw sort input into an array of SortItem objects.
510
860
  *
511
861
  * Supports:
512
862
  * - String: `"-createdAt,name"` → `[{ field: 'createdAt', direction: 'desc' }, { field: 'name', direction: 'asc' }]`
863
+ * - Array of tokens: `["-createdAt", "name"]` — same token rules as the string form
513
864
  * - Array of SortItem objects: passed through as-is
865
+ * - Arrays mixing the two: each element is read by its own type
514
866
  * - Falsy values: returns empty array
515
867
  *
516
868
  * Minus prefix = desc, no prefix = asc (JSON:API convention).
@@ -569,7 +921,32 @@ export declare class FilterRunner {
569
921
  /**
570
922
  * Applies offset or cursor pagination to the query builder.
571
923
  * Cursor pagination logs a warning (not yet implemented).
924
+ *
925
+ * `trusted` bypasses the `maxPageSize` ceiling — see
926
+ * {@link FilterRunner.resolvePageSize}.
572
927
  */
573
928
  private applyPagination;
929
+ /**
930
+ * Resolves the effective page size from a requested one.
931
+ *
932
+ * `maxPageSize` is a MODULE-level ceiling, so one number has to answer two
933
+ * different questions, and the right answers disagree. For a size that came
934
+ * off an HTTP request the ceiling is the whole point: it is what stops a
935
+ * client asking for a million rows. For a size that came from the server's
936
+ * own code — an export writing a CSV, a scheduled report, a batch job — the
937
+ * ceiling is not protection, it is a silent wrong answer: the runner is
938
+ * handed `size: 10_000`, returns 100 rows, and the export loop reads
939
+ * `100 < 10_000` as "table exhausted" and writes a truncated file with no
940
+ * error anywhere.
941
+ *
942
+ * The runner cannot tell the two apart by looking at the number, so the CALL
943
+ * SITE says which it is. `trusted` means "this size is server-authored, not
944
+ * client input" — the one fact the caller knows and the runner never can.
945
+ *
946
+ * The minimum of 1 still applies either way: that is not a safety cap but a
947
+ * correctness one (a `LIMIT 0` or a negative limit is not a page), and
948
+ * trusting the caller's intent does not make `size: -5` mean anything.
949
+ */
950
+ private resolvePageSize;
574
951
  }
575
952
  //# sourceMappingURL=runner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}