@dudousxd/nestjs-filter 1.30.0 → 1.32.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 +99 -0
- package/dist/adapter/adapter.d.ts +15 -1
- 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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interceptor/apply-filter.interceptor.d.ts +10 -0
- package/dist/interceptor/apply-filter.interceptor.d.ts.map +1 -1
- package/dist/interceptor/apply-filter.interceptor.js +36 -6
- package/dist/interceptor/apply-filter.interceptor.js.map +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +23 -3
- package/dist/module.js.map +1 -1
- package/dist/runner.d.ts +38 -5
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +91 -33
- package/dist/runner.js.map +1 -1
- package/dist/tokens.d.ts +7 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +7 -0
- package/dist/tokens.js.map +1 -1
- package/dist/types.d.ts +56 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/runner.js
CHANGED
|
@@ -93,14 +93,19 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
93
93
|
options;
|
|
94
94
|
contextAccessor;
|
|
95
95
|
logger = new Logger(FilterRunner_1.name);
|
|
96
|
-
adapter
|
|
96
|
+
/** The application-wide adapter (`FilterModule.forRoot`'s), used by every filter that does not
|
|
97
|
+
* name its own — see {@link FilterableOptions.adapter}. */
|
|
98
|
+
defaultAdapter;
|
|
99
|
+
/** Adapters resolved from a filter-declared token, memoized per token: they are application-scoped
|
|
100
|
+
* providers, so re-resolving them per request is pure overhead. */
|
|
101
|
+
scopedAdapters = new Map();
|
|
97
102
|
/** Per-entity metadata cache for `describe()`. Metadata is static at runtime. */
|
|
98
103
|
descriptionCache = new WeakMap();
|
|
99
104
|
constructor(moduleRef, options, injectedAdapter, contextAccessor) {
|
|
100
105
|
this.moduleRef = moduleRef;
|
|
101
106
|
this.options = options;
|
|
102
107
|
this.contextAccessor = contextAccessor;
|
|
103
|
-
this.
|
|
108
|
+
this.defaultAdapter = injectedAdapter;
|
|
104
109
|
}
|
|
105
110
|
/**
|
|
106
111
|
* Soft-detects the current-request context accessor owned by
|
|
@@ -147,15 +152,47 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
147
152
|
}
|
|
148
153
|
adapter.applyAutoField(qb, field, tenantId);
|
|
149
154
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
/**
|
|
156
|
+
* The adapter that answers for a request, given the classes it is made against.
|
|
157
|
+
*
|
|
158
|
+
* The FIRST source declaring `@Filterable({ adapter })` wins (a filter class before the entity it
|
|
159
|
+
* targets, since the class is the more specific declaration), and everything else falls through to
|
|
160
|
+
* the application-wide adapter. Sources are passed as they are known at the call site; `undefined`
|
|
161
|
+
* entries are skipped, so a dynamic call with no filter class behaves exactly as before.
|
|
162
|
+
*
|
|
163
|
+
* A declared token that does not resolve THROWS rather than falling back: a filter naming the
|
|
164
|
+
* adapter it needs, then quietly running on a different backend's, would answer with rows from the
|
|
165
|
+
* wrong data source — a failure that looks like a successful query.
|
|
166
|
+
*/
|
|
167
|
+
resolveAdapter(...sources) {
|
|
168
|
+
for (const source of sources) {
|
|
169
|
+
const token = source ? getFilterableMetadata(source)?.adapter : undefined;
|
|
170
|
+
if (token === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
const cached = this.scopedAdapters.get(token);
|
|
173
|
+
if (cached)
|
|
174
|
+
return cached;
|
|
175
|
+
let resolved = null;
|
|
176
|
+
try {
|
|
177
|
+
resolved = this.moduleRef.get(token, { strict: false });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
resolved = null;
|
|
181
|
+
}
|
|
182
|
+
if (!resolved) {
|
|
183
|
+
throw new Error(`@Filterable on ${source?.name} names an adapter token that is not registered. Provide it in a module reachable from this one.`);
|
|
184
|
+
}
|
|
185
|
+
this.scopedAdapters.set(token, resolved);
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
if (this.defaultAdapter)
|
|
189
|
+
return this.defaultAdapter;
|
|
153
190
|
try {
|
|
154
191
|
const resolved = this.moduleRef.get(FILTER_ADAPTER, { strict: false });
|
|
155
192
|
if (resolved) {
|
|
156
|
-
this.
|
|
193
|
+
this.defaultAdapter = resolved;
|
|
157
194
|
}
|
|
158
|
-
return this.
|
|
195
|
+
return this.defaultAdapter;
|
|
159
196
|
}
|
|
160
197
|
catch {
|
|
161
198
|
return null;
|
|
@@ -239,7 +276,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
239
276
|
const cached = this.descriptionCache.get(entity);
|
|
240
277
|
if (cached)
|
|
241
278
|
return cached;
|
|
242
|
-
const adapter = this.resolveAdapter();
|
|
279
|
+
const adapter = this.resolveAdapter(entity);
|
|
243
280
|
const fields = {};
|
|
244
281
|
const relations = {};
|
|
245
282
|
for (const field of adapter?.getEntityFields?.(entity) ?? []) {
|
|
@@ -413,11 +450,11 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
413
450
|
}
|
|
414
451
|
async apply(FilterClass, input, qb, context = {}, internal = {}) {
|
|
415
452
|
const filter = await this.resolveFilter(FilterClass);
|
|
416
|
-
const adapter = this.resolveAdapter();
|
|
417
453
|
// Resolved once and reused for every alias choke point below (column
|
|
418
454
|
// filters, structured filter keys, sort, distinct, select) as well as
|
|
419
455
|
// the computed-field/entity lookups further down.
|
|
420
456
|
const filterableMeta = getFilterableMetadata(FilterClass);
|
|
457
|
+
const adapter = this.resolveAdapter(FilterClass, filterableMeta?.entity);
|
|
421
458
|
// Merges the inline `computed` map and `@Computed` methods into one
|
|
422
459
|
// alias → source registry, resolved once per apply() call.
|
|
423
460
|
const computedRegistry = buildComputedRegistry(FilterClass, filter);
|
|
@@ -862,7 +899,10 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
862
899
|
* via the adapter's applyRelationConstraint.
|
|
863
900
|
*/
|
|
864
901
|
async applyRelation(RelatedFilterClass, qb, relationName, entries, context) {
|
|
865
|
-
|
|
902
|
+
// The RELATED filter's adapter, not the outer one: a relation is constrained through the class
|
|
903
|
+
// that declares it, and that class may name its own backend.
|
|
904
|
+
const adapter = this.resolveAdapter(RelatedFilterClass);
|
|
905
|
+
if (!adapter?.applyRelationConstraint) {
|
|
866
906
|
this.warnUnsupported(`Relation "${relationName}" provided`, 'applyRelationConstraint');
|
|
867
907
|
return;
|
|
868
908
|
}
|
|
@@ -870,7 +910,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
870
910
|
for (const [key, value] of entries) {
|
|
871
911
|
inputObj[key] = value;
|
|
872
912
|
}
|
|
873
|
-
await
|
|
913
|
+
await adapter.applyRelationConstraint(qb, relationName, async (relationQb) => {
|
|
874
914
|
await this.apply(RelatedFilterClass, { filter: inputObj }, relationQb, context, {
|
|
875
915
|
native: true,
|
|
876
916
|
});
|
|
@@ -1012,7 +1052,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1012
1052
|
return set;
|
|
1013
1053
|
}
|
|
1014
1054
|
// Introspect entity metadata to restrict auto-fields to real columns
|
|
1015
|
-
const adapter = this.resolveAdapter();
|
|
1055
|
+
const adapter = this.resolveAdapter(FilterClass, meta.entity);
|
|
1016
1056
|
if (adapter?.getEntityFields) {
|
|
1017
1057
|
const entityFields = adapter.getEntityFields(meta.entity);
|
|
1018
1058
|
if (entityFields) {
|
|
@@ -1181,7 +1221,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1181
1221
|
* @returns The query builder with filters applied.
|
|
1182
1222
|
*/
|
|
1183
1223
|
async applyDynamic(entity, input, qb, context = {}, internal = {}) {
|
|
1184
|
-
const adapter = this.resolveAdapter();
|
|
1224
|
+
const adapter = this.resolveAdapter(internal.filterClass, entity);
|
|
1185
1225
|
// Dynamic mode has no FilterClass — an entity class can still carry
|
|
1186
1226
|
// `@Filterable` metadata (declared directly on the entity, decorating
|
|
1187
1227
|
// itself) purely to supply `aliases` for endpoints that query it
|
|
@@ -1337,7 +1377,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1337
1377
|
* else this method does.
|
|
1338
1378
|
*/
|
|
1339
1379
|
async findAndCount(entity, input, opts = {}) {
|
|
1340
|
-
const adapter = this.resolveAdapter();
|
|
1380
|
+
const adapter = this.resolveAdapter(entity);
|
|
1341
1381
|
const qb = opts.qb ?? adapter?.createQueryBuilder(entity);
|
|
1342
1382
|
const structured = this.extractStructuredInput(input);
|
|
1343
1383
|
const includes = this.parseIncludes(structured.include);
|
|
@@ -1435,7 +1475,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1435
1475
|
* count }[]` with `bucketEnd = bucketStart + bucket`.
|
|
1436
1476
|
*/
|
|
1437
1477
|
async groupByCount(entity, input, opts = {}) {
|
|
1438
|
-
const adapter = this.resolveAdapter();
|
|
1478
|
+
const adapter = this.resolveAdapter(opts.filterClass, entity);
|
|
1439
1479
|
const structured = this.extractStructuredInput(input);
|
|
1440
1480
|
const spec = this.parseGroupByCount(structured.groupByCount);
|
|
1441
1481
|
if (!spec) {
|
|
@@ -1478,9 +1518,11 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1478
1518
|
// Apply WHERE + search only (no sort/pagination/distinct/select — this mode
|
|
1479
1519
|
// replaces entity rows). `skipSortAndPagination` also keeps applyDynamic
|
|
1480
1520
|
// from ordering/slicing the pre-aggregation query.
|
|
1481
|
-
await this.applyDynamic(entity, { filter: structured.filter, search: structured.search }, qb, opts.context, { skipSortAndPagination: true, native: true });
|
|
1521
|
+
await this.applyDynamic(entity, { filter: structured.filter, search: structured.search }, qb, opts.context, { skipSortAndPagination: true, native: true, filterClass: opts.filterClass });
|
|
1482
1522
|
const bucket = spec.bucket;
|
|
1483
|
-
const
|
|
1523
|
+
const limit = spec.limit;
|
|
1524
|
+
const aggregateOpts = bucket || limit ? { ...(bucket && { bucket }), ...(limit && { limit }) } : undefined;
|
|
1525
|
+
const rows = await adapter.groupByCount(qb, groupField, entity, aggregateOpts);
|
|
1484
1526
|
if (bucket) {
|
|
1485
1527
|
return rows.map((r) => {
|
|
1486
1528
|
const bucketStart = Number(r.value);
|
|
@@ -1491,10 +1533,18 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1491
1533
|
}
|
|
1492
1534
|
/**
|
|
1493
1535
|
* Parses and validates the raw `groupByCount` structured-input block into a
|
|
1494
|
-
* canonical `{ field, bucket? }`. Returns `null` when no usable
|
|
1495
|
-
* present. `bucket` is kept only when it is a finite positive
|
|
1496
|
-
* zero/negative/NaN/non-number bucket degrades to the plain
|
|
1497
|
-
* group-by-count rather than emitting a divide-by-zero or
|
|
1536
|
+
* canonical `{ field, bucket?, limit? }`. Returns `null` when no usable
|
|
1537
|
+
* `field` is present. `bucket` is kept only when it is a finite positive
|
|
1538
|
+
* number — a zero/negative/NaN/non-number bucket degrades to the plain
|
|
1539
|
+
* (non-bucketed) group-by-count rather than emitting a divide-by-zero or
|
|
1540
|
+
* nonsensical width. `limit` is kept on the same terms and must additionally
|
|
1541
|
+
* be an integer; anything else degrades to the unbounded form, since a
|
|
1542
|
+
* fractional or negative row count is not a narrower question, just a
|
|
1543
|
+
* malformed one.
|
|
1544
|
+
*
|
|
1545
|
+
* Both arrive over the wire as strings on a GET route (`?groupByCount[limit]=20`),
|
|
1546
|
+
* so numeric strings are coerced before those checks — otherwise the bound a
|
|
1547
|
+
* caller asked for would be silently dropped by the shape test alone.
|
|
1498
1548
|
*/
|
|
1499
1549
|
parseGroupByCount(raw) {
|
|
1500
1550
|
if (!raw || typeof raw !== 'object')
|
|
@@ -1502,10 +1552,18 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1502
1552
|
const obj = raw;
|
|
1503
1553
|
if (typeof obj.field !== 'string' || obj.field.length === 0)
|
|
1504
1554
|
return null;
|
|
1505
|
-
const
|
|
1506
|
-
?
|
|
1507
|
-
: undefined;
|
|
1508
|
-
|
|
1555
|
+
const numeric = (value) => {
|
|
1556
|
+
const n = typeof value === 'string' ? Number(value) : value;
|
|
1557
|
+
return typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : undefined;
|
|
1558
|
+
};
|
|
1559
|
+
const bucket = numeric(obj.bucket);
|
|
1560
|
+
const limitValue = numeric(obj.limit);
|
|
1561
|
+
const limit = limitValue !== undefined && Number.isInteger(limitValue) ? limitValue : undefined;
|
|
1562
|
+
return {
|
|
1563
|
+
field: obj.field,
|
|
1564
|
+
...(bucket !== undefined && { bucket }),
|
|
1565
|
+
...(limit !== undefined && { limit }),
|
|
1566
|
+
};
|
|
1509
1567
|
}
|
|
1510
1568
|
/**
|
|
1511
1569
|
* **Field extent**: the `MIN`/`MAX` of the requested field(s) over the rows
|
|
@@ -1571,7 +1629,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1571
1629
|
// alongside its rows read and get nothing back when nobody asked.
|
|
1572
1630
|
if (requested.length === 0)
|
|
1573
1631
|
return {};
|
|
1574
|
-
const adapter = this.resolveAdapter();
|
|
1632
|
+
const adapter = this.resolveAdapter(opts.filterClass, entity);
|
|
1575
1633
|
if (!adapter?.fieldExtent) {
|
|
1576
1634
|
throw new Error('extent is not supported by the active adapter (it does not implement fieldExtent()).');
|
|
1577
1635
|
}
|
|
@@ -1608,7 +1666,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1608
1666
|
if (targets.length === 0)
|
|
1609
1667
|
return {};
|
|
1610
1668
|
const qb = opts.qb ?? adapter.createQueryBuilder(entity);
|
|
1611
|
-
await this.applyDynamic(entity, { filter: structured.filter, search: structured.search }, qb, opts.context, { skipSortAndPagination: true, native: true });
|
|
1669
|
+
await this.applyDynamic(entity, { filter: structured.filter, search: structured.search }, qb, opts.context, { skipSortAndPagination: true, native: true, filterClass: opts.filterClass });
|
|
1612
1670
|
return adapter.fieldExtent(qb, targets, entity);
|
|
1613
1671
|
}
|
|
1614
1672
|
/**
|
|
@@ -1643,9 +1701,9 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1643
1701
|
* set, and an ORDER BY on a column an aggregate SELECT no longer projects is
|
|
1644
1702
|
* MySQL error 3065.
|
|
1645
1703
|
*/
|
|
1646
|
-
async buildFilteredQb(entity, structured, adapter, context) {
|
|
1704
|
+
async buildFilteredQb(entity, structured, adapter, context, filterClass) {
|
|
1647
1705
|
const qb = adapter.createQueryBuilder(entity);
|
|
1648
|
-
await this.applyDynamic(entity, { filter: structured.filter, search: structured.search }, qb, context, { skipSortAndPagination: true, native: true });
|
|
1706
|
+
await this.applyDynamic(entity, { filter: structured.filter, search: structured.search }, qb, context, { skipSortAndPagination: true, native: true, filterClass });
|
|
1649
1707
|
return qb;
|
|
1650
1708
|
}
|
|
1651
1709
|
/**
|
|
@@ -1727,7 +1785,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1727
1785
|
if (!spec) {
|
|
1728
1786
|
throw new BadRequestException('histogram requires a `{ field }` specification (with an optional positive `buckets` count).');
|
|
1729
1787
|
}
|
|
1730
|
-
const adapter = this.resolveAdapter();
|
|
1788
|
+
const adapter = this.resolveAdapter(opts.filterClass, entity);
|
|
1731
1789
|
// Named separately: an adapter can plausibly have one and not the other,
|
|
1732
1790
|
// and "histogram is unsupported" would send its author looking for a method
|
|
1733
1791
|
// by that name, which does not and will not exist.
|
|
@@ -1755,7 +1813,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1755
1813
|
}
|
|
1756
1814
|
this.assertBucketable(target, entity, adapter);
|
|
1757
1815
|
// ── pass 1: where do the endpoints sit ───────────────────────────────────
|
|
1758
|
-
const extentQb = await this.buildFilteredQb(entity, structured, adapter, opts.context);
|
|
1816
|
+
const extentQb = await this.buildFilteredQb(entity, structured, adapter, opts.context, opts.filterClass);
|
|
1759
1817
|
const measured = await adapter.fieldExtent(extentQb, [target], entity);
|
|
1760
1818
|
const key = typeof target === 'string' ? target : target.alias;
|
|
1761
1819
|
const bounds = measured[key];
|
|
@@ -1776,7 +1834,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
1776
1834
|
return { min: null, max: null, bucketWidth: null, buckets: [] };
|
|
1777
1835
|
}
|
|
1778
1836
|
// ── pass 2: how are the rows distributed across them ─────────────────────
|
|
1779
|
-
const countQb = await this.buildFilteredQb(entity, structured, adapter, opts.context);
|
|
1837
|
+
const countQb = await this.buildFilteredQb(entity, structured, adapter, opts.context, opts.filterClass);
|
|
1780
1838
|
// Degenerate span: one row, or many rows sharing one value. A width of
|
|
1781
1839
|
// (max - min) / n is 0, and `FLOOR(col / 0)` is a null group on MySQL and a
|
|
1782
1840
|
// division-by-zero ERROR on Postgres — so the bucketed variant must not be
|
|
@@ -2001,7 +2059,7 @@ let FilterRunner = FilterRunner_1 = class FilterRunner {
|
|
|
2001
2059
|
* cannot opt out.
|
|
2002
2060
|
*/
|
|
2003
2061
|
async findPage(entity, input, opts = {}) {
|
|
2004
|
-
const adapter = this.resolveAdapter();
|
|
2062
|
+
const adapter = this.resolveAdapter(entity);
|
|
2005
2063
|
if (!adapter?.getResult ||
|
|
2006
2064
|
!adapter.getPrimaryKey ||
|
|
2007
2065
|
!adapter.applyKeysetPagination ||
|