ng-qubee 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -74,11 +74,15 @@ 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";
81
+ DriverEnum["NESTJSX_CRUD"] = "nestjsx-crud";
80
82
  DriverEnum["POSTGREST"] = "postgrest";
83
+ DriverEnum["SIEVE"] = "sieve";
81
84
  DriverEnum["SPATIE"] = "spatie";
85
+ DriverEnum["SPRING"] = "spring";
82
86
  DriverEnum["STRAPI"] = "strapi";
83
87
  })(DriverEnum || (DriverEnum = {}));
84
88
 
@@ -124,6 +128,35 @@ class ResponseOptions {
124
128
  this.total = options.total || 'total';
125
129
  }
126
130
  }
131
+ /**
132
+ * Pre-configured ResponseOptions for the Django REST Framework (DRF) driver
133
+ *
134
+ * DRF's `PageNumberPagination` envelope is `{ count, next, previous,
135
+ * results }`, with no body field naming the current page, per-page, or
136
+ * last-page. The strategy parses those from the `next` / `previous`
137
+ * URLs, so the corresponding key paths default to empty strings; the
138
+ * strategy ignores `options.currentPage`, `options.perPage`,
139
+ * `options.lastPage`, `options.from`, `options.to`, `options.path`,
140
+ * `options.firstPageUrl`, and `options.lastPageUrl`.
141
+ */
142
+ class DrfResponseOptions extends ResponseOptions {
143
+ constructor(options) {
144
+ super({
145
+ currentPage: options.currentPage || '',
146
+ data: options.data || 'results',
147
+ firstPageUrl: options.firstPageUrl || '',
148
+ from: options.from || '',
149
+ lastPage: options.lastPage || '',
150
+ lastPageUrl: options.lastPageUrl || '',
151
+ nextPageUrl: options.nextPageUrl || 'next',
152
+ path: options.path || '',
153
+ perPage: options.perPage || '',
154
+ prevPageUrl: options.prevPageUrl || 'previous',
155
+ to: options.to || '',
156
+ total: options.total || 'count'
157
+ });
158
+ }
159
+ }
127
160
  /**
128
161
  * Pre-configured ResponseOptions for the JSON:API driver
129
162
  *
@@ -172,6 +205,95 @@ class NestjsResponseOptions extends ResponseOptions {
172
205
  });
173
206
  }
174
207
  }
208
+ /**
209
+ * Pre-configured ResponseOptions for the @nestjsx/crud driver
210
+ *
211
+ * The `getMany` envelope is flat: `{ data, count, total, page,
212
+ * pageCount }`. `perPage` defaults to the `count` field — the number of
213
+ * entities on the **current** page, which equals the requested limit on
214
+ * every page except a partial last one. The envelope carries no
215
+ * `from`/`to` indices and no navigation links, so those paths default to
216
+ * empty strings (the strategy derives `from`/`to` and leaves the URLs
217
+ * `undefined`); consumers can override any path via `IPaginationConfig`.
218
+ */
219
+ class NestjsxCrudResponseOptions extends ResponseOptions {
220
+ constructor(options) {
221
+ super({
222
+ currentPage: options.currentPage || 'page',
223
+ data: options.data || 'data',
224
+ firstPageUrl: options.firstPageUrl || '',
225
+ from: options.from || '',
226
+ lastPage: options.lastPage || 'pageCount',
227
+ lastPageUrl: options.lastPageUrl || '',
228
+ nextPageUrl: options.nextPageUrl || '',
229
+ path: options.path || '',
230
+ perPage: options.perPage || 'count',
231
+ prevPageUrl: options.prevPageUrl || '',
232
+ to: options.to || '',
233
+ total: options.total || 'total'
234
+ });
235
+ }
236
+ }
237
+ /**
238
+ * Pre-configured ResponseOptions for the Sieve (.NET) driver
239
+ *
240
+ * Sieve defines no response envelope (it returns an `IQueryable` the
241
+ * developer wraps), so these defaults target the common hand-rolled
242
+ * `PagedResult<T>` shape: `{ data, page, pageSize, total, totalPages }`.
243
+ * Every path is overridable via `IPaginationConfig` — dot notation is
244
+ * supported, so nested wrappers (`meta.page`, `pagination.total`) map
245
+ * without subclassing. `from`/`to` default to empty paths and are
246
+ * derived; the navigation-URL slots resolve to `undefined` unless paths
247
+ * are provided.
248
+ */
249
+ class SieveResponseOptions extends ResponseOptions {
250
+ constructor(options) {
251
+ super({
252
+ currentPage: options.currentPage || 'page',
253
+ data: options.data || 'data',
254
+ firstPageUrl: options.firstPageUrl || '',
255
+ from: options.from || '',
256
+ lastPage: options.lastPage || 'totalPages',
257
+ lastPageUrl: options.lastPageUrl || '',
258
+ nextPageUrl: options.nextPageUrl || '',
259
+ path: options.path || '',
260
+ perPage: options.perPage || 'pageSize',
261
+ prevPageUrl: options.prevPageUrl || '',
262
+ to: options.to || '',
263
+ total: options.total || 'total'
264
+ });
265
+ }
266
+ }
267
+ /**
268
+ * Pre-configured ResponseOptions for the Spring Data REST driver
269
+ *
270
+ * Uses dot-notation paths into the HAL envelope: pagination metadata
271
+ * lives under `page.*` and navigation links under `_links.*.href`.
272
+ * `currentPage` points at the **0-indexed** `page.number`; the strategy
273
+ * adds 1 when reading it. `data` defaults to plain `_embedded` because
274
+ * the collection key underneath is the resource rel name (e.g.
275
+ * `_embedded.users`) and cannot be known statically — the strategy picks
276
+ * the first array inside; pin an exact path via `IPaginationConfig` when
277
+ * needed. `from`/`to` default to empty paths and are derived.
278
+ */
279
+ class SpringResponseOptions extends ResponseOptions {
280
+ constructor(options) {
281
+ super({
282
+ currentPage: options.currentPage || 'page.number',
283
+ data: options.data || '_embedded',
284
+ firstPageUrl: options.firstPageUrl || '_links.first.href',
285
+ from: options.from || '',
286
+ lastPage: options.lastPage || 'page.totalPages',
287
+ lastPageUrl: options.lastPageUrl || '_links.last.href',
288
+ nextPageUrl: options.nextPageUrl || '_links.next.href',
289
+ path: options.path || '',
290
+ perPage: options.perPage || 'page.size',
291
+ prevPageUrl: options.prevPageUrl || '_links.prev.href',
292
+ to: options.to || '',
293
+ total: options.total || 'page.totalElements'
294
+ });
295
+ }
296
+ }
175
297
  /**
176
298
  * Pre-configured ResponseOptions for the Strapi driver
177
299
  *
@@ -199,15 +321,85 @@ class StrapiResponseOptions extends ResponseOptions {
199
321
  }
200
322
  }
201
323
 
324
+ /**
325
+ * Enum representing the available filter operators for explicit operator
326
+ * filters
327
+ *
328
+ * NestJS encodes these with the `$` prefix at the wire level
329
+ * (`filter.field=$operator:value`); PostgREST translates them to its own
330
+ * prefix notation (`col=eq.val`, `col=is.null`, etc.). The enum values are
331
+ * intentionally the NestJS form; each driver's request strategy is
332
+ * responsible for mapping them into its own shape.
333
+ *
334
+ * `FTS`, `PLFTS`, `PHFTS`, `WFTS` are PostgREST-native full-text search
335
+ * variants; they throw `UnsupportedFilterOperatorError` on every other
336
+ * driver that does not recognise them.
337
+ *
338
+ * @see https://github.com/ppetzold/nestjs-paginate
339
+ * @see https://postgrest.org/en/stable/api.html#operators
340
+ */
341
+ var FilterOperatorEnum;
342
+ (function (FilterOperatorEnum) {
343
+ FilterOperatorEnum["BTW"] = "$btw";
344
+ FilterOperatorEnum["CONTAINS"] = "$contains";
345
+ FilterOperatorEnum["EQ"] = "$eq";
346
+ FilterOperatorEnum["FTS"] = "$fts";
347
+ FilterOperatorEnum["GT"] = "$gt";
348
+ FilterOperatorEnum["GTE"] = "$gte";
349
+ FilterOperatorEnum["ILIKE"] = "$ilike";
350
+ FilterOperatorEnum["IN"] = "$in";
351
+ FilterOperatorEnum["LT"] = "$lt";
352
+ FilterOperatorEnum["LTE"] = "$lte";
353
+ FilterOperatorEnum["NOT"] = "$not";
354
+ FilterOperatorEnum["NULL"] = "$null";
355
+ FilterOperatorEnum["PHFTS"] = "$phfts";
356
+ FilterOperatorEnum["PLFTS"] = "$plfts";
357
+ FilterOperatorEnum["SW"] = "$sw";
358
+ FilterOperatorEnum["WFTS"] = "$wfts";
359
+ })(FilterOperatorEnum || (FilterOperatorEnum = {}));
360
+
202
361
  var SortEnum;
203
362
  (function (SortEnum) {
204
363
  SortEnum["ASC"] = "asc";
205
364
  SortEnum["DESC"] = "desc";
206
365
  })(SortEnum || (SortEnum = {}));
207
366
 
208
- class UnselectableModelError extends Error {
209
- constructor(model) {
210
- super(`Unselectable Model: the selected model (${model}) is not present neither in the "model" property, nor in the includes object.`);
367
+ /**
368
+ * Thrown when a filter operator receives a value array of the wrong shape
369
+ *
370
+ * Some operators have arity or type constraints that the library enforces
371
+ * at call time so misuse fails loudly instead of silently emitting invalid
372
+ * server requests:
373
+ *
374
+ * - `BTW` requires exactly two values (min, max).
375
+ * - `NULL` requires exactly one boolean value (`true` for `IS NULL`,
376
+ * `false` for `IS NOT NULL`).
377
+ *
378
+ * Operators with looser shape rules leave validation to the server; this
379
+ * error is reserved for cases where the library itself can detect the
380
+ * problem unambiguously from the call site.
381
+ */
382
+ class InvalidFilterOperatorValueError extends Error {
383
+ /**
384
+ * @param operator - The operator that rejected the values
385
+ * @param reason - Short human-readable explanation of the constraint
386
+ */
387
+ constructor(operator, reason) {
388
+ super(`Invalid values for filter operator ${operator}: ${reason}`);
389
+ this.name = 'InvalidFilterOperatorValueError';
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Error thrown when filter operators are attempted with a driver that does not support them
395
+ *
396
+ * Filter operators are only supported by the NestJS driver.
397
+ * Use `addFilter()` for Spatie implicit equality filters.
398
+ */
399
+ class UnsupportedFilterOperatorError extends Error {
400
+ constructor() {
401
+ super('Filter operators are only supported by the NestJS driver. Use addFilter() for Spatie.');
402
+ this.name = 'UnsupportedFilterOperatorError';
211
403
  }
212
404
  }
213
405
 
@@ -303,17 +495,439 @@ class AbstractRequestStrategy {
303
495
  return state.baseUrl ? `${state.baseUrl}/${state.resource}` : `/${state.resource}`;
304
496
  }
305
497
  /**
306
- * Glue the base URI and the per-driver query-string segments
498
+ * Glue the base URI and the per-driver query-string segments
499
+ *
500
+ * Returns the bare base when no segments were emitted (e.g. PostgREST
501
+ * in RANGE mode with no filters), otherwise joins with `?` + `&`.
502
+ *
503
+ * @param base - The base URI from `_baseUri`
504
+ * @param segments - The query-string fragments from `parts(...)`
505
+ * @returns The full URI
506
+ */
507
+ join(base, segments) {
508
+ return segments.length ? `${base}?${segments.join('&')}` : base;
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Request strategy for the Django REST Framework (DRF) driver
514
+ *
515
+ * Generates URIs in DRF's flat query-parameter format, augmented by
516
+ * django-filter's double-underscore lookup convention:
517
+ *
518
+ * - Simple filters: `field=value` (multi-value collapses to `field__in=v1,v2`)
519
+ * - Operator filters: `field__lookup=value` (translated from
520
+ * `FilterOperatorEnum` — `GTE`→`__gte`, `ILIKE`→`__icontains`,
521
+ * `BTW`→`__range`, `NULL`→`__isnull`, etc.)
522
+ * - Ordering: `ordering=-field1,field2` (`-` prefix = DESC)
523
+ * - Search: `search=term` (DRF's SearchFilter)
524
+ * - Pagination: `page=N&page_size=M` (PageNumberPagination)
525
+ *
526
+ * `ordering` and `page_size` are DRF-idiomatic param names and are
527
+ * intentionally not configurable via `QueryBuilderOptions` — same
528
+ * precedent as PostgREST's `order` and `offset`. PostgREST's full-text
529
+ * search operators (`FTS`, `PHFTS`, `PLFTS`, `WFTS`) and the generic
530
+ * `NOT` have no django-filter equivalent and throw
531
+ * `UnsupportedFilterOperatorError`.
532
+ *
533
+ * @see https://www.django-rest-framework.org/api-guide/filtering/
534
+ * @see https://django-filter.readthedocs.io/
535
+ */
536
+ class DrfRequestStrategy extends AbstractRequestStrategy {
537
+ /**
538
+ * Simple filters, operator filters (django-filter lookups), sorts, and
539
+ * global search — no per-model fields, no relation includes, no flat
540
+ * select (django-restql adds it but is not core DRF)
541
+ */
542
+ capabilities = {
543
+ fields: false,
544
+ filters: true,
545
+ includes: false,
546
+ operatorFilters: true,
547
+ search: true,
548
+ select: false,
549
+ sort: true
550
+ };
551
+ /**
552
+ * DRF-native names of the three hardcoded query keys
553
+ *
554
+ * `ordering` and `page_size` are DRF/django-filter conventions and are
555
+ * intentionally not configurable through `QueryBuilderOptions`. `page`
556
+ * matches the default `QueryBuilderOptions.page`, and `search` matches
557
+ * the default `QueryBuilderOptions.search`, so those flow through the
558
+ * shared options object.
559
+ */
560
+ static _orderingKey = 'ordering';
561
+ static _pageSizeKey = 'page_size';
562
+ /**
563
+ * Emit DRF-format query-string segments in canonical order:
564
+ * filters → operator filters → ordering → search → pagination
565
+ *
566
+ * @param state - The current query builder state
567
+ * @param options - The query parameter key name configuration
568
+ * @returns Ordered query-string fragments
569
+ */
570
+ parts(state, options) {
571
+ const out = [];
572
+ this._appendFilters(state, out);
573
+ this._appendOperatorFilters(state, out);
574
+ this._appendOrdering(state, out);
575
+ this._appendSearch(state, options, out);
576
+ this._appendPagination(state, options, out);
577
+ return out;
578
+ }
579
+ /**
580
+ * Append simple filter parameters
581
+ *
582
+ * Single-value filters emit `field=value` (django-filter's default
583
+ * exact match). Multi-value filters collapse to django-filter's
584
+ * `field__in=v1,v2,v3` form, which is the idiomatic way to express
585
+ * "value in list" in DRF.
586
+ *
587
+ * @param state - The current query builder state
588
+ * @param out - The accumulator the caller joins into the URI
589
+ */
590
+ _appendFilters(state, out) {
591
+ const keys = Object.keys(state.filters);
592
+ if (!keys.length) {
593
+ return;
594
+ }
595
+ keys.forEach(key => {
596
+ const values = state.filters[key];
597
+ if (!values.length) {
598
+ return;
599
+ }
600
+ if (values.length === 1) {
601
+ out.push(`${key}=${values[0]}`);
602
+ return;
603
+ }
604
+ out.push(`${key}__in=${values.join(',')}`);
605
+ });
606
+ }
607
+ /**
608
+ * Append operator-filter parameters as `field__lookup=value`
609
+ *
610
+ * Maps each `FilterOperatorEnum` value to a django-filter lookup
611
+ * suffix. `BTW` expands to `field__range=min,max`; `NULL` emits
612
+ * `field__isnull=true|false`; the generic `NOT` and PostgREST-only
613
+ * FTS operators are unsupported.
614
+ *
615
+ * @param state - The current query builder state
616
+ * @param out - The accumulator the caller joins into the URI
617
+ * @throws {InvalidFilterOperatorValueError} If `BTW` does not receive
618
+ * exactly two values, or `NULL` does not receive exactly one boolean
619
+ * @throws {UnsupportedFilterOperatorError} If the operator has no
620
+ * django-filter equivalent
621
+ */
622
+ _appendOperatorFilters(state, out) {
623
+ if (!state.operatorFilters.length) {
624
+ return;
625
+ }
626
+ state.operatorFilters.forEach((filter) => {
627
+ const [suffix, value] = this._formatOperatorPayload(filter);
628
+ const key = suffix ? `${filter.field}__${suffix}` : filter.field;
629
+ out.push(`${key}=${value}`);
630
+ });
631
+ }
632
+ /**
633
+ * Append `ordering=-field1,field2` (django's `-` prefix = DESC)
634
+ *
635
+ * @param state - The current query builder state
636
+ * @param out - The accumulator the caller joins into the URI
637
+ */
638
+ _appendOrdering(state, out) {
639
+ if (!state.sorts.length) {
640
+ return;
641
+ }
642
+ const pairs = state.sorts.map(sort => `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`);
643
+ out.push(`${DrfRequestStrategy._orderingKey}=${pairs.join(',')}`);
644
+ }
645
+ /**
646
+ * Append `page=N&page_size=M`
647
+ *
648
+ * `page` follows `options.page` (default `page`, matching DRF); the
649
+ * size key is hardcoded to DRF's idiomatic `page_size`.
650
+ *
651
+ * @param state - The current query builder state
652
+ * @param options - The query parameter key name configuration
653
+ * @param out - The accumulator the caller joins into the URI
654
+ */
655
+ _appendPagination(state, options, out) {
656
+ out.push(`${options.page}=${state.page}`);
657
+ out.push(`${DrfRequestStrategy._pageSizeKey}=${state.limit}`);
658
+ }
659
+ /**
660
+ * Append `search=term` when a search term is set
661
+ *
662
+ * @param state - The current query builder state
663
+ * @param options - The query parameter key name configuration
664
+ * @param out - The accumulator the caller joins into the URI
665
+ */
666
+ _appendSearch(state, options, out) {
667
+ if (!state.search) {
668
+ return;
669
+ }
670
+ out.push(`${options.search}=${state.search}`);
671
+ }
672
+ /**
673
+ * Translate a `FilterOperatorEnum` operator filter into a
674
+ * `[suffix, serializedValue]` pair
675
+ *
676
+ * The suffix is appended to the field name with a `__` separator on the
677
+ * wire side (`field__gte=18`). The empty string means "no suffix" —
678
+ * django-filter's implicit `exact` lookup.
679
+ *
680
+ * Mapping:
681
+ * - `EQ` → `''` (no suffix; default exact match)
682
+ * - `GT`/`GTE`/`LT`/`LTE`/`CONTAINS` → identity (lowercased name)
683
+ * - `ILIKE` → `icontains` (closest case-insensitive analog)
684
+ * - `IN` → `in` with comma-joined values
685
+ * - `SW` → `startswith`
686
+ * - `BTW` → `range` with comma-joined `[min, max]` (arity-checked)
687
+ * - `NULL` → `isnull` with boolean value (arity- and type-checked)
688
+ * - `NOT` → `UnsupportedFilterOperatorError` (no generic negation in
689
+ * django-filter; use `__exclude` on the queryset instead)
690
+ * - `FTS`/`PLFTS`/`PHFTS`/`WFTS` → `UnsupportedFilterOperatorError`
691
+ *
692
+ * @param filter - The operator filter to translate
693
+ * @returns A `[lookupSuffix, serializedValue]` tuple
694
+ * @throws {InvalidFilterOperatorValueError} If `BTW` does not receive
695
+ * exactly two values, or `NULL` does not receive exactly one boolean
696
+ * @throws {UnsupportedFilterOperatorError} If the operator has no
697
+ * django-filter equivalent
698
+ */
699
+ _formatOperatorPayload(filter) {
700
+ const { operator, values } = filter;
701
+ const first = values[0];
702
+ switch (operator) {
703
+ case FilterOperatorEnum.EQ: return ['', String(first)];
704
+ case FilterOperatorEnum.GT: return ['gt', String(first)];
705
+ case FilterOperatorEnum.GTE: return ['gte', String(first)];
706
+ case FilterOperatorEnum.LT: return ['lt', String(first)];
707
+ case FilterOperatorEnum.LTE: return ['lte', String(first)];
708
+ case FilterOperatorEnum.CONTAINS: return ['contains', String(first)];
709
+ case FilterOperatorEnum.ILIKE: return ['icontains', String(first)];
710
+ case FilterOperatorEnum.IN: return ['in', values.join(',')];
711
+ case FilterOperatorEnum.SW: return ['startswith', String(first)];
712
+ case FilterOperatorEnum.BTW: {
713
+ if (values.length !== 2) {
714
+ throw new InvalidFilterOperatorValueError(operator, 'BTW requires exactly 2 values (min, max)');
715
+ }
716
+ return ['range', values.join(',')];
717
+ }
718
+ case FilterOperatorEnum.NULL: {
719
+ if (values.length !== 1 || typeof first !== 'boolean') {
720
+ throw new InvalidFilterOperatorValueError(operator, 'NULL requires exactly 1 boolean value (true → IS NULL, false → IS NOT NULL)');
721
+ }
722
+ return ['isnull', String(first)];
723
+ }
724
+ case FilterOperatorEnum.NOT:
725
+ case FilterOperatorEnum.FTS:
726
+ case FilterOperatorEnum.PHFTS:
727
+ case FilterOperatorEnum.PLFTS:
728
+ case FilterOperatorEnum.WFTS:
729
+ throw new UnsupportedFilterOperatorError();
730
+ }
731
+ }
732
+ }
733
+
734
+ /**
735
+ * Response strategy for the Django REST Framework (DRF) driver
736
+ *
737
+ * Parses DRF `PageNumberPagination` responses:
738
+ *
739
+ * ```json
740
+ * {
741
+ * "count": 100,
742
+ * "next": "http://api.example.com/items/?page=3",
743
+ * "previous": "http://api.example.com/items/?page=1",
744
+ * "results": [...]
745
+ * }
746
+ * ```
747
+ *
748
+ * DRF emits no `current_page` field in the body, so this strategy
749
+ * **derives** the current page (and the page size) by inspecting the
750
+ * `next` / `previous` URLs:
751
+ *
752
+ * - `previous === null` → current page is **1**.
753
+ * - `previous` set but has no `?page=N` param → DRF omits `page=1` from
754
+ * URLs when the previous page is the first, so we infer **2**.
755
+ * - `previous` has `?page=N` → current page is **N + 1**.
756
+ *
757
+ * Similarly, `perPage` is parsed from any `?page_size=N` query param on
758
+ * `next` or `previous`, and `lastPage` is computed as
759
+ * `ceil(count / perPage)`. When `perPage` cannot be discovered (e.g. on a
760
+ * single-page response that emits both URLs as `null`), `perPage` and
761
+ * `lastPage` are left undefined.
762
+ *
763
+ * Key paths are resolved through `DrfResponseOptions`, which defaults
764
+ * `data → 'results'`, `total → 'count'`, `nextPageUrl → 'next'`,
765
+ * `prevPageUrl → 'previous'`. The current-page / per-page / last-page
766
+ * paths default to empty strings — the strategy ignores `options` for
767
+ * those slots and uses URL inspection instead.
768
+ *
769
+ * @see https://www.django-rest-framework.org/api-guide/pagination/#pagenumberpagination
770
+ */
771
+ class DrfResponseStrategy {
772
+ /**
773
+ * Parse a DRF pagination response into a PaginatedCollection
774
+ *
775
+ * @param response - The raw API response body
776
+ * @param options - The response key name configuration
777
+ * @returns A typed PaginatedCollection instance
778
+ */
779
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
780
+ paginate(response, options) {
781
+ const data = response[options.data];
782
+ const total = response[options.total];
783
+ const prevPageUrl = (response[options.prevPageUrl] ?? null);
784
+ const nextPageUrl = (response[options.nextPageUrl] ?? null);
785
+ const currentPage = this._deriveCurrentPage(prevPageUrl);
786
+ const perPage = this._derivePerPage(nextPageUrl, prevPageUrl);
787
+ const lastPage = this._deriveLastPage(total, perPage);
788
+ const from = this._deriveFrom(currentPage, perPage);
789
+ const to = this._deriveTo(currentPage, perPage, total);
790
+ return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl ?? undefined, nextPageUrl ?? undefined, lastPage, undefined, undefined);
791
+ }
792
+ /**
793
+ * Derive the current page number from the `previous` URL
794
+ *
795
+ * - `null` → page 1
796
+ * - URL without `?page=N` → page 2 (DRF omits `page=1` from URLs)
797
+ * - URL with `?page=N` → N + 1
798
+ *
799
+ * @param prevPageUrl - The `previous` link from the DRF response, or null
800
+ * @returns The current page number
801
+ */
802
+ _deriveCurrentPage(prevPageUrl) {
803
+ if (prevPageUrl === null) {
804
+ return 1;
805
+ }
806
+ const prevPage = this._extractPageParam(prevPageUrl);
807
+ return prevPage === undefined ? 2 : prevPage + 1;
808
+ }
809
+ /**
810
+ * Derive `from` as the 1-indexed offset of the first item on this page
811
+ *
812
+ * @param currentPage - The current page number
813
+ * @param perPage - The page size (may be undefined)
814
+ * @returns The 1-indexed `from` index, or undefined when perPage is unknown
815
+ */
816
+ _deriveFrom(currentPage, perPage) {
817
+ if (!perPage) {
818
+ return undefined;
819
+ }
820
+ return (currentPage - 1) * perPage + 1;
821
+ }
822
+ /**
823
+ * Derive the last page number as `ceil(total / perPage)`
824
+ *
825
+ * Both inputs must be defined; an empty result set (`total === 0`)
826
+ * yields `lastPage = 0` which the caller treats as "no useful info"
827
+ * and skips the sync to `NestService.lastPage`.
828
+ *
829
+ * @param total - The total item count
830
+ * @param perPage - The page size
831
+ * @returns The last page number, or undefined when either input is missing
832
+ */
833
+ _deriveLastPage(total, perPage) {
834
+ if (total === undefined || perPage === undefined || perPage <= 0) {
835
+ return undefined;
836
+ }
837
+ return Math.ceil(total / perPage);
838
+ }
839
+ /**
840
+ * Derive `perPage` by parsing `?page_size=N` from any available URL
841
+ *
842
+ * Tries `next` first (page 1 always has a `next` URL with `page_size`
843
+ * if any non-default size was requested), then falls back to
844
+ * `previous`. Returns undefined when neither URL contains the param —
845
+ * the consumer is then on a single-page result with the server's
846
+ * default page size, which is not introspectable from the body alone.
847
+ *
848
+ * @param nextPageUrl - The `next` link from the response, or null
849
+ * @param prevPageUrl - The `previous` link from the response, or null
850
+ * @returns The page size, or undefined
851
+ */
852
+ _derivePerPage(nextPageUrl, prevPageUrl) {
853
+ return this._extractPageSizeParam(nextPageUrl) ?? this._extractPageSizeParam(prevPageUrl);
854
+ }
855
+ /**
856
+ * Derive `to` as the 1-indexed offset of the last item on this page
857
+ *
858
+ * Clamped to `total` so the last page does not report past the end.
859
+ *
860
+ * @param currentPage - The current page number
861
+ * @param perPage - The page size (may be undefined)
862
+ * @param total - The total item count (may be undefined)
863
+ * @returns The 1-indexed `to` index, or undefined when inputs insufficient
864
+ */
865
+ _deriveTo(currentPage, perPage, total) {
866
+ if (perPage === undefined || total === undefined) {
867
+ return undefined;
868
+ }
869
+ return Math.min(currentPage * perPage, total);
870
+ }
871
+ /**
872
+ * Extract the `page` query parameter from a DRF pagination URL
873
+ *
874
+ * Returns the integer value of `?page=N`, or undefined when the URL is
875
+ * malformed or has no `page` param (which, by DRF convention, means
876
+ * page 1 — the caller infers the semantics).
877
+ *
878
+ * @param url - The URL to parse
879
+ * @returns The integer page value, or undefined
880
+ */
881
+ _extractPageParam(url) {
882
+ const raw = this._extractQueryParam(url, 'page');
883
+ if (raw === undefined) {
884
+ return undefined;
885
+ }
886
+ const parsed = Number.parseInt(raw, 10);
887
+ return Number.isNaN(parsed) ? undefined : parsed;
888
+ }
889
+ /**
890
+ * Extract the `page_size` query parameter from a DRF pagination URL
891
+ *
892
+ * @param url - The URL to parse (or null)
893
+ * @returns The integer page-size value, or undefined
894
+ */
895
+ _extractPageSizeParam(url) {
896
+ if (url === null) {
897
+ return undefined;
898
+ }
899
+ const raw = this._extractQueryParam(url, 'page_size');
900
+ if (raw === undefined) {
901
+ return undefined;
902
+ }
903
+ const parsed = Number.parseInt(raw, 10);
904
+ return Number.isNaN(parsed) ? undefined : parsed;
905
+ }
906
+ /**
907
+ * Extract a single query parameter from a URL via the WHATWG URL parser
307
908
  *
308
- * Returns the bare base when no segments were emitted (e.g. PostgREST
309
- * in RANGE mode with no filters), otherwise joins with `?` + `&`.
909
+ * Returns undefined when the URL is unparseable (relative URL without a
910
+ * base, or malformed) or when the parameter is absent.
310
911
  *
311
- * @param base - The base URI from `_baseUri`
312
- * @param segments - The query-string fragments from `parts(...)`
313
- * @returns The full URI
912
+ * @param url - The URL to parse
913
+ * @param name - The query-parameter name to look up
914
+ * @returns The raw string value of the parameter, or undefined
314
915
  */
315
- join(base, segments) {
316
- return segments.length ? `${base}?${segments.join('&')}` : base;
916
+ _extractQueryParam(url, name) {
917
+ try {
918
+ const parsed = new URL(url);
919
+ const value = parsed.searchParams.get(name);
920
+ return value === null ? undefined : value;
921
+ }
922
+ catch {
923
+ return undefined;
924
+ }
925
+ }
926
+ }
927
+
928
+ class UnselectableModelError extends Error {
929
+ constructor(model) {
930
+ super(`Unselectable Model: the selected model (${model}) is not present neither in the "model" property, nor in the includes object.`);
317
931
  }
318
932
  }
319
933
 
@@ -625,6 +1239,39 @@ class LaravelRequestStrategy extends AbstractRequestStrategy {
625
1239
  }
626
1240
  }
627
1241
 
1242
+ /**
1243
+ * Base class for response strategies whose pagination metadata is a flat
1244
+ * key-value envelope on the response body
1245
+ *
1246
+ * Laravel's stock pagination and Spatie's `QueryBuilder` both emit the
1247
+ * same flat shape — `{ data, current_page, total, per_page, from, to,
1248
+ * next_page_url, prev_page_url, first_page_url, last_page, last_page_url
1249
+ * }` — and both response strategies were duplicating the byte-identical
1250
+ * `new PaginatedCollection(response[options.X], ...)` body before this
1251
+ * base existed. Concrete classes now extend and provide only the
1252
+ * docstring describing their driver's specific shape (see
1253
+ * `LaravelResponseStrategy`, `SpatieResponseStrategy`).
1254
+ *
1255
+ * Drivers whose pagination metadata is a nested envelope (JSON:API,
1256
+ * NestJS, Strapi) extend `AbstractDotPathResponseStrategy` instead.
1257
+ * Drivers whose metadata comes from HTTP headers (PostgREST) or is
1258
+ * derived from response URLs (DRF) implement `IResponseStrategy`
1259
+ * directly.
1260
+ */
1261
+ class AbstractFlatResponseStrategy {
1262
+ /**
1263
+ * Parse a flat-envelope pagination response into a PaginatedCollection
1264
+ *
1265
+ * @param response - The raw API response object
1266
+ * @param options - The response key name configuration
1267
+ * @returns A typed PaginatedCollection instance
1268
+ */
1269
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1270
+ paginate(response, options) {
1271
+ 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]);
1272
+ }
1273
+ }
1274
+
628
1275
  /**
629
1276
  * Response strategy for the Laravel (pagination-only) driver
630
1277
  *
@@ -637,22 +1284,20 @@ class LaravelRequestStrategy extends AbstractRequestStrategy {
637
1284
  * "per_page": 15,
638
1285
  * "from": 1,
639
1286
  * "to": 15,
640
- * ...
1287
+ * "next_page_url": "...",
1288
+ * "prev_page_url": "...",
1289
+ * "first_page_url": "...",
1290
+ * "last_page": 7,
1291
+ * "last_page_url": "..."
641
1292
  * }
642
1293
  * ```
1294
+ *
1295
+ * The traversal algorithm (flat `response[options.X]` lookups) is
1296
+ * inherited from `AbstractFlatResponseStrategy`; this class exists so
1297
+ * `DriverEnum.LARAVEL` resolves to a distinct identity at the DI layer
1298
+ * even though the parsing logic is shared with Spatie.
643
1299
  */
644
- class LaravelResponseStrategy {
645
- /**
646
- * Parse a flat Laravel pagination response into a PaginatedCollection
647
- *
648
- * @param response - The raw API response object
649
- * @param options - The response key name configuration
650
- * @returns A typed PaginatedCollection instance
651
- */
652
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
653
- paginate(response, options) {
654
- 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]);
655
- }
1300
+ class LaravelResponseStrategy extends AbstractFlatResponseStrategy {
656
1301
  }
657
1302
 
658
1303
  /**
@@ -848,86 +1493,290 @@ class NestjsResponseStrategy extends AbstractDotPathResponseStrategy {
848
1493
  }
849
1494
 
850
1495
  /**
851
- * Enum representing the available filter operators for explicit operator
852
- * filters
853
- *
854
- * NestJS encodes these with the `$` prefix at the wire level
855
- * (`filter.field=$operator:value`); PostgREST translates them to its own
856
- * prefix notation (`col=eq.val`, `col=is.null`, etc.). The enum values are
857
- * intentionally the NestJS form; each driver's request strategy is
858
- * responsible for mapping them into its own shape.
859
- *
860
- * `FTS`, `PLFTS`, `PHFTS`, `WFTS` are PostgREST-native full-text search
861
- * variants; they throw `UnsupportedFilterOperatorError` on every other
862
- * driver that does not recognise them.
1496
+ * Request strategy for the @nestjsx/crud driver
863
1497
  *
864
- * @see https://github.com/ppetzold/nestjs-paginate
865
- * @see https://postgrest.org/en/stable/api.html#operators
866
- */
867
- var FilterOperatorEnum;
868
- (function (FilterOperatorEnum) {
869
- FilterOperatorEnum["BTW"] = "$btw";
870
- FilterOperatorEnum["CONTAINS"] = "$contains";
871
- FilterOperatorEnum["EQ"] = "$eq";
872
- FilterOperatorEnum["FTS"] = "$fts";
873
- FilterOperatorEnum["GT"] = "$gt";
874
- FilterOperatorEnum["GTE"] = "$gte";
875
- FilterOperatorEnum["ILIKE"] = "$ilike";
876
- FilterOperatorEnum["IN"] = "$in";
877
- FilterOperatorEnum["LT"] = "$lt";
878
- FilterOperatorEnum["LTE"] = "$lte";
879
- FilterOperatorEnum["NOT"] = "$not";
880
- FilterOperatorEnum["NULL"] = "$null";
881
- FilterOperatorEnum["PHFTS"] = "$phfts";
882
- FilterOperatorEnum["PLFTS"] = "$plfts";
883
- FilterOperatorEnum["SW"] = "$sw";
884
- FilterOperatorEnum["WFTS"] = "$wfts";
885
- })(FilterOperatorEnum || (FilterOperatorEnum = {}));
886
-
887
- /**
888
- * Enum representing the wire-level pagination mechanism
1498
+ * Generates URIs in [@nestjsx/crud's pipe-delimited query format](https://github.com/nestjsx/crud/wiki/Requests):
1499
+ * - Filters: `filter=field||$eq||value` (repeatable; multi-value
1500
+ * collapses to `filter=field||$in||v1,v2`)
1501
+ * - Operator filters: `filter=field||$op||value` (translated from
1502
+ * `FilterOperatorEnum` — `CONTAINS`→`$cont`, `ILIKE`→`$contL`,
1503
+ * `SW`→`$starts`, `BTW`→`$between`, `NOT`→`$ne`/`$notin`,
1504
+ * `NULL`→`$isnull`/`$notnull` with no value segment)
1505
+ * - Joins: `join=relation` (repeatable, from `addIncludes`)
1506
+ * - Field selection (flat): `fields=col1,col2` (from `addSelect`)
1507
+ * - Sorts: `sort=field,ASC` (repeatable, uppercase direction)
1508
+ * - Pagination (page-based): `page=N&limit=N`
889
1509
  *
890
- * `QUERY` (default) the request strategy emits `limit` and `offset` (or
891
- * equivalent) query parameters on the URL.
1510
+ * The JSON-shaped `s={...}` search parameter is intentionally out of
1511
+ * scope: it is not a plain search term, so `setSearch` throws
1512
+ * `UnsupportedSearchError` on this driver. PostgREST-native full-text
1513
+ * search operators (`FTS`, `PHFTS`, `PLFTS`, `WFTS`) throw
1514
+ * `UnsupportedFilterOperatorError`.
892
1515
  *
893
- * `RANGE` — the request strategy omits URL-based pagination and the
894
- * consumer instead applies HTTP request headers returned by
895
- * `NgQubeeService.paginationHeaders()`. Currently honoured only by the
896
- * PostgREST driver, which maps it to `Range-Unit: items` + `Range: 0-9`.
897
- * Other drivers ignore the setting.
1516
+ * @see https://github.com/nestjsx/crud/wiki/Requests
898
1517
  */
899
- var PaginationModeEnum;
900
- (function (PaginationModeEnum) {
901
- PaginationModeEnum["QUERY"] = "query";
902
- PaginationModeEnum["RANGE"] = "range";
903
- })(PaginationModeEnum || (PaginationModeEnum = {}));
1518
+ class NestjsxCrudRequestStrategy extends AbstractRequestStrategy {
1519
+ /**
1520
+ * Filters, operator filters, joins (`includes`), flat field selection
1521
+ * (`select`), sorts — no per-model fields, no global search (the `s`
1522
+ * parameter is JSON-shaped, not a plain term)
1523
+ */
1524
+ capabilities = {
1525
+ fields: false,
1526
+ filters: true,
1527
+ includes: true,
1528
+ operatorFilters: true,
1529
+ search: false,
1530
+ select: true,
1531
+ sort: true
1532
+ };
1533
+ /**
1534
+ * @nestjsx/crud-native names of the four hardcoded query keys
1535
+ *
1536
+ * The wire format is fixed (the server's `CrudRequestInterceptor`
1537
+ * reads `fields`, `filter`, `join`, and `sort`); these keys are
1538
+ * intentionally not configurable through `QueryBuilderOptions` and
1539
+ * live as private statics so they are visible in one place.
1540
+ */
1541
+ static _fieldsKey = 'fields';
1542
+ static _filterKey = 'filter';
1543
+ static _joinKey = 'join';
1544
+ static _sortKey = 'sort';
1545
+ /**
1546
+ * Pipe delimiter separating field, operator, and value segments
1547
+ */
1548
+ static _separator = '||';
1549
+ /**
1550
+ * Emit @nestjsx/crud-format query-string segments in canonical order:
1551
+ * fields → filters → operator filters → join → sort → limit → page
1552
+ *
1553
+ * @param state - The current query builder state
1554
+ * @param options - The query parameter key name configuration (used
1555
+ * for `page` / `limit`, whose defaults match the wire format; the
1556
+ * `fields` / `filter` / `join` / `sort` keys are fixed by the server)
1557
+ * @returns Ordered query-string fragments
1558
+ */
1559
+ parts(state, options) {
1560
+ const out = [];
1561
+ this._appendFields(state, out);
1562
+ this._appendFilters(state, out);
1563
+ this._appendOperatorFilters(state, out);
1564
+ this._appendJoin(state, out);
1565
+ this._appendSort(state, out);
1566
+ this._appendLimit(state, options, out);
1567
+ this._appendPage(state, options, out);
1568
+ return out;
1569
+ }
1570
+ /**
1571
+ * Append `fields=col1,col2` from the flat select array
1572
+ *
1573
+ * @param state - The current query builder state
1574
+ * @param out - The accumulator the caller joins into the URI
1575
+ */
1576
+ _appendFields(state, out) {
1577
+ if (!state.select.length) {
1578
+ return;
1579
+ }
1580
+ out.push(`${NestjsxCrudRequestStrategy._fieldsKey}=${state.select.join(',')}`);
1581
+ }
1582
+ /**
1583
+ * Append simple filters as repeatable `filter=field||$eq||value` params
1584
+ *
1585
+ * Single-value filters fold to `$eq`; multi-value filters fold to
1586
+ * `$in` with comma-joined values.
1587
+ *
1588
+ * @param state - The current query builder state
1589
+ * @param out - The accumulator the caller joins into the URI
1590
+ */
1591
+ _appendFilters(state, out) {
1592
+ const sep = NestjsxCrudRequestStrategy._separator;
1593
+ Object.keys(state.filters).forEach(field => {
1594
+ const values = state.filters[field];
1595
+ if (!values.length) {
1596
+ return;
1597
+ }
1598
+ const condition = values.length === 1
1599
+ ? `$eq${sep}${values[0]}`
1600
+ : `$in${sep}${values.join(',')}`;
1601
+ out.push(`${NestjsxCrudRequestStrategy._filterKey}=${field}${sep}${condition}`);
1602
+ });
1603
+ }
1604
+ /**
1605
+ * Append `join=relation` params from the includes array
1606
+ *
1607
+ * Per-relation field projection (`join=relation||f1,f2`) is not
1608
+ * expressible through the current state shape and is out of scope.
1609
+ *
1610
+ * @param state - The current query builder state
1611
+ * @param out - The accumulator the caller joins into the URI
1612
+ */
1613
+ _appendJoin(state, out) {
1614
+ state.includes.forEach(relation => {
1615
+ out.push(`${NestjsxCrudRequestStrategy._joinKey}=${relation}`);
1616
+ });
1617
+ }
1618
+ /**
1619
+ * Append the limit parameter
1620
+ *
1621
+ * @param state - The current query builder state
1622
+ * @param options - The query parameter key name configuration
1623
+ * @param out - The accumulator the caller joins into the URI
1624
+ */
1625
+ _appendLimit(state, options, out) {
1626
+ out.push(`${options.limit}=${state.limit}`);
1627
+ }
1628
+ /**
1629
+ * Append operator filters as repeatable `filter=field||$op||value` params
1630
+ *
1631
+ * @param state - The current query builder state
1632
+ * @param out - The accumulator the caller joins into the URI
1633
+ */
1634
+ _appendOperatorFilters(state, out) {
1635
+ const sep = NestjsxCrudRequestStrategy._separator;
1636
+ state.operatorFilters.forEach((filter) => {
1637
+ const condition = this._formatOperatorCondition(filter);
1638
+ out.push(`${NestjsxCrudRequestStrategy._filterKey}=${filter.field}${sep}${condition}`);
1639
+ });
1640
+ }
1641
+ /**
1642
+ * Append the page parameter
1643
+ *
1644
+ * @param state - The current query builder state
1645
+ * @param options - The query parameter key name configuration
1646
+ * @param out - The accumulator the caller joins into the URI
1647
+ */
1648
+ _appendPage(state, options, out) {
1649
+ out.push(`${options.page}=${state.page}`);
1650
+ }
1651
+ /**
1652
+ * Append `sort=field,ASC` params, one per sort rule
1653
+ *
1654
+ * @param state - The current query builder state
1655
+ * @param out - The accumulator the caller joins into the URI
1656
+ */
1657
+ _appendSort(state, out) {
1658
+ state.sorts.forEach(sort => {
1659
+ const direction = sort.order === SortEnum.DESC ? 'DESC' : 'ASC';
1660
+ out.push(`${NestjsxCrudRequestStrategy._sortKey}=${sort.field},${direction}`);
1661
+ });
1662
+ }
1663
+ /**
1664
+ * Translate a `FilterOperatorEnum` operator filter into @nestjsx/crud's
1665
+ * `$operator||value` condition segment
1666
+ *
1667
+ * The mapping is library-canonical → @nestjsx/crud-native:
1668
+ * - `EQ`/`GT`/`GTE`/`LT`/`LTE`/`IN` → identity (same operator name)
1669
+ * - `CONTAINS` → `$cont`; `ILIKE` → `$contL` (case-insensitive contains)
1670
+ * - `SW` → `$starts`
1671
+ * - `BTW` → `$between` with `min,max` (arity-checked)
1672
+ * - `NOT` → `$ne` (single value) / `$notin` (multi-value)
1673
+ * - `NULL` → `$isnull` (when value is `true`) / `$notnull` (when value
1674
+ * is `false`); both emit **no value segment**; arity- and type-checked
1675
+ *
1676
+ * PostgREST's full-text-search operators (`FTS`, `PHFTS`, `PLFTS`,
1677
+ * `WFTS`) have no @nestjsx/crud equivalent and throw
1678
+ * `UnsupportedFilterOperatorError`.
1679
+ *
1680
+ * @param filter - The operator filter to translate
1681
+ * @returns The `$operator||value` (or bare `$operator`) condition segment
1682
+ * @throws {InvalidFilterOperatorValueError} If `BTW` does not receive
1683
+ * exactly two values, or `NULL` does not receive exactly one boolean
1684
+ * @throws {UnsupportedFilterOperatorError} If the operator is a
1685
+ * PostgREST-only FTS variant
1686
+ */
1687
+ _formatOperatorCondition(filter) {
1688
+ const sep = NestjsxCrudRequestStrategy._separator;
1689
+ const { operator, values } = filter;
1690
+ const first = values[0];
1691
+ switch (operator) {
1692
+ case FilterOperatorEnum.EQ: return `$eq${sep}${first}`;
1693
+ case FilterOperatorEnum.GT: return `$gt${sep}${first}`;
1694
+ case FilterOperatorEnum.GTE: return `$gte${sep}${first}`;
1695
+ case FilterOperatorEnum.LT: return `$lt${sep}${first}`;
1696
+ case FilterOperatorEnum.LTE: return `$lte${sep}${first}`;
1697
+ case FilterOperatorEnum.CONTAINS: return `$cont${sep}${first}`;
1698
+ case FilterOperatorEnum.ILIKE: return `$contL${sep}${first}`;
1699
+ case FilterOperatorEnum.IN: return `$in${sep}${values.join(',')}`;
1700
+ case FilterOperatorEnum.SW: return `$starts${sep}${first}`;
1701
+ case FilterOperatorEnum.BTW: {
1702
+ if (values.length !== 2) {
1703
+ throw new InvalidFilterOperatorValueError(operator, 'BTW requires exactly 2 values (min, max)');
1704
+ }
1705
+ return `$between${sep}${values.join(',')}`;
1706
+ }
1707
+ case FilterOperatorEnum.NOT:
1708
+ return values.length === 1
1709
+ ? `$ne${sep}${first}`
1710
+ : `$notin${sep}${values.join(',')}`;
1711
+ case FilterOperatorEnum.NULL: {
1712
+ if (values.length !== 1 || typeof first !== 'boolean') {
1713
+ throw new InvalidFilterOperatorValueError(operator, 'NULL requires exactly 1 boolean value (true → IS NULL, false → IS NOT NULL)');
1714
+ }
1715
+ return first ? '$isnull' : '$notnull';
1716
+ }
1717
+ case FilterOperatorEnum.FTS:
1718
+ case FilterOperatorEnum.PHFTS:
1719
+ case FilterOperatorEnum.PLFTS:
1720
+ case FilterOperatorEnum.WFTS:
1721
+ throw new UnsupportedFilterOperatorError();
1722
+ }
1723
+ }
1724
+ }
904
1725
 
905
1726
  /**
906
- * Thrown when a filter operator receives a value array of the wrong shape
1727
+ * Response strategy for the @nestjsx/crud driver
907
1728
  *
908
- * Some operators have arity or type constraints that the library enforces
909
- * at call time so misuse fails loudly instead of silently emitting invalid
910
- * server requests:
911
- *
912
- * - `BTW` requires exactly two values (min, max).
913
- * - `NULL` requires exactly one boolean value (`true` for `IS NULL`,
914
- * `false` for `IS NOT NULL`).
1729
+ * Parses @nestjsx/crud's `getMany` envelope:
1730
+ * ```json
1731
+ * {
1732
+ * "data": [{ "id": 1, "name": "John" }],
1733
+ * "count": 10,
1734
+ * "total": 48,
1735
+ * "page": 2,
1736
+ * "pageCount": 5
1737
+ * }
1738
+ * ```
915
1739
  *
916
- * Operators with looser shape rules leave validation to the server; this
917
- * error is reserved for cases where the library itself can detect the
918
- * problem unambiguously from the call site.
919
- */
920
- class InvalidFilterOperatorValueError extends Error {
921
- /**
922
- * @param operator - The operator that rejected the values
923
- * @param reason - Short human-readable explanation of the constraint
924
- */
925
- constructor(operator, reason) {
926
- super(`Invalid values for filter operator ${operator}: ${reason}`);
927
- this.name = 'InvalidFilterOperatorValueError';
928
- }
1740
+ * Default key paths are configured in `NestjsxCrudResponseOptions`. The
1741
+ * envelope carries no `from`/`to` indices and no navigation links, so
1742
+ * `from`/`to` are computed from `page` × `count` by the inherited
1743
+ * traversal algorithm and the URL slots resolve to `undefined` unless
1744
+ * the consumer overrides their paths via `IPaginationConfig`.
1745
+ *
1746
+ * Note that `count` is the number of entities **on the current page**,
1747
+ * not the requested page size on the last page of a result set the
1748
+ * derived `from`/`to` can underestimate. Consumers needing exact
1749
+ * indices should compute them from the requested limit instead.
1750
+ *
1751
+ * The dot-notation traversal is inherited from
1752
+ * `AbstractDotPathResponseStrategy`; this class exists so
1753
+ * `DriverEnum.NESTJSX_CRUD` resolves to a distinct identity at the DI
1754
+ * layer even though the parsing logic is shared with JSON:API, NestJS,
1755
+ * and Strapi.
1756
+ *
1757
+ * @see https://github.com/nestjsx/crud/wiki/Requests
1758
+ */
1759
+ class NestjsxCrudResponseStrategy extends AbstractDotPathResponseStrategy {
929
1760
  }
930
1761
 
1762
+ /**
1763
+ * Enum representing the wire-level pagination mechanism
1764
+ *
1765
+ * `QUERY` (default) — the request strategy emits `limit` and `offset` (or
1766
+ * equivalent) query parameters on the URL.
1767
+ *
1768
+ * `RANGE` — the request strategy omits URL-based pagination and the
1769
+ * consumer instead applies HTTP request headers returned by
1770
+ * `NgQubeeService.paginationHeaders()`. Currently honoured only by the
1771
+ * PostgREST driver, which maps it to `Range-Unit: items` + `Range: 0-9`.
1772
+ * Other drivers ignore the setting.
1773
+ */
1774
+ var PaginationModeEnum;
1775
+ (function (PaginationModeEnum) {
1776
+ PaginationModeEnum["QUERY"] = "query";
1777
+ PaginationModeEnum["RANGE"] = "range";
1778
+ })(PaginationModeEnum || (PaginationModeEnum = {}));
1779
+
931
1780
  /**
932
1781
  * Request strategy for the PostgREST driver
933
1782
  *
@@ -1296,6 +2145,228 @@ class PostgrestResponseStrategy {
1296
2145
  }
1297
2146
  }
1298
2147
 
2148
+ /**
2149
+ * Request strategy for the Sieve (.NET) driver
2150
+ *
2151
+ * Generates URIs in [Sieve's compact expression format](https://github.com/Biarity/Sieve):
2152
+ * - Filters: a single `filters=` parameter holding comma-joined (AND)
2153
+ * `Field{op}Value` terms; multi-value terms use the pipe (OR) on the
2154
+ * value side (`status==active|pending`)
2155
+ * - Operator filters: translated from `FilterOperatorEnum` — see the
2156
+ * mapping on `_formatOperatorTerms`
2157
+ * - Sorts: `sorts=field,-other` (CSV, `-` prefix = DESC)
2158
+ * - Pagination: `page=N&pageSize=N`
2159
+ *
2160
+ * Sieve has no per-model field selection, no relation includes, no flat
2161
+ * column selection, and no global search parameter — the corresponding
2162
+ * fluent methods throw the matching `Unsupported*Error`. PostgREST-native
2163
+ * full-text search operators (`FTS`, `PHFTS`, `PLFTS`, `WFTS`) throw
2164
+ * `UnsupportedFilterOperatorError`.
2165
+ *
2166
+ * @see https://github.com/Biarity/Sieve
2167
+ */
2168
+ class SieveRequestStrategy extends AbstractRequestStrategy {
2169
+ /**
2170
+ * Filters, operator filters, sorts — no per-model fields, no includes,
2171
+ * no flat select, no global search (use `CONTAINS` / `ILIKE` operator
2172
+ * filters for partial matches)
2173
+ */
2174
+ capabilities = {
2175
+ fields: false,
2176
+ filters: true,
2177
+ includes: false,
2178
+ operatorFilters: true,
2179
+ search: false,
2180
+ select: false,
2181
+ sort: true
2182
+ };
2183
+ /**
2184
+ * Sieve-native names of the three hardcoded query keys
2185
+ *
2186
+ * Sieve's model binder reads `filters`, `sorts`, and `pageSize` (the
2187
+ * plural forms differ from the library-wide `filter` / `sort` /
2188
+ * `limit` defaults); these keys are intentionally not configurable
2189
+ * through `QueryBuilderOptions` and live as private statics so they
2190
+ * are visible in one place.
2191
+ */
2192
+ static _filtersKey = 'filters';
2193
+ static _pageSizeKey = 'pageSize';
2194
+ static _sortsKey = 'sorts';
2195
+ /**
2196
+ * Emit Sieve-format query-string segments in canonical order:
2197
+ * filters → sorts → page → pageSize
2198
+ *
2199
+ * @param state - The current query builder state
2200
+ * @param options - The query parameter key name configuration (used
2201
+ * for `page`, whose default matches the wire format; the `filters` /
2202
+ * `sorts` / `pageSize` keys are fixed by the server)
2203
+ * @returns Ordered query-string fragments
2204
+ */
2205
+ parts(state, options) {
2206
+ const out = [];
2207
+ this._appendFilters(state, out);
2208
+ this._appendSorts(state, out);
2209
+ this._appendPage(state, options, out);
2210
+ this._appendPageSize(state, out);
2211
+ return out;
2212
+ }
2213
+ /**
2214
+ * Append the single `filters=` parameter combining simple and operator
2215
+ * filters
2216
+ *
2217
+ * Each term is one `Field{op}Value` expression; terms join with the
2218
+ * comma (Sieve's AND). Simple single-value filters fold to `==`;
2219
+ * simple multi-value filters fold to a value-level pipe OR
2220
+ * (`field==v1|v2`).
2221
+ *
2222
+ * @param state - The current query builder state
2223
+ * @param out - The accumulator the caller joins into the URI
2224
+ */
2225
+ _appendFilters(state, out) {
2226
+ const terms = [];
2227
+ Object.keys(state.filters).forEach(field => {
2228
+ const values = state.filters[field];
2229
+ if (!values.length) {
2230
+ return;
2231
+ }
2232
+ terms.push(`${field}==${values.join('|')}`);
2233
+ });
2234
+ state.operatorFilters.forEach((filter) => {
2235
+ terms.push(...this._formatOperatorTerms(filter));
2236
+ });
2237
+ if (!terms.length) {
2238
+ return;
2239
+ }
2240
+ out.push(`${SieveRequestStrategy._filtersKey}=${terms.join(',')}`);
2241
+ }
2242
+ /**
2243
+ * Append the page parameter
2244
+ *
2245
+ * @param state - The current query builder state
2246
+ * @param options - The query parameter key name configuration
2247
+ * @param out - The accumulator the caller joins into the URI
2248
+ */
2249
+ _appendPage(state, options, out) {
2250
+ out.push(`${options.page}=${state.page}`);
2251
+ }
2252
+ /**
2253
+ * Append the pageSize parameter
2254
+ *
2255
+ * @param state - The current query builder state
2256
+ * @param out - The accumulator the caller joins into the URI
2257
+ */
2258
+ _appendPageSize(state, out) {
2259
+ out.push(`${SieveRequestStrategy._pageSizeKey}=${state.limit}`);
2260
+ }
2261
+ /**
2262
+ * Append the `sorts=field,-other` CSV parameter
2263
+ *
2264
+ * @param state - The current query builder state
2265
+ * @param out - The accumulator the caller joins into the URI
2266
+ */
2267
+ _appendSorts(state, out) {
2268
+ if (!state.sorts.length) {
2269
+ return;
2270
+ }
2271
+ const fields = state.sorts.map(sort => sort.order === SortEnum.DESC ? `-${sort.field}` : sort.field);
2272
+ out.push(`${SieveRequestStrategy._sortsKey}=${fields.join(',')}`);
2273
+ }
2274
+ /**
2275
+ * Translate a `FilterOperatorEnum` operator filter into one or more
2276
+ * Sieve `Field{op}Value` terms
2277
+ *
2278
+ * The mapping is library-canonical → Sieve-native:
2279
+ * - `EQ` → `==`; `GT`/`GTE`/`LT`/`LTE` → `>` / `>=` / `<` / `<=`
2280
+ * - `CONTAINS` → `@=`; `ILIKE` → `@=*` (case-insensitive contains)
2281
+ * - `SW` → `_=` (starts with)
2282
+ * - `IN` → `==` with a value-level pipe OR (`field==v1|v2`)
2283
+ * - `BTW` → **two** AND-ed terms (`field>=min` and `field<=max`,
2284
+ * arity-checked)
2285
+ * - `NOT` → `!=` — one term per value, AND-ed (`field!=v1,field!=v2`)
2286
+ * - `NULL` → `==null` (when value is `true`) / `!=null` (when value is
2287
+ * `false`); arity- and type-checked
2288
+ *
2289
+ * PostgREST's full-text-search operators (`FTS`, `PHFTS`, `PLFTS`,
2290
+ * `WFTS`) have no Sieve equivalent and throw
2291
+ * `UnsupportedFilterOperatorError`.
2292
+ *
2293
+ * @param filter - The operator filter to translate
2294
+ * @returns One or more `Field{op}Value` terms ready to AND-join
2295
+ * @throws {InvalidFilterOperatorValueError} If `BTW` does not receive
2296
+ * exactly two values, or `NULL` does not receive exactly one boolean
2297
+ * @throws {UnsupportedFilterOperatorError} If the operator is a
2298
+ * PostgREST-only FTS variant
2299
+ */
2300
+ _formatOperatorTerms(filter) {
2301
+ const { field, operator, values } = filter;
2302
+ const first = values[0];
2303
+ switch (operator) {
2304
+ case FilterOperatorEnum.EQ: return [`${field}==${first}`];
2305
+ case FilterOperatorEnum.GT: return [`${field}>${first}`];
2306
+ case FilterOperatorEnum.GTE: return [`${field}>=${first}`];
2307
+ case FilterOperatorEnum.LT: return [`${field}<${first}`];
2308
+ case FilterOperatorEnum.LTE: return [`${field}<=${first}`];
2309
+ case FilterOperatorEnum.CONTAINS: return [`${field}@=${first}`];
2310
+ case FilterOperatorEnum.ILIKE: return [`${field}@=*${first}`];
2311
+ case FilterOperatorEnum.SW: return [`${field}_=${first}`];
2312
+ case FilterOperatorEnum.IN: return [`${field}==${values.join('|')}`];
2313
+ case FilterOperatorEnum.BTW: {
2314
+ if (values.length !== 2) {
2315
+ throw new InvalidFilterOperatorValueError(operator, 'BTW requires exactly 2 values (min, max)');
2316
+ }
2317
+ return [`${field}>=${values[0]}`, `${field}<=${values[1]}`];
2318
+ }
2319
+ case FilterOperatorEnum.NOT:
2320
+ return values.map(value => `${field}!=${value}`);
2321
+ case FilterOperatorEnum.NULL: {
2322
+ if (values.length !== 1 || typeof first !== 'boolean') {
2323
+ throw new InvalidFilterOperatorValueError(operator, 'NULL requires exactly 1 boolean value (true → IS NULL, false → IS NOT NULL)');
2324
+ }
2325
+ return first ? [`${field}==null`] : [`${field}!=null`];
2326
+ }
2327
+ case FilterOperatorEnum.FTS:
2328
+ case FilterOperatorEnum.PHFTS:
2329
+ case FilterOperatorEnum.PLFTS:
2330
+ case FilterOperatorEnum.WFTS:
2331
+ throw new UnsupportedFilterOperatorError();
2332
+ }
2333
+ }
2334
+ }
2335
+
2336
+ /**
2337
+ * Response strategy for the Sieve (.NET) driver
2338
+ *
2339
+ * Sieve itself does not define a response envelope — it returns an
2340
+ * `IQueryable` that the ASP.NET developer wraps in a paging DTO of their
2341
+ * choosing. This strategy therefore ships a **sensible default mapping**
2342
+ * for the common hand-rolled `PagedResult<T>` shape:
2343
+ * ```json
2344
+ * {
2345
+ * "data": [{ "id": 1, "title": "Hello" }],
2346
+ * "page": 2,
2347
+ * "pageSize": 10,
2348
+ * "total": 48,
2349
+ * "totalPages": 5
2350
+ * }
2351
+ * ```
2352
+ *
2353
+ * Every key path is configurable through `IConfig.response` (dot
2354
+ * notation supported), so any wrapper shape — `{ items, meta: {...} }`,
2355
+ * `{ results, pagination: {...} }` — can be mapped without subclassing.
2356
+ * Defaults are encoded in `SieveResponseOptions`. `from`/`to` are
2357
+ * computed from `page` × `pageSize` by the inherited traversal
2358
+ * algorithm, and the navigation-URL slots resolve to `undefined` unless
2359
+ * paths are provided.
2360
+ *
2361
+ * The dot-notation traversal is inherited from
2362
+ * `AbstractDotPathResponseStrategy`; this class exists so
2363
+ * `DriverEnum.SIEVE` resolves to a distinct identity at the DI layer.
2364
+ *
2365
+ * @see https://github.com/Biarity/Sieve
2366
+ */
2367
+ class SieveResponseStrategy extends AbstractDotPathResponseStrategy {
2368
+ }
2369
+
1299
2370
  /**
1300
2371
  * Request strategy for the Spatie Query Builder driver
1301
2372
  *
@@ -1442,7 +2513,8 @@ class SpatieRequestStrategy extends AbstractRequestStrategy {
1442
2513
  /**
1443
2514
  * Response strategy for the Spatie Query Builder driver
1444
2515
  *
1445
- * Parses flat Laravel pagination responses:
2516
+ * Parses flat Laravel-style pagination responses (Spatie's Query Builder
2517
+ * is built on Laravel's pagination):
1446
2518
  * ```json
1447
2519
  * {
1448
2520
  * "data": [...],
@@ -1455,32 +2527,216 @@ class SpatieRequestStrategy extends AbstractRequestStrategy {
1455
2527
  * }
1456
2528
  * ```
1457
2529
  *
2530
+ * The traversal algorithm (flat `response[options.X]` lookups) is
2531
+ * inherited from `AbstractFlatResponseStrategy`; this class exists so
2532
+ * `DriverEnum.SPATIE` resolves to a distinct identity at the DI layer
2533
+ * even though the parsing logic is shared with the plain Laravel driver.
2534
+ *
1458
2535
  * @see https://spatie.be/docs/laravel-query-builder
1459
2536
  */
1460
- class SpatieResponseStrategy {
2537
+ class SpatieResponseStrategy extends AbstractFlatResponseStrategy {
2538
+ }
2539
+
2540
+ /**
2541
+ * Request strategy for the Spring Data REST driver
2542
+ *
2543
+ * Generates URIs in [Spring Data REST's pagination format](https://docs.spring.io/spring-data/rest/reference/paging-and-sorting.html):
2544
+ * - Sorts: `sort=field,asc&sort=other,desc` (repeatable param, one per rule)
2545
+ * - Pagination: `page=N&size=N` — **`page` is 0-indexed on the wire**;
2546
+ * the library state stays 1-indexed and the strategy subtracts 1 at
2547
+ * emission time
2548
+ *
2549
+ * Spring Data REST defines no standard query parameter convention for
2550
+ * filtering, field selection, includes, or global search — those are
2551
+ * implemented server-side via custom query methods or Specifications.
2552
+ * The corresponding fluent methods (`addFilter`, `addFilterOperator`,
2553
+ * `addSelect`, `addFields`, `addIncludes`, `setSearch`) throw the
2554
+ * matching `Unsupported*Error` on this driver.
2555
+ *
2556
+ * @see https://docs.spring.io/spring-data/rest/reference/paging-and-sorting.html
2557
+ */
2558
+ class SpringRequestStrategy extends AbstractRequestStrategy {
2559
+ /**
2560
+ * Sorts only — Spring Data REST has no standard wire convention for
2561
+ * filters, operator filters, per-model fields, flat select, includes,
2562
+ * or global search
2563
+ */
2564
+ capabilities = {
2565
+ fields: false,
2566
+ filters: false,
2567
+ includes: false,
2568
+ operatorFilters: false,
2569
+ search: false,
2570
+ select: false,
2571
+ sort: true
2572
+ };
1461
2573
  /**
1462
- * Parse a flat Laravel pagination response into a PaginatedCollection
2574
+ * Spring-native name of the hardcoded page-size query key
1463
2575
  *
1464
- * @param response - The raw API response object
1465
- * @param options - The response key name configuration
1466
- * @returns A typed PaginatedCollection instance
2576
+ * The wire format is fixed (Spring's `PageableHandlerMethodArgumentResolver`
2577
+ * reads `size` by default); the key is intentionally not configurable
2578
+ * through `QueryBuilderOptions` and lives as a private static so it is
2579
+ * visible in one place.
1467
2580
  */
1468
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1469
- paginate(response, options) {
1470
- 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]);
2581
+ static _sizeKey = 'size';
2582
+ /**
2583
+ * Emit Spring-format query-string segments in canonical order:
2584
+ * sort → page → size
2585
+ *
2586
+ * @param state - The current query builder state
2587
+ * @param options - The query parameter key name configuration (used
2588
+ * for `page` and `sort`, whose defaults match the wire format; the
2589
+ * `size` key is fixed by the server)
2590
+ * @returns Ordered query-string fragments
2591
+ */
2592
+ parts(state, options) {
2593
+ const out = [];
2594
+ this._appendSort(state, options, out);
2595
+ this._appendPage(state, options, out);
2596
+ this._appendSize(state, out);
2597
+ return out;
2598
+ }
2599
+ /**
2600
+ * Append the 0-indexed page parameter
2601
+ *
2602
+ * The library state is 1-indexed (page 1 is the first page); Spring's
2603
+ * `page` request parameter is 0-indexed, so the strategy subtracts 1
2604
+ * at emission time.
2605
+ *
2606
+ * @param state - The current query builder state
2607
+ * @param options - The query parameter key name configuration
2608
+ * @param out - The accumulator the caller joins into the URI
2609
+ */
2610
+ _appendPage(state, options, out) {
2611
+ out.push(`${options.page}=${state.page - 1}`);
2612
+ }
2613
+ /**
2614
+ * Append the size parameter
2615
+ *
2616
+ * @param state - The current query builder state
2617
+ * @param out - The accumulator the caller joins into the URI
2618
+ */
2619
+ _appendSize(state, out) {
2620
+ out.push(`${SpringRequestStrategy._sizeKey}=${state.limit}`);
2621
+ }
2622
+ /**
2623
+ * Append `sort=field,asc` params, one per sort rule (repeatable)
2624
+ *
2625
+ * Spring parses each `sort` occurrence independently — multiple rules
2626
+ * are expressed by repeating the parameter, not by comma-joining the
2627
+ * fields.
2628
+ *
2629
+ * @param state - The current query builder state
2630
+ * @param options - The query parameter key name configuration
2631
+ * @param out - The accumulator the caller joins into the URI
2632
+ */
2633
+ _appendSort(state, options, out) {
2634
+ state.sorts.forEach(sort => {
2635
+ const direction = sort.order === SortEnum.DESC ? 'desc' : 'asc';
2636
+ out.push(`${options.sort}=${sort.field},${direction}`);
2637
+ });
1471
2638
  }
1472
2639
  }
1473
2640
 
1474
2641
  /**
1475
- * Error thrown when filter operators are attempted with a driver that does not support them
2642
+ * Response strategy for the Spring Data REST driver
1476
2643
  *
1477
- * Filter operators are only supported by the NestJS driver.
1478
- * Use `addFilter()` for Spatie implicit equality filters.
2644
+ * Parses Spring Data REST's HAL envelope:
2645
+ * ```json
2646
+ * {
2647
+ * "_embedded": { "users": [{ "id": 1, "name": "John" }] },
2648
+ * "_links": {
2649
+ * "first": { "href": "..." },
2650
+ * "prev": { "href": "..." },
2651
+ * "next": { "href": "..." },
2652
+ * "last": { "href": "..." }
2653
+ * },
2654
+ * "page": { "size": 20, "totalElements": 100, "totalPages": 5, "number": 1 }
2655
+ * }
2656
+ * ```
2657
+ *
2658
+ * Two HAL quirks are absorbed here on top of the inherited dot-path
2659
+ * traversal:
2660
+ *
2661
+ * - **`page.number` is 0-indexed** — the strategy adds 1 so the library
2662
+ * state stays 1-indexed (mirroring `SpringRequestStrategy`, which
2663
+ * subtracts 1 on the way out).
2664
+ * - **The collection key under `_embedded` is the resource rel name**
2665
+ * (e.g. `_embedded.users`), which cannot be known statically. The
2666
+ * default `data` path is plain `_embedded`; when it resolves to an
2667
+ * object rather than an array, the strategy picks the first array
2668
+ * value inside it. Consumers with multiple embedded rels can pin the
2669
+ * exact path via `IConfig.response` (e.g. `data: '_embedded.users'`).
2670
+ *
2671
+ * Default key paths are configured in `SpringResponseOptions`.
2672
+ *
2673
+ * @see https://docs.spring.io/spring-data/rest/reference/paging-and-sorting.html
1479
2674
  */
1480
- class UnsupportedFilterOperatorError extends Error {
1481
- constructor() {
1482
- super('Filter operators are only supported by the NestJS driver. Use addFilter() for Spatie.');
1483
- this.name = 'UnsupportedFilterOperatorError';
2675
+ class SpringResponseStrategy extends AbstractDotPathResponseStrategy {
2676
+ /**
2677
+ * Parse a Spring Data REST HAL response into a PaginatedCollection
2678
+ *
2679
+ * @param response - The raw API response body
2680
+ * @param options - The response key name configuration
2681
+ * @returns A typed PaginatedCollection instance
2682
+ */
2683
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2684
+ paginate(response, options) {
2685
+ const data = this._resolveData(response, options);
2686
+ const currentPage = this._resolveCurrentPage(response, options);
2687
+ const total = this.resolve(response, options.total);
2688
+ const perPage = this.resolve(response, options.perPage);
2689
+ const lastPage = this.resolve(response, options.lastPage);
2690
+ const from = this.resolveFrom(response, options, currentPage, perPage);
2691
+ const to = this.resolveTo(response, options, currentPage, perPage, total);
2692
+ const prevPageUrl = this.resolve(response, options.prevPageUrl);
2693
+ const nextPageUrl = this.resolve(response, options.nextPageUrl);
2694
+ const firstPageUrl = this.resolve(response, options.firstPageUrl);
2695
+ const lastPageUrl = this.resolve(response, options.lastPageUrl);
2696
+ return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl, nextPageUrl, lastPage, firstPageUrl, lastPageUrl);
2697
+ }
2698
+ /**
2699
+ * Resolve the 1-indexed current page from the 0-indexed `page.number`
2700
+ *
2701
+ * Falls back to page 1 when the path is missing entirely (defensive —
2702
+ * Spring always emits the `page` block on paged endpoints).
2703
+ *
2704
+ * @param response - The raw response object
2705
+ * @param options - The response key name configuration
2706
+ * @returns The 1-indexed current page number
2707
+ */
2708
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2709
+ _resolveCurrentPage(response, options) {
2710
+ const pageNumber = this.resolve(response, options.currentPage);
2711
+ return (pageNumber ?? 0) + 1;
2712
+ }
2713
+ /**
2714
+ * Resolve the data array from the HAL `_embedded` wrapper
2715
+ *
2716
+ * When the configured path resolves directly to an array (a consumer
2717
+ * pinned `data: '_embedded.users'`), it is used as-is. When it
2718
+ * resolves to an object (the default `_embedded` path), the first
2719
+ * array value inside it is used — Spring emits exactly one collection
2720
+ * rel per listing endpoint. An empty array is returned when nothing
2721
+ * matches (e.g. Spring omits `_embedded` on empty result sets).
2722
+ *
2723
+ * @param response - The raw response object
2724
+ * @param options - The response key name configuration
2725
+ * @returns The resolved data array (possibly empty)
2726
+ */
2727
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2728
+ _resolveData(response, options) {
2729
+ const raw = this.resolve(response, options.data);
2730
+ if (Array.isArray(raw)) {
2731
+ return raw;
2732
+ }
2733
+ if (raw && typeof raw === 'object') {
2734
+ const firstArray = Object.values(raw).find(value => Array.isArray(value));
2735
+ if (firstArray) {
2736
+ return firstArray;
2737
+ }
2738
+ }
2739
+ return [];
1484
2740
  }
1485
2741
  }
1486
2742
 
@@ -1764,6 +3020,11 @@ class StrapiResponseStrategy extends AbstractDotPathResponseStrategy {
1764
3020
  * `switch` blocks.
1765
3021
  */
1766
3022
  const DRIVERS = {
3023
+ [DriverEnum.DRF]: {
3024
+ createRequestStrategy: () => new DrfRequestStrategy(),
3025
+ createResponseStrategy: () => new DrfResponseStrategy(),
3026
+ createResponseOptions: (config) => new DrfResponseOptions(config)
3027
+ },
1767
3028
  [DriverEnum.JSON_API]: {
1768
3029
  createRequestStrategy: () => new JsonApiRequestStrategy(),
1769
3030
  createResponseStrategy: () => new JsonApiResponseStrategy(),
@@ -1779,16 +3040,31 @@ const DRIVERS = {
1779
3040
  createResponseStrategy: () => new NestjsResponseStrategy(),
1780
3041
  createResponseOptions: (config) => new NestjsResponseOptions(config)
1781
3042
  },
3043
+ [DriverEnum.NESTJSX_CRUD]: {
3044
+ createRequestStrategy: () => new NestjsxCrudRequestStrategy(),
3045
+ createResponseStrategy: () => new NestjsxCrudResponseStrategy(),
3046
+ createResponseOptions: (config) => new NestjsxCrudResponseOptions(config)
3047
+ },
1782
3048
  [DriverEnum.POSTGREST]: {
1783
3049
  createRequestStrategy: (mode) => new PostgrestRequestStrategy(mode),
1784
3050
  createResponseStrategy: () => new PostgrestResponseStrategy(),
1785
3051
  createResponseOptions: (config) => new ResponseOptions(config)
1786
3052
  },
3053
+ [DriverEnum.SIEVE]: {
3054
+ createRequestStrategy: () => new SieveRequestStrategy(),
3055
+ createResponseStrategy: () => new SieveResponseStrategy(),
3056
+ createResponseOptions: (config) => new SieveResponseOptions(config)
3057
+ },
1787
3058
  [DriverEnum.SPATIE]: {
1788
3059
  createRequestStrategy: () => new SpatieRequestStrategy(),
1789
3060
  createResponseStrategy: () => new SpatieResponseStrategy(),
1790
3061
  createResponseOptions: (config) => new ResponseOptions(config)
1791
3062
  },
3063
+ [DriverEnum.SPRING]: {
3064
+ createRequestStrategy: () => new SpringRequestStrategy(),
3065
+ createResponseStrategy: () => new SpringResponseStrategy(),
3066
+ createResponseOptions: (config) => new SpringResponseOptions(config)
3067
+ },
1792
3068
  [DriverEnum.STRAPI]: {
1793
3069
  createRequestStrategy: () => new StrapiRequestStrategy(),
1794
3070
  createResponseStrategy: () => new StrapiResponseStrategy(),
@@ -3149,5 +4425,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImpor
3149
4425
  * Generated bundle index. Do not edit.
3150
4426
  */
3151
4427
 
3152
- 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 };
4428
+ export { DrfRequestStrategy, DrfResponseStrategy, 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, NestjsxCrudRequestStrategy, NestjsxCrudResponseStrategy, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationModeEnum, PaginationNotSyncedError, PaginationService, PostgrestRequestStrategy, PostgrestResponseStrategy, SieveRequestStrategy, SieveResponseStrategy, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, SpringRequestStrategy, SpringResponseStrategy, StrapiRequestStrategy, StrapiResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, buildNgQubeeProviders, provideNgQubee, provideNgQubeeInstance, readHeader };
3153
4429
  //# sourceMappingURL=ng-qubee.mjs.map