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.
@@ -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
- class UnselectableModelError extends Error {
182
- constructor(model) {
183
- super(`Unselectable Model: the selected model (${model}) is not present neither in the "model" property, nor in the includes object.`);
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
- * Parse a flat Laravel pagination response into a PaginatedCollection
1994
+ * Strapi-native names of the four hardcoded query keys
1436
1995
  *
1437
- * @param response - The raw API response object
1438
- * @param options - The response key name configuration
1439
- * @returns A typed PaginatedCollection instance
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1442
- paginate(response, options) {
1443
- 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]);
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