ng-qubee 3.2.0 → 3.4.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.
@@ -49,7 +49,26 @@ declare enum DriverEnum {
49
49
  JSON_API = "json-api",
50
50
  LARAVEL = "laravel",
51
51
  NESTJS = "nestjs",
52
- SPATIE = "spatie"
52
+ POSTGREST = "postgrest",
53
+ SPATIE = "spatie",
54
+ STRAPI = "strapi"
55
+ }
56
+
57
+ /**
58
+ * Enum representing the wire-level pagination mechanism
59
+ *
60
+ * `QUERY` (default) — the request strategy emits `limit` and `offset` (or
61
+ * equivalent) query parameters on the URL.
62
+ *
63
+ * `RANGE` — the request strategy omits URL-based pagination and the
64
+ * consumer instead applies HTTP request headers returned by
65
+ * `NgQubeeService.paginationHeaders()`. Currently honoured only by the
66
+ * PostgREST driver, which maps it to `Range-Unit: items` + `Range: 0-9`.
67
+ * Other drivers ignore the setting.
68
+ */
69
+ declare enum PaginationModeEnum {
70
+ QUERY = "query",
71
+ RANGE = "range"
53
72
  }
54
73
 
55
74
  /**
@@ -135,6 +154,12 @@ interface IQueryBuilderConfig {
135
154
  interface IConfig {
136
155
  /** The pagination driver to use */
137
156
  driver: DriverEnum;
157
+ /**
158
+ * Wire-level pagination mechanism. Defaults to `PaginationModeEnum.QUERY`
159
+ * when omitted. Currently honoured only by the PostgREST driver; other
160
+ * drivers ignore it.
161
+ */
162
+ pagination?: PaginationModeEnum;
138
163
  /** Custom key names for request query parameters */
139
164
  request?: IQueryBuilderConfig;
140
165
  /** Custom key names for response field mapping */
@@ -158,6 +183,11 @@ declare class NgQubeeModule {
158
183
  * Build the core provider list shared by `provideNgQubee()` and
159
184
  * `NgQubeeModule.forRoot()`
160
185
  *
186
+ * Looks up the driver definition from the registry and calls its three
187
+ * factories — request strategy, response strategy, response options.
188
+ * Adding a driver means adding one entry to `DRIVERS`; this function
189
+ * does not change.
190
+ *
161
191
  * Exposes the driver, strategies, and options via injection tokens so that
162
192
  * consumers can request a component-scoped instance of the services through
163
193
  * `provideNgQubeeInstance()`.
@@ -240,17 +270,27 @@ declare function provideNgQubee(config: IConfig): EnvironmentProviders;
240
270
  declare function provideNgQubeeInstance(): Provider[];
241
271
 
242
272
  /**
243
- * Enum representing the available filter operators for the NestJS driver
273
+ * Enum representing the available filter operators for explicit operator
274
+ * filters
244
275
  *
245
- * These operators map to the nestjs-paginate filter syntax:
246
- * `filter.field=$operator:value`
276
+ * NestJS encodes these with the `$` prefix at the wire level
277
+ * (`filter.field=$operator:value`); PostgREST translates them to its own
278
+ * prefix notation (`col=eq.val`, `col=is.null`, etc.). The enum values are
279
+ * intentionally the NestJS form; each driver's request strategy is
280
+ * responsible for mapping them into its own shape.
281
+ *
282
+ * `FTS`, `PLFTS`, `PHFTS`, `WFTS` are PostgREST-native full-text search
283
+ * variants; they throw `UnsupportedFilterOperatorError` on every other
284
+ * driver that does not recognise them.
247
285
  *
248
286
  * @see https://github.com/ppetzold/nestjs-paginate
287
+ * @see https://postgrest.org/en/stable/api.html#operators
249
288
  */
250
289
  declare enum FilterOperatorEnum {
251
290
  BTW = "$btw",
252
291
  CONTAINS = "$contains",
253
292
  EQ = "$eq",
293
+ FTS = "$fts",
254
294
  GT = "$gt",
255
295
  GTE = "$gte",
256
296
  ILIKE = "$ilike",
@@ -259,7 +299,10 @@ declare enum FilterOperatorEnum {
259
299
  LTE = "$lte",
260
300
  NOT = "$not",
261
301
  NULL = "$null",
262
- SW = "$sw"
302
+ PHFTS = "$phfts",
303
+ PLFTS = "$plfts",
304
+ SW = "$sw",
305
+ WFTS = "$wfts"
263
306
  }
264
307
 
265
308
  declare enum SortEnum {
@@ -340,6 +383,33 @@ interface IQueryBuilderState {
340
383
  sorts: ISort[];
341
384
  }
342
385
 
386
+ /**
387
+ * Capability flags declared by an `IRequestStrategy`
388
+ *
389
+ * Single source of truth for what a driver supports. Replaces the inline
390
+ * `DriverEnum` allowlists previously scattered across `NgQubeeService`'s
391
+ * `_assertDriver(...)` call sites.
392
+ *
393
+ * Adding a new driver means defining one of these objects on the new
394
+ * strategy class — `NgQubeeService` does not need to be touched.
395
+ */
396
+ interface IStrategyCapabilities {
397
+ /** Per-model field selection (e.g. JSON:API `fields[type]=col1,col2`) */
398
+ readonly fields: boolean;
399
+ /** Simple key-value filters (e.g. `filter.status=active`) */
400
+ readonly filters: boolean;
401
+ /** Related-resource includes (e.g. JSON:API/Spatie `include=author`) */
402
+ readonly includes: boolean;
403
+ /** Filters with explicit operators (e.g. NestJS `$gte`, PostgREST `gte.`) */
404
+ readonly operatorFilters: boolean;
405
+ /** Global full-text search via a single term (NestJS `search=…`) */
406
+ readonly search: boolean;
407
+ /** Flat column-list selection (NestJS / PostgREST `select=col1,col2`) */
408
+ readonly select: boolean;
409
+ /** Sort ordering on one or more fields */
410
+ readonly sort: boolean;
411
+ }
412
+
343
413
  /**
344
414
  * Resolved query parameter key names with defaults applied
345
415
  *
@@ -367,6 +437,14 @@ declare class QueryBuilderOptions {
367
437
  * in the format expected by the corresponding backend.
368
438
  */
369
439
  interface IRequestStrategy {
440
+ /**
441
+ * Capability flags declared by this driver
442
+ *
443
+ * Read by `NgQubeeService` to gate feature methods (e.g. `addFilter`)
444
+ * without hardcoding `DriverEnum` checks. Each strategy returns a
445
+ * static, immutable capability map.
446
+ */
447
+ readonly capabilities: IStrategyCapabilities;
370
448
  /**
371
449
  * Build a URI string from the given query builder state
372
450
  *
@@ -375,6 +453,23 @@ interface IRequestStrategy {
375
453
  * @returns The composed URI string
376
454
  */
377
455
  buildUri(state: IQueryBuilderState, options: QueryBuilderOptions): string;
456
+ /**
457
+ * Compute HTTP request headers carrying pagination metadata
458
+ *
459
+ * Honoured only by drivers that support header-based pagination (the
460
+ * PostgREST driver configured with `PaginationModeEnum.RANGE`). All
461
+ * other drivers should return `null` — which is also the default when
462
+ * a driver does not override this method.
463
+ *
464
+ * When the method returns a non-null object, `NgQubeeService.buildUri`
465
+ * is expected to have already omitted URL-level pagination params for
466
+ * that request; the consumer then merges these headers into the HTTP
467
+ * call so the server knows the requested range.
468
+ *
469
+ * @param state - The current query builder state
470
+ * @returns A map of header name → value, or `null` when not applicable
471
+ */
472
+ buildPaginationHeaders?(state: IQueryBuilderState): Record<string, string> | null;
378
473
  /**
379
474
  * Assert that the given limit value is valid for this driver
380
475
  *
@@ -654,13 +749,17 @@ declare class NgQubeeService {
654
749
  uri$: Observable<string>;
655
750
  constructor(_nestService: NestService, requestStrategy: IRequestStrategy, driver: DriverEnum, options?: QueryBuilderOptions);
656
751
  /**
657
- * Assert that the active driver is one of the allowed drivers
752
+ * Assert that the active strategy declares support for a capability
658
753
  *
659
- * @param allowed - The allowed drivers
660
- * @param error - The error to throw if the driver is not allowed
661
- * @throws The provided error if the active driver is not in the allowed list
754
+ * Reads from `IRequestStrategy.capabilities` rather than the driver
755
+ * enum so adding a new driver only requires declaring its capability
756
+ * map this method does not change.
757
+ *
758
+ * @param flag - The capability key to check
759
+ * @param error - The error to throw if the capability is unsupported
760
+ * @throws The provided error if the active strategy lacks the capability
662
761
  */
663
- private _assertDriver;
762
+ private _assertCapability;
664
763
  /**
665
764
  * Add fields to the select statement for the given model (JSON:API and Spatie only)
666
765
  *
@@ -671,7 +770,7 @@ declare class NgQubeeService {
671
770
  */
672
771
  addFields(model: string, fields: string[]): this;
673
772
  /**
674
- * Add a filter with the given value(s) (JSON:API, NestJS, and Spatie)
773
+ * Add a filter with the given value(s) (JSON:API, NestJS, PostgREST, and Spatie)
675
774
  *
676
775
  * Produces: `filter[field]=value` (JSON:API / Spatie) or `filter.field=value` (NestJS)
677
776
  *
@@ -682,7 +781,7 @@ declare class NgQubeeService {
682
781
  */
683
782
  addFilter(field: string, ...values: (string | number | boolean)[]): this;
684
783
  /**
685
- * Add a filter with an explicit operator (NestJS only)
784
+ * Add a filter with an explicit operator (NestJS and PostgREST)
686
785
  *
687
786
  * Produces: `filter.field=$operator:value`
688
787
  *
@@ -702,7 +801,7 @@ declare class NgQubeeService {
702
801
  */
703
802
  addIncludes(...models: string[]): this;
704
803
  /**
705
- * Add flat field selection (NestJS only)
804
+ * Add flat field selection (NestJS and PostgREST)
706
805
  *
707
806
  * Produces: `select=col1,col2`
708
807
  *
@@ -712,7 +811,7 @@ declare class NgQubeeService {
712
811
  */
713
812
  addSelect(...fields: string[]): this;
714
813
  /**
715
- * Add a field with a sort criteria (JSON:API, NestJS, and Spatie)
814
+ * Add a field with a sort criteria (JSON:API, NestJS, PostgREST, and Spatie)
716
815
  *
717
816
  * @param field - Field to use for sorting
718
817
  * @param {SortEnum} order - A value from the SortEnum enumeration
@@ -756,7 +855,7 @@ declare class NgQubeeService {
756
855
  */
757
856
  deleteFieldsByModel(model: string, ...fields: string[]): this;
758
857
  /**
759
- * Remove given filters from the query builder state (JSON:API, NestJS, and Spatie)
858
+ * Remove given filters from the query builder state (JSON:API, NestJS, PostgREST, and Spatie)
760
859
  *
761
860
  * @param {string[]} filters - Filters to remove
762
861
  * @returns {this}
@@ -772,7 +871,7 @@ declare class NgQubeeService {
772
871
  */
773
872
  deleteIncludes(...includes: string[]): this;
774
873
  /**
775
- * Remove operator filters by field name (NestJS only)
874
+ * Remove operator filters by field name (NestJS and PostgREST)
776
875
  *
777
876
  * @param {string[]} fields - Field names of operator filters to remove
778
877
  * @returns {this}
@@ -787,7 +886,7 @@ declare class NgQubeeService {
787
886
  */
788
887
  deleteSearch(): this;
789
888
  /**
790
- * Remove flat field selections from the query builder state (NestJS only)
889
+ * Remove flat field selections from the query builder state (NestJS and PostgREST)
791
890
  *
792
891
  * @param {string[]} fields - Fields to remove from selection
793
892
  * @returns {this}
@@ -795,7 +894,7 @@ declare class NgQubeeService {
795
894
  */
796
895
  deleteSelect(...fields: string[]): this;
797
896
  /**
798
- * Remove sort rules from the query builder state (JSON:API, NestJS, and Spatie)
897
+ * Remove sort rules from the query builder state (JSON:API, NestJS, PostgREST, and Spatie)
799
898
  *
800
899
  * @param sorts - Fields used for sorting to remove
801
900
  * @returns {this}
@@ -870,6 +969,19 @@ declare class NgQubeeService {
870
969
  * @returns {this}
871
970
  */
872
971
  nextPage(): this;
972
+ /**
973
+ * HTTP request headers the active driver wants the consumer to apply
974
+ *
975
+ * Returns `null` for drivers that pass all pagination metadata on the
976
+ * URL (Laravel, Spatie, JSON:API, NestJS, and PostgREST in its default
977
+ * QUERY mode). Returns a map of header name → value when the active
978
+ * driver uses HTTP headers instead — today, only the PostgREST driver
979
+ * configured with `PaginationModeEnum.RANGE`, which yields
980
+ * `{ 'Range-Unit': 'items', 'Range': 'from-to' }`.
981
+ *
982
+ * @returns Map of headers to apply to the HTTP request, or `null` when not needed
983
+ */
984
+ paginationHeaders(): Record<string, string> | null;
873
985
  /**
874
986
  * Navigate to the previous page
875
987
  *
@@ -939,6 +1051,27 @@ declare class NgQubeeService {
939
1051
  static ɵprov: i0.ɵɵInjectableDeclaration<NgQubeeService>;
940
1052
  }
941
1053
 
1054
+ /**
1055
+ * A minimal bag of HTTP response headers that a response strategy can read
1056
+ * by name.
1057
+ *
1058
+ * Accepts anything that exposes a `.get(name): string | null` method
1059
+ * (Angular's `HttpHeaders`, the DOM `Headers` class) or a plain object
1060
+ * keyed by header name. Consumers should not need to convert between them.
1061
+ */
1062
+ type HeaderBag = {
1063
+ get(name: string): string | null;
1064
+ } | Record<string, string | null | undefined>;
1065
+ /**
1066
+ * Read a header value by name from a `HeaderBag`, regardless of whether the
1067
+ * bag exposes a `.get()` accessor or plain property access.
1068
+ *
1069
+ * @param bag - The header bag to read from
1070
+ * @param name - The header name (case-sensitivity follows the underlying bag)
1071
+ * @returns The header value, or `null` if absent or the bag itself is falsy
1072
+ */
1073
+ declare function readHeader(bag: HeaderBag | null | undefined, name: string): string | null;
1074
+
942
1075
  /**
943
1076
  * Resolved response field key names with defaults applied
944
1077
  *
@@ -979,11 +1112,16 @@ interface IResponseStrategy {
979
1112
  /**
980
1113
  * Parse a raw API response into a typed PaginatedCollection
981
1114
  *
982
- * @param response - The raw API response object
1115
+ * @param response - The raw API response object (body). For drivers that
1116
+ * emit a bare array body (e.g. PostgREST), pass the array here.
983
1117
  * @param options - The response key name configuration
1118
+ * @param headers - Optional HTTP response headers. Drivers that carry
1119
+ * pagination metadata in headers (PostgREST's `Content-Range`) read from
1120
+ * this bag; body-only drivers ignore it. Accepts anything with a `.get()`
1121
+ * accessor (`HttpHeaders`, `Headers`) or a plain `Record<string, string>`.
984
1122
  * @returns A typed PaginatedCollection instance
985
1123
  */
986
- paginate<T extends IPaginatedObject>(response: Record<string, unknown>, options: ResponseOptions): PaginatedCollection<T>;
1124
+ paginate<T extends IPaginatedObject>(response: Record<string, unknown>, options: ResponseOptions, headers?: HeaderBag): PaginatedCollection<T>;
987
1125
  }
988
1126
 
989
1127
  declare class PaginationService {
@@ -1015,16 +1153,44 @@ declare class PaginationService {
1015
1153
  * Server-emitted `0` (empty collection edge case) and absent fields are
1016
1154
  * treated as "no useful info" and leave `isLastPageKnown: false`.
1017
1155
  *
1018
- * @param response - The raw API response object
1156
+ * @param response - The raw API response body. For drivers that emit a
1157
+ * bare array (PostgREST), pass the array.
1158
+ * @param headers - Optional HTTP response headers. Required by the
1159
+ * PostgREST driver (reads `Content-Range` for pagination metadata);
1160
+ * body-only drivers ignore it. Accepts Angular's `HttpHeaders`, the
1161
+ * native `Headers` class, or a plain `Record<string, string>`.
1019
1162
  * @returns A typed PaginatedCollection instance
1020
1163
  */
1021
1164
  paginate<T extends IPaginatedObject>(response: {
1022
1165
  [key: string]: any;
1023
- }): PaginatedCollection<T>;
1166
+ }, headers?: HeaderBag): PaginatedCollection<T>;
1024
1167
  static ɵfac: i0.ɵɵFactoryDeclaration<PaginationService, never>;
1025
1168
  static ɵprov: i0.ɵɵInjectableDeclaration<PaginationService>;
1026
1169
  }
1027
1170
 
1171
+ /**
1172
+ * Thrown when a filter operator receives a value array of the wrong shape
1173
+ *
1174
+ * Some operators have arity or type constraints that the library enforces
1175
+ * at call time so misuse fails loudly instead of silently emitting invalid
1176
+ * server requests:
1177
+ *
1178
+ * - `BTW` requires exactly two values (min, max).
1179
+ * - `NULL` requires exactly one boolean value (`true` for `IS NULL`,
1180
+ * `false` for `IS NOT NULL`).
1181
+ *
1182
+ * Operators with looser shape rules leave validation to the server; this
1183
+ * error is reserved for cases where the library itself can detect the
1184
+ * problem unambiguously from the call site.
1185
+ */
1186
+ declare class InvalidFilterOperatorValueError extends Error {
1187
+ /**
1188
+ * @param operator - The operator that rejected the values
1189
+ * @param reason - Short human-readable explanation of the constraint
1190
+ */
1191
+ constructor(operator: FilterOperatorEnum, reason: string);
1192
+ }
1193
+
1028
1194
  /**
1029
1195
  * Thrown when a limit value does not satisfy the active driver's constraints
1030
1196
  *
@@ -1193,185 +1359,264 @@ declare const NG_QUBEE_RESPONSE_STRATEGY: InjectionToken<IResponseStrategy>;
1193
1359
  declare const NG_QUBEE_RESPONSE_OPTIONS: InjectionToken<ResponseOptions>;
1194
1360
 
1195
1361
  /**
1196
- * Request strategy for the JSON:API driver
1362
+ * Base class for request strategies
1197
1363
  *
1198
- * Generates URIs in the JSON:API format:
1199
- * - Fields: `fields[articles]=title,body&fields[people]=name`
1200
- * - Filters: `filter[status]=active`
1201
- * - Includes: `include=author,comments.author`
1202
- * - Pagination: `page[number]=1&page[size]=15`
1203
- * - Sort: `sort=-created_at,name` (- prefix = DESC)
1364
+ * Concentrates the glue every concrete strategy used to copy: the
1365
+ * resource-required guard, the `?`/`&` URL composition, and the default
1366
+ * positive-integer `validateLimit`. Concrete strategies override only
1367
+ * the parts that differ — the per-driver wire format goes into a single
1368
+ * `protected parts(state, options): string[]` method that returns the
1369
+ * ordered query-string segments the base then joins.
1204
1370
  *
1205
- * @see https://jsonapi.org/format/
1371
+ * Drivers that need a non-default `validateLimit` (e.g. NestJS, which
1372
+ * accepts `-1` as a fetch-all sentinel) override that method directly.
1206
1373
  */
1207
- declare class JsonApiRequestStrategy implements IRequestStrategy {
1374
+ declare abstract class AbstractRequestStrategy implements IRequestStrategy {
1208
1375
  /**
1209
- * Accumulator for composing the URI string
1376
+ * Capability declaration for this driver
1377
+ *
1378
+ * Concrete strategies must provide a static, immutable capability map
1379
+ * so `NgQubeeService._assertCapability(...)` can read it.
1210
1380
  */
1211
- private _uri;
1381
+ abstract readonly capabilities: IStrategyCapabilities;
1212
1382
  /**
1213
- * Build a URI string from the given state using the JSON:API format
1383
+ * Compose the full request URI from the given state
1384
+ *
1385
+ * Template method: validates the resource, computes the base path,
1386
+ * delegates the per-driver query-string segments to `parts(...)`, and
1387
+ * joins them with the conventional `?`/`&` separators.
1214
1388
  *
1215
1389
  * @param state - The current query builder state
1216
1390
  * @param options - The query parameter key name configuration
1217
1391
  * @returns The composed URI string
1218
- * @throws Error if resource is not set
1392
+ * @throws Error if the resource is not set
1219
1393
  */
1220
1394
  buildUri(state: IQueryBuilderState, options: QueryBuilderOptions): string;
1221
1395
  /**
1222
- * Validate that the given limit is accepted by the JSON:API driver
1396
+ * Validate that a limit value is acceptable for this driver
1223
1397
  *
1224
- * The JSON:API specification leaves pagination semantics to the server and
1225
- * does not define a "fetch all" sentinel, so only positive integers are
1226
- * accepted.
1398
+ * Default policy: positive integer. Drivers that recognise a sentinel
1399
+ * (NestJS treats `-1` as "fetch all") override this method.
1227
1400
  *
1228
1401
  * @param limit - The limit value to validate
1229
1402
  * @throws {InvalidLimitError} If the value is not a positive integer
1230
1403
  */
1231
1404
  validateLimit(limit: number): void;
1232
1405
  /**
1233
- * Parse and append field selection parameters
1406
+ * Per-driver query-string segments, in emission order
1234
1407
  *
1235
- * Validates that each field model exists either as the main resource
1236
- * or in the includes list. Fields are grouped by type in bracket notation.
1408
+ * Each entry is one `key=value` (or `key=v1&key=v2` for compound
1409
+ * params like PostgREST's `BTW`). Empty arrays are valid and produce
1410
+ * a URI containing only the resource path.
1237
1411
  *
1238
1412
  * @param state - The current query builder state
1239
1413
  * @param options - The query parameter key name configuration
1240
- * @returns The generated field selection parameter string
1241
- * @throws Error if resource is required but not set
1242
- * @throws UnselectableModelError if a field model is not in resource or includes
1414
+ * @returns Ordered list of query-string fragments
1243
1415
  */
1244
- private _parseFields;
1416
+ protected abstract parts(state: IQueryBuilderState, options: QueryBuilderOptions): string[];
1245
1417
  /**
1246
- * Parse and append filter parameters
1418
+ * Throw if the resource is not set on the state
1247
1419
  *
1248
- * Generates filter parameters in bracket notation: `filter[key]=value1,value2`
1420
+ * Centralises the message that was previously copy-pasted across four
1421
+ * of the five concrete strategies.
1249
1422
  *
1250
1423
  * @param state - The current query builder state
1251
- * @param options - The query parameter key name configuration
1252
- * @returns The generated filter parameter string
1424
+ * @throws Error if `state.resource` is empty
1425
+ */
1426
+ protected assertResource(state: IQueryBuilderState): void;
1427
+ /**
1428
+ * Compute the base path (no query string)
1429
+ *
1430
+ * @param state - The current query builder state
1431
+ * @returns The base URI without the query separator (e.g. `/users` or `https://api.example.com/users`)
1253
1432
  */
1254
- private _parseFilters;
1433
+ protected baseUri(state: IQueryBuilderState): string;
1255
1434
  /**
1256
- * Parse and append include parameters
1435
+ * Glue the base URI and the per-driver query-string segments
1257
1436
  *
1258
- * Generates: `include=author,comments.author`
1437
+ * Returns the bare base when no segments were emitted (e.g. PostgREST
1438
+ * in RANGE mode with no filters), otherwise joins with `?` + `&`.
1439
+ *
1440
+ * @param base - The base URI from `_baseUri`
1441
+ * @param segments - The query-string fragments from `parts(...)`
1442
+ * @returns The full URI
1443
+ */
1444
+ protected join(base: string, segments: string[]): string;
1445
+ }
1446
+
1447
+ /**
1448
+ * Request strategy for the JSON:API driver
1449
+ *
1450
+ * Generates URIs in the JSON:API format:
1451
+ * - Fields: `fields[articles]=title,body&fields[people]=name`
1452
+ * - Filters: `filter[status]=active`
1453
+ * - Includes: `include=author,comments.author`
1454
+ * - Pagination: `page[number]=1&page[size]=15`
1455
+ * - Sort: `sort=-created_at,name` (- prefix = DESC)
1456
+ *
1457
+ * @see https://jsonapi.org/format/
1458
+ */
1459
+ declare class JsonApiRequestStrategy extends AbstractRequestStrategy {
1460
+ /**
1461
+ * Filters, sorts, includes, per-model fields — same shape as Spatie
1462
+ * but with bracket-style pagination
1463
+ */
1464
+ readonly capabilities: IStrategyCapabilities;
1465
+ /**
1466
+ * Emit JSON:API-format query-string segments in canonical order:
1467
+ * include → fields → filters → pagination → sort
1259
1468
  *
1260
1469
  * @param state - The current query builder state
1261
1470
  * @param options - The query parameter key name configuration
1262
- * @returns The generated include parameter string
1471
+ * @returns Ordered query-string fragments
1263
1472
  */
1264
- private _parseIncludes;
1473
+ protected parts(state: IQueryBuilderState, options: QueryBuilderOptions): string[];
1265
1474
  /**
1266
- * Parse and append pagination parameters in JSON:API bracket notation
1267
- *
1268
- * Generates: `page[number]=1&page[size]=15`
1475
+ * Append per-type field selection in bracket notation
1269
1476
  *
1270
1477
  * @param state - The current query builder state
1271
1478
  * @param options - The query parameter key name configuration
1272
- * @returns The generated pagination parameter string
1479
+ * @param out - The accumulator the caller joins into the URI
1480
+ * @throws Error if the resource is missing from the fields object
1481
+ * @throws UnselectableModelError if a field type is not the resource or in includes
1273
1482
  */
1274
- private _parsePagination;
1483
+ private _appendFields;
1275
1484
  /**
1276
- * Parse and append sort parameters
1485
+ * Append filter parameters in bracket notation: `filter[key]=value`
1277
1486
  *
1278
- * Generates: `sort=-field1,field2` where `-` prefix indicates DESC order
1487
+ * @param state - The current query builder state
1488
+ * @param options - The query parameter key name configuration
1489
+ * @param out - The accumulator the caller joins into the URI
1490
+ */
1491
+ private _appendFilters;
1492
+ /**
1493
+ * Append include parameter as `include=author,comments.author`
1279
1494
  *
1280
1495
  * @param state - The current query builder state
1281
1496
  * @param options - The query parameter key name configuration
1282
- * @returns The generated sort parameter string
1497
+ * @param out - The accumulator the caller joins into the URI
1283
1498
  */
1284
- private _parseSort;
1499
+ private _appendIncludes;
1285
1500
  /**
1286
- * Determine the appropriate URI prefix based on the current accumulator state
1501
+ * Append JSON:API bracket pagination as `page[number]=1&page[size]=15`
1287
1502
  *
1288
- * Returns the full base path with `?` for the first parameter,
1289
- * or `&` for subsequent parameters.
1503
+ * `qs.stringify` already returns the two segments joined with `&`, so we
1504
+ * push the whole string as one accumulator entry — `_join` will glue
1505
+ * it onto the rest with the same separator.
1290
1506
  *
1291
1507
  * @param state - The current query builder state
1292
- * @returns The prefix string to prepend to the next parameter
1508
+ * @param options - The query parameter key name configuration
1509
+ * @param out - The accumulator the caller joins into the URI
1293
1510
  */
1294
- private _prepend;
1511
+ private _appendPagination;
1512
+ /**
1513
+ * Append sort parameter as `sort=-field1,field2` (`-` prefix = DESC)
1514
+ *
1515
+ * @param state - The current query builder state
1516
+ * @param options - The query parameter key name configuration
1517
+ * @param out - The accumulator the caller joins into the URI
1518
+ */
1519
+ private _appendSort;
1295
1520
  }
1296
1521
 
1297
1522
  /**
1298
- * Response strategy for the JSON:API driver
1523
+ * Base class for response strategies whose pagination metadata lives at
1524
+ * dot-notation paths inside the response body
1299
1525
  *
1300
- * Parses JSON:API pagination responses:
1301
- * ```json
1302
- * {
1303
- * "data": [...],
1304
- * "meta": {
1305
- * "current-page": 1,
1306
- * "per-page": 10,
1307
- * "total": 100,
1308
- * "page-count": 10,
1309
- * "from": 1,
1310
- * "to": 10
1311
- * },
1312
- * "links": {
1313
- * "first": "url",
1314
- * "prev": "url",
1315
- * "next": "url",
1316
- * "last": "url"
1317
- * }
1318
- * }
1319
- * ```
1526
+ * JSON:API and NestJS share an identical body-traversal algorithm: the
1527
+ * total / current-page / etc. live at nested keys like `meta.total`, and
1528
+ * `from`/`to` are either present directly or must be derived from
1529
+ * `currentPage` × `perPage`. Both strategies were duplicating this
1530
+ * verbatim before this base existed; concrete classes now extend and
1531
+ * provide only the docstring describing their driver's specific path
1532
+ * conventions (see `JsonApiResponseStrategy`, `NestjsResponseStrategy`).
1320
1533
  *
1321
- * @see https://jsonapi.org/format/
1534
+ * Drivers whose pagination metadata travels via HTTP headers (PostgREST)
1535
+ * or whose body has a flat shape with no dot paths (Laravel, Spatie) do
1536
+ * not extend this class — they implement `IResponseStrategy` directly.
1322
1537
  */
1323
- declare class JsonApiResponseStrategy implements IResponseStrategy {
1538
+ declare abstract class AbstractDotPathResponseStrategy implements IResponseStrategy {
1324
1539
  /**
1325
- * Parse a JSON:API pagination response into a PaginatedCollection
1326
- *
1327
- * Supports dot-notation key paths for accessing nested values.
1328
- * Computes `from` and `to` from `currentPage` and `perPage` when
1329
- * they are not directly available in the response.
1540
+ * Parse a nested-envelope pagination response into a PaginatedCollection
1330
1541
  *
1331
1542
  * @param response - The raw API response object
1332
- * @param options - The response key name configuration
1543
+ * @param options - The response key name configuration (dot-notation paths supported)
1333
1544
  * @returns A typed PaginatedCollection instance
1334
1545
  */
1335
1546
  paginate<T extends IPaginatedObject>(response: Record<string, any>, options: ResponseOptions): PaginatedCollection<T>;
1336
1547
  /**
1337
1548
  * Resolve a value from a response object using a dot-notation path
1338
1549
  *
1339
- * Supports both flat keys ('data') and nested paths ('meta.current-page').
1550
+ * Supports both flat keys (`'data'`) and nested paths (`'meta.totalItems'`).
1340
1551
  *
1341
1552
  * @param response - The raw response object
1342
1553
  * @param path - The dot-notation path to resolve
1343
- * @returns The resolved value, or undefined if not found
1554
+ * @returns The resolved value, or undefined if any segment is missing
1344
1555
  */
1345
- private _resolve;
1556
+ protected resolve(response: Record<string, any>, path: string): unknown;
1346
1557
  /**
1347
1558
  * Resolve the "from" index value
1348
1559
  *
1349
- * If the path resolves to a value in the response, use it.
1350
- * Otherwise, compute it from currentPage and perPage:
1351
- * `(currentPage - 1) * perPage + 1`
1560
+ * If `options.from` resolves to a value in the response, use it.
1561
+ * Otherwise compute `(currentPage - 1) * perPage + 1` when both are known.
1352
1562
  *
1353
1563
  * @param response - The raw response object
1354
1564
  * @param options - The response key name configuration
1355
1565
  * @param currentPage - The current page number
1356
1566
  * @param perPage - The number of items per page
1357
- * @returns The computed "from" index
1567
+ * @returns The "from" index, or `undefined` when neither path nor inputs suffice
1358
1568
  */
1359
- private _resolveFrom;
1569
+ protected resolveFrom(response: Record<string, any>, options: ResponseOptions, currentPage: number, perPage?: number): number | undefined;
1360
1570
  /**
1361
1571
  * Resolve the "to" index value
1362
1572
  *
1363
- * If the path resolves to a value in the response, use it.
1364
- * Otherwise, compute it from currentPage, perPage, and total:
1365
- * `Math.min(currentPage * perPage, total)`
1573
+ * If `options.to` resolves to a value in the response, use it.
1574
+ * Otherwise compute `Math.min(currentPage * perPage, total)` when all
1575
+ * three are known.
1366
1576
  *
1367
1577
  * @param response - The raw response object
1368
1578
  * @param options - The response key name configuration
1369
1579
  * @param currentPage - The current page number
1370
1580
  * @param perPage - The number of items per page
1371
1581
  * @param total - The total number of items
1372
- * @returns The computed "to" index
1582
+ * @returns The "to" index, or `undefined` when neither path nor inputs suffice
1373
1583
  */
1374
- private _resolveTo;
1584
+ protected resolveTo(response: Record<string, any>, options: ResponseOptions, currentPage: number, perPage?: number, total?: number): number | undefined;
1585
+ }
1586
+
1587
+ /**
1588
+ * Response strategy for the JSON:API driver
1589
+ *
1590
+ * Parses JSON:API pagination responses:
1591
+ * ```json
1592
+ * {
1593
+ * "data": [...],
1594
+ * "meta": {
1595
+ * "current-page": 1,
1596
+ * "per-page": 10,
1597
+ * "total": 100,
1598
+ * "page-count": 10,
1599
+ * "from": 1,
1600
+ * "to": 10
1601
+ * },
1602
+ * "links": {
1603
+ * "first": "url",
1604
+ * "prev": "url",
1605
+ * "next": "url",
1606
+ * "last": "url"
1607
+ * }
1608
+ * }
1609
+ * ```
1610
+ *
1611
+ * Default key paths are configured in `JsonApiResponseOptions`. The
1612
+ * traversal algorithm (dot-notation resolution + computed `from`/`to`) is
1613
+ * inherited from `AbstractDotPathResponseStrategy`; this class exists so
1614
+ * `DriverEnum.JSON_API` resolves to a distinct identity at the DI layer
1615
+ * even though the parsing logic is shared with NestJS.
1616
+ *
1617
+ * @see https://jsonapi.org/format/
1618
+ */
1619
+ declare class JsonApiResponseStrategy extends AbstractDotPathResponseStrategy {
1375
1620
  }
1376
1621
 
1377
1622
  /**
@@ -1382,26 +1627,19 @@ declare class JsonApiResponseStrategy implements IResponseStrategy {
1382
1627
  *
1383
1628
  * Filters, sorts, fields, includes, search, and select in state are ignored.
1384
1629
  */
1385
- declare class LaravelRequestStrategy implements IRequestStrategy {
1630
+ declare class LaravelRequestStrategy extends AbstractRequestStrategy {
1386
1631
  /**
1387
- * Build a pagination-only URI from the given state
1388
- *
1389
- * @param state - The current query builder state
1390
- * @param options - The query parameter key name configuration
1391
- * @returns The composed URI string
1392
- * @throws Error if resource is not set
1632
+ * Pagination-only driver no filtering, sorting, or column selection
1393
1633
  */
1394
- buildUri(state: IQueryBuilderState, options: QueryBuilderOptions): string;
1634
+ readonly capabilities: IStrategyCapabilities;
1395
1635
  /**
1396
- * Validate that the given limit is accepted by the Laravel driver
1397
- *
1398
- * Laravel pagination does not recognize `-1` as a "fetch all" sentinel,
1399
- * so only positive integers are accepted.
1636
+ * Emit only the pagination params; filters/sorts/etc. are ignored
1400
1637
  *
1401
- * @param limit - The limit value to validate
1402
- * @throws {InvalidLimitError} If the value is not a positive integer
1638
+ * @param state - The current query builder state
1639
+ * @param options - The query parameter key name configuration
1640
+ * @returns The two pagination query-string fragments
1403
1641
  */
1404
- validateLimit(limit: number): void;
1642
+ protected parts(state: IQueryBuilderState, options: QueryBuilderOptions): string[];
1405
1643
  }
1406
1644
 
1407
1645
  /**
@@ -1444,20 +1682,12 @@ declare class LaravelResponseStrategy implements IResponseStrategy {
1444
1682
  *
1445
1683
  * @see https://github.com/ppetzold/nestjs-paginate
1446
1684
  */
1447
- declare class NestjsRequestStrategy implements IRequestStrategy {
1448
- /**
1449
- * Accumulator for composing the URI string
1450
- */
1451
- private _uri;
1685
+ declare class NestjsRequestStrategy extends AbstractRequestStrategy {
1452
1686
  /**
1453
- * Build a URI string from the given state using the NestJS paginate format
1454
- *
1455
- * @param state - The current query builder state
1456
- * @param options - The query parameter key name configuration
1457
- * @returns The composed URI string
1458
- * @throws Error if model is not set
1687
+ * Filters, operator filters, sorts, flat select, global search no
1688
+ * per-model fields, no includes
1459
1689
  */
1460
- buildUri(state: IQueryBuilderState, options: QueryBuilderOptions): string;
1690
+ readonly capabilities: IStrategyCapabilities;
1461
1691
  /**
1462
1692
  * Validate that the given limit is accepted by nestjs-paginate
1463
1693
  *
@@ -1469,76 +1699,72 @@ declare class NestjsRequestStrategy implements IRequestStrategy {
1469
1699
  */
1470
1700
  validateLimit(limit: number): void;
1471
1701
  /**
1472
- * Parse and append simple filter parameters
1473
- *
1474
- * Generates: `filter.field=value1,value2` for each filter
1702
+ * Emit NestJS-format query-string segments in canonical order:
1703
+ * filters → operator filters → sortBy → select → search → limit → page
1475
1704
  *
1476
1705
  * @param state - The current query builder state
1477
1706
  * @param options - The query parameter key name configuration
1707
+ * @returns Ordered query-string fragments
1478
1708
  */
1479
- private _parseFilters;
1709
+ protected parts(state: IQueryBuilderState, options: QueryBuilderOptions): string[];
1480
1710
  /**
1481
- * Parse and append the limit parameter
1711
+ * Append simple filter parameters as `filter.field=value1,value2`
1482
1712
  *
1483
1713
  * @param state - The current query builder state
1484
1714
  * @param options - The query parameter key name configuration
1715
+ * @param out - The accumulator the caller joins into the URI
1485
1716
  */
1486
- private _parseLimit;
1717
+ private _appendFilters;
1487
1718
  /**
1488
- * Parse and append operator filter parameters
1489
- *
1490
- * Groups operator filters by field and generates:
1491
- * - Single value: `filter.field=$operator:value`
1492
- * - Multiple values ($in, $btw): `filter.field=$operator:val1,val2`
1719
+ * Append the limit parameter
1493
1720
  *
1494
1721
  * @param state - The current query builder state
1495
1722
  * @param options - The query parameter key name configuration
1723
+ * @param out - The accumulator the caller joins into the URI
1496
1724
  */
1497
- private _parseOperatorFilters;
1725
+ private _appendLimit;
1498
1726
  /**
1499
- * Parse and append the page parameter
1727
+ * Append operator-filter parameters as `filter.field=$op:value`
1728
+ *
1729
+ * Groups by field; multi-value operators ($in, $btw) join values with commas.
1500
1730
  *
1501
1731
  * @param state - The current query builder state
1502
1732
  * @param options - The query parameter key name configuration
1733
+ * @param out - The accumulator the caller joins into the URI
1503
1734
  */
1504
- private _parsePage;
1735
+ private _appendOperatorFilters;
1505
1736
  /**
1506
- * Parse and append the search parameter
1507
- *
1508
- * Generates: `search=term`
1737
+ * Append the page parameter
1509
1738
  *
1510
1739
  * @param state - The current query builder state
1511
1740
  * @param options - The query parameter key name configuration
1741
+ * @param out - The accumulator the caller joins into the URI
1512
1742
  */
1513
- private _parseSearch;
1743
+ private _appendPage;
1514
1744
  /**
1515
- * Parse and append the select parameter
1516
- *
1517
- * Generates: `select=col1,col2`
1745
+ * Append the search parameter as `search=term`
1518
1746
  *
1519
1747
  * @param state - The current query builder state
1520
1748
  * @param options - The query parameter key name configuration
1749
+ * @param out - The accumulator the caller joins into the URI
1521
1750
  */
1522
- private _parseSelect;
1751
+ private _appendSearch;
1523
1752
  /**
1524
- * Parse and append sort parameters
1525
- *
1526
- * Generates: `sortBy=field1:DESC,field2:ASC`
1753
+ * Append the select parameter as `select=col1,col2`
1527
1754
  *
1528
1755
  * @param state - The current query builder state
1529
1756
  * @param options - The query parameter key name configuration
1757
+ * @param out - The accumulator the caller joins into the URI
1530
1758
  */
1531
- private _parseSort;
1759
+ private _appendSelect;
1532
1760
  /**
1533
- * Determine the appropriate URI prefix based on the current accumulator state
1534
- *
1535
- * Returns the full base path with `?` for the first parameter,
1536
- * or `&` for subsequent parameters.
1761
+ * Append sort parameter as `sortBy=field1:DESC,field2:ASC`
1537
1762
  *
1538
1763
  * @param state - The current query builder state
1539
- * @returns The prefix string to prepend to the next parameter
1764
+ * @param options - The query parameter key name configuration
1765
+ * @param out - The accumulator the caller joins into the URI
1540
1766
  */
1541
- private _prepend;
1767
+ private _appendSort;
1542
1768
  }
1543
1769
 
1544
1770
  /**
@@ -1564,60 +1790,214 @@ declare class NestjsRequestStrategy implements IRequestStrategy {
1564
1790
  * }
1565
1791
  * ```
1566
1792
  *
1793
+ * Default key paths are configured in `NestjsResponseOptions`. The
1794
+ * traversal algorithm (dot-notation resolution + computed `from`/`to`) is
1795
+ * inherited from `AbstractDotPathResponseStrategy`; this class exists so
1796
+ * `DriverEnum.NESTJS` resolves to a distinct identity at the DI layer
1797
+ * even though the parsing logic is shared with JSON:API.
1798
+ *
1567
1799
  * @see https://github.com/ppetzold/nestjs-paginate
1568
1800
  */
1569
- declare class NestjsResponseStrategy implements IResponseStrategy {
1801
+ declare class NestjsResponseStrategy extends AbstractDotPathResponseStrategy {
1802
+ }
1803
+
1804
+ /**
1805
+ * Request strategy for the PostgREST driver
1806
+ *
1807
+ * PostgREST auto-generates REST APIs from PostgreSQL schemas and is the
1808
+ * backbone of Supabase's data API. This strategy produces URIs in
1809
+ * PostgREST's native query-string format:
1810
+ *
1811
+ * - Filters: `col=eq.val` (single value) / `col=in.(v1,v2,v3)` (multi-value)
1812
+ * - Order: `order=col1.asc,col2.desc`
1813
+ * - Select: `select=col1,col2`
1814
+ * - Pagination: `limit=N&offset=M` (offset derived from state.page)
1815
+ *
1816
+ * The `order` and `offset` query-parameter names are PostgREST conventions
1817
+ * and are intentionally not configurable via `QueryBuilderOptions` (see
1818
+ * issue #50 MVP scope). `limit`, `select`, and `filters` (per-column name)
1819
+ * honour the existing option keys.
1820
+ *
1821
+ * @see https://postgrest.org/en/stable/api.html
1822
+ * @see https://supabase.com/docs/reference/javascript/select
1823
+ */
1824
+ declare class PostgrestRequestStrategy extends AbstractRequestStrategy {
1570
1825
  /**
1571
- * Parse a nested NestJS pagination response into a PaginatedCollection
1826
+ * Filters, operator filters (incl. FTS), sorts, flat select — no
1827
+ * per-model fields, no JSON:API/Spatie-style includes, no global
1828
+ * search (per-column FTS via the operator family covers it)
1829
+ */
1830
+ readonly capabilities: IStrategyCapabilities;
1831
+ private static readonly _offsetKey;
1832
+ private static readonly _orderKey;
1833
+ /**
1834
+ * Active pagination mode
1572
1835
  *
1573
- * Supports dot-notation key paths for accessing nested values.
1574
- * Computes `from` and `to` from `currentPage` and `itemsPerPage` when
1575
- * they are not directly available in the response.
1836
+ * QUERY (default) URL emits limit/offset.
1837
+ * RANGE URL omits them; `buildPaginationHeaders()` returns the
1838
+ * `Range-Unit` / `Range` HTTP headers instead.
1839
+ */
1840
+ private readonly _paginationMode;
1841
+ /**
1842
+ * @param paginationMode - Wire-level pagination mechanism. Defaults to
1843
+ * `PaginationModeEnum.QUERY`; `provideNgQubee` wires this from
1844
+ * `IConfig.pagination`.
1845
+ */
1846
+ constructor(paginationMode?: PaginationModeEnum);
1847
+ /**
1848
+ * Compute `Range-Unit` / `Range` HTTP headers for RANGE pagination mode
1576
1849
  *
1577
- * @param response - The raw API response object
1578
- * @param options - The response key name configuration
1579
- * @returns A typed PaginatedCollection instance
1850
+ * In QUERY mode this returns `null` so `NgQubeeService.paginationHeaders()`
1851
+ * conveys "no headers needed" to the consumer. In RANGE mode the method
1852
+ * converts the 1-indexed `state.page` + `state.limit` into PostgREST's
1853
+ * 0-indexed inclusive range (`from = (page - 1) * limit`,
1854
+ * `to = from + limit - 1`) and returns both header values.
1855
+ *
1856
+ * @param state - The current query builder state
1857
+ * @returns `{ 'Range-Unit': 'items', 'Range': 'from-to' }` or `null`
1580
1858
  */
1581
- paginate<T extends IPaginatedObject>(response: Record<string, any>, options: ResponseOptions): PaginatedCollection<T>;
1859
+ buildPaginationHeaders(state: IQueryBuilderState): Record<string, string> | null;
1582
1860
  /**
1583
- * Resolve a value from a response object using a dot-notation path
1861
+ * Emit PostgREST-format query-string segments in canonical order:
1862
+ * filters → operator filters → order → select → (limit + offset in
1863
+ * QUERY mode only — RANGE mode passes pagination via headers instead)
1584
1864
  *
1585
- * Supports both flat keys ('data') and nested paths ('meta.currentPage').
1865
+ * @param state - The current query builder state
1866
+ * @param options - The query parameter key name configuration
1867
+ * @returns Ordered query-string fragments
1868
+ */
1869
+ protected parts(state: IQueryBuilderState, options: QueryBuilderOptions): string[];
1870
+ /**
1871
+ * Append filter parameters in PostgREST format
1586
1872
  *
1587
- * @param response - The raw response object
1588
- * @param path - The dot-notation path to resolve
1589
- * @returns The resolved value, or undefined if not found
1873
+ * Every filter is operator-prefixed (PostgREST has no implicit equality):
1874
+ * a single value yields `col=eq.val`; multiple values collapse into
1875
+ * PostgREST's native IN-list syntax `col=in.(v1,v2,v3)`.
1876
+ *
1877
+ * @param state - The current query builder state
1878
+ * @param out - The accumulator the caller joins into the URI
1590
1879
  */
1591
- private _resolve;
1880
+ private _appendFilters;
1592
1881
  /**
1593
- * Resolve the "from" index value
1882
+ * Append the limit parameter
1594
1883
  *
1595
- * If the path resolves to a value in the response, use it.
1596
- * Otherwise, compute it from currentPage and perPage:
1597
- * `(currentPage - 1) * perPage + 1`
1884
+ * @param state - The current query builder state
1885
+ * @param options - The query parameter key name configuration
1886
+ * @param out - The accumulator the caller joins into the URI
1887
+ */
1888
+ private _appendLimit;
1889
+ /**
1890
+ * Append the offset parameter, derived from state.page
1598
1891
  *
1599
- * @param response - The raw response object
1600
- * @param options - The response key name configuration
1601
- * @param currentPage - The current page number
1602
- * @param perPage - The number of items per page
1603
- * @returns The computed "from" index
1892
+ * PostgREST uses offset-based pagination, not page-based. The offset is
1893
+ * computed as `(page - 1) * limit`. Omitted when offset would be 0
1894
+ * (i.e. page 1) since PostgREST defaults to offset=0 anyway and dropping
1895
+ * it keeps the URI shorter.
1896
+ *
1897
+ * @param state - The current query builder state
1898
+ * @param out - The accumulator the caller joins into the URI
1604
1899
  */
1605
- private _resolveFrom;
1900
+ private _appendOffset;
1606
1901
  /**
1607
- * Resolve the "to" index value
1902
+ * Append explicit operator filters
1608
1903
  *
1609
- * If the path resolves to a value in the response, use it.
1610
- * Otherwise, compute it from currentPage, perPage, and total:
1611
- * `Math.min(currentPage * perPage, total)`
1904
+ * Maps each `FilterOperatorEnum` value to PostgREST's prefix-operator
1905
+ * syntax. `BTW` expands to two query params (`gte` + `lte`); `NULL`
1906
+ * emits `is.null` / `is.not.null` based on the boolean value; `NOT`
1907
+ * picks its inner operator by arity (`not.eq.val` for single values,
1908
+ * `not.in.(v1,v2)` for multi-value).
1612
1909
  *
1613
- * @param response - The raw response object
1614
- * @param options - The response key name configuration
1615
- * @param currentPage - The current page number
1616
- * @param perPage - The number of items per page
1617
- * @param total - The total number of items
1618
- * @returns The computed "to" index
1910
+ * @param state - The current query builder state
1911
+ * @param out - The accumulator the caller joins into the URI
1912
+ * @throws {InvalidFilterOperatorValueError} If `BTW` does not receive exactly 2 values, or `NULL` does not receive exactly 1 boolean
1619
1913
  */
1620
- private _resolveTo;
1914
+ private _appendOperatorFilters;
1915
+ /**
1916
+ * Append a `BTW` operator filter as two PostgREST segments
1917
+ *
1918
+ * Produces: `col=gte.min` and `col=lte.max`. Values must be exactly
1919
+ * `[min, max]`.
1920
+ *
1921
+ * @param filter - The operator filter carrying the BTW bounds
1922
+ * @param out - The accumulator the caller joins into the URI
1923
+ * @throws {InvalidFilterOperatorValueError} If values.length !== 2
1924
+ */
1925
+ private _appendBetweenFilter;
1926
+ /**
1927
+ * Build the right-hand-side of a PostgREST filter param for the given operator
1928
+ *
1929
+ * Kept as a separate helper so each operator's shape is visible in one
1930
+ * place and the dispatch is exhaustively typed against
1931
+ * `FilterOperatorEnum`.
1932
+ *
1933
+ * @param filter - The operator filter (field, operator, values)
1934
+ * @returns The PostgREST-formatted value portion (right of the `=` sign)
1935
+ * @throws {InvalidFilterOperatorValueError} If NULL receives a non-boolean or wrong arity
1936
+ */
1937
+ private _formatOperatorRhs;
1938
+ /**
1939
+ * Append the order parameter as `order=col1.asc,col2.desc`
1940
+ *
1941
+ * @param state - The current query builder state
1942
+ * @param out - The accumulator the caller joins into the URI
1943
+ */
1944
+ private _appendOrder;
1945
+ /**
1946
+ * Append the select parameter as `select=col1,col2`
1947
+ *
1948
+ * PostgREST uses a `select` query param for column pruning, matching
1949
+ * NestJS semantics.
1950
+ *
1951
+ * @param state - The current query builder state
1952
+ * @param options - The query parameter key name configuration
1953
+ * @param out - The accumulator the caller joins into the URI
1954
+ */
1955
+ private _appendSelect;
1956
+ }
1957
+
1958
+ /**
1959
+ * Response strategy for the PostgREST driver
1960
+ *
1961
+ * PostgREST (and Supabase, which wraps it) returns a bare array body for
1962
+ * collection endpoints. Pagination metadata is carried in the
1963
+ * `Content-Range` HTTP response header, e.g. `0-9/50` meaning "items 0–9
1964
+ * out of 50 total". Consumers opt into totals by sending the
1965
+ * `Prefer: count=exact` request header.
1966
+ *
1967
+ * This strategy expects the consumer to pass the array body as `response`
1968
+ * (or a plain object with `response[options.data]` pointing at the array)
1969
+ * and the response headers via the optional `headers` bag. See
1970
+ * `PaginationService.paginate()` for the call-site shape.
1971
+ *
1972
+ * @see https://postgrest.org/en/stable/references/api/pagination_count.html
1973
+ */
1974
+ declare class PostgrestResponseStrategy implements IResponseStrategy {
1975
+ private static readonly _contentRangeHeader;
1976
+ private static readonly _contentRangeRegex;
1977
+ /**
1978
+ * Parse a PostgREST response into a typed PaginatedCollection
1979
+ *
1980
+ * @param response - The raw response. Either the array body directly, or
1981
+ * an object with the array at `response[options.data]`.
1982
+ * @param options - The response key configuration (only `options.data` is
1983
+ * consulted; all pagination metadata comes from the Content-Range header).
1984
+ * @param headers - Optional HTTP response headers. The `Content-Range`
1985
+ * header drives page/total derivation; omission is tolerated and yields
1986
+ * a collection with `undefined` bounds (auto-sync will leave
1987
+ * `isLastPageKnown` at `false`).
1988
+ * @returns A typed PaginatedCollection instance
1989
+ */
1990
+ paginate<T extends IPaginatedObject>(response: Record<string, unknown>, options: ResponseOptions, headers?: HeaderBag): PaginatedCollection<T>;
1991
+ /**
1992
+ * Extract `{from, to, total}` from a PostgREST `Content-Range` value
1993
+ *
1994
+ * Expected format: `<from>-<to>/<total|*>`. Any shape mismatch returns
1995
+ * an empty object; `*` as the total yields `total: undefined`.
1996
+ *
1997
+ * @param value - Raw header value (possibly null/undefined)
1998
+ * @returns Parsed integers; missing fields indicate an unparseable header
1999
+ */
2000
+ private _parseContentRange;
1621
2001
  }
1622
2002
 
1623
2003
  /**
@@ -1632,99 +2012,74 @@ declare class NestjsResponseStrategy implements IResponseStrategy {
1632
2012
  *
1633
2013
  * @see https://spatie.be/docs/laravel-query-builder
1634
2014
  */
1635
- declare class SpatieRequestStrategy implements IRequestStrategy {
2015
+ declare class SpatieRequestStrategy extends AbstractRequestStrategy {
1636
2016
  /**
1637
- * Accumulator for composing the URI string
2017
+ * Filters, sorts, includes, per-model fields — no operators, no flat
2018
+ * select, no global search
1638
2019
  */
1639
- private _uri;
2020
+ readonly capabilities: IStrategyCapabilities;
1640
2021
  /**
1641
- * Build a URI string from the given state using the Spatie format
2022
+ * Emit Spatie-format query-string segments in canonical order:
2023
+ * include → fields → filters → limit → page → sort
1642
2024
  *
1643
2025
  * @param state - The current query builder state
1644
2026
  * @param options - The query parameter key name configuration
1645
- * @returns The composed URI string
1646
- * @throws Error if resource is not set
1647
- */
1648
- buildUri(state: IQueryBuilderState, options: QueryBuilderOptions): string;
1649
- /**
1650
- * Validate that the given limit is accepted by the Spatie driver
1651
- *
1652
- * Spatie query-builder does not recognize `-1` as a "fetch all" sentinel,
1653
- * so only positive integers are accepted.
1654
- *
1655
- * @param limit - The limit value to validate
1656
- * @throws {InvalidLimitError} If the value is not a positive integer
2027
+ * @returns Ordered query-string fragments
1657
2028
  */
1658
- validateLimit(limit: number): void;
2029
+ protected parts(state: IQueryBuilderState, options: QueryBuilderOptions): string[];
1659
2030
  /**
1660
- * Parse and append field selection parameters
2031
+ * Append per-model field selection in bracket notation
1661
2032
  *
1662
2033
  * Validates that each field model exists either as the main resource
1663
- * or in the includes list. Fields are grouped by model in bracket notation.
2034
+ * or in the includes list.
1664
2035
  *
1665
2036
  * @param state - The current query builder state
1666
2037
  * @param options - The query parameter key name configuration
1667
- * @returns The generated field selection parameter string
1668
- * @throws Error if resource is required but not set
2038
+ * @param out - The accumulator the caller joins into the URI
2039
+ * @throws Error if the resource is required but not set
1669
2040
  * @throws UnselectableModelError if a field model is not in resource or includes
1670
2041
  */
1671
- private _parseFields;
2042
+ private _appendFields;
1672
2043
  /**
1673
- * Parse and append filter parameters
1674
- *
1675
- * Generates filter parameters in bracket notation: `filter[key]=value1,value2`
2044
+ * Append filter parameters in bracket notation: `filter[key]=value`
1676
2045
  *
1677
2046
  * @param state - The current query builder state
1678
2047
  * @param options - The query parameter key name configuration
1679
- * @returns The generated filter parameter string
2048
+ * @param out - The accumulator the caller joins into the URI
1680
2049
  */
1681
- private _parseFilters;
2050
+ private _appendFilters;
1682
2051
  /**
1683
- * Parse and append include parameters
1684
- *
1685
- * Generates: `include=model1,model2`
2052
+ * Append include parameter as `include=model1,model2`
1686
2053
  *
1687
2054
  * @param state - The current query builder state
1688
2055
  * @param options - The query parameter key name configuration
1689
- * @returns The generated include parameter string
2056
+ * @param out - The accumulator the caller joins into the URI
1690
2057
  */
1691
- private _parseIncludes;
2058
+ private _appendIncludes;
1692
2059
  /**
1693
- * Parse and append the limit parameter
2060
+ * Append the limit parameter
1694
2061
  *
1695
2062
  * @param state - The current query builder state
1696
2063
  * @param options - The query parameter key name configuration
1697
- * @returns The generated limit parameter string
2064
+ * @param out - The accumulator the caller joins into the URI
1698
2065
  */
1699
- private _parseLimit;
2066
+ private _appendLimit;
1700
2067
  /**
1701
- * Parse and append the page parameter
2068
+ * Append the page parameter
1702
2069
  *
1703
2070
  * @param state - The current query builder state
1704
2071
  * @param options - The query parameter key name configuration
1705
- * @returns The generated page parameter string
2072
+ * @param out - The accumulator the caller joins into the URI
1706
2073
  */
1707
- private _parsePage;
2074
+ private _appendPage;
1708
2075
  /**
1709
- * Parse and append sort parameters
1710
- *
1711
- * Generates: `sort=-field1,field2` where `-` prefix indicates DESC order
2076
+ * Append sort parameter as `sort=-field1,field2` (`-` prefix = DESC)
1712
2077
  *
1713
2078
  * @param state - The current query builder state
1714
2079
  * @param options - The query parameter key name configuration
1715
- * @returns The generated sort parameter string
1716
- */
1717
- private _parseSort;
1718
- /**
1719
- * Determine the appropriate URI prefix based on the current accumulator state
1720
- *
1721
- * Returns the full base path with `?` for the first parameter,
1722
- * or `&` for subsequent parameters.
1723
- *
1724
- * @param state - The current query builder state
1725
- * @returns The prefix string to prepend to the next parameter
2080
+ * @param out - The accumulator the caller joins into the URI
1726
2081
  */
1727
- private _prepend;
2082
+ private _appendSort;
1728
2083
  }
1729
2084
 
1730
2085
  /**
@@ -1756,5 +2111,173 @@ declare class SpatieResponseStrategy implements IResponseStrategy {
1756
2111
  paginate<T extends IPaginatedObject>(response: Record<string, any>, options: ResponseOptions): PaginatedCollection<T>;
1757
2112
  }
1758
2113
 
1759
- export { DriverEnum, FilterOperatorEnum, 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, PaginationNotSyncedError, PaginationService, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, buildNgQubeeProviders, provideNgQubee, provideNgQubeeInstance };
1760
- export type { IConfig, IFields, IFilters, INestState, IOperatorFilter, IPage, IPaginatedObject, IPaginationConfig, IQueryBuilderConfig, IQueryBuilderState, IRequestStrategy, IResponseStrategy, ISort };
2114
+ /**
2115
+ * Request strategy for the Strapi driver
2116
+ *
2117
+ * Generates URIs in [Strapi's filter API format](https://docs.strapi.io/dev-docs/api/rest/filters-locale-publication):
2118
+ * - Filters: `filters[field][$eq]=value` (multi-value collapses to `$in`)
2119
+ * - Operator filters: `filters[field][$op]=value` (translated from
2120
+ * `FilterOperatorEnum` — `BTW`→`$between`, `SW`→`$startsWith`,
2121
+ * `ILIKE`→`$containsi`, `NOT`→`$ne`/`$notIn`,
2122
+ * `NULL`→`$null`/`$notNull`)
2123
+ * - Sorts: `sort[0]=field:asc&sort[1]=field:desc`
2124
+ * - Field selection (flat): `fields[0]=col1&fields[1]=col2`
2125
+ * - Population: `populate[0]=relation`
2126
+ * - Pagination (page-based): `pagination[page]=N&pagination[pageSize]=N`
2127
+ *
2128
+ * Strapi-native full-text search (`FTS`, `PHFTS`, `PLFTS`, `WFTS`) is
2129
+ * PostgREST-only and throws `UnsupportedFilterOperatorError` here.
2130
+ *
2131
+ * @see https://docs.strapi.io/dev-docs/api/rest/filters-locale-publication
2132
+ */
2133
+ declare class StrapiRequestStrategy extends AbstractRequestStrategy {
2134
+ /**
2135
+ * Filters, operator filters, sorts, populate (`includes`), flat field
2136
+ * selection (`select`) — no per-model fields, no global search (use
2137
+ * `$contains` / `$containsi` operator filters instead)
2138
+ */
2139
+ readonly capabilities: IStrategyCapabilities;
2140
+ /**
2141
+ * Strapi-native names of the four hardcoded query keys
2142
+ *
2143
+ * Strapi's wire format is fixed (the server reads `pagination[page]`,
2144
+ * `populate`, `sort`, `fields`); these keys are intentionally not
2145
+ * configurable through `QueryBuilderOptions` and live as private
2146
+ * statics so they are visible in one place.
2147
+ */
2148
+ private static readonly _fieldsKey;
2149
+ private static readonly _paginationKey;
2150
+ private static readonly _populateKey;
2151
+ private static readonly _sortKey;
2152
+ /**
2153
+ * Emit Strapi-format query-string segments in canonical order:
2154
+ * populate → fields → filters (merged) → sort → pagination
2155
+ *
2156
+ * Simple filters and operator filters share a single `filters` wrapper
2157
+ * so qs emits one ordered, deeply-nested bracket structure rather than
2158
+ * two duplicate top-level `filters[...]` blocks.
2159
+ *
2160
+ * @param state - The current query builder state
2161
+ * @param _options - The query parameter key name configuration (unused;
2162
+ * Strapi's wire keys are fixed by the server)
2163
+ * @returns Ordered query-string fragments
2164
+ */
2165
+ protected parts(state: IQueryBuilderState, _options: QueryBuilderOptions): string[];
2166
+ /**
2167
+ * Append `fields[0]=col1&fields[1]=col2` from the flat select array
2168
+ *
2169
+ * Strapi's `fields` parameter is the column-pruner for the main
2170
+ * resource; per-relation field selection is expressed through the
2171
+ * `populate` deep syntax (out of scope for this driver).
2172
+ *
2173
+ * @param state - The current query builder state
2174
+ * @param out - The accumulator the caller joins into the URI
2175
+ */
2176
+ private _appendFields;
2177
+ /**
2178
+ * Append the unified `filters[...]` wrapper combining simple filters
2179
+ * and operator filters
2180
+ *
2181
+ * Both kinds emit into the same nested object under `filters` so qs
2182
+ * produces a single deeply-bracketed block per request. Simple
2183
+ * single-value filters fold to `$eq`; simple multi-value filters fold
2184
+ * to `$in`. Operator filters then merge into the same per-field map,
2185
+ * potentially co-existing with a simple filter on the same field.
2186
+ *
2187
+ * @param state - The current query builder state
2188
+ * @param out - The accumulator the caller joins into the URI
2189
+ */
2190
+ private _appendFilters;
2191
+ /**
2192
+ * Append the `pagination[page]` / `pagination[pageSize]` wrapper
2193
+ *
2194
+ * Page-based mode is the Strapi default; offset-based
2195
+ * (`pagination[start]` / `pagination[limit]`) is out of scope for this
2196
+ * driver until cursor/offset pagination lands library-wide.
2197
+ *
2198
+ * @param state - The current query builder state
2199
+ * @param out - The accumulator the caller joins into the URI
2200
+ */
2201
+ private _appendPagination;
2202
+ /**
2203
+ * Append the `populate` parameter from the includes array
2204
+ *
2205
+ * Emits `populate[0]=relation1&populate[1]=relation2`; deep-populate
2206
+ * syntax (`populate[author][fields][0]=name`) is not exposed through
2207
+ * the current state shape.
2208
+ *
2209
+ * @param state - The current query builder state
2210
+ * @param out - The accumulator the caller joins into the URI
2211
+ */
2212
+ private _appendPopulate;
2213
+ /**
2214
+ * Append the `sort[N]=field:dir` array
2215
+ *
2216
+ * @param state - The current query builder state
2217
+ * @param out - The accumulator the caller joins into the URI
2218
+ */
2219
+ private _appendSort;
2220
+ /**
2221
+ * Translate a `FilterOperatorEnum` operator filter into Strapi's
2222
+ * `$operator → value` payload shape
2223
+ *
2224
+ * The mapping is library-canonical → Strapi-native:
2225
+ * - `EQ`/`GT`/`GTE`/`LT`/`LTE`/`CONTAINS` → identity (same key name)
2226
+ * - `ILIKE` → `$containsi` (case-insensitive contains)
2227
+ * - `IN` → `$in` (array)
2228
+ * - `SW` → `$startsWith`
2229
+ * - `BTW` → `$between` with `[min, max]` (arity-checked)
2230
+ * - `NOT` → `$ne` (single value) / `$notIn` (multi-value)
2231
+ * - `NULL` → `$null=true` (when value is `true`) / `$notNull=true`
2232
+ * (when value is `false`); arity- and type-checked
2233
+ *
2234
+ * PostgREST's full-text-search operators (`FTS`, `PHFTS`, `PLFTS`,
2235
+ * `WFTS`) have no Strapi equivalent and throw
2236
+ * `UnsupportedFilterOperatorError`.
2237
+ *
2238
+ * @param filter - The operator filter to translate
2239
+ * @returns A `{ $operator: value }` payload ready to merge under
2240
+ * `filters[field]`
2241
+ * @throws {InvalidFilterOperatorValueError} If `BTW` does not receive
2242
+ * exactly two values, or `NULL` does not receive exactly one boolean
2243
+ * @throws {UnsupportedFilterOperatorError} If the operator is a
2244
+ * PostgREST-only FTS variant
2245
+ */
2246
+ private _formatOperatorPayload;
2247
+ }
2248
+
2249
+ /**
2250
+ * Response strategy for the Strapi driver
2251
+ *
2252
+ * Parses Strapi v4/v5 pagination responses:
2253
+ * ```json
2254
+ * {
2255
+ * "data": [{ "id": 1, "documentId": "abc", "title": "Hello" }],
2256
+ * "meta": {
2257
+ * "pagination": {
2258
+ * "page": 1,
2259
+ * "pageSize": 10,
2260
+ * "pageCount": 5,
2261
+ * "total": 48
2262
+ * }
2263
+ * }
2264
+ * }
2265
+ * ```
2266
+ *
2267
+ * Default key paths are configured in `StrapiResponseOptions`. Strapi
2268
+ * does not include navigation links in the envelope, so `firstPageUrl`,
2269
+ * `prevPageUrl`, `nextPageUrl`, and `lastPageUrl` resolve to `undefined`
2270
+ * unless the consumer overrides their paths via `IPaginationConfig`. The
2271
+ * traversal algorithm (dot-notation resolution + computed `from`/`to`)
2272
+ * is inherited from `AbstractDotPathResponseStrategy`; this class exists
2273
+ * so `DriverEnum.STRAPI` resolves to a distinct identity at the DI
2274
+ * layer even though the parsing logic is shared with JSON:API and
2275
+ * NestJS.
2276
+ *
2277
+ * @see https://docs.strapi.io/dev-docs/api/rest/sort-pagination
2278
+ */
2279
+ declare class StrapiResponseStrategy extends AbstractDotPathResponseStrategy {
2280
+ }
2281
+
2282
+ 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 };
2283
+ export type { HeaderBag, IConfig, IFields, IFilters, INestState, IOperatorFilter, IPage, IPaginatedObject, IPaginationConfig, IQueryBuilderConfig, IQueryBuilderState, IRequestStrategy, IResponseStrategy, ISort };