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.
- package/README.md +6 -2
- package/fesm2022/ng-qubee.mjs +1387 -111
- package/fesm2022/ng-qubee.mjs.map +1 -1
- package/package.json +10 -1
- package/types/ng-qubee.d.ts +790 -15
package/fesm2022/ng-qubee.mjs
CHANGED
|
@@ -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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
309
|
-
*
|
|
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
|
|
312
|
-
* @param
|
|
313
|
-
* @returns The
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
865
|
-
*
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
* `
|
|
891
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
*
|
|
1727
|
+
* Response strategy for the @nestjsx/crud driver
|
|
907
1728
|
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
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
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
*
|
|
2574
|
+
* Spring-native name of the hardcoded page-size query key
|
|
1463
2575
|
*
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
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
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
-
*
|
|
2642
|
+
* Response strategy for the Spring Data REST driver
|
|
1476
2643
|
*
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
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
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|