@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/CHANGELOG.md +155 -0
- package/README.md +2 -1
- package/dist/adapter/adapter.d.ts +74 -0
- package/dist/adapter/adapter.d.ts.map +1 -1
- package/dist/decorator/filterable.decorator.d.ts.map +1 -1
- package/dist/decorator/filterable.decorator.js +1 -0
- package/dist/decorator/filterable.decorator.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interceptor/apply-filter.interceptor.d.ts.map +1 -1
- package/dist/interceptor/apply-filter.interceptor.js +6 -1
- package/dist/interceptor/apply-filter.interceptor.js.map +1 -1
- package/dist/runner.d.ts +381 -4
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +768 -54
- package/dist/runner.js.map +1 -1
- package/dist/types.d.ts +147 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/filter-safety/SKILL.md +20 -4
- package/skills/structured-querying/SKILL.md +14 -1
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
|
|
397
|
-
*
|
|
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
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,IAAI,EACV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
|