ng-qubee 3.3.0 → 3.5.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/README.md +41 -883
- package/fesm2022/ng-qubee.mjs +880 -102
- package/fesm2022/ng-qubee.mjs.map +1 -1
- package/package.json +18 -9
- package/types/ng-qubee.d.ts +218 -19
package/fesm2022/ng-qubee.mjs
CHANGED
|
@@ -74,11 +74,13 @@ class PaginatedCollection {
|
|
|
74
74
|
*/
|
|
75
75
|
var DriverEnum;
|
|
76
76
|
(function (DriverEnum) {
|
|
77
|
+
DriverEnum["DRF"] = "drf";
|
|
77
78
|
DriverEnum["JSON_API"] = "json-api";
|
|
78
79
|
DriverEnum["LARAVEL"] = "laravel";
|
|
79
80
|
DriverEnum["NESTJS"] = "nestjs";
|
|
80
81
|
DriverEnum["POSTGREST"] = "postgrest";
|
|
81
82
|
DriverEnum["SPATIE"] = "spatie";
|
|
83
|
+
DriverEnum["STRAPI"] = "strapi";
|
|
82
84
|
})(DriverEnum || (DriverEnum = {}));
|
|
83
85
|
|
|
84
86
|
/**
|
|
@@ -123,6 +125,35 @@ class ResponseOptions {
|
|
|
123
125
|
this.total = options.total || 'total';
|
|
124
126
|
}
|
|
125
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Pre-configured ResponseOptions for the Django REST Framework (DRF) driver
|
|
130
|
+
*
|
|
131
|
+
* DRF's `PageNumberPagination` envelope is `{ count, next, previous,
|
|
132
|
+
* results }`, with no body field naming the current page, per-page, or
|
|
133
|
+
* last-page. The strategy parses those from the `next` / `previous`
|
|
134
|
+
* URLs, so the corresponding key paths default to empty strings; the
|
|
135
|
+
* strategy ignores `options.currentPage`, `options.perPage`,
|
|
136
|
+
* `options.lastPage`, `options.from`, `options.to`, `options.path`,
|
|
137
|
+
* `options.firstPageUrl`, and `options.lastPageUrl`.
|
|
138
|
+
*/
|
|
139
|
+
class DrfResponseOptions extends ResponseOptions {
|
|
140
|
+
constructor(options) {
|
|
141
|
+
super({
|
|
142
|
+
currentPage: options.currentPage || '',
|
|
143
|
+
data: options.data || 'results',
|
|
144
|
+
firstPageUrl: options.firstPageUrl || '',
|
|
145
|
+
from: options.from || '',
|
|
146
|
+
lastPage: options.lastPage || '',
|
|
147
|
+
lastPageUrl: options.lastPageUrl || '',
|
|
148
|
+
nextPageUrl: options.nextPageUrl || 'next',
|
|
149
|
+
path: options.path || '',
|
|
150
|
+
perPage: options.perPage || '',
|
|
151
|
+
prevPageUrl: options.prevPageUrl || 'previous',
|
|
152
|
+
to: options.to || '',
|
|
153
|
+
total: options.total || 'count'
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
126
157
|
/**
|
|
127
158
|
* Pre-configured ResponseOptions for the JSON:API driver
|
|
128
159
|
*
|
|
@@ -171,6 +202,69 @@ class NestjsResponseOptions extends ResponseOptions {
|
|
|
171
202
|
});
|
|
172
203
|
}
|
|
173
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Pre-configured ResponseOptions for the Strapi driver
|
|
207
|
+
*
|
|
208
|
+
* Uses dot-notation paths to access the nested `meta.pagination.*` envelope
|
|
209
|
+
* Strapi v4/v5 emits. Strapi does not include navigation links by default,
|
|
210
|
+
* so the URL paths point at locations that will resolve to `undefined`
|
|
211
|
+
* unless the consumer overrides them.
|
|
212
|
+
*/
|
|
213
|
+
class StrapiResponseOptions extends ResponseOptions {
|
|
214
|
+
constructor(options) {
|
|
215
|
+
super({
|
|
216
|
+
currentPage: options.currentPage || 'meta.pagination.page',
|
|
217
|
+
data: options.data || 'data',
|
|
218
|
+
firstPageUrl: options.firstPageUrl || 'links.first',
|
|
219
|
+
from: options.from || 'meta.pagination.from',
|
|
220
|
+
lastPage: options.lastPage || 'meta.pagination.pageCount',
|
|
221
|
+
lastPageUrl: options.lastPageUrl || 'links.last',
|
|
222
|
+
nextPageUrl: options.nextPageUrl || 'links.next',
|
|
223
|
+
path: options.path || 'path',
|
|
224
|
+
perPage: options.perPage || 'meta.pagination.pageSize',
|
|
225
|
+
prevPageUrl: options.prevPageUrl || 'links.prev',
|
|
226
|
+
to: options.to || 'meta.pagination.to',
|
|
227
|
+
total: options.total || 'meta.pagination.total'
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Enum representing the available filter operators for explicit operator
|
|
234
|
+
* filters
|
|
235
|
+
*
|
|
236
|
+
* NestJS encodes these with the `$` prefix at the wire level
|
|
237
|
+
* (`filter.field=$operator:value`); PostgREST translates them to its own
|
|
238
|
+
* prefix notation (`col=eq.val`, `col=is.null`, etc.). The enum values are
|
|
239
|
+
* intentionally the NestJS form; each driver's request strategy is
|
|
240
|
+
* responsible for mapping them into its own shape.
|
|
241
|
+
*
|
|
242
|
+
* `FTS`, `PLFTS`, `PHFTS`, `WFTS` are PostgREST-native full-text search
|
|
243
|
+
* variants; they throw `UnsupportedFilterOperatorError` on every other
|
|
244
|
+
* driver that does not recognise them.
|
|
245
|
+
*
|
|
246
|
+
* @see https://github.com/ppetzold/nestjs-paginate
|
|
247
|
+
* @see https://postgrest.org/en/stable/api.html#operators
|
|
248
|
+
*/
|
|
249
|
+
var FilterOperatorEnum;
|
|
250
|
+
(function (FilterOperatorEnum) {
|
|
251
|
+
FilterOperatorEnum["BTW"] = "$btw";
|
|
252
|
+
FilterOperatorEnum["CONTAINS"] = "$contains";
|
|
253
|
+
FilterOperatorEnum["EQ"] = "$eq";
|
|
254
|
+
FilterOperatorEnum["FTS"] = "$fts";
|
|
255
|
+
FilterOperatorEnum["GT"] = "$gt";
|
|
256
|
+
FilterOperatorEnum["GTE"] = "$gte";
|
|
257
|
+
FilterOperatorEnum["ILIKE"] = "$ilike";
|
|
258
|
+
FilterOperatorEnum["IN"] = "$in";
|
|
259
|
+
FilterOperatorEnum["LT"] = "$lt";
|
|
260
|
+
FilterOperatorEnum["LTE"] = "$lte";
|
|
261
|
+
FilterOperatorEnum["NOT"] = "$not";
|
|
262
|
+
FilterOperatorEnum["NULL"] = "$null";
|
|
263
|
+
FilterOperatorEnum["PHFTS"] = "$phfts";
|
|
264
|
+
FilterOperatorEnum["PLFTS"] = "$plfts";
|
|
265
|
+
FilterOperatorEnum["SW"] = "$sw";
|
|
266
|
+
FilterOperatorEnum["WFTS"] = "$wfts";
|
|
267
|
+
})(FilterOperatorEnum || (FilterOperatorEnum = {}));
|
|
174
268
|
|
|
175
269
|
var SortEnum;
|
|
176
270
|
(function (SortEnum) {
|
|
@@ -178,9 +272,42 @@ var SortEnum;
|
|
|
178
272
|
SortEnum["DESC"] = "desc";
|
|
179
273
|
})(SortEnum || (SortEnum = {}));
|
|
180
274
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Thrown when a filter operator receives a value array of the wrong shape
|
|
277
|
+
*
|
|
278
|
+
* Some operators have arity or type constraints that the library enforces
|
|
279
|
+
* at call time so misuse fails loudly instead of silently emitting invalid
|
|
280
|
+
* server requests:
|
|
281
|
+
*
|
|
282
|
+
* - `BTW` requires exactly two values (min, max).
|
|
283
|
+
* - `NULL` requires exactly one boolean value (`true` for `IS NULL`,
|
|
284
|
+
* `false` for `IS NOT NULL`).
|
|
285
|
+
*
|
|
286
|
+
* Operators with looser shape rules leave validation to the server; this
|
|
287
|
+
* error is reserved for cases where the library itself can detect the
|
|
288
|
+
* problem unambiguously from the call site.
|
|
289
|
+
*/
|
|
290
|
+
class InvalidFilterOperatorValueError extends Error {
|
|
291
|
+
/**
|
|
292
|
+
* @param operator - The operator that rejected the values
|
|
293
|
+
* @param reason - Short human-readable explanation of the constraint
|
|
294
|
+
*/
|
|
295
|
+
constructor(operator, reason) {
|
|
296
|
+
super(`Invalid values for filter operator ${operator}: ${reason}`);
|
|
297
|
+
this.name = 'InvalidFilterOperatorValueError';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Error thrown when filter operators are attempted with a driver that does not support them
|
|
303
|
+
*
|
|
304
|
+
* Filter operators are only supported by the NestJS driver.
|
|
305
|
+
* Use `addFilter()` for Spatie implicit equality filters.
|
|
306
|
+
*/
|
|
307
|
+
class UnsupportedFilterOperatorError extends Error {
|
|
308
|
+
constructor() {
|
|
309
|
+
super('Filter operators are only supported by the NestJS driver. Use addFilter() for Spatie.');
|
|
310
|
+
this.name = 'UnsupportedFilterOperatorError';
|
|
184
311
|
}
|
|
185
312
|
}
|
|
186
313
|
|
|
@@ -290,6 +417,428 @@ class AbstractRequestStrategy {
|
|
|
290
417
|
}
|
|
291
418
|
}
|
|
292
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Request strategy for the Django REST Framework (DRF) driver
|
|
422
|
+
*
|
|
423
|
+
* Generates URIs in DRF's flat query-parameter format, augmented by
|
|
424
|
+
* django-filter's double-underscore lookup convention:
|
|
425
|
+
*
|
|
426
|
+
* - Simple filters: `field=value` (multi-value collapses to `field__in=v1,v2`)
|
|
427
|
+
* - Operator filters: `field__lookup=value` (translated from
|
|
428
|
+
* `FilterOperatorEnum` — `GTE`→`__gte`, `ILIKE`→`__icontains`,
|
|
429
|
+
* `BTW`→`__range`, `NULL`→`__isnull`, etc.)
|
|
430
|
+
* - Ordering: `ordering=-field1,field2` (`-` prefix = DESC)
|
|
431
|
+
* - Search: `search=term` (DRF's SearchFilter)
|
|
432
|
+
* - Pagination: `page=N&page_size=M` (PageNumberPagination)
|
|
433
|
+
*
|
|
434
|
+
* `ordering` and `page_size` are DRF-idiomatic param names and are
|
|
435
|
+
* intentionally not configurable via `QueryBuilderOptions` — same
|
|
436
|
+
* precedent as PostgREST's `order` and `offset`. PostgREST's full-text
|
|
437
|
+
* search operators (`FTS`, `PHFTS`, `PLFTS`, `WFTS`) and the generic
|
|
438
|
+
* `NOT` have no django-filter equivalent and throw
|
|
439
|
+
* `UnsupportedFilterOperatorError`.
|
|
440
|
+
*
|
|
441
|
+
* @see https://www.django-rest-framework.org/api-guide/filtering/
|
|
442
|
+
* @see https://django-filter.readthedocs.io/
|
|
443
|
+
*/
|
|
444
|
+
class DrfRequestStrategy extends AbstractRequestStrategy {
|
|
445
|
+
/**
|
|
446
|
+
* Simple filters, operator filters (django-filter lookups), sorts, and
|
|
447
|
+
* global search — no per-model fields, no relation includes, no flat
|
|
448
|
+
* select (django-restql adds it but is not core DRF)
|
|
449
|
+
*/
|
|
450
|
+
capabilities = {
|
|
451
|
+
fields: false,
|
|
452
|
+
filters: true,
|
|
453
|
+
includes: false,
|
|
454
|
+
operatorFilters: true,
|
|
455
|
+
search: true,
|
|
456
|
+
select: false,
|
|
457
|
+
sort: true
|
|
458
|
+
};
|
|
459
|
+
/**
|
|
460
|
+
* DRF-native names of the three hardcoded query keys
|
|
461
|
+
*
|
|
462
|
+
* `ordering` and `page_size` are DRF/django-filter conventions and are
|
|
463
|
+
* intentionally not configurable through `QueryBuilderOptions`. `page`
|
|
464
|
+
* matches the default `QueryBuilderOptions.page`, and `search` matches
|
|
465
|
+
* the default `QueryBuilderOptions.search`, so those flow through the
|
|
466
|
+
* shared options object.
|
|
467
|
+
*/
|
|
468
|
+
static _orderingKey = 'ordering';
|
|
469
|
+
static _pageSizeKey = 'page_size';
|
|
470
|
+
/**
|
|
471
|
+
* Emit DRF-format query-string segments in canonical order:
|
|
472
|
+
* filters → operator filters → ordering → search → pagination
|
|
473
|
+
*
|
|
474
|
+
* @param state - The current query builder state
|
|
475
|
+
* @param options - The query parameter key name configuration
|
|
476
|
+
* @returns Ordered query-string fragments
|
|
477
|
+
*/
|
|
478
|
+
parts(state, options) {
|
|
479
|
+
const out = [];
|
|
480
|
+
this._appendFilters(state, out);
|
|
481
|
+
this._appendOperatorFilters(state, out);
|
|
482
|
+
this._appendOrdering(state, out);
|
|
483
|
+
this._appendSearch(state, options, out);
|
|
484
|
+
this._appendPagination(state, options, out);
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Append simple filter parameters
|
|
489
|
+
*
|
|
490
|
+
* Single-value filters emit `field=value` (django-filter's default
|
|
491
|
+
* exact match). Multi-value filters collapse to django-filter's
|
|
492
|
+
* `field__in=v1,v2,v3` form, which is the idiomatic way to express
|
|
493
|
+
* "value in list" in DRF.
|
|
494
|
+
*
|
|
495
|
+
* @param state - The current query builder state
|
|
496
|
+
* @param out - The accumulator the caller joins into the URI
|
|
497
|
+
*/
|
|
498
|
+
_appendFilters(state, out) {
|
|
499
|
+
const keys = Object.keys(state.filters);
|
|
500
|
+
if (!keys.length) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
keys.forEach(key => {
|
|
504
|
+
const values = state.filters[key];
|
|
505
|
+
if (!values.length) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (values.length === 1) {
|
|
509
|
+
out.push(`${key}=${values[0]}`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
out.push(`${key}__in=${values.join(',')}`);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Append operator-filter parameters as `field__lookup=value`
|
|
517
|
+
*
|
|
518
|
+
* Maps each `FilterOperatorEnum` value to a django-filter lookup
|
|
519
|
+
* suffix. `BTW` expands to `field__range=min,max`; `NULL` emits
|
|
520
|
+
* `field__isnull=true|false`; the generic `NOT` and PostgREST-only
|
|
521
|
+
* FTS operators are unsupported.
|
|
522
|
+
*
|
|
523
|
+
* @param state - The current query builder state
|
|
524
|
+
* @param out - The accumulator the caller joins into the URI
|
|
525
|
+
* @throws {InvalidFilterOperatorValueError} If `BTW` does not receive
|
|
526
|
+
* exactly two values, or `NULL` does not receive exactly one boolean
|
|
527
|
+
* @throws {UnsupportedFilterOperatorError} If the operator has no
|
|
528
|
+
* django-filter equivalent
|
|
529
|
+
*/
|
|
530
|
+
_appendOperatorFilters(state, out) {
|
|
531
|
+
if (!state.operatorFilters.length) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
state.operatorFilters.forEach((filter) => {
|
|
535
|
+
const [suffix, value] = this._formatOperatorPayload(filter);
|
|
536
|
+
const key = suffix ? `${filter.field}__${suffix}` : filter.field;
|
|
537
|
+
out.push(`${key}=${value}`);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Append `ordering=-field1,field2` (django's `-` prefix = DESC)
|
|
542
|
+
*
|
|
543
|
+
* @param state - The current query builder state
|
|
544
|
+
* @param out - The accumulator the caller joins into the URI
|
|
545
|
+
*/
|
|
546
|
+
_appendOrdering(state, out) {
|
|
547
|
+
if (!state.sorts.length) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const pairs = state.sorts.map(sort => `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`);
|
|
551
|
+
out.push(`${DrfRequestStrategy._orderingKey}=${pairs.join(',')}`);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Append `page=N&page_size=M`
|
|
555
|
+
*
|
|
556
|
+
* `page` follows `options.page` (default `page`, matching DRF); the
|
|
557
|
+
* size key is hardcoded to DRF's idiomatic `page_size`.
|
|
558
|
+
*
|
|
559
|
+
* @param state - The current query builder state
|
|
560
|
+
* @param options - The query parameter key name configuration
|
|
561
|
+
* @param out - The accumulator the caller joins into the URI
|
|
562
|
+
*/
|
|
563
|
+
_appendPagination(state, options, out) {
|
|
564
|
+
out.push(`${options.page}=${state.page}`);
|
|
565
|
+
out.push(`${DrfRequestStrategy._pageSizeKey}=${state.limit}`);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Append `search=term` when a search term is set
|
|
569
|
+
*
|
|
570
|
+
* @param state - The current query builder state
|
|
571
|
+
* @param options - The query parameter key name configuration
|
|
572
|
+
* @param out - The accumulator the caller joins into the URI
|
|
573
|
+
*/
|
|
574
|
+
_appendSearch(state, options, out) {
|
|
575
|
+
if (!state.search) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
out.push(`${options.search}=${state.search}`);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Translate a `FilterOperatorEnum` operator filter into a
|
|
582
|
+
* `[suffix, serializedValue]` pair
|
|
583
|
+
*
|
|
584
|
+
* The suffix is appended to the field name with a `__` separator on the
|
|
585
|
+
* wire side (`field__gte=18`). The empty string means "no suffix" —
|
|
586
|
+
* django-filter's implicit `exact` lookup.
|
|
587
|
+
*
|
|
588
|
+
* Mapping:
|
|
589
|
+
* - `EQ` → `''` (no suffix; default exact match)
|
|
590
|
+
* - `GT`/`GTE`/`LT`/`LTE`/`CONTAINS` → identity (lowercased name)
|
|
591
|
+
* - `ILIKE` → `icontains` (closest case-insensitive analog)
|
|
592
|
+
* - `IN` → `in` with comma-joined values
|
|
593
|
+
* - `SW` → `startswith`
|
|
594
|
+
* - `BTW` → `range` with comma-joined `[min, max]` (arity-checked)
|
|
595
|
+
* - `NULL` → `isnull` with boolean value (arity- and type-checked)
|
|
596
|
+
* - `NOT` → `UnsupportedFilterOperatorError` (no generic negation in
|
|
597
|
+
* django-filter; use `__exclude` on the queryset instead)
|
|
598
|
+
* - `FTS`/`PLFTS`/`PHFTS`/`WFTS` → `UnsupportedFilterOperatorError`
|
|
599
|
+
*
|
|
600
|
+
* @param filter - The operator filter to translate
|
|
601
|
+
* @returns A `[lookupSuffix, serializedValue]` tuple
|
|
602
|
+
* @throws {InvalidFilterOperatorValueError} If `BTW` does not receive
|
|
603
|
+
* exactly two values, or `NULL` does not receive exactly one boolean
|
|
604
|
+
* @throws {UnsupportedFilterOperatorError} If the operator has no
|
|
605
|
+
* django-filter equivalent
|
|
606
|
+
*/
|
|
607
|
+
_formatOperatorPayload(filter) {
|
|
608
|
+
const { operator, values } = filter;
|
|
609
|
+
const first = values[0];
|
|
610
|
+
switch (operator) {
|
|
611
|
+
case FilterOperatorEnum.EQ: return ['', String(first)];
|
|
612
|
+
case FilterOperatorEnum.GT: return ['gt', String(first)];
|
|
613
|
+
case FilterOperatorEnum.GTE: return ['gte', String(first)];
|
|
614
|
+
case FilterOperatorEnum.LT: return ['lt', String(first)];
|
|
615
|
+
case FilterOperatorEnum.LTE: return ['lte', String(first)];
|
|
616
|
+
case FilterOperatorEnum.CONTAINS: return ['contains', String(first)];
|
|
617
|
+
case FilterOperatorEnum.ILIKE: return ['icontains', String(first)];
|
|
618
|
+
case FilterOperatorEnum.IN: return ['in', values.join(',')];
|
|
619
|
+
case FilterOperatorEnum.SW: return ['startswith', String(first)];
|
|
620
|
+
case FilterOperatorEnum.BTW: {
|
|
621
|
+
if (values.length !== 2) {
|
|
622
|
+
throw new InvalidFilterOperatorValueError(operator, 'BTW requires exactly 2 values (min, max)');
|
|
623
|
+
}
|
|
624
|
+
return ['range', values.join(',')];
|
|
625
|
+
}
|
|
626
|
+
case FilterOperatorEnum.NULL: {
|
|
627
|
+
if (values.length !== 1 || typeof first !== 'boolean') {
|
|
628
|
+
throw new InvalidFilterOperatorValueError(operator, 'NULL requires exactly 1 boolean value (true → IS NULL, false → IS NOT NULL)');
|
|
629
|
+
}
|
|
630
|
+
return ['isnull', String(first)];
|
|
631
|
+
}
|
|
632
|
+
case FilterOperatorEnum.NOT:
|
|
633
|
+
case FilterOperatorEnum.FTS:
|
|
634
|
+
case FilterOperatorEnum.PHFTS:
|
|
635
|
+
case FilterOperatorEnum.PLFTS:
|
|
636
|
+
case FilterOperatorEnum.WFTS:
|
|
637
|
+
throw new UnsupportedFilterOperatorError();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Response strategy for the Django REST Framework (DRF) driver
|
|
644
|
+
*
|
|
645
|
+
* Parses DRF `PageNumberPagination` responses:
|
|
646
|
+
*
|
|
647
|
+
* ```json
|
|
648
|
+
* {
|
|
649
|
+
* "count": 100,
|
|
650
|
+
* "next": "http://api.example.com/items/?page=3",
|
|
651
|
+
* "previous": "http://api.example.com/items/?page=1",
|
|
652
|
+
* "results": [...]
|
|
653
|
+
* }
|
|
654
|
+
* ```
|
|
655
|
+
*
|
|
656
|
+
* DRF emits no `current_page` field in the body, so this strategy
|
|
657
|
+
* **derives** the current page (and the page size) by inspecting the
|
|
658
|
+
* `next` / `previous` URLs:
|
|
659
|
+
*
|
|
660
|
+
* - `previous === null` → current page is **1**.
|
|
661
|
+
* - `previous` set but has no `?page=N` param → DRF omits `page=1` from
|
|
662
|
+
* URLs when the previous page is the first, so we infer **2**.
|
|
663
|
+
* - `previous` has `?page=N` → current page is **N + 1**.
|
|
664
|
+
*
|
|
665
|
+
* Similarly, `perPage` is parsed from any `?page_size=N` query param on
|
|
666
|
+
* `next` or `previous`, and `lastPage` is computed as
|
|
667
|
+
* `ceil(count / perPage)`. When `perPage` cannot be discovered (e.g. on a
|
|
668
|
+
* single-page response that emits both URLs as `null`), `perPage` and
|
|
669
|
+
* `lastPage` are left undefined.
|
|
670
|
+
*
|
|
671
|
+
* Key paths are resolved through `DrfResponseOptions`, which defaults
|
|
672
|
+
* `data → 'results'`, `total → 'count'`, `nextPageUrl → 'next'`,
|
|
673
|
+
* `prevPageUrl → 'previous'`. The current-page / per-page / last-page
|
|
674
|
+
* paths default to empty strings — the strategy ignores `options` for
|
|
675
|
+
* those slots and uses URL inspection instead.
|
|
676
|
+
*
|
|
677
|
+
* @see https://www.django-rest-framework.org/api-guide/pagination/#pagenumberpagination
|
|
678
|
+
*/
|
|
679
|
+
class DrfResponseStrategy {
|
|
680
|
+
/**
|
|
681
|
+
* Parse a DRF pagination response into a PaginatedCollection
|
|
682
|
+
*
|
|
683
|
+
* @param response - The raw API response body
|
|
684
|
+
* @param options - The response key name configuration
|
|
685
|
+
* @returns A typed PaginatedCollection instance
|
|
686
|
+
*/
|
|
687
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
688
|
+
paginate(response, options) {
|
|
689
|
+
const data = response[options.data];
|
|
690
|
+
const total = response[options.total];
|
|
691
|
+
const prevPageUrl = (response[options.prevPageUrl] ?? null);
|
|
692
|
+
const nextPageUrl = (response[options.nextPageUrl] ?? null);
|
|
693
|
+
const currentPage = this._deriveCurrentPage(prevPageUrl);
|
|
694
|
+
const perPage = this._derivePerPage(nextPageUrl, prevPageUrl);
|
|
695
|
+
const lastPage = this._deriveLastPage(total, perPage);
|
|
696
|
+
const from = this._deriveFrom(currentPage, perPage);
|
|
697
|
+
const to = this._deriveTo(currentPage, perPage, total);
|
|
698
|
+
return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl ?? undefined, nextPageUrl ?? undefined, lastPage, undefined, undefined);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Derive the current page number from the `previous` URL
|
|
702
|
+
*
|
|
703
|
+
* - `null` → page 1
|
|
704
|
+
* - URL without `?page=N` → page 2 (DRF omits `page=1` from URLs)
|
|
705
|
+
* - URL with `?page=N` → N + 1
|
|
706
|
+
*
|
|
707
|
+
* @param prevPageUrl - The `previous` link from the DRF response, or null
|
|
708
|
+
* @returns The current page number
|
|
709
|
+
*/
|
|
710
|
+
_deriveCurrentPage(prevPageUrl) {
|
|
711
|
+
if (prevPageUrl === null) {
|
|
712
|
+
return 1;
|
|
713
|
+
}
|
|
714
|
+
const prevPage = this._extractPageParam(prevPageUrl);
|
|
715
|
+
return prevPage === undefined ? 2 : prevPage + 1;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Derive `from` as the 1-indexed offset of the first item on this page
|
|
719
|
+
*
|
|
720
|
+
* @param currentPage - The current page number
|
|
721
|
+
* @param perPage - The page size (may be undefined)
|
|
722
|
+
* @returns The 1-indexed `from` index, or undefined when perPage is unknown
|
|
723
|
+
*/
|
|
724
|
+
_deriveFrom(currentPage, perPage) {
|
|
725
|
+
if (!perPage) {
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
return (currentPage - 1) * perPage + 1;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Derive the last page number as `ceil(total / perPage)`
|
|
732
|
+
*
|
|
733
|
+
* Both inputs must be defined; an empty result set (`total === 0`)
|
|
734
|
+
* yields `lastPage = 0` which the caller treats as "no useful info"
|
|
735
|
+
* and skips the sync to `NestService.lastPage`.
|
|
736
|
+
*
|
|
737
|
+
* @param total - The total item count
|
|
738
|
+
* @param perPage - The page size
|
|
739
|
+
* @returns The last page number, or undefined when either input is missing
|
|
740
|
+
*/
|
|
741
|
+
_deriveLastPage(total, perPage) {
|
|
742
|
+
if (total === undefined || perPage === undefined || perPage <= 0) {
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
return Math.ceil(total / perPage);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Derive `perPage` by parsing `?page_size=N` from any available URL
|
|
749
|
+
*
|
|
750
|
+
* Tries `next` first (page 1 always has a `next` URL with `page_size`
|
|
751
|
+
* if any non-default size was requested), then falls back to
|
|
752
|
+
* `previous`. Returns undefined when neither URL contains the param —
|
|
753
|
+
* the consumer is then on a single-page result with the server's
|
|
754
|
+
* default page size, which is not introspectable from the body alone.
|
|
755
|
+
*
|
|
756
|
+
* @param nextPageUrl - The `next` link from the response, or null
|
|
757
|
+
* @param prevPageUrl - The `previous` link from the response, or null
|
|
758
|
+
* @returns The page size, or undefined
|
|
759
|
+
*/
|
|
760
|
+
_derivePerPage(nextPageUrl, prevPageUrl) {
|
|
761
|
+
return this._extractPageSizeParam(nextPageUrl) ?? this._extractPageSizeParam(prevPageUrl);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Derive `to` as the 1-indexed offset of the last item on this page
|
|
765
|
+
*
|
|
766
|
+
* Clamped to `total` so the last page does not report past the end.
|
|
767
|
+
*
|
|
768
|
+
* @param currentPage - The current page number
|
|
769
|
+
* @param perPage - The page size (may be undefined)
|
|
770
|
+
* @param total - The total item count (may be undefined)
|
|
771
|
+
* @returns The 1-indexed `to` index, or undefined when inputs insufficient
|
|
772
|
+
*/
|
|
773
|
+
_deriveTo(currentPage, perPage, total) {
|
|
774
|
+
if (perPage === undefined || total === undefined) {
|
|
775
|
+
return undefined;
|
|
776
|
+
}
|
|
777
|
+
return Math.min(currentPage * perPage, total);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Extract the `page` query parameter from a DRF pagination URL
|
|
781
|
+
*
|
|
782
|
+
* Returns the integer value of `?page=N`, or undefined when the URL is
|
|
783
|
+
* malformed or has no `page` param (which, by DRF convention, means
|
|
784
|
+
* page 1 — the caller infers the semantics).
|
|
785
|
+
*
|
|
786
|
+
* @param url - The URL to parse
|
|
787
|
+
* @returns The integer page value, or undefined
|
|
788
|
+
*/
|
|
789
|
+
_extractPageParam(url) {
|
|
790
|
+
const raw = this._extractQueryParam(url, 'page');
|
|
791
|
+
if (raw === undefined) {
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
794
|
+
const parsed = Number.parseInt(raw, 10);
|
|
795
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Extract the `page_size` query parameter from a DRF pagination URL
|
|
799
|
+
*
|
|
800
|
+
* @param url - The URL to parse (or null)
|
|
801
|
+
* @returns The integer page-size value, or undefined
|
|
802
|
+
*/
|
|
803
|
+
_extractPageSizeParam(url) {
|
|
804
|
+
if (url === null) {
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
807
|
+
const raw = this._extractQueryParam(url, 'page_size');
|
|
808
|
+
if (raw === undefined) {
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
const parsed = Number.parseInt(raw, 10);
|
|
812
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Extract a single query parameter from a URL via the WHATWG URL parser
|
|
816
|
+
*
|
|
817
|
+
* Returns undefined when the URL is unparseable (relative URL without a
|
|
818
|
+
* base, or malformed) or when the parameter is absent.
|
|
819
|
+
*
|
|
820
|
+
* @param url - The URL to parse
|
|
821
|
+
* @param name - The query-parameter name to look up
|
|
822
|
+
* @returns The raw string value of the parameter, or undefined
|
|
823
|
+
*/
|
|
824
|
+
_extractQueryParam(url, name) {
|
|
825
|
+
try {
|
|
826
|
+
const parsed = new URL(url);
|
|
827
|
+
const value = parsed.searchParams.get(name);
|
|
828
|
+
return value === null ? undefined : value;
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
return undefined;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
class UnselectableModelError extends Error {
|
|
837
|
+
constructor(model) {
|
|
838
|
+
super(`Unselectable Model: the selected model (${model}) is not present neither in the "model" property, nor in the includes object.`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
293
842
|
/**
|
|
294
843
|
* Request strategy for the JSON:API driver
|
|
295
844
|
*
|
|
@@ -598,6 +1147,39 @@ class LaravelRequestStrategy extends AbstractRequestStrategy {
|
|
|
598
1147
|
}
|
|
599
1148
|
}
|
|
600
1149
|
|
|
1150
|
+
/**
|
|
1151
|
+
* Base class for response strategies whose pagination metadata is a flat
|
|
1152
|
+
* key-value envelope on the response body
|
|
1153
|
+
*
|
|
1154
|
+
* Laravel's stock pagination and Spatie's `QueryBuilder` both emit the
|
|
1155
|
+
* same flat shape — `{ data, current_page, total, per_page, from, to,
|
|
1156
|
+
* next_page_url, prev_page_url, first_page_url, last_page, last_page_url
|
|
1157
|
+
* }` — and both response strategies were duplicating the byte-identical
|
|
1158
|
+
* `new PaginatedCollection(response[options.X], ...)` body before this
|
|
1159
|
+
* base existed. Concrete classes now extend and provide only the
|
|
1160
|
+
* docstring describing their driver's specific shape (see
|
|
1161
|
+
* `LaravelResponseStrategy`, `SpatieResponseStrategy`).
|
|
1162
|
+
*
|
|
1163
|
+
* Drivers whose pagination metadata is a nested envelope (JSON:API,
|
|
1164
|
+
* NestJS, Strapi) extend `AbstractDotPathResponseStrategy` instead.
|
|
1165
|
+
* Drivers whose metadata comes from HTTP headers (PostgREST) or is
|
|
1166
|
+
* derived from response URLs (DRF) implement `IResponseStrategy`
|
|
1167
|
+
* directly.
|
|
1168
|
+
*/
|
|
1169
|
+
class AbstractFlatResponseStrategy {
|
|
1170
|
+
/**
|
|
1171
|
+
* Parse a flat-envelope pagination response into a PaginatedCollection
|
|
1172
|
+
*
|
|
1173
|
+
* @param response - The raw API response object
|
|
1174
|
+
* @param options - The response key name configuration
|
|
1175
|
+
* @returns A typed PaginatedCollection instance
|
|
1176
|
+
*/
|
|
1177
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1178
|
+
paginate(response, options) {
|
|
1179
|
+
return new PaginatedCollection(response[options.data], response[options.currentPage], response[options.from], response[options.to], response[options.total], response[options.perPage], response[options.prevPageUrl], response[options.nextPageUrl], response[options.lastPage], response[options.firstPageUrl], response[options.lastPageUrl]);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
601
1183
|
/**
|
|
602
1184
|
* Response strategy for the Laravel (pagination-only) driver
|
|
603
1185
|
*
|
|
@@ -610,22 +1192,20 @@ class LaravelRequestStrategy extends AbstractRequestStrategy {
|
|
|
610
1192
|
* "per_page": 15,
|
|
611
1193
|
* "from": 1,
|
|
612
1194
|
* "to": 15,
|
|
613
|
-
* ...
|
|
1195
|
+
* "next_page_url": "...",
|
|
1196
|
+
* "prev_page_url": "...",
|
|
1197
|
+
* "first_page_url": "...",
|
|
1198
|
+
* "last_page": 7,
|
|
1199
|
+
* "last_page_url": "..."
|
|
614
1200
|
* }
|
|
615
1201
|
* ```
|
|
1202
|
+
*
|
|
1203
|
+
* The traversal algorithm (flat `response[options.X]` lookups) is
|
|
1204
|
+
* inherited from `AbstractFlatResponseStrategy`; this class exists so
|
|
1205
|
+
* `DriverEnum.LARAVEL` resolves to a distinct identity at the DI layer
|
|
1206
|
+
* even though the parsing logic is shared with Spatie.
|
|
616
1207
|
*/
|
|
617
|
-
class LaravelResponseStrategy {
|
|
618
|
-
/**
|
|
619
|
-
* Parse a flat Laravel pagination response into a PaginatedCollection
|
|
620
|
-
*
|
|
621
|
-
* @param response - The raw API response object
|
|
622
|
-
* @param options - The response key name configuration
|
|
623
|
-
* @returns A typed PaginatedCollection instance
|
|
624
|
-
*/
|
|
625
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
626
|
-
paginate(response, options) {
|
|
627
|
-
return new PaginatedCollection(response[options.data], response[options.currentPage], response[options.from], response[options.to], response[options.total], response[options.perPage], response[options.prevPageUrl], response[options.nextPageUrl], response[options.lastPage], response[options.firstPageUrl], response[options.lastPageUrl]);
|
|
628
|
-
}
|
|
1208
|
+
class LaravelResponseStrategy extends AbstractFlatResponseStrategy {
|
|
629
1209
|
}
|
|
630
1210
|
|
|
631
1211
|
/**
|
|
@@ -820,43 +1400,6 @@ class NestjsRequestStrategy extends AbstractRequestStrategy {
|
|
|
820
1400
|
class NestjsResponseStrategy extends AbstractDotPathResponseStrategy {
|
|
821
1401
|
}
|
|
822
1402
|
|
|
823
|
-
/**
|
|
824
|
-
* Enum representing the available filter operators for explicit operator
|
|
825
|
-
* filters
|
|
826
|
-
*
|
|
827
|
-
* NestJS encodes these with the `$` prefix at the wire level
|
|
828
|
-
* (`filter.field=$operator:value`); PostgREST translates them to its own
|
|
829
|
-
* prefix notation (`col=eq.val`, `col=is.null`, etc.). The enum values are
|
|
830
|
-
* intentionally the NestJS form; each driver's request strategy is
|
|
831
|
-
* responsible for mapping them into its own shape.
|
|
832
|
-
*
|
|
833
|
-
* `FTS`, `PLFTS`, `PHFTS`, `WFTS` are PostgREST-native full-text search
|
|
834
|
-
* variants; they throw `UnsupportedFilterOperatorError` on every other
|
|
835
|
-
* driver that does not recognise them.
|
|
836
|
-
*
|
|
837
|
-
* @see https://github.com/ppetzold/nestjs-paginate
|
|
838
|
-
* @see https://postgrest.org/en/stable/api.html#operators
|
|
839
|
-
*/
|
|
840
|
-
var FilterOperatorEnum;
|
|
841
|
-
(function (FilterOperatorEnum) {
|
|
842
|
-
FilterOperatorEnum["BTW"] = "$btw";
|
|
843
|
-
FilterOperatorEnum["CONTAINS"] = "$contains";
|
|
844
|
-
FilterOperatorEnum["EQ"] = "$eq";
|
|
845
|
-
FilterOperatorEnum["FTS"] = "$fts";
|
|
846
|
-
FilterOperatorEnum["GT"] = "$gt";
|
|
847
|
-
FilterOperatorEnum["GTE"] = "$gte";
|
|
848
|
-
FilterOperatorEnum["ILIKE"] = "$ilike";
|
|
849
|
-
FilterOperatorEnum["IN"] = "$in";
|
|
850
|
-
FilterOperatorEnum["LT"] = "$lt";
|
|
851
|
-
FilterOperatorEnum["LTE"] = "$lte";
|
|
852
|
-
FilterOperatorEnum["NOT"] = "$not";
|
|
853
|
-
FilterOperatorEnum["NULL"] = "$null";
|
|
854
|
-
FilterOperatorEnum["PHFTS"] = "$phfts";
|
|
855
|
-
FilterOperatorEnum["PLFTS"] = "$plfts";
|
|
856
|
-
FilterOperatorEnum["SW"] = "$sw";
|
|
857
|
-
FilterOperatorEnum["WFTS"] = "$wfts";
|
|
858
|
-
})(FilterOperatorEnum || (FilterOperatorEnum = {}));
|
|
859
|
-
|
|
860
1403
|
/**
|
|
861
1404
|
* Enum representing the wire-level pagination mechanism
|
|
862
1405
|
*
|
|
@@ -875,32 +1418,6 @@ var PaginationModeEnum;
|
|
|
875
1418
|
PaginationModeEnum["RANGE"] = "range";
|
|
876
1419
|
})(PaginationModeEnum || (PaginationModeEnum = {}));
|
|
877
1420
|
|
|
878
|
-
/**
|
|
879
|
-
* Thrown when a filter operator receives a value array of the wrong shape
|
|
880
|
-
*
|
|
881
|
-
* Some operators have arity or type constraints that the library enforces
|
|
882
|
-
* at call time so misuse fails loudly instead of silently emitting invalid
|
|
883
|
-
* server requests:
|
|
884
|
-
*
|
|
885
|
-
* - `BTW` requires exactly two values (min, max).
|
|
886
|
-
* - `NULL` requires exactly one boolean value (`true` for `IS NULL`,
|
|
887
|
-
* `false` for `IS NOT NULL`).
|
|
888
|
-
*
|
|
889
|
-
* Operators with looser shape rules leave validation to the server; this
|
|
890
|
-
* error is reserved for cases where the library itself can detect the
|
|
891
|
-
* problem unambiguously from the call site.
|
|
892
|
-
*/
|
|
893
|
-
class InvalidFilterOperatorValueError extends Error {
|
|
894
|
-
/**
|
|
895
|
-
* @param operator - The operator that rejected the values
|
|
896
|
-
* @param reason - Short human-readable explanation of the constraint
|
|
897
|
-
*/
|
|
898
|
-
constructor(operator, reason) {
|
|
899
|
-
super(`Invalid values for filter operator ${operator}: ${reason}`);
|
|
900
|
-
this.name = 'InvalidFilterOperatorValueError';
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
1421
|
/**
|
|
905
1422
|
* Request strategy for the PostgREST driver
|
|
906
1423
|
*
|
|
@@ -1415,7 +1932,8 @@ class SpatieRequestStrategy extends AbstractRequestStrategy {
|
|
|
1415
1932
|
/**
|
|
1416
1933
|
* Response strategy for the Spatie Query Builder driver
|
|
1417
1934
|
*
|
|
1418
|
-
* Parses flat Laravel pagination responses
|
|
1935
|
+
* Parses flat Laravel-style pagination responses (Spatie's Query Builder
|
|
1936
|
+
* is built on Laravel's pagination):
|
|
1419
1937
|
* ```json
|
|
1420
1938
|
* {
|
|
1421
1939
|
* "data": [...],
|
|
@@ -1428,20 +1946,283 @@ class SpatieRequestStrategy extends AbstractRequestStrategy {
|
|
|
1428
1946
|
* }
|
|
1429
1947
|
* ```
|
|
1430
1948
|
*
|
|
1949
|
+
* The traversal algorithm (flat `response[options.X]` lookups) is
|
|
1950
|
+
* inherited from `AbstractFlatResponseStrategy`; this class exists so
|
|
1951
|
+
* `DriverEnum.SPATIE` resolves to a distinct identity at the DI layer
|
|
1952
|
+
* even though the parsing logic is shared with the plain Laravel driver.
|
|
1953
|
+
*
|
|
1431
1954
|
* @see https://spatie.be/docs/laravel-query-builder
|
|
1432
1955
|
*/
|
|
1433
|
-
class SpatieResponseStrategy {
|
|
1956
|
+
class SpatieResponseStrategy extends AbstractFlatResponseStrategy {
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* Request strategy for the Strapi driver
|
|
1961
|
+
*
|
|
1962
|
+
* Generates URIs in [Strapi's filter API format](https://docs.strapi.io/dev-docs/api/rest/filters-locale-publication):
|
|
1963
|
+
* - Filters: `filters[field][$eq]=value` (multi-value collapses to `$in`)
|
|
1964
|
+
* - Operator filters: `filters[field][$op]=value` (translated from
|
|
1965
|
+
* `FilterOperatorEnum` — `BTW`→`$between`, `SW`→`$startsWith`,
|
|
1966
|
+
* `ILIKE`→`$containsi`, `NOT`→`$ne`/`$notIn`,
|
|
1967
|
+
* `NULL`→`$null`/`$notNull`)
|
|
1968
|
+
* - Sorts: `sort[0]=field:asc&sort[1]=field:desc`
|
|
1969
|
+
* - Field selection (flat): `fields[0]=col1&fields[1]=col2`
|
|
1970
|
+
* - Population: `populate[0]=relation`
|
|
1971
|
+
* - Pagination (page-based): `pagination[page]=N&pagination[pageSize]=N`
|
|
1972
|
+
*
|
|
1973
|
+
* Strapi-native full-text search (`FTS`, `PHFTS`, `PLFTS`, `WFTS`) is
|
|
1974
|
+
* PostgREST-only and throws `UnsupportedFilterOperatorError` here.
|
|
1975
|
+
*
|
|
1976
|
+
* @see https://docs.strapi.io/dev-docs/api/rest/filters-locale-publication
|
|
1977
|
+
*/
|
|
1978
|
+
class StrapiRequestStrategy extends AbstractRequestStrategy {
|
|
1979
|
+
/**
|
|
1980
|
+
* Filters, operator filters, sorts, populate (`includes`), flat field
|
|
1981
|
+
* selection (`select`) — no per-model fields, no global search (use
|
|
1982
|
+
* `$contains` / `$containsi` operator filters instead)
|
|
1983
|
+
*/
|
|
1984
|
+
capabilities = {
|
|
1985
|
+
fields: false,
|
|
1986
|
+
filters: true,
|
|
1987
|
+
includes: true,
|
|
1988
|
+
operatorFilters: true,
|
|
1989
|
+
search: false,
|
|
1990
|
+
select: true,
|
|
1991
|
+
sort: true
|
|
1992
|
+
};
|
|
1434
1993
|
/**
|
|
1435
|
-
*
|
|
1994
|
+
* Strapi-native names of the four hardcoded query keys
|
|
1436
1995
|
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1996
|
+
* Strapi's wire format is fixed (the server reads `pagination[page]`,
|
|
1997
|
+
* `populate`, `sort`, `fields`); these keys are intentionally not
|
|
1998
|
+
* configurable through `QueryBuilderOptions` and live as private
|
|
1999
|
+
* statics so they are visible in one place.
|
|
1440
2000
|
*/
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
2001
|
+
static _fieldsKey = 'fields';
|
|
2002
|
+
static _paginationKey = 'pagination';
|
|
2003
|
+
static _populateKey = 'populate';
|
|
2004
|
+
static _sortKey = 'sort';
|
|
2005
|
+
/**
|
|
2006
|
+
* Emit Strapi-format query-string segments in canonical order:
|
|
2007
|
+
* populate → fields → filters (merged) → sort → pagination
|
|
2008
|
+
*
|
|
2009
|
+
* Simple filters and operator filters share a single `filters` wrapper
|
|
2010
|
+
* so qs emits one ordered, deeply-nested bracket structure rather than
|
|
2011
|
+
* two duplicate top-level `filters[...]` blocks.
|
|
2012
|
+
*
|
|
2013
|
+
* @param state - The current query builder state
|
|
2014
|
+
* @param _options - The query parameter key name configuration (unused;
|
|
2015
|
+
* Strapi's wire keys are fixed by the server)
|
|
2016
|
+
* @returns Ordered query-string fragments
|
|
2017
|
+
*/
|
|
2018
|
+
parts(state, _options) {
|
|
2019
|
+
const out = [];
|
|
2020
|
+
this._appendPopulate(state, out);
|
|
2021
|
+
this._appendFields(state, out);
|
|
2022
|
+
this._appendFilters(state, out);
|
|
2023
|
+
this._appendSort(state, out);
|
|
2024
|
+
this._appendPagination(state, out);
|
|
2025
|
+
return out;
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Append `fields[0]=col1&fields[1]=col2` from the flat select array
|
|
2029
|
+
*
|
|
2030
|
+
* Strapi's `fields` parameter is the column-pruner for the main
|
|
2031
|
+
* resource; per-relation field selection is expressed through the
|
|
2032
|
+
* `populate` deep syntax (out of scope for this driver).
|
|
2033
|
+
*
|
|
2034
|
+
* @param state - The current query builder state
|
|
2035
|
+
* @param out - The accumulator the caller joins into the URI
|
|
2036
|
+
*/
|
|
2037
|
+
_appendFields(state, out) {
|
|
2038
|
+
if (!state.select.length) {
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
out.push(qs.stringify({ [StrapiRequestStrategy._fieldsKey]: state.select }, { encode: false }));
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Append the unified `filters[...]` wrapper combining simple filters
|
|
2045
|
+
* and operator filters
|
|
2046
|
+
*
|
|
2047
|
+
* Both kinds emit into the same nested object under `filters` so qs
|
|
2048
|
+
* produces a single deeply-bracketed block per request. Simple
|
|
2049
|
+
* single-value filters fold to `$eq`; simple multi-value filters fold
|
|
2050
|
+
* to `$in`. Operator filters then merge into the same per-field map,
|
|
2051
|
+
* potentially co-existing with a simple filter on the same field.
|
|
2052
|
+
*
|
|
2053
|
+
* @param state - The current query builder state
|
|
2054
|
+
* @param out - The accumulator the caller joins into the URI
|
|
2055
|
+
*/
|
|
2056
|
+
_appendFilters(state, out) {
|
|
2057
|
+
const simpleKeys = Object.keys(state.filters);
|
|
2058
|
+
if (!simpleKeys.length && !state.operatorFilters.length) {
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
const filters = {};
|
|
2062
|
+
simpleKeys.forEach(key => {
|
|
2063
|
+
const values = state.filters[key];
|
|
2064
|
+
if (!values.length) {
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
filters[key] = values.length === 1
|
|
2068
|
+
? { $eq: values[0] }
|
|
2069
|
+
: { $in: values };
|
|
2070
|
+
});
|
|
2071
|
+
state.operatorFilters.forEach((filter) => {
|
|
2072
|
+
const payload = this._formatOperatorPayload(filter);
|
|
2073
|
+
filters[filter.field] = {
|
|
2074
|
+
...(filters[filter.field] ?? {}),
|
|
2075
|
+
...payload
|
|
2076
|
+
};
|
|
2077
|
+
});
|
|
2078
|
+
if (!Object.keys(filters).length) {
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
out.push(qs.stringify({ filters }, { encode: false }));
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Append the `pagination[page]` / `pagination[pageSize]` wrapper
|
|
2085
|
+
*
|
|
2086
|
+
* Page-based mode is the Strapi default; offset-based
|
|
2087
|
+
* (`pagination[start]` / `pagination[limit]`) is out of scope for this
|
|
2088
|
+
* driver until cursor/offset pagination lands library-wide.
|
|
2089
|
+
*
|
|
2090
|
+
* @param state - The current query builder state
|
|
2091
|
+
* @param out - The accumulator the caller joins into the URI
|
|
2092
|
+
*/
|
|
2093
|
+
_appendPagination(state, out) {
|
|
2094
|
+
const wrapper = {
|
|
2095
|
+
[StrapiRequestStrategy._paginationKey]: {
|
|
2096
|
+
page: state.page,
|
|
2097
|
+
pageSize: state.limit
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
out.push(qs.stringify(wrapper, { encode: false }));
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Append the `populate` parameter from the includes array
|
|
2104
|
+
*
|
|
2105
|
+
* Emits `populate[0]=relation1&populate[1]=relation2`; deep-populate
|
|
2106
|
+
* syntax (`populate[author][fields][0]=name`) is not exposed through
|
|
2107
|
+
* the current state shape.
|
|
2108
|
+
*
|
|
2109
|
+
* @param state - The current query builder state
|
|
2110
|
+
* @param out - The accumulator the caller joins into the URI
|
|
2111
|
+
*/
|
|
2112
|
+
_appendPopulate(state, out) {
|
|
2113
|
+
if (!state.includes.length) {
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
out.push(qs.stringify({ [StrapiRequestStrategy._populateKey]: state.includes }, { encode: false }));
|
|
1444
2117
|
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Append the `sort[N]=field:dir` array
|
|
2120
|
+
*
|
|
2121
|
+
* @param state - The current query builder state
|
|
2122
|
+
* @param out - The accumulator the caller joins into the URI
|
|
2123
|
+
*/
|
|
2124
|
+
_appendSort(state, out) {
|
|
2125
|
+
if (!state.sorts.length) {
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
const pairs = state.sorts.map(sort => `${sort.field}:${sort.order === SortEnum.DESC ? 'desc' : 'asc'}`);
|
|
2129
|
+
out.push(qs.stringify({ [StrapiRequestStrategy._sortKey]: pairs }, { encode: false }));
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Translate a `FilterOperatorEnum` operator filter into Strapi's
|
|
2133
|
+
* `$operator → value` payload shape
|
|
2134
|
+
*
|
|
2135
|
+
* The mapping is library-canonical → Strapi-native:
|
|
2136
|
+
* - `EQ`/`GT`/`GTE`/`LT`/`LTE`/`CONTAINS` → identity (same key name)
|
|
2137
|
+
* - `ILIKE` → `$containsi` (case-insensitive contains)
|
|
2138
|
+
* - `IN` → `$in` (array)
|
|
2139
|
+
* - `SW` → `$startsWith`
|
|
2140
|
+
* - `BTW` → `$between` with `[min, max]` (arity-checked)
|
|
2141
|
+
* - `NOT` → `$ne` (single value) / `$notIn` (multi-value)
|
|
2142
|
+
* - `NULL` → `$null=true` (when value is `true`) / `$notNull=true`
|
|
2143
|
+
* (when value is `false`); arity- and type-checked
|
|
2144
|
+
*
|
|
2145
|
+
* PostgREST's full-text-search operators (`FTS`, `PHFTS`, `PLFTS`,
|
|
2146
|
+
* `WFTS`) have no Strapi equivalent and throw
|
|
2147
|
+
* `UnsupportedFilterOperatorError`.
|
|
2148
|
+
*
|
|
2149
|
+
* @param filter - The operator filter to translate
|
|
2150
|
+
* @returns A `{ $operator: value }` payload ready to merge under
|
|
2151
|
+
* `filters[field]`
|
|
2152
|
+
* @throws {InvalidFilterOperatorValueError} If `BTW` does not receive
|
|
2153
|
+
* exactly two values, or `NULL` does not receive exactly one boolean
|
|
2154
|
+
* @throws {UnsupportedFilterOperatorError} If the operator is a
|
|
2155
|
+
* PostgREST-only FTS variant
|
|
2156
|
+
*/
|
|
2157
|
+
_formatOperatorPayload(filter) {
|
|
2158
|
+
const { operator, values } = filter;
|
|
2159
|
+
const first = values[0];
|
|
2160
|
+
switch (operator) {
|
|
2161
|
+
case FilterOperatorEnum.EQ: return { $eq: first };
|
|
2162
|
+
case FilterOperatorEnum.GT: return { $gt: first };
|
|
2163
|
+
case FilterOperatorEnum.GTE: return { $gte: first };
|
|
2164
|
+
case FilterOperatorEnum.LT: return { $lt: first };
|
|
2165
|
+
case FilterOperatorEnum.LTE: return { $lte: first };
|
|
2166
|
+
case FilterOperatorEnum.CONTAINS: return { $contains: first };
|
|
2167
|
+
case FilterOperatorEnum.ILIKE: return { $containsi: first };
|
|
2168
|
+
case FilterOperatorEnum.IN: return { $in: values };
|
|
2169
|
+
case FilterOperatorEnum.SW: return { $startsWith: first };
|
|
2170
|
+
case FilterOperatorEnum.BTW: {
|
|
2171
|
+
if (values.length !== 2) {
|
|
2172
|
+
throw new InvalidFilterOperatorValueError(operator, 'BTW requires exactly 2 values (min, max)');
|
|
2173
|
+
}
|
|
2174
|
+
return { $between: values };
|
|
2175
|
+
}
|
|
2176
|
+
case FilterOperatorEnum.NOT:
|
|
2177
|
+
return values.length === 1
|
|
2178
|
+
? { $ne: first }
|
|
2179
|
+
: { $notIn: values };
|
|
2180
|
+
case FilterOperatorEnum.NULL: {
|
|
2181
|
+
if (values.length !== 1 || typeof first !== 'boolean') {
|
|
2182
|
+
throw new InvalidFilterOperatorValueError(operator, 'NULL requires exactly 1 boolean value (true → IS NULL, false → IS NOT NULL)');
|
|
2183
|
+
}
|
|
2184
|
+
return first ? { $null: true } : { $notNull: true };
|
|
2185
|
+
}
|
|
2186
|
+
case FilterOperatorEnum.FTS:
|
|
2187
|
+
case FilterOperatorEnum.PHFTS:
|
|
2188
|
+
case FilterOperatorEnum.PLFTS:
|
|
2189
|
+
case FilterOperatorEnum.WFTS:
|
|
2190
|
+
throw new UnsupportedFilterOperatorError();
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Response strategy for the Strapi driver
|
|
2197
|
+
*
|
|
2198
|
+
* Parses Strapi v4/v5 pagination responses:
|
|
2199
|
+
* ```json
|
|
2200
|
+
* {
|
|
2201
|
+
* "data": [{ "id": 1, "documentId": "abc", "title": "Hello" }],
|
|
2202
|
+
* "meta": {
|
|
2203
|
+
* "pagination": {
|
|
2204
|
+
* "page": 1,
|
|
2205
|
+
* "pageSize": 10,
|
|
2206
|
+
* "pageCount": 5,
|
|
2207
|
+
* "total": 48
|
|
2208
|
+
* }
|
|
2209
|
+
* }
|
|
2210
|
+
* }
|
|
2211
|
+
* ```
|
|
2212
|
+
*
|
|
2213
|
+
* Default key paths are configured in `StrapiResponseOptions`. Strapi
|
|
2214
|
+
* does not include navigation links in the envelope, so `firstPageUrl`,
|
|
2215
|
+
* `prevPageUrl`, `nextPageUrl`, and `lastPageUrl` resolve to `undefined`
|
|
2216
|
+
* unless the consumer overrides their paths via `IPaginationConfig`. The
|
|
2217
|
+
* traversal algorithm (dot-notation resolution + computed `from`/`to`)
|
|
2218
|
+
* is inherited from `AbstractDotPathResponseStrategy`; this class exists
|
|
2219
|
+
* so `DriverEnum.STRAPI` resolves to a distinct identity at the DI
|
|
2220
|
+
* layer even though the parsing logic is shared with JSON:API and
|
|
2221
|
+
* NestJS.
|
|
2222
|
+
*
|
|
2223
|
+
* @see https://docs.strapi.io/dev-docs/api/rest/sort-pagination
|
|
2224
|
+
*/
|
|
2225
|
+
class StrapiResponseStrategy extends AbstractDotPathResponseStrategy {
|
|
1445
2226
|
}
|
|
1446
2227
|
|
|
1447
2228
|
/**
|
|
@@ -1455,6 +2236,11 @@ class SpatieResponseStrategy {
|
|
|
1455
2236
|
* `switch` blocks.
|
|
1456
2237
|
*/
|
|
1457
2238
|
const DRIVERS = {
|
|
2239
|
+
[DriverEnum.DRF]: {
|
|
2240
|
+
createRequestStrategy: () => new DrfRequestStrategy(),
|
|
2241
|
+
createResponseStrategy: () => new DrfResponseStrategy(),
|
|
2242
|
+
createResponseOptions: (config) => new DrfResponseOptions(config)
|
|
2243
|
+
},
|
|
1458
2244
|
[DriverEnum.JSON_API]: {
|
|
1459
2245
|
createRequestStrategy: () => new JsonApiRequestStrategy(),
|
|
1460
2246
|
createResponseStrategy: () => new JsonApiResponseStrategy(),
|
|
@@ -1479,6 +2265,11 @@ const DRIVERS = {
|
|
|
1479
2265
|
createRequestStrategy: () => new SpatieRequestStrategy(),
|
|
1480
2266
|
createResponseStrategy: () => new SpatieResponseStrategy(),
|
|
1481
2267
|
createResponseOptions: (config) => new ResponseOptions(config)
|
|
2268
|
+
},
|
|
2269
|
+
[DriverEnum.STRAPI]: {
|
|
2270
|
+
createRequestStrategy: () => new StrapiRequestStrategy(),
|
|
2271
|
+
createResponseStrategy: () => new StrapiResponseStrategy(),
|
|
2272
|
+
createResponseOptions: (config) => new StrapiResponseOptions(config)
|
|
1482
2273
|
}
|
|
1483
2274
|
};
|
|
1484
2275
|
|
|
@@ -2011,19 +2802,6 @@ class UnsupportedFilterError extends Error {
|
|
|
2011
2802
|
}
|
|
2012
2803
|
}
|
|
2013
2804
|
|
|
2014
|
-
/**
|
|
2015
|
-
* Error thrown when filter operators are attempted with a driver that does not support them
|
|
2016
|
-
*
|
|
2017
|
-
* Filter operators are only supported by the NestJS driver.
|
|
2018
|
-
* Use `addFilter()` for Spatie implicit equality filters.
|
|
2019
|
-
*/
|
|
2020
|
-
class UnsupportedFilterOperatorError extends Error {
|
|
2021
|
-
constructor() {
|
|
2022
|
-
super('Filter operators are only supported by the NestJS driver. Use addFilter() for Spatie.');
|
|
2023
|
-
this.name = 'UnsupportedFilterOperatorError';
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
2805
|
/**
|
|
2028
2806
|
* Error thrown when includes are attempted with a driver that does not support them
|
|
2029
2807
|
*
|
|
@@ -2848,5 +3626,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImpor
|
|
|
2848
3626
|
* Generated bundle index. Do not edit.
|
|
2849
3627
|
*/
|
|
2850
3628
|
|
|
2851
|
-
export { DriverEnum, FilterOperatorEnum, InvalidFilterOperatorValueError, InvalidLimitError, InvalidPageNumberError, InvalidResourceNameError, JsonApiRequestStrategy, JsonApiResponseStrategy, KeyNotFoundError, LaravelRequestStrategy, LaravelResponseStrategy, NG_QUBEE_DRIVER, NG_QUBEE_REQUEST_OPTIONS, NG_QUBEE_REQUEST_STRATEGY, NG_QUBEE_RESPONSE_OPTIONS, NG_QUBEE_RESPONSE_STRATEGY, NestjsRequestStrategy, NestjsResponseStrategy, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationModeEnum, PaginationNotSyncedError, PaginationService, PostgrestRequestStrategy, PostgrestResponseStrategy, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, buildNgQubeeProviders, provideNgQubee, provideNgQubeeInstance, readHeader };
|
|
3629
|
+
export { DriverEnum, FilterOperatorEnum, InvalidFilterOperatorValueError, InvalidLimitError, InvalidPageNumberError, InvalidResourceNameError, JsonApiRequestStrategy, JsonApiResponseStrategy, KeyNotFoundError, LaravelRequestStrategy, LaravelResponseStrategy, NG_QUBEE_DRIVER, NG_QUBEE_REQUEST_OPTIONS, NG_QUBEE_REQUEST_STRATEGY, NG_QUBEE_RESPONSE_OPTIONS, NG_QUBEE_RESPONSE_STRATEGY, NestjsRequestStrategy, NestjsResponseStrategy, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationModeEnum, PaginationNotSyncedError, PaginationService, PostgrestRequestStrategy, PostgrestResponseStrategy, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, StrapiRequestStrategy, StrapiResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, buildNgQubeeProviders, provideNgQubee, provideNgQubeeInstance, readHeader };
|
|
2852
3630
|
//# sourceMappingURL=ng-qubee.mjs.map
|