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