ng-qubee 2.1.0 → 3.1.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.
Files changed (32) hide show
  1. package/README.md +378 -54
  2. package/fesm2022/ng-qubee.mjs +1791 -419
  3. package/fesm2022/ng-qubee.mjs.map +1 -1
  4. package/package.json +4 -4
  5. package/types/ng-qubee.d.ts +1544 -0
  6. package/index.d.ts +0 -5
  7. package/lib/enums/sort.enum.d.ts +0 -4
  8. package/lib/errors/invalid-limit.error.d.ts +0 -3
  9. package/lib/errors/invalid-model-name.error.d.ts +0 -3
  10. package/lib/errors/invalid-page-number.error.d.ts +0 -3
  11. package/lib/errors/key-not-found.error.d.ts +0 -3
  12. package/lib/errors/unselectable-model.error.d.ts +0 -3
  13. package/lib/interfaces/config.interface.d.ts +0 -6
  14. package/lib/interfaces/fields.interface.d.ts +0 -3
  15. package/lib/interfaces/filters.interface.d.ts +0 -3
  16. package/lib/interfaces/nest-state.interface.d.ts +0 -4
  17. package/lib/interfaces/normalized.interface.d.ts +0 -3
  18. package/lib/interfaces/page.interface.d.ts +0 -2
  19. package/lib/interfaces/paginated-object.interface.d.ts +0 -3
  20. package/lib/interfaces/pagination-config.interface.d.ts +0 -14
  21. package/lib/interfaces/query-builder-config.interface.d.ts +0 -9
  22. package/lib/interfaces/query-builder-state.interface.d.ts +0 -13
  23. package/lib/interfaces/sort.interface.d.ts +0 -5
  24. package/lib/models/paginated-collection.d.ts +0 -30
  25. package/lib/models/query-builder-options.d.ts +0 -11
  26. package/lib/models/response-options.d.ts +0 -16
  27. package/lib/ng-qubee.module.d.ts +0 -9
  28. package/lib/provide-ngqubee.d.ts +0 -21
  29. package/lib/services/nest.service.d.ts +0 -182
  30. package/lib/services/ng-qubee.service.d.ts +0 -147
  31. package/lib/services/pagination.service.d.ts +0 -13
  32. package/public-api.d.ts +0 -20
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, Inject, Optional, NgModule, makeEnvironmentProviders } from '@angular/core';
3
- import * as qs from 'qs';
2
+ import { signal, computed, Injectable, NgModule, makeEnvironmentProviders } from '@angular/core';
4
3
  import { BehaviorSubject, filter, throwError } from 'rxjs';
4
+ import * as qs from 'qs';
5
5
 
6
6
  class KeyNotFoundError extends Error {
7
7
  constructor(key) {
@@ -66,18 +66,204 @@ class PaginatedCollection {
66
66
  }
67
67
  }
68
68
 
69
- var SortEnum;
70
- (function (SortEnum) {
71
- SortEnum["ASC"] = "asc";
72
- SortEnum["DESC"] = "desc";
73
- })(SortEnum || (SortEnum = {}));
69
+ /**
70
+ * Enum representing the available pagination driver types
71
+ *
72
+ * Each driver encapsulates the full format knowledge for both
73
+ * request building (URI generation) and response parsing.
74
+ */
75
+ var DriverEnum;
76
+ (function (DriverEnum) {
77
+ DriverEnum["JSON_API"] = "json-api";
78
+ DriverEnum["LARAVEL"] = "laravel";
79
+ DriverEnum["NESTJS"] = "nestjs";
80
+ DriverEnum["SPATIE"] = "spatie";
81
+ })(DriverEnum || (DriverEnum = {}));
74
82
 
75
- class UnselectableModelError extends Error {
76
- constructor(model) {
77
- super(`Unselectable Model: the selected model (${model}) is not present neither in the "model" property, nor in the includes object.`);
83
+ /**
84
+ * Resolved response field key names with defaults applied
85
+ *
86
+ * Maps logical pagination concepts to the actual key names
87
+ * used in the API response. Unset values fall back to Laravel defaults.
88
+ *
89
+ * For NestJS responses, use dot-notation paths:
90
+ * ```typescript
91
+ * new ResponseOptions({
92
+ * currentPage: 'meta.currentPage',
93
+ * total: 'meta.totalItems'
94
+ * });
95
+ * ```
96
+ */
97
+ class ResponseOptions {
98
+ currentPage;
99
+ data;
100
+ firstPageUrl;
101
+ from;
102
+ lastPage;
103
+ lastPageUrl;
104
+ nextPageUrl;
105
+ path;
106
+ perPage;
107
+ prevPageUrl;
108
+ to;
109
+ total;
110
+ constructor(options) {
111
+ this.currentPage = options.currentPage || 'current_page';
112
+ this.data = options.data || 'data';
113
+ this.firstPageUrl = options.firstPageUrl || 'first_page_url';
114
+ this.from = options.from || 'from';
115
+ this.lastPage = options.lastPage || 'last_page';
116
+ this.lastPageUrl = options.lastPageUrl || 'last_page_url';
117
+ this.nextPageUrl = options.nextPageUrl || 'next_page_url';
118
+ this.path = options.path || 'path';
119
+ this.perPage = options.perPage || 'per_page';
120
+ this.prevPageUrl = options.prevPageUrl || 'prev_page_url';
121
+ this.to = options.to || 'to';
122
+ this.total = options.total || 'total';
123
+ }
124
+ }
125
+ /**
126
+ * Pre-configured ResponseOptions for the JSON:API driver
127
+ *
128
+ * Uses dot-notation paths to access nested values in the JSON:API response format.
129
+ * JSON:API meta key names vary by implementation; these defaults cover the most
130
+ * common conventions and can be fully customised via `IPaginationConfig`.
131
+ */
132
+ class JsonApiResponseOptions extends ResponseOptions {
133
+ constructor(options) {
134
+ super({
135
+ currentPage: options.currentPage || 'meta.current-page',
136
+ data: options.data || 'data',
137
+ firstPageUrl: options.firstPageUrl || 'links.first',
138
+ from: options.from || 'meta.from',
139
+ lastPage: options.lastPage || 'meta.page-count',
140
+ lastPageUrl: options.lastPageUrl || 'links.last',
141
+ nextPageUrl: options.nextPageUrl || 'links.next',
142
+ path: options.path || 'path',
143
+ perPage: options.perPage || 'meta.per-page',
144
+ prevPageUrl: options.prevPageUrl || 'links.prev',
145
+ to: options.to || 'meta.to',
146
+ total: options.total || 'meta.total'
147
+ });
148
+ }
149
+ }
150
+ /**
151
+ * Pre-configured ResponseOptions for the NestJS driver
152
+ *
153
+ * Uses dot-notation paths to access nested values in the NestJS response format.
154
+ */
155
+ class NestjsResponseOptions extends ResponseOptions {
156
+ constructor(options) {
157
+ super({
158
+ currentPage: options.currentPage || 'meta.currentPage',
159
+ data: options.data || 'data',
160
+ firstPageUrl: options.firstPageUrl || 'links.first',
161
+ from: options.from || 'meta.from',
162
+ lastPage: options.lastPage || 'meta.totalPages',
163
+ lastPageUrl: options.lastPageUrl || 'links.last',
164
+ nextPageUrl: options.nextPageUrl || 'links.next',
165
+ path: options.path || 'path',
166
+ perPage: options.perPage || 'meta.itemsPerPage',
167
+ prevPageUrl: options.prevPageUrl || 'links.previous',
168
+ to: options.to || 'meta.to',
169
+ total: options.total || 'meta.totalItems'
170
+ });
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Error thrown when per-model field selection is attempted with a driver that does not support it
176
+ *
177
+ * Per-model field selection is only supported by the Spatie driver.
178
+ * Use `addSelect()` for NestJS flat field selection.
179
+ */
180
+ class UnsupportedFieldSelectionError extends Error {
181
+ constructor() {
182
+ super('Per-model field selection is only supported by the Spatie driver. Use addSelect() for NestJS.');
183
+ this.name = 'UnsupportedFieldSelectionError';
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Error thrown when filters are attempted with a driver that does not support them
189
+ *
190
+ * Filters are only supported by the Spatie and NestJS drivers.
191
+ */
192
+ class UnsupportedFilterError extends Error {
193
+ constructor() {
194
+ super('Filters are only supported by the Spatie and NestJS drivers.');
195
+ this.name = 'UnsupportedFilterError';
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Error thrown when filter operators are attempted with a driver that does not support them
201
+ *
202
+ * Filter operators are only supported by the NestJS driver.
203
+ * Use `addFilter()` for Spatie implicit equality filters.
204
+ */
205
+ class UnsupportedFilterOperatorError extends Error {
206
+ constructor() {
207
+ super('Filter operators are only supported by the NestJS driver. Use addFilter() for Spatie.');
208
+ this.name = 'UnsupportedFilterOperatorError';
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Error thrown when includes are attempted with a driver that does not support them
214
+ *
215
+ * Includes are only supported by the Spatie driver.
216
+ */
217
+ class UnsupportedIncludesError extends Error {
218
+ constructor() {
219
+ super('Includes are only supported by the Spatie driver.');
220
+ this.name = 'UnsupportedIncludesError';
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Error thrown when search is attempted with a driver that does not support it
226
+ *
227
+ * Search is only supported by the NestJS driver.
228
+ */
229
+ class UnsupportedSearchError extends Error {
230
+ constructor() {
231
+ super('Search is only supported by the NestJS driver.');
232
+ this.name = 'UnsupportedSearchError';
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Error thrown when flat field selection is attempted with a driver that does not support it
238
+ *
239
+ * Flat field selection is only supported by the NestJS driver.
240
+ * Use `addFields()` for Spatie per-model field selection.
241
+ */
242
+ class UnsupportedSelectError extends Error {
243
+ constructor() {
244
+ super('Flat field selection is only supported by the NestJS driver. Use addFields() for Spatie.');
245
+ this.name = 'UnsupportedSelectError';
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Error thrown when sorts are attempted with a driver that does not support them
251
+ *
252
+ * Sorts are only supported by the Spatie and NestJS drivers.
253
+ */
254
+ class UnsupportedSortError extends Error {
255
+ constructor() {
256
+ super('Sorts are only supported by the Spatie and NestJS drivers.');
257
+ this.name = 'UnsupportedSortError';
78
258
  }
79
259
  }
80
260
 
261
+ /**
262
+ * Resolved query parameter key names with defaults applied
263
+ *
264
+ * Maps logical query concepts to the actual query parameter names
265
+ * used in the generated URI. Unset values fall back to defaults.
266
+ */
81
267
  class QueryBuilderOptions {
82
268
  appends;
83
269
  fields;
@@ -85,7 +271,10 @@ class QueryBuilderOptions {
85
271
  includes;
86
272
  limit;
87
273
  page;
274
+ search;
275
+ select;
88
276
  sort;
277
+ sortBy;
89
278
  constructor(options) {
90
279
  this.appends = options.appends || 'append';
91
280
  this.fields = options.fields || 'fields';
@@ -93,176 +282,523 @@ class QueryBuilderOptions {
93
282
  this.includes = options.includes || 'include';
94
283
  this.limit = options.limit || 'limit';
95
284
  this.page = options.page || 'page';
285
+ this.search = options.search || 'search';
286
+ this.select = options.select || 'select';
96
287
  this.sort = options.sort || 'sort';
288
+ this.sortBy = options.sortBy || 'sortBy';
97
289
  }
98
290
  }
99
291
 
100
- class InvalidModelNameError extends Error {
101
- constructor(model) {
102
- super(`Invalid model name: Model name must be a non-empty string. Received: ${JSON.stringify(model)}`);
103
- this.name = 'InvalidModelNameError';
104
- }
105
- }
106
-
107
- class InvalidPageNumberError extends Error {
108
- constructor(page) {
109
- super(`Invalid page number: Page must be a positive integer greater than 0. Received: ${page}`);
110
- this.name = 'InvalidPageNumberError';
111
- }
112
- }
113
-
114
- class InvalidLimitError extends Error {
115
- constructor(limit) {
116
- super(`Invalid limit value: Limit must be a positive integer greater than 0. Received: ${limit}`);
117
- this.name = 'InvalidLimitError';
118
- }
119
- }
120
-
121
- const INITIAL_STATE = {
122
- baseUrl: '',
123
- fields: {},
124
- filters: {},
125
- includes: [],
126
- limit: 15,
127
- model: '',
128
- page: 1,
129
- sorts: []
130
- };
131
- class NestService {
292
+ class NgQubeeService {
293
+ _nestService;
132
294
  /**
133
- * Private writable signal that holds the Query Builder state
134
- *
135
- * @type {IQueryBuilderState}
295
+ * The active pagination driver
136
296
  */
137
- _nest = signal(this._clone(INITIAL_STATE));
297
+ _driver;
138
298
  /**
139
- * A computed signal that makes readonly the writable signal _nest
140
- *
141
- * @type {Signal<IQueryBuilderState>}
299
+ * Resolved query parameter key name options
142
300
  */
143
- nest = computed(() => this._clone(this._nest()));
144
- constructor() {
145
- // Nothing to see here 👮🏻‍♀️
146
- }
301
+ _options;
147
302
  /**
148
- * Set the base URL for the API
149
- *
150
- * @param {string} baseUrl - The base URL to prepend to generated URIs
151
- * @example
152
- * service.baseUrl = 'https://api.example.com';
303
+ * The request strategy that builds URIs for the active driver
153
304
  */
154
- set baseUrl(baseUrl) {
155
- this._nest.update(nest => ({
156
- ...nest,
157
- baseUrl
158
- }));
305
+ _requestStrategy;
306
+ /**
307
+ * Internal BehaviorSubject that holds the latest generated URI
308
+ */
309
+ _uri$ = new BehaviorSubject('');
310
+ /**
311
+ * Observable that emits non-empty generated URIs
312
+ */
313
+ uri$ = this._uri$.asObservable().pipe(filter(uri => !!uri));
314
+ constructor(_nestService, requestStrategy, driver, options = {}) {
315
+ this._nestService = _nestService;
316
+ this._driver = driver;
317
+ this._options = new QueryBuilderOptions(options);
318
+ this._requestStrategy = requestStrategy;
159
319
  }
160
320
  /**
161
- * Set the limit for paginated results
162
- * Must be a positive integer greater than 0
321
+ * Assert that the active driver is one of the allowed drivers
163
322
  *
164
- * @param {number} limit - The number of items per page
165
- * @throws {InvalidLimitError} If limit is not a positive integer
166
- * @example
167
- * service.limit = 25;
323
+ * @param allowed - The allowed drivers
324
+ * @param error - The error to throw if the driver is not allowed
325
+ * @throws The provided error if the active driver is not in the allowed list
168
326
  */
169
- set limit(limit) {
170
- this._validateLimit(limit);
171
- this._nest.update(nest => ({
172
- ...nest,
173
- limit
174
- }));
327
+ _assertDriver(allowed, error) {
328
+ if (!allowed.includes(this._driver)) {
329
+ throw error;
330
+ }
175
331
  }
176
332
  /**
177
- * Set the model name for the query
178
- * Must be a non-empty string
333
+ * Add fields to the select statement for the given model (JSON:API and Spatie only)
179
334
  *
180
- * @param {string} model - The model/resource name (e.g., 'users', 'posts')
181
- * @throws {InvalidModelNameError} If model is not a non-empty string
182
- * @example
183
- * service.model = 'users';
335
+ * @param model - Model that holds the fields
336
+ * @param fields - Fields to select
337
+ * @returns {this}
338
+ * @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
184
339
  */
185
- set model(model) {
186
- this._validateModelName(model);
187
- this._nest.update(nest => ({
188
- ...nest,
189
- model
190
- }));
340
+ addFields(model, fields) {
341
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedFieldSelectionError());
342
+ if (!fields.length) {
343
+ return this;
344
+ }
345
+ this._nestService.addFields({ [model]: fields });
346
+ return this;
191
347
  }
192
348
  /**
193
- * Set the page number for pagination
194
- * Must be a positive integer greater than 0
349
+ * Add a filter with the given value(s) (JSON:API, NestJS, and Spatie)
195
350
  *
196
- * @param {number} page - The page number to fetch
197
- * @throws {InvalidPageNumberError} If page is not a positive integer
198
- * @example
199
- * service.page = 2;
351
+ * Produces: `filter[field]=value` (JSON:API / Spatie) or `filter.field=value` (NestJS)
352
+ *
353
+ * @param {string} field - Name of the field to filter
354
+ * @param {(string | number | boolean)[]} values - The needle(s)
355
+ * @returns {this}
356
+ * @throws {UnsupportedFilterError} If the active driver does not support filters
200
357
  */
201
- set page(page) {
202
- this._validatePageNumber(page);
203
- this._nest.update(nest => ({
204
- ...nest,
205
- page
206
- }));
207
- }
208
- _clone(obj) {
209
- return JSON.parse(JSON.stringify(obj));
358
+ addFilter(field, ...values) {
359
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedFilterError());
360
+ if (!values.length) {
361
+ return this;
362
+ }
363
+ this._nestService.addFilters({
364
+ [field]: values
365
+ });
366
+ return this;
210
367
  }
211
368
  /**
212
- * Validates that the model name is a non-empty string
369
+ * Add a filter with an explicit operator (NestJS only)
213
370
  *
214
- * @param {string} model - The model name to validate
215
- * @throws {InvalidModelNameError} If model is not a non-empty string
216
- * @private
371
+ * Produces: `filter.field=$operator:value`
372
+ *
373
+ * @param {string} field - Name of the field to filter
374
+ * @param {FilterOperatorEnum} operator - The filter operator to apply
375
+ * @param {(string | number | boolean)[]} values - The value(s) for the filter
376
+ * @returns {this}
377
+ * @throws {UnsupportedFilterOperatorError} If the active driver does not support filter operators
217
378
  */
218
- _validateModelName(model) {
219
- if (!model || typeof model !== 'string' || model.trim().length === 0) {
220
- throw new InvalidModelNameError(model);
379
+ addFilterOperator(field, operator, ...values) {
380
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedFilterOperatorError());
381
+ if (!values.length) {
382
+ return this;
221
383
  }
384
+ this._nestService.addOperatorFilters([{ field, operator, values }]);
385
+ return this;
222
386
  }
223
387
  /**
224
- * Validates that the page number is a positive integer
388
+ * Add related entities to include in the request (JSON:API and Spatie only)
225
389
  *
226
- * @param {number} page - The page number to validate
227
- * @throws {InvalidPageNumberError} If page is not a positive integer
228
- * @private
390
+ * @param {string[]} models - Models to include
391
+ * @returns {this}
392
+ * @throws {UnsupportedIncludesError} If the active driver does not support includes
229
393
  */
230
- _validatePageNumber(page) {
231
- if (!Number.isInteger(page) || page < 1) {
232
- throw new InvalidPageNumberError(page);
394
+ addIncludes(...models) {
395
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedIncludesError());
396
+ if (!models.length) {
397
+ return this;
233
398
  }
399
+ this._nestService.addIncludes(models);
400
+ return this;
234
401
  }
235
402
  /**
236
- * Validates that the limit is a positive integer
403
+ * Add flat field selection (NestJS only)
237
404
  *
238
- * @param {number} limit - The limit value to validate
239
- * @throws {InvalidLimitError} If limit is not a positive integer
240
- * @private
405
+ * Produces: `select=col1,col2`
406
+ *
407
+ * @param {string[]} fields - Fields to select
408
+ * @returns {this}
409
+ * @throws {UnsupportedSelectError} If the active driver does not support flat field selection
241
410
  */
242
- _validateLimit(limit) {
243
- if (!Number.isInteger(limit) || limit < 1) {
244
- throw new InvalidLimitError(limit);
411
+ addSelect(...fields) {
412
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedSelectError());
413
+ if (!fields.length) {
414
+ return this;
245
415
  }
416
+ this._nestService.addSelect(fields);
417
+ return this;
246
418
  }
247
419
  /**
248
- * Add selectable fields for the given model to the request
249
- * Automatically prevents duplicate fields for each model
420
+ * Add a field with a sort criteria (JSON:API, NestJS, and Spatie)
250
421
  *
251
- * @param {IFields} fields - Object mapping model names to arrays of field names
252
- * @return {void}
253
- * @example
254
- * service.addFields({ users: ['id', 'email', 'username'] });
255
- * service.addFields({ posts: ['title', 'content'] });
422
+ * @param field - Field to use for sorting
423
+ * @param {SortEnum} order - A value from the SortEnum enumeration
424
+ * @returns {this}
425
+ * @throws {UnsupportedSortError} If the active driver does not support sorts
256
426
  */
257
- addFields(fields) {
258
- this._nest.update(nest => {
259
- const mergedFields = { ...nest.fields };
260
- Object.keys(fields).forEach(model => {
261
- const existingFields = mergedFields[model] || [];
262
- const newFields = fields[model];
263
- // Use Set to prevent duplicates
264
- const uniqueFields = Array.from(new Set([...existingFields, ...newFields]));
265
- mergedFields[model] = uniqueFields;
427
+ addSort(field, order) {
428
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedSortError());
429
+ this._nestService.addSort({
430
+ field,
431
+ order
432
+ });
433
+ return this;
434
+ }
435
+ /**
436
+ * Delete selected fields for the given models in the current query builder state (JSON:API and Spatie only)
437
+ *
438
+ * ```
439
+ * ngQubeeService.deleteFields({
440
+ * users: ['email', 'password'],
441
+ * address: ['zipcode']
442
+ * });
443
+ * ```
444
+ *
445
+ * @param {IFields} fields - Object mapping model names to field arrays to remove
446
+ * @returns {this}
447
+ * @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
448
+ */
449
+ deleteFields(fields) {
450
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedFieldSelectionError());
451
+ this._nestService.deleteFields(fields);
452
+ return this;
453
+ }
454
+ /**
455
+ * Delete selected fields for the given model in the current query builder state (JSON:API and Spatie only)
456
+ *
457
+ * ```
458
+ * ngQubeeService.deleteFieldsByModel('users', 'email', 'password');
459
+ * ```
460
+ *
461
+ * @param model - Model that holds the fields
462
+ * @param {string[]} fields - Fields to delete from the state
463
+ * @returns {this}
464
+ * @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
465
+ */
466
+ deleteFieldsByModel(model, ...fields) {
467
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedFieldSelectionError());
468
+ if (!fields.length) {
469
+ return this;
470
+ }
471
+ this._nestService.deleteFields({
472
+ [model]: fields
473
+ });
474
+ return this;
475
+ }
476
+ /**
477
+ * Remove given filters from the query builder state (JSON:API, NestJS, and Spatie)
478
+ *
479
+ * @param {string[]} filters - Filters to remove
480
+ * @returns {this}
481
+ * @throws {UnsupportedFilterError} If the active driver does not support filters
482
+ */
483
+ deleteFilters(...filters) {
484
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedFilterError());
485
+ if (!filters.length) {
486
+ return this;
487
+ }
488
+ this._nestService.deleteFilters(...filters);
489
+ return this;
490
+ }
491
+ /**
492
+ * Remove selected related models from the query builder state (JSON:API and Spatie only)
493
+ *
494
+ * @param {string[]} includes - Models to remove
495
+ * @returns {this}
496
+ * @throws {UnsupportedIncludesError} If the active driver does not support includes
497
+ */
498
+ deleteIncludes(...includes) {
499
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedIncludesError());
500
+ if (!includes.length) {
501
+ return this;
502
+ }
503
+ this._nestService.deleteIncludes(...includes);
504
+ return this;
505
+ }
506
+ /**
507
+ * Remove operator filters by field name (NestJS only)
508
+ *
509
+ * @param {string[]} fields - Field names of operator filters to remove
510
+ * @returns {this}
511
+ * @throws {UnsupportedFilterOperatorError} If the active driver does not support filter operators
512
+ */
513
+ deleteOperatorFilters(...fields) {
514
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedFilterOperatorError());
515
+ if (!fields.length) {
516
+ return this;
517
+ }
518
+ this._nestService.deleteOperatorFilters(...fields);
519
+ return this;
520
+ }
521
+ /**
522
+ * Remove search term from the query builder state (NestJS only)
523
+ *
524
+ * @returns {this}
525
+ * @throws {UnsupportedSearchError} If the active driver does not support search
526
+ */
527
+ deleteSearch() {
528
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedSearchError());
529
+ this._nestService.deleteSearch();
530
+ return this;
531
+ }
532
+ /**
533
+ * Remove flat field selections from the query builder state (NestJS only)
534
+ *
535
+ * @param {string[]} fields - Fields to remove from selection
536
+ * @returns {this}
537
+ * @throws {UnsupportedSelectError} If the active driver does not support flat field selection
538
+ */
539
+ deleteSelect(...fields) {
540
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedSelectError());
541
+ if (!fields.length) {
542
+ return this;
543
+ }
544
+ this._nestService.deleteSelect(...fields);
545
+ return this;
546
+ }
547
+ /**
548
+ * Remove sort rules from the query builder state (JSON:API, NestJS, and Spatie)
549
+ *
550
+ * @param sorts - Fields used for sorting to remove
551
+ * @returns {this}
552
+ * @throws {UnsupportedSortError} If the active driver does not support sorts
553
+ */
554
+ deleteSorts(...sorts) {
555
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedSortError());
556
+ this._nestService.deleteSorts(...sorts);
557
+ return this;
558
+ }
559
+ /**
560
+ * Generate a URI accordingly to the given data and active driver
561
+ *
562
+ * @returns {Observable<string>} An observable that emits the generated URI
563
+ */
564
+ generateUri() {
565
+ try {
566
+ this._uri$.next(this._requestStrategy.buildUri(this._nestService.nest(), this._options));
567
+ return this.uri$;
568
+ }
569
+ catch (error) {
570
+ return throwError(() => error);
571
+ }
572
+ }
573
+ /**
574
+ * Clear the current state and reset the Query Builder to a fresh, clean condition
575
+ *
576
+ * @returns {this}
577
+ */
578
+ reset() {
579
+ this._nestService.reset();
580
+ return this;
581
+ }
582
+ /**
583
+ * Set the base URL to use for composing the address
584
+ *
585
+ * @param {string} baseUrl - The base URL
586
+ * @returns {this}
587
+ */
588
+ setBaseUrl(baseUrl) {
589
+ this._nestService.baseUrl = baseUrl;
590
+ return this;
591
+ }
592
+ /**
593
+ * Set the items per page number
594
+ *
595
+ * Validation is delegated to the active request strategy because the
596
+ * accepted range is driver-specific: nestjs-paginate additionally accepts
597
+ * `-1` as a "fetch all" sentinel, while Laravel, Spatie, and JSON:API
598
+ * require a positive integer.
599
+ *
600
+ * @param limit - Number of items per page (or `-1` to fetch all, NestJS only)
601
+ * @returns {this}
602
+ * @throws {import('../errors/invalid-limit.error').InvalidLimitError} If the value is not accepted by the active driver
603
+ */
604
+ setLimit(limit) {
605
+ this._requestStrategy.validateLimit(limit);
606
+ this._nestService.limit = limit;
607
+ return this;
608
+ }
609
+ /**
610
+ * Set the page that the backend will use to paginate the result set
611
+ *
612
+ * @param page - Page number
613
+ * @returns {this}
614
+ */
615
+ setPage(page) {
616
+ this._nestService.page = page;
617
+ return this;
618
+ }
619
+ /**
620
+ * Set the API resource to run the query against
621
+ *
622
+ * @param {string} resource - Resource name (e.g. 'users' produces /users)
623
+ * @returns {this}
624
+ */
625
+ setResource(resource) {
626
+ this._nestService.resource = resource;
627
+ return this;
628
+ }
629
+ /**
630
+ * Set the search term for full-text search (NestJS only)
631
+ *
632
+ * Produces: `search=term`
633
+ *
634
+ * @param {string} search - The search term
635
+ * @returns {this}
636
+ * @throws {UnsupportedSearchError} If the active driver does not support search
637
+ */
638
+ setSearch(search) {
639
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedSearchError());
640
+ this._nestService.setSearch(search);
641
+ return this;
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Error thrown when an invalid resource name is provided
647
+ *
648
+ * Resource name must be a non-empty string.
649
+ */
650
+ class InvalidResourceNameError extends Error {
651
+ constructor(resource) {
652
+ super(`Invalid resource name: Resource name must be a non-empty string. Received: ${JSON.stringify(resource)}`);
653
+ this.name = 'InvalidResourceNameError';
654
+ }
655
+ }
656
+
657
+ class InvalidPageNumberError extends Error {
658
+ constructor(page) {
659
+ super(`Invalid page number: Page must be a positive integer greater than 0. Received: ${page}`);
660
+ this.name = 'InvalidPageNumberError';
661
+ }
662
+ }
663
+
664
+ const INITIAL_STATE = {
665
+ baseUrl: '',
666
+ fields: {},
667
+ filters: {},
668
+ includes: [],
669
+ limit: 15,
670
+ operatorFilters: [],
671
+ page: 1,
672
+ resource: '',
673
+ search: '',
674
+ select: [],
675
+ sorts: []
676
+ };
677
+ class NestService {
678
+ /**
679
+ * Private writable signal that holds the Query Builder state
680
+ *
681
+ * @type {IQueryBuilderState}
682
+ */
683
+ _nest = signal(this._clone(INITIAL_STATE), ...(ngDevMode ? [{ debugName: "_nest" }] : []));
684
+ /**
685
+ * A computed signal that makes readonly the writable signal _nest
686
+ *
687
+ * @type {Signal<IQueryBuilderState>}
688
+ */
689
+ nest = computed(() => this._clone(this._nest()), ...(ngDevMode ? [{ debugName: "nest" }] : []));
690
+ constructor() {
691
+ // Nothing to see here
692
+ }
693
+ /**
694
+ * Set the base URL for the API
695
+ *
696
+ * @param {string} baseUrl - The base URL to prepend to generated URIs
697
+ * @example
698
+ * service.baseUrl = 'https://api.example.com';
699
+ */
700
+ set baseUrl(baseUrl) {
701
+ this._nest.update(nest => ({
702
+ ...nest,
703
+ baseUrl
704
+ }));
705
+ }
706
+ /**
707
+ * Set the limit for paginated results
708
+ *
709
+ * This setter performs a raw state write. Validation of the value is the
710
+ * responsibility of the active request strategy and is enforced upstream
711
+ * by `NgQubeeService.setLimit()`, because the accepted range depends on
712
+ * the driver (e.g. nestjs-paginate accepts `-1` for "fetch all").
713
+ *
714
+ * @param {number} limit - The number of items per page
715
+ * @example
716
+ * service.limit = 25;
717
+ */
718
+ set limit(limit) {
719
+ this._nest.update(nest => ({
720
+ ...nest,
721
+ limit
722
+ }));
723
+ }
724
+ /**
725
+ * Set the page number for pagination
726
+ * Must be a positive integer greater than 0
727
+ *
728
+ * @param {number} page - The page number to fetch
729
+ * @throws {InvalidPageNumberError} If page is not a positive integer
730
+ * @example
731
+ * service.page = 2;
732
+ */
733
+ set page(page) {
734
+ this._validatePageNumber(page);
735
+ this._nest.update(nest => ({
736
+ ...nest,
737
+ page
738
+ }));
739
+ }
740
+ /**
741
+ * Set the resource name for the query
742
+ * Must be a non-empty string
743
+ *
744
+ * @param {string} resource - The API resource name (e.g., 'users', 'posts')
745
+ * @throws {InvalidResourceNameError} If resource is not a non-empty string
746
+ * @example
747
+ * service.resource = 'users';
748
+ */
749
+ set resource(resource) {
750
+ this._validateResourceName(resource);
751
+ this._nest.update(nest => ({
752
+ ...nest,
753
+ resource
754
+ }));
755
+ }
756
+ _clone(obj) {
757
+ return JSON.parse(JSON.stringify(obj));
758
+ }
759
+ /**
760
+ * Validates that the page number is a positive integer
761
+ *
762
+ * @param {number} page - The page number to validate
763
+ * @throws {InvalidPageNumberError} If page is not a positive integer
764
+ * @private
765
+ */
766
+ _validatePageNumber(page) {
767
+ if (!Number.isInteger(page) || page < 1) {
768
+ throw new InvalidPageNumberError(page);
769
+ }
770
+ }
771
+ /**
772
+ * Validates that the resource name is a non-empty string
773
+ *
774
+ * @param {string} resource - The resource name to validate
775
+ * @throws {InvalidResourceNameError} If resource is not a non-empty string
776
+ * @private
777
+ */
778
+ _validateResourceName(resource) {
779
+ if (!resource || typeof resource !== 'string' || resource.trim().length === 0) {
780
+ throw new InvalidResourceNameError(resource);
781
+ }
782
+ }
783
+ /**
784
+ * Add selectable fields for the given model to the request
785
+ * Automatically prevents duplicate fields for each model
786
+ *
787
+ * @param {IFields} fields - Object mapping model names to arrays of field names
788
+ * @return {void}
789
+ * @example
790
+ * service.addFields({ users: ['id', 'email', 'username'] });
791
+ * service.addFields({ posts: ['title', 'content'] });
792
+ */
793
+ addFields(fields) {
794
+ this._nest.update(nest => {
795
+ const mergedFields = { ...nest.fields };
796
+ Object.keys(fields).forEach(model => {
797
+ const existingFields = mergedFields[model] || [];
798
+ const newFields = fields[model];
799
+ // Use Set to prevent duplicates
800
+ const uniqueFields = Array.from(new Set([...existingFields, ...newFields]));
801
+ mergedFields[model] = uniqueFields;
266
802
  });
267
803
  return {
268
804
  ...nest,
@@ -292,27 +828,77 @@ class NestService {
292
828
  });
293
829
  return {
294
830
  ...nest,
295
- filters: mergedFilters
831
+ filters: mergedFilters
832
+ };
833
+ });
834
+ }
835
+ /**
836
+ * Add resources to include with the request
837
+ * Automatically prevents duplicate includes
838
+ *
839
+ * @param {string[]} includes - Array of resource names to include in the response
840
+ * @return {void}
841
+ * @example
842
+ * service.addIncludes(['profile', 'posts']);
843
+ * service.addIncludes(['comments']);
844
+ */
845
+ addIncludes(includes) {
846
+ this._nest.update(nest => {
847
+ // Use Set to prevent duplicates
848
+ const uniqueIncludes = Array.from(new Set([...nest.includes, ...includes]));
849
+ return {
850
+ ...nest,
851
+ includes: uniqueIncludes
852
+ };
853
+ });
854
+ }
855
+ /**
856
+ * Add filters with explicit operators (NestJS only)
857
+ * Automatically prevents duplicate operator filters for the same field + operator combination
858
+ *
859
+ * @param {IOperatorFilter[]} filters - Array of operator filter configurations
860
+ * @return {void}
861
+ * @example
862
+ * import { FilterOperatorEnum } from 'ng-qubee';
863
+ * service.addOperatorFilters([{ field: 'age', operator: FilterOperatorEnum.GTE, values: [18] }]);
864
+ */
865
+ addOperatorFilters(filters) {
866
+ this._nest.update(nest => {
867
+ const merged = [...nest.operatorFilters];
868
+ filters.forEach(newFilter => {
869
+ const existingIdx = merged.findIndex(f => f.field === newFilter.field && f.operator === newFilter.operator);
870
+ if (existingIdx > -1) {
871
+ const existingValues = merged[existingIdx].values;
872
+ merged[existingIdx] = {
873
+ ...merged[existingIdx],
874
+ values: Array.from(new Set([...existingValues, ...newFilter.values]))
875
+ };
876
+ }
877
+ else {
878
+ merged.push({ ...newFilter });
879
+ }
880
+ });
881
+ return {
882
+ ...nest,
883
+ operatorFilters: merged
296
884
  };
297
885
  });
298
886
  }
299
887
  /**
300
- * Add resources to include with the request
301
- * Automatically prevents duplicate includes
888
+ * Add flat field selection columns (NestJS only)
889
+ * Automatically prevents duplicate select fields
302
890
  *
303
- * @param {string[]} includes - Array of model names to include in the response
891
+ * @param {string[]} fields - Array of column names to select
304
892
  * @return {void}
305
893
  * @example
306
- * service.addIncludes(['profile', 'posts']);
307
- * service.addIncludes(['comments']);
894
+ * service.addSelect(['id', 'name', 'email']);
308
895
  */
309
- addIncludes(includes) {
896
+ addSelect(fields) {
310
897
  this._nest.update(nest => {
311
- // Use Set to prevent duplicates
312
- const uniqueIncludes = Array.from(new Set([...nest.includes, ...includes]));
898
+ const uniqueSelect = Array.from(new Set([...nest.select, ...fields]));
313
899
  return {
314
900
  ...nest,
315
- includes: uniqueIncludes
901
+ select: uniqueSelect
316
902
  };
317
903
  });
318
904
  }
@@ -390,6 +976,49 @@ class NestService {
390
976
  includes: nest.includes.filter(v => !includes.includes(v))
391
977
  }));
392
978
  }
979
+ /**
980
+ * Remove operator filters by field name (NestJS only)
981
+ *
982
+ * @param {...string[]} fields - Field names of operator filters to remove
983
+ * @return {void}
984
+ * @example
985
+ * service.deleteOperatorFilters('age');
986
+ * service.deleteOperatorFilters('price', 'quantity');
987
+ */
988
+ deleteOperatorFilters(...fields) {
989
+ this._nest.update(nest => ({
990
+ ...nest,
991
+ operatorFilters: nest.operatorFilters.filter(f => !fields.includes(f.field))
992
+ }));
993
+ }
994
+ /**
995
+ * Remove the search term from the state (NestJS only)
996
+ *
997
+ * @return {void}
998
+ * @example
999
+ * service.deleteSearch();
1000
+ */
1001
+ deleteSearch() {
1002
+ this._nest.update(nest => ({
1003
+ ...nest,
1004
+ search: ''
1005
+ }));
1006
+ }
1007
+ /**
1008
+ * Remove flat field selections from the state (NestJS only)
1009
+ *
1010
+ * @param {...string[]} fields - Field names to remove from selection
1011
+ * @return {void}
1012
+ * @example
1013
+ * service.deleteSelect('email');
1014
+ * service.deleteSelect('name', 'email');
1015
+ */
1016
+ deleteSelect(...fields) {
1017
+ this._nest.update(nest => ({
1018
+ ...nest,
1019
+ select: nest.select.filter(f => !fields.includes(f))
1020
+ }));
1021
+ }
393
1022
  /**
394
1023
  * Remove sorts from the request by field name
395
1024
  *
@@ -412,6 +1041,20 @@ class NestService {
412
1041
  sorts: s
413
1042
  }));
414
1043
  }
1044
+ /**
1045
+ * Set the full-text search term (NestJS only)
1046
+ *
1047
+ * @param {string} search - The search term
1048
+ * @return {void}
1049
+ * @example
1050
+ * service.setSearch('john doe');
1051
+ */
1052
+ setSearch(search) {
1053
+ this._nest.update(nest => ({
1054
+ ...nest,
1055
+ search
1056
+ }));
1057
+ }
415
1058
  /**
416
1059
  * Reset the query builder state to initial values
417
1060
  * Clears all fields, filters, includes, sorts, and resets pagination
@@ -419,385 +1062,1018 @@ class NestService {
419
1062
  * @return {void}
420
1063
  * @example
421
1064
  * service.reset();
422
- * // State is now: { baseUrl: '', fields: {}, filters: {}, includes: [], limit: 15, model: '', page: 1, sorts: [] }
423
1065
  */
424
1066
  reset() {
425
1067
  this._nest.update(_ => this._clone(INITIAL_STATE));
426
1068
  }
427
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NestService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
428
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NestService });
1069
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1070
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService });
429
1071
  }
430
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NestService, decorators: [{
1072
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, decorators: [{
431
1073
  type: Injectable
432
1074
  }], ctorParameters: () => [] });
433
1075
 
434
- class NgQubeeService {
435
- _nestService;
1076
+ class PaginationService {
1077
+ /**
1078
+ * Resolved response key name options
1079
+ */
436
1080
  _options;
437
1081
  /**
438
- * This property serves as an accumulator for holding the composed string with each query param
1082
+ * The response strategy that parses responses for the active driver
1083
+ */
1084
+ _responseStrategy;
1085
+ constructor(responseStrategy, options = {}) {
1086
+ this._options = new ResponseOptions(options);
1087
+ this._responseStrategy = responseStrategy;
1088
+ }
1089
+ /**
1090
+ * Transform a raw API response into a typed PaginatedCollection
1091
+ *
1092
+ * Delegates to the active driver's response strategy for parsing.
1093
+ *
1094
+ * @param response - The raw API response object
1095
+ * @returns A typed PaginatedCollection instance
1096
+ */
1097
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1098
+ paginate(response) {
1099
+ return this._responseStrategy.paginate(response, this._options);
1100
+ }
1101
+ }
1102
+
1103
+ var SortEnum;
1104
+ (function (SortEnum) {
1105
+ SortEnum["ASC"] = "asc";
1106
+ SortEnum["DESC"] = "desc";
1107
+ })(SortEnum || (SortEnum = {}));
1108
+
1109
+ /**
1110
+ * Thrown when a limit value does not satisfy the active driver's constraints
1111
+ *
1112
+ * Validation is driver-scoped: most drivers require an integer `>= 1`, while
1113
+ * the NestJS driver additionally accepts `-1` as a "fetch all items" sentinel
1114
+ * (as documented by nestjs-paginate). The message is tailored accordingly so
1115
+ * the caller understands which values are permitted.
1116
+ */
1117
+ class InvalidLimitError extends Error {
1118
+ /**
1119
+ * @param limit - The rejected limit value
1120
+ * @param allowFetchAll - Whether the active driver accepts `-1` (fetch all)
1121
+ */
1122
+ constructor(limit, allowFetchAll = false) {
1123
+ const allowed = allowFetchAll
1124
+ ? 'a positive integer greater than 0, or -1 to fetch all items'
1125
+ : 'a positive integer greater than 0';
1126
+ super(`Invalid limit value: Limit must be ${allowed}. Received: ${limit}`);
1127
+ this.name = 'InvalidLimitError';
1128
+ }
1129
+ }
1130
+
1131
+ class UnselectableModelError extends Error {
1132
+ constructor(model) {
1133
+ super(`Unselectable Model: the selected model (${model}) is not present neither in the "model" property, nor in the includes object.`);
1134
+ }
1135
+ }
1136
+
1137
+ /**
1138
+ * Request strategy for the JSON:API driver
1139
+ *
1140
+ * Generates URIs in the JSON:API format:
1141
+ * - Fields: `fields[articles]=title,body&fields[people]=name`
1142
+ * - Filters: `filter[status]=active`
1143
+ * - Includes: `include=author,comments.author`
1144
+ * - Pagination: `page[number]=1&page[size]=15`
1145
+ * - Sort: `sort=-created_at,name` (- prefix = DESC)
1146
+ *
1147
+ * @see https://jsonapi.org/format/
1148
+ */
1149
+ class JsonApiRequestStrategy {
1150
+ /**
1151
+ * Accumulator for composing the URI string
439
1152
  */
440
1153
  _uri = '';
441
- _uri$ = new BehaviorSubject('');
442
- uri$ = this._uri$.asObservable().pipe(filter(uri => !!uri));
443
- constructor(_nestService, options = {}) {
444
- this._nestService = _nestService;
445
- this._options = new QueryBuilderOptions(options);
1154
+ /**
1155
+ * Build a URI string from the given state using the JSON:API format
1156
+ *
1157
+ * @param state - The current query builder state
1158
+ * @param options - The query parameter key name configuration
1159
+ * @returns The composed URI string
1160
+ * @throws Error if resource is not set
1161
+ */
1162
+ buildUri(state, options) {
1163
+ if (!state.resource) {
1164
+ throw new Error('Set the resource property BEFORE adding filters or calling the url() / get() methods');
1165
+ }
1166
+ this._uri = '';
1167
+ this._parseIncludes(state, options);
1168
+ this._parseFields(state, options);
1169
+ this._parseFilters(state, options);
1170
+ this._parsePagination(state, options);
1171
+ this._parseSort(state, options);
1172
+ return this._uri;
1173
+ }
1174
+ /**
1175
+ * Validate that the given limit is accepted by the JSON:API driver
1176
+ *
1177
+ * The JSON:API specification leaves pagination semantics to the server and
1178
+ * does not define a "fetch all" sentinel, so only positive integers are
1179
+ * accepted.
1180
+ *
1181
+ * @param limit - The limit value to validate
1182
+ * @throws {InvalidLimitError} If the value is not a positive integer
1183
+ */
1184
+ validateLimit(limit) {
1185
+ if (Number.isInteger(limit) && limit >= 1) {
1186
+ return;
1187
+ }
1188
+ throw new InvalidLimitError(limit);
446
1189
  }
447
- _parseFields(s) {
448
- if (!Object.keys(s.fields).length) {
1190
+ /**
1191
+ * Parse and append field selection parameters
1192
+ *
1193
+ * Validates that each field model exists either as the main resource
1194
+ * or in the includes list. Fields are grouped by type in bracket notation.
1195
+ *
1196
+ * @param state - The current query builder state
1197
+ * @param options - The query parameter key name configuration
1198
+ * @returns The generated field selection parameter string
1199
+ * @throws Error if resource is required but not set
1200
+ * @throws UnselectableModelError if a field model is not in resource or includes
1201
+ */
1202
+ _parseFields(state, options) {
1203
+ if (!Object.keys(state.fields).length) {
449
1204
  return this._uri;
450
1205
  }
451
- if (!s.model) {
452
- throw new Error('While selecting fields, the -> model <- is required');
1206
+ if (!state.resource) {
1207
+ throw new Error('While selecting fields, the -> resource <- is required');
453
1208
  }
454
- if (!(s.model in s.fields)) {
455
- throw new Error(`Key ${s.model} is missing in the fields object`);
1209
+ if (!(state.resource in state.fields)) {
1210
+ throw new Error(`Key ${state.resource} is missing in the fields object`);
456
1211
  }
457
1212
  const f = {};
458
- for (const k in s.fields) {
459
- if (s.fields.hasOwnProperty(k)) {
460
- // Check if the key is the model or is declared in "includes".
461
- // If not, it means that has not been selected anywhere and that will cause an error on the API
462
- if (k !== s.model && !s.includes.includes(k)) {
1213
+ for (const k in state.fields) {
1214
+ if (state.fields.hasOwnProperty(k)) {
1215
+ if (k !== state.resource && !state.includes.includes(k)) {
463
1216
  throw new UnselectableModelError(k);
464
1217
  }
465
- Object.assign(f, { [`${this._options.fields}[${k}]`]: s.fields[k].join(',') });
1218
+ Object.assign(f, { [`${options.fields}[${k}]`]: state.fields[k].join(',') });
466
1219
  }
467
1220
  }
468
- const param = `${this._prepend(s.model)}${qs.stringify(f, { encode: false })}`;
1221
+ const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
469
1222
  this._uri += param;
470
1223
  return param;
471
1224
  }
472
- _parseFilters(s) {
473
- const keys = Object.keys(s.filters);
1225
+ /**
1226
+ * Parse and append filter parameters
1227
+ *
1228
+ * Generates filter parameters in bracket notation: `filter[key]=value1,value2`
1229
+ *
1230
+ * @param state - The current query builder state
1231
+ * @param options - The query parameter key name configuration
1232
+ * @returns The generated filter parameter string
1233
+ */
1234
+ _parseFilters(state, options) {
1235
+ const keys = Object.keys(state.filters);
474
1236
  if (!keys.length) {
475
1237
  return this._uri;
476
1238
  }
477
1239
  const f = {
478
- [`${this._options.filters}`]: keys.reduce((acc, key) => {
479
- return Object.assign(acc, { [key]: s.filters[key].join(',') });
1240
+ [`${options.filters}`]: keys.reduce((acc, key) => {
1241
+ return Object.assign(acc, { [key]: state.filters[key].join(',') });
480
1242
  }, {})
481
1243
  };
482
- const param = `${this._prepend(s.model)}${qs.stringify(f, { encode: false })}`;
1244
+ const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
483
1245
  this._uri += param;
484
1246
  return param;
485
1247
  }
486
- _parseIncludes(s) {
487
- if (!s.includes.length) {
1248
+ /**
1249
+ * Parse and append include parameters
1250
+ *
1251
+ * Generates: `include=author,comments.author`
1252
+ *
1253
+ * @param state - The current query builder state
1254
+ * @param options - The query parameter key name configuration
1255
+ * @returns The generated include parameter string
1256
+ */
1257
+ _parseIncludes(state, options) {
1258
+ if (!state.includes.length) {
488
1259
  return this._uri;
489
1260
  }
490
- const param = `${this._prepend(s.model)}${this._options.includes}=${s.includes}`;
491
- this._uri += param;
492
- return param;
493
- }
494
- _parseLimit(s) {
495
- const param = `${this._prepend(s.model)}${this._options.limit}=${s.limit}`;
1261
+ const param = `${this._prepend(state)}${options.includes}=${state.includes}`;
496
1262
  this._uri += param;
497
1263
  return param;
498
1264
  }
499
- _parsePage(s) {
500
- const param = `${this._prepend(s.model)}${this._options.page}=${s.page}`;
1265
+ /**
1266
+ * Parse and append pagination parameters in JSON:API bracket notation
1267
+ *
1268
+ * Generates: `page[number]=1&page[size]=15`
1269
+ *
1270
+ * @param state - The current query builder state
1271
+ * @param options - The query parameter key name configuration
1272
+ * @returns The generated pagination parameter string
1273
+ */
1274
+ _parsePagination(state, options) {
1275
+ const pagination = qs.stringify({ [options.page]: { number: state.page, size: state.limit } }, { encode: false });
1276
+ const param = `${this._prepend(state)}${pagination}`;
501
1277
  this._uri += param;
502
1278
  return param;
503
1279
  }
504
- _parseSort(s) {
1280
+ /**
1281
+ * Parse and append sort parameters
1282
+ *
1283
+ * Generates: `sort=-field1,field2` where `-` prefix indicates DESC order
1284
+ *
1285
+ * @param state - The current query builder state
1286
+ * @param options - The query parameter key name configuration
1287
+ * @returns The generated sort parameter string
1288
+ */
1289
+ _parseSort(state, options) {
505
1290
  let param = '';
506
- if (!s.sorts.length) {
1291
+ if (!state.sorts.length) {
507
1292
  return param;
508
1293
  }
509
- param = `${this._prepend(s.model)}${this._options.sort}=`;
510
- s.sorts.forEach((sort, idx) => {
1294
+ param = `${this._prepend(state)}${options.sort}=`;
1295
+ state.sorts.forEach((sort, idx) => {
511
1296
  param += `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`;
512
- if (idx < s.sorts.length - 1) {
1297
+ if (idx < state.sorts.length - 1) {
513
1298
  param += ',';
514
1299
  }
515
1300
  });
516
1301
  this._uri += param;
517
1302
  return param;
518
1303
  }
519
- _parse(s) {
520
- if (!s.model) {
521
- throw new Error('Set the model property BEFORE adding filters or calling the url() / get() methods');
1304
+ /**
1305
+ * Determine the appropriate URI prefix based on the current accumulator state
1306
+ *
1307
+ * Returns the full base path with `?` for the first parameter,
1308
+ * or `&` for subsequent parameters.
1309
+ *
1310
+ * @param state - The current query builder state
1311
+ * @returns The prefix string to prepend to the next parameter
1312
+ */
1313
+ _prepend(state) {
1314
+ if (this._uri) {
1315
+ return '&';
1316
+ }
1317
+ return state.baseUrl ? `${state.baseUrl}/${state.resource}?` : `/${state.resource}?`;
1318
+ }
1319
+ }
1320
+
1321
+ /**
1322
+ * Response strategy for the JSON:API driver
1323
+ *
1324
+ * Parses JSON:API pagination responses:
1325
+ * ```json
1326
+ * {
1327
+ * "data": [...],
1328
+ * "meta": {
1329
+ * "current-page": 1,
1330
+ * "per-page": 10,
1331
+ * "total": 100,
1332
+ * "page-count": 10,
1333
+ * "from": 1,
1334
+ * "to": 10
1335
+ * },
1336
+ * "links": {
1337
+ * "first": "url",
1338
+ * "prev": "url",
1339
+ * "next": "url",
1340
+ * "last": "url"
1341
+ * }
1342
+ * }
1343
+ * ```
1344
+ *
1345
+ * @see https://jsonapi.org/format/
1346
+ */
1347
+ class JsonApiResponseStrategy {
1348
+ /**
1349
+ * Parse a JSON:API pagination response into a PaginatedCollection
1350
+ *
1351
+ * Supports dot-notation key paths for accessing nested values.
1352
+ * Computes `from` and `to` from `currentPage` and `perPage` when
1353
+ * they are not directly available in the response.
1354
+ *
1355
+ * @param response - The raw API response object
1356
+ * @param options - The response key name configuration
1357
+ * @returns A typed PaginatedCollection instance
1358
+ */
1359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1360
+ paginate(response, options) {
1361
+ const data = this._resolve(response, options.data);
1362
+ const currentPage = this._resolve(response, options.currentPage);
1363
+ const total = this._resolve(response, options.total);
1364
+ const perPage = this._resolve(response, options.perPage);
1365
+ const lastPage = this._resolve(response, options.lastPage);
1366
+ // Compute from/to if not directly available
1367
+ const from = this._resolveFrom(response, options, currentPage, perPage);
1368
+ const to = this._resolveTo(response, options, currentPage, perPage, total);
1369
+ const prevPageUrl = this._resolve(response, options.prevPageUrl);
1370
+ const nextPageUrl = this._resolve(response, options.nextPageUrl);
1371
+ const firstPageUrl = this._resolve(response, options.firstPageUrl);
1372
+ const lastPageUrl = this._resolve(response, options.lastPageUrl);
1373
+ return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl, nextPageUrl, lastPage, firstPageUrl, lastPageUrl);
1374
+ }
1375
+ /**
1376
+ * Resolve a value from a response object using a dot-notation path
1377
+ *
1378
+ * Supports both flat keys ('data') and nested paths ('meta.current-page').
1379
+ *
1380
+ * @param response - The raw response object
1381
+ * @param path - The dot-notation path to resolve
1382
+ * @returns The resolved value, or undefined if not found
1383
+ */
1384
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1385
+ _resolve(response, path) {
1386
+ return path.split('.').reduce((obj, key) => obj?.[key], response);
1387
+ }
1388
+ /**
1389
+ * Resolve the "from" index value
1390
+ *
1391
+ * If the path resolves to a value in the response, use it.
1392
+ * Otherwise, compute it from currentPage and perPage:
1393
+ * `(currentPage - 1) * perPage + 1`
1394
+ *
1395
+ * @param response - The raw response object
1396
+ * @param options - The response key name configuration
1397
+ * @param currentPage - The current page number
1398
+ * @param perPage - The number of items per page
1399
+ * @returns The computed "from" index
1400
+ */
1401
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1402
+ _resolveFrom(response, options, currentPage, perPage) {
1403
+ const direct = this._resolve(response, options.from);
1404
+ if (direct !== undefined) {
1405
+ return direct;
1406
+ }
1407
+ if (currentPage && perPage) {
1408
+ return (currentPage - 1) * perPage + 1;
1409
+ }
1410
+ return undefined;
1411
+ }
1412
+ /**
1413
+ * Resolve the "to" index value
1414
+ *
1415
+ * If the path resolves to a value in the response, use it.
1416
+ * Otherwise, compute it from currentPage, perPage, and total:
1417
+ * `Math.min(currentPage * perPage, total)`
1418
+ *
1419
+ * @param response - The raw response object
1420
+ * @param options - The response key name configuration
1421
+ * @param currentPage - The current page number
1422
+ * @param perPage - The number of items per page
1423
+ * @param total - The total number of items
1424
+ * @returns The computed "to" index
1425
+ */
1426
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1427
+ _resolveTo(response, options, currentPage, perPage, total) {
1428
+ const direct = this._resolve(response, options.to);
1429
+ if (direct !== undefined) {
1430
+ return direct;
1431
+ }
1432
+ if (currentPage && perPage && total) {
1433
+ return Math.min(currentPage * perPage, total);
1434
+ }
1435
+ return undefined;
1436
+ }
1437
+ }
1438
+
1439
+ /**
1440
+ * Request strategy for the Laravel (pagination-only) driver
1441
+ *
1442
+ * Generates simple pagination URIs:
1443
+ * - `/{resource}?limit=N&page=N`
1444
+ *
1445
+ * Filters, sorts, fields, includes, search, and select in state are ignored.
1446
+ */
1447
+ class LaravelRequestStrategy {
1448
+ /**
1449
+ * Build a pagination-only URI from the given state
1450
+ *
1451
+ * @param state - The current query builder state
1452
+ * @param options - The query parameter key name configuration
1453
+ * @returns The composed URI string
1454
+ * @throws Error if resource is not set
1455
+ */
1456
+ buildUri(state, options) {
1457
+ if (!state.resource) {
1458
+ throw new Error('Set the resource property BEFORE calling the url() / get() methods');
1459
+ }
1460
+ const base = state.baseUrl ? `${state.baseUrl}/${state.resource}` : `/${state.resource}`;
1461
+ return `${base}?${options.limit}=${state.limit}&${options.page}=${state.page}`;
1462
+ }
1463
+ /**
1464
+ * Validate that the given limit is accepted by the Laravel driver
1465
+ *
1466
+ * Laravel pagination does not recognize `-1` as a "fetch all" sentinel,
1467
+ * so only positive integers are accepted.
1468
+ *
1469
+ * @param limit - The limit value to validate
1470
+ * @throws {InvalidLimitError} If the value is not a positive integer
1471
+ */
1472
+ validateLimit(limit) {
1473
+ if (Number.isInteger(limit) && limit >= 1) {
1474
+ return;
1475
+ }
1476
+ throw new InvalidLimitError(limit);
1477
+ }
1478
+ }
1479
+
1480
+ /**
1481
+ * Response strategy for the Laravel (pagination-only) driver
1482
+ *
1483
+ * Parses flat Laravel pagination responses:
1484
+ * ```json
1485
+ * {
1486
+ * "data": [...],
1487
+ * "current_page": 1,
1488
+ * "total": 100,
1489
+ * "per_page": 15,
1490
+ * "from": 1,
1491
+ * "to": 15,
1492
+ * ...
1493
+ * }
1494
+ * ```
1495
+ */
1496
+ class LaravelResponseStrategy {
1497
+ /**
1498
+ * Parse a flat Laravel pagination response into a PaginatedCollection
1499
+ *
1500
+ * @param response - The raw API response object
1501
+ * @param options - The response key name configuration
1502
+ * @returns A typed PaginatedCollection instance
1503
+ */
1504
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1505
+ paginate(response, options) {
1506
+ return new PaginatedCollection(response[options.data], response[options.currentPage], response[options.from], response[options.to], response[options.total], response[options.perPage], response[options.prevPageUrl], response[options.nextPageUrl], response[options.lastPage], response[options.firstPageUrl], response[options.lastPageUrl]);
1507
+ }
1508
+ }
1509
+
1510
+ /**
1511
+ * Request strategy for the NestJS (nestjs-paginate) driver
1512
+ *
1513
+ * Generates URIs in the NestJS paginate format:
1514
+ * - Simple filters: `filter.field=value`
1515
+ * - Operator filters: `filter.field=$operator:value`
1516
+ * - Sorts: `sortBy=field1:DESC,field2:ASC`
1517
+ * - Select: `select=col1,col2`
1518
+ * - Search: `search=term`
1519
+ * - Pagination: `limit=N&page=N`
1520
+ *
1521
+ * @see https://github.com/ppetzold/nestjs-paginate
1522
+ */
1523
+ class NestjsRequestStrategy {
1524
+ /**
1525
+ * Accumulator for composing the URI string
1526
+ */
1527
+ _uri = '';
1528
+ /**
1529
+ * Build a URI string from the given state using the NestJS paginate format
1530
+ *
1531
+ * @param state - The current query builder state
1532
+ * @param options - The query parameter key name configuration
1533
+ * @returns The composed URI string
1534
+ * @throws Error if model is not set
1535
+ */
1536
+ buildUri(state, options) {
1537
+ if (!state.resource) {
1538
+ throw new Error('Set the resource property BEFORE adding filters or calling the url() / get() methods');
522
1539
  }
523
- // Cleanup the previously generated URI
524
1540
  this._uri = '';
525
- this._parseIncludes(s);
526
- this._parseFields(s);
527
- this._parseFilters(s);
528
- this._parseLimit(s);
529
- this._parsePage(s);
530
- this._parseSort(s);
1541
+ this._parseFilters(state, options);
1542
+ this._parseOperatorFilters(state, options);
1543
+ this._parseSort(state, options);
1544
+ this._parseSelect(state, options);
1545
+ this._parseSearch(state, options);
1546
+ this._parseLimit(state, options);
1547
+ this._parsePage(state, options);
531
1548
  return this._uri;
532
1549
  }
533
- _prepend(model) {
534
- const state = this._nestService.nest();
535
- const baseUrl = state.baseUrl;
536
- if (this._uri) {
537
- return '&';
1550
+ /**
1551
+ * Validate that the given limit is accepted by nestjs-paginate
1552
+ *
1553
+ * Accepts any integer `>= 1` as a page size, plus `-1` which nestjs-paginate
1554
+ * interprets as "fetch all items" (server must opt-in via `maxLimit: -1`).
1555
+ *
1556
+ * @param limit - The limit value to validate
1557
+ * @throws {InvalidLimitError} If the value is not an integer, or is 0, or is a negative number other than -1
1558
+ */
1559
+ validateLimit(limit) {
1560
+ if (Number.isInteger(limit) && (limit === -1 || limit >= 1)) {
1561
+ return;
1562
+ }
1563
+ throw new InvalidLimitError(limit, true);
1564
+ }
1565
+ /**
1566
+ * Parse and append simple filter parameters
1567
+ *
1568
+ * Generates: `filter.field=value1,value2` for each filter
1569
+ *
1570
+ * @param state - The current query builder state
1571
+ * @param options - The query parameter key name configuration
1572
+ */
1573
+ _parseFilters(state, options) {
1574
+ const keys = Object.keys(state.filters);
1575
+ if (!keys.length) {
1576
+ return;
1577
+ }
1578
+ keys.forEach(key => {
1579
+ const values = state.filters[key].join(',');
1580
+ const param = `${this._prepend(state)}${options.filters}.${key}=${values}`;
1581
+ this._uri += param;
1582
+ });
1583
+ }
1584
+ /**
1585
+ * Parse and append the limit parameter
1586
+ *
1587
+ * @param state - The current query builder state
1588
+ * @param options - The query parameter key name configuration
1589
+ */
1590
+ _parseLimit(state, options) {
1591
+ const param = `${this._prepend(state)}${options.limit}=${state.limit}`;
1592
+ this._uri += param;
1593
+ }
1594
+ /**
1595
+ * Parse and append operator filter parameters
1596
+ *
1597
+ * Groups operator filters by field and generates:
1598
+ * - Single value: `filter.field=$operator:value`
1599
+ * - Multiple values ($in, $btw): `filter.field=$operator:val1,val2`
1600
+ *
1601
+ * @param state - The current query builder state
1602
+ * @param options - The query parameter key name configuration
1603
+ */
1604
+ _parseOperatorFilters(state, options) {
1605
+ if (!state.operatorFilters.length) {
1606
+ return;
1607
+ }
1608
+ state.operatorFilters.forEach((opFilter) => {
1609
+ const values = opFilter.values.join(',');
1610
+ const param = `${this._prepend(state)}${options.filters}.${opFilter.field}=${opFilter.operator}:${values}`;
1611
+ this._uri += param;
1612
+ });
1613
+ }
1614
+ /**
1615
+ * Parse and append the page parameter
1616
+ *
1617
+ * @param state - The current query builder state
1618
+ * @param options - The query parameter key name configuration
1619
+ */
1620
+ _parsePage(state, options) {
1621
+ const param = `${this._prepend(state)}${options.page}=${state.page}`;
1622
+ this._uri += param;
1623
+ }
1624
+ /**
1625
+ * Parse and append the search parameter
1626
+ *
1627
+ * Generates: `search=term`
1628
+ *
1629
+ * @param state - The current query builder state
1630
+ * @param options - The query parameter key name configuration
1631
+ */
1632
+ _parseSearch(state, options) {
1633
+ if (!state.search) {
1634
+ return;
1635
+ }
1636
+ const param = `${this._prepend(state)}${options.search}=${state.search}`;
1637
+ this._uri += param;
1638
+ }
1639
+ /**
1640
+ * Parse and append the select parameter
1641
+ *
1642
+ * Generates: `select=col1,col2`
1643
+ *
1644
+ * @param state - The current query builder state
1645
+ * @param options - The query parameter key name configuration
1646
+ */
1647
+ _parseSelect(state, options) {
1648
+ if (!state.select.length) {
1649
+ return;
538
1650
  }
539
- return baseUrl ? `${baseUrl}/${model}?` : `/${model}?`;
1651
+ const param = `${this._prepend(state)}${options.select}=${state.select.join(',')}`;
1652
+ this._uri += param;
540
1653
  }
541
1654
  /**
542
- * Add fields to the select statement for the given model
1655
+ * Parse and append sort parameters
543
1656
  *
544
- * @param model Model that holds the fields
545
- * @param fields Fields to select
546
- * @returns {this}
1657
+ * Generates: `sortBy=field1:DESC,field2:ASC`
1658
+ *
1659
+ * @param state - The current query builder state
1660
+ * @param options - The query parameter key name configuration
547
1661
  */
548
- addFields(model, fields) {
549
- if (!fields.length) {
550
- return this;
1662
+ _parseSort(state, options) {
1663
+ if (!state.sorts.length) {
1664
+ return;
551
1665
  }
552
- this._nestService.addFields({ [model]: fields });
553
- return this;
1666
+ const sortPairs = state.sorts.map(sort => `${sort.field}:${sort.order === SortEnum.DESC ? 'DESC' : 'ASC'}`);
1667
+ const param = `${this._prepend(state)}${options.sortBy}=${sortPairs.join(',')}`;
1668
+ this._uri += param;
554
1669
  }
555
1670
  /**
556
- * Add a filter with the given value(s)
557
- * I.e. filter[field]=1 or filter[field]=1,2,3
1671
+ * Determine the appropriate URI prefix based on the current accumulator state
558
1672
  *
559
- * @param {string} field Name of the field to filter
560
- * @param {string[]} value The needle(s)
561
- * @returns {this}
1673
+ * Returns the full base path with `?` for the first parameter,
1674
+ * or `&` for subsequent parameters.
1675
+ *
1676
+ * @param state - The current query builder state
1677
+ * @returns The prefix string to prepend to the next parameter
562
1678
  */
563
- addFilter(field, ...values) {
564
- if (!values.length) {
565
- return this;
1679
+ _prepend(state) {
1680
+ if (this._uri) {
1681
+ return '&';
566
1682
  }
567
- this._nestService.addFilters({
568
- [field]: values
569
- });
570
- return this;
1683
+ return state.baseUrl ? `${state.baseUrl}/${state.resource}?` : `/${state.resource}?`;
571
1684
  }
1685
+ }
1686
+
1687
+ /**
1688
+ * Response strategy for the NestJS (nestjs-paginate) driver
1689
+ *
1690
+ * Parses nested NestJS pagination responses:
1691
+ * ```json
1692
+ * {
1693
+ * "data": [...],
1694
+ * "meta": {
1695
+ * "currentPage": 1,
1696
+ * "totalItems": 100,
1697
+ * "itemsPerPage": 10,
1698
+ * "totalPages": 10
1699
+ * },
1700
+ * "links": {
1701
+ * "first": "url",
1702
+ * "previous": "url",
1703
+ * "next": "url",
1704
+ * "last": "url",
1705
+ * "current": "url"
1706
+ * }
1707
+ * }
1708
+ * ```
1709
+ *
1710
+ * @see https://github.com/ppetzold/nestjs-paginate
1711
+ */
1712
+ class NestjsResponseStrategy {
572
1713
  /**
573
- * Add related entities to include in the request
1714
+ * Parse a nested NestJS pagination response into a PaginatedCollection
574
1715
  *
575
- * @param {string[]} models
576
- * @returns
1716
+ * Supports dot-notation key paths for accessing nested values.
1717
+ * Computes `from` and `to` from `currentPage` and `itemsPerPage` when
1718
+ * they are not directly available in the response.
1719
+ *
1720
+ * @param response - The raw API response object
1721
+ * @param options - The response key name configuration
1722
+ * @returns A typed PaginatedCollection instance
577
1723
  */
578
- addIncludes(...models) {
579
- if (!models.length) {
580
- return this;
581
- }
582
- this._nestService.addIncludes(models);
583
- return this;
1724
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1725
+ paginate(response, options) {
1726
+ const data = this._resolve(response, options.data);
1727
+ const currentPage = this._resolve(response, options.currentPage);
1728
+ const total = this._resolve(response, options.total);
1729
+ const perPage = this._resolve(response, options.perPage);
1730
+ const lastPage = this._resolve(response, options.lastPage);
1731
+ // Compute from/to if not directly available
1732
+ const from = this._resolveFrom(response, options, currentPage, perPage);
1733
+ const to = this._resolveTo(response, options, currentPage, perPage, total);
1734
+ const prevPageUrl = this._resolve(response, options.prevPageUrl);
1735
+ const nextPageUrl = this._resolve(response, options.nextPageUrl);
1736
+ const firstPageUrl = this._resolve(response, options.firstPageUrl);
1737
+ const lastPageUrl = this._resolve(response, options.lastPageUrl);
1738
+ return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl, nextPageUrl, lastPage, firstPageUrl, lastPageUrl);
584
1739
  }
585
1740
  /**
586
- * Add a field with a sort criteria
1741
+ * Resolve a value from a response object using a dot-notation path
587
1742
  *
588
- * @param field Field to use for sorting
589
- * @param {SortEnum} order A value from the SortEnum enumeration
590
- * @returns {this}
1743
+ * Supports both flat keys ('data') and nested paths ('meta.currentPage').
1744
+ *
1745
+ * @param response - The raw response object
1746
+ * @param path - The dot-notation path to resolve
1747
+ * @returns The resolved value, or undefined if not found
591
1748
  */
592
- addSort(field, order) {
593
- this._nestService.addSort({
594
- field,
595
- order
596
- });
597
- return this;
1749
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1750
+ _resolve(response, path) {
1751
+ return path.split('.').reduce((obj, key) => obj?.[key], response);
598
1752
  }
599
1753
  /**
600
- * Delete selected fields for the given models in the current query builder state
1754
+ * Resolve the "from" index value
601
1755
  *
602
- * ```
603
- * ngQubeeService.deleteFields({
604
- * users: ['email', 'password'],
605
- * address: ['zipcode']
606
- * });
607
- * ```
1756
+ * If the path resolves to a value in the response, use it.
1757
+ * Otherwise, compute it from currentPage and perPage:
1758
+ * `(currentPage - 1) * perPage + 1`
608
1759
  *
609
- * @param {IFields} fields
610
- * @returns
1760
+ * @param response - The raw response object
1761
+ * @param options - The response key name configuration
1762
+ * @param currentPage - The current page number
1763
+ * @param perPage - The number of items per page
1764
+ * @returns The computed "from" index
611
1765
  */
612
- deleteFields(fields) {
613
- this._nestService.deleteFields(fields);
614
- return this;
1766
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1767
+ _resolveFrom(response, options, currentPage, perPage) {
1768
+ const direct = this._resolve(response, options.from);
1769
+ if (direct !== undefined) {
1770
+ return direct;
1771
+ }
1772
+ if (currentPage && perPage) {
1773
+ return (currentPage - 1) * perPage + 1;
1774
+ }
1775
+ return undefined;
615
1776
  }
616
1777
  /**
617
- * Delete selected fields for the given model in the current query builder state
1778
+ * Resolve the "to" index value
618
1779
  *
619
- * ```
620
- * ngQubeeService.deleteFieldsByModel('users', 'email', 'password']);
621
- * ```
1780
+ * If the path resolves to a value in the response, use it.
1781
+ * Otherwise, compute it from currentPage, perPage, and total:
1782
+ * `Math.min(currentPage * perPage, total)`
622
1783
  *
623
- * @param model Model that holds the fields
624
- * @param {string[]} fields Fields to delete from the state
625
- * @returns {this}
1784
+ * @param response - The raw response object
1785
+ * @param options - The response key name configuration
1786
+ * @param currentPage - The current page number
1787
+ * @param perPage - The number of items per page
1788
+ * @param total - The total number of items
1789
+ * @returns The computed "to" index
626
1790
  */
627
- deleteFieldsByModel(model, ...fields) {
628
- if (!fields.length) {
629
- return this;
1791
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1792
+ _resolveTo(response, options, currentPage, perPage, total) {
1793
+ const direct = this._resolve(response, options.to);
1794
+ if (direct !== undefined) {
1795
+ return direct;
630
1796
  }
631
- this._nestService.deleteFields({
632
- [model]: fields
633
- });
634
- return this;
1797
+ if (currentPage && perPage && total) {
1798
+ return Math.min(currentPage * perPage, total);
1799
+ }
1800
+ return undefined;
635
1801
  }
1802
+ }
1803
+
1804
+ /**
1805
+ * Request strategy for the Spatie Query Builder driver
1806
+ *
1807
+ * Generates URIs in the Spatie format:
1808
+ * - Fields: `fields[model]=col1,col2`
1809
+ * - Filters: `filter[field]=value`
1810
+ * - Includes: `include=model1,model2`
1811
+ * - Sorts: `sort=-field1,field2` (- prefix = DESC)
1812
+ * - Pagination: `limit=N&page=N`
1813
+ *
1814
+ * @see https://spatie.be/docs/laravel-query-builder
1815
+ */
1816
+ class SpatieRequestStrategy {
1817
+ /**
1818
+ * Accumulator for composing the URI string
1819
+ */
1820
+ _uri = '';
636
1821
  /**
637
- * Remove given filters from the query builder state
1822
+ * Build a URI string from the given state using the Spatie format
638
1823
  *
639
- * @param {string[]} filters Filters to remove
640
- * @returns {this}
1824
+ * @param state - The current query builder state
1825
+ * @param options - The query parameter key name configuration
1826
+ * @returns The composed URI string
1827
+ * @throws Error if resource is not set
641
1828
  */
642
- deleteFilters(...filters) {
643
- if (!filters.length) {
644
- return this;
1829
+ buildUri(state, options) {
1830
+ if (!state.resource) {
1831
+ throw new Error('Set the resource property BEFORE adding filters or calling the url() / get() methods');
645
1832
  }
646
- this._nestService.deleteFilters(...filters);
647
- return this;
1833
+ this._uri = '';
1834
+ this._parseIncludes(state, options);
1835
+ this._parseFields(state, options);
1836
+ this._parseFilters(state, options);
1837
+ this._parseLimit(state, options);
1838
+ this._parsePage(state, options);
1839
+ this._parseSort(state, options);
1840
+ return this._uri;
648
1841
  }
649
1842
  /**
650
- * Remove selected related models from the query builder state
1843
+ * Validate that the given limit is accepted by the Spatie driver
651
1844
  *
652
- * @param {string[]} includes Models to remove
653
- * @returns
1845
+ * Spatie query-builder does not recognize `-1` as a "fetch all" sentinel,
1846
+ * so only positive integers are accepted.
1847
+ *
1848
+ * @param limit - The limit value to validate
1849
+ * @throws {InvalidLimitError} If the value is not a positive integer
654
1850
  */
655
- deleteIncludes(...includes) {
656
- if (!includes.length) {
657
- return this;
1851
+ validateLimit(limit) {
1852
+ if (Number.isInteger(limit) && limit >= 1) {
1853
+ return;
658
1854
  }
659
- this._nestService.deleteIncludes(...includes);
660
- return this;
1855
+ throw new InvalidLimitError(limit);
661
1856
  }
662
1857
  /**
663
- * Remove sorts rules from the query builder state
1858
+ * Parse and append field selection parameters
664
1859
  *
665
- * @param sorts Fields used for sorting to remove
666
- * @returns {this}
1860
+ * Validates that each field model exists either as the main resource
1861
+ * or in the includes list. Fields are grouped by model in bracket notation.
1862
+ *
1863
+ * @param state - The current query builder state
1864
+ * @param options - The query parameter key name configuration
1865
+ * @returns The generated field selection parameter string
1866
+ * @throws Error if resource is required but not set
1867
+ * @throws UnselectableModelError if a field model is not in resource or includes
667
1868
  */
668
- deleteSorts(...sorts) {
669
- this._nestService.deleteSorts(...sorts);
670
- return this;
1869
+ _parseFields(state, options) {
1870
+ if (!Object.keys(state.fields).length) {
1871
+ return this._uri;
1872
+ }
1873
+ if (!state.resource) {
1874
+ throw new Error('While selecting fields, the -> resource <- is required');
1875
+ }
1876
+ if (!(state.resource in state.fields)) {
1877
+ throw new Error(`Key ${state.resource} is missing in the fields object`);
1878
+ }
1879
+ const f = {};
1880
+ for (const k in state.fields) {
1881
+ if (state.fields.hasOwnProperty(k)) {
1882
+ if (k !== state.resource && !state.includes.includes(k)) {
1883
+ throw new UnselectableModelError(k);
1884
+ }
1885
+ Object.assign(f, { [`${options.fields}[${k}]`]: state.fields[k].join(',') });
1886
+ }
1887
+ }
1888
+ const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
1889
+ this._uri += param;
1890
+ return param;
671
1891
  }
672
1892
  /**
673
- * Generate an URI accordingly to the given data
1893
+ * Parse and append filter parameters
674
1894
  *
675
- * @returns {Observable<string>} An observable that emits the generated uri
1895
+ * Generates filter parameters in bracket notation: `filter[key]=value1,value2`
1896
+ *
1897
+ * @param state - The current query builder state
1898
+ * @param options - The query parameter key name configuration
1899
+ * @returns The generated filter parameter string
676
1900
  */
677
- generateUri() {
678
- try {
679
- this._uri$.next(this._parse(this._nestService.nest()));
680
- return this.uri$;
681
- }
682
- catch (error) {
683
- return throwError(() => error);
1901
+ _parseFilters(state, options) {
1902
+ const keys = Object.keys(state.filters);
1903
+ if (!keys.length) {
1904
+ return this._uri;
684
1905
  }
1906
+ const f = {
1907
+ [`${options.filters}`]: keys.reduce((acc, key) => {
1908
+ return Object.assign(acc, { [key]: state.filters[key].join(',') });
1909
+ }, {})
1910
+ };
1911
+ const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
1912
+ this._uri += param;
1913
+ return param;
685
1914
  }
686
1915
  /**
687
- * Clear the current state and reset the Query Builder to a fresh, clean condition
1916
+ * Parse and append include parameters
688
1917
  *
689
- * @returns {this}
1918
+ * Generates: `include=model1,model2`
1919
+ *
1920
+ * @param state - The current query builder state
1921
+ * @param options - The query parameter key name configuration
1922
+ * @returns The generated include parameter string
690
1923
  */
691
- reset() {
692
- this._nestService.reset();
693
- return this;
1924
+ _parseIncludes(state, options) {
1925
+ if (!state.includes.length) {
1926
+ return this._uri;
1927
+ }
1928
+ const param = `${this._prepend(state)}${options.includes}=${state.includes}`;
1929
+ this._uri += param;
1930
+ return param;
694
1931
  }
695
1932
  /**
696
- * Set the base url to use for composing the address
1933
+ * Parse and append the limit parameter
697
1934
  *
698
- * @param {string} baseUrl
699
- * @returns {this}
1935
+ * @param state - The current query builder state
1936
+ * @param options - The query parameter key name configuration
1937
+ * @returns The generated limit parameter string
700
1938
  */
701
- setBaseUrl(baseUrl) {
702
- this._nestService.baseUrl = baseUrl;
703
- return this;
1939
+ _parseLimit(state, options) {
1940
+ const param = `${this._prepend(state)}${options.limit}=${state.limit}`;
1941
+ this._uri += param;
1942
+ return param;
704
1943
  }
705
1944
  /**
706
- * Set the items per page number
1945
+ * Parse and append the page parameter
707
1946
  *
708
- * @param limit
709
- * @returns {this}
1947
+ * @param state - The current query builder state
1948
+ * @param options - The query parameter key name configuration
1949
+ * @returns The generated page parameter string
710
1950
  */
711
- setLimit(limit) {
712
- this._nestService.limit = limit;
713
- return this;
1951
+ _parsePage(state, options) {
1952
+ const param = `${this._prepend(state)}${options.page}=${state.page}`;
1953
+ this._uri += param;
1954
+ return param;
714
1955
  }
715
1956
  /**
716
- * Set the model to use for running the query against
717
- * - I.e. the model "users" will return /users
1957
+ * Parse and append sort parameters
718
1958
  *
719
- * @param {string} model Model name
720
- * @returns {this}
1959
+ * Generates: `sort=-field1,field2` where `-` prefix indicates DESC order
1960
+ *
1961
+ * @param state - The current query builder state
1962
+ * @param options - The query parameter key name configuration
1963
+ * @returns The generated sort parameter string
721
1964
  */
722
- setModel(model) {
723
- this._nestService.model = model;
724
- return this;
1965
+ _parseSort(state, options) {
1966
+ let param = '';
1967
+ if (!state.sorts.length) {
1968
+ return param;
1969
+ }
1970
+ param = `${this._prepend(state)}${options.sort}=`;
1971
+ state.sorts.forEach((sort, idx) => {
1972
+ param += `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`;
1973
+ if (idx < state.sorts.length - 1) {
1974
+ param += ',';
1975
+ }
1976
+ });
1977
+ this._uri += param;
1978
+ return param;
725
1979
  }
726
1980
  /**
727
- * Set the page that the backend will use to paginate the result set
1981
+ * Determine the appropriate URI prefix based on the current accumulator state
728
1982
  *
729
- * @param page Page param
730
- * @returns {this}
1983
+ * Returns the full base path with `?` for the first parameter,
1984
+ * or `&` for subsequent parameters.
1985
+ *
1986
+ * @param state - The current query builder state
1987
+ * @returns The prefix string to prepend to the next parameter
731
1988
  */
732
- setPage(page) {
733
- this._nestService.page = page;
734
- return this;
1989
+ _prepend(state) {
1990
+ if (this._uri) {
1991
+ return '&';
1992
+ }
1993
+ return state.baseUrl ? `${state.baseUrl}/${state.resource}?` : `/${state.resource}?`;
735
1994
  }
736
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeService, deps: [{ token: NestService }, { token: 'QUERY_PARAMS_CONFIG', optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
737
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeService });
738
1995
  }
739
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeService, decorators: [{
740
- type: Injectable
741
- }], ctorParameters: () => [{ type: NestService }, { type: undefined, decorators: [{
742
- type: Inject,
743
- args: ['QUERY_PARAMS_CONFIG']
744
- }, {
745
- type: Optional
746
- }] }] });
747
1996
 
748
- class ResponseOptions {
749
- currentPage;
750
- data;
751
- firstPageUrl;
752
- from;
753
- lastPage;
754
- lastPageUrl;
755
- nextPageUrl;
756
- path;
757
- perPage;
758
- prevPageUrl;
759
- to;
760
- total;
761
- constructor(options) {
762
- this.currentPage = options.currentPage || 'current_page';
763
- this.data = options.data || 'data';
764
- this.firstPageUrl = options.firstPageUrl || 'first_page_url';
765
- this.from = options.from || 'from';
766
- this.lastPage = options.lastPage || 'last_page';
767
- this.lastPageUrl = options.lastPageUrl || 'last_page_url';
768
- this.nextPageUrl = options.nextPageUrl || 'next_page_url';
769
- this.path = options.path || 'path';
770
- this.perPage = options.perPage || 'per_page';
771
- this.prevPageUrl = options.prevPageUrl || 'prev_page_url';
772
- this.to = options.to || 'to';
773
- this.total = options.total || 'total';
1997
+ /**
1998
+ * Response strategy for the Spatie Query Builder driver
1999
+ *
2000
+ * Parses flat Laravel pagination responses:
2001
+ * ```json
2002
+ * {
2003
+ * "data": [...],
2004
+ * "current_page": 1,
2005
+ * "total": 100,
2006
+ * "per_page": 15,
2007
+ * "from": 1,
2008
+ * "to": 15,
2009
+ * ...
2010
+ * }
2011
+ * ```
2012
+ *
2013
+ * @see https://spatie.be/docs/laravel-query-builder
2014
+ */
2015
+ class SpatieResponseStrategy {
2016
+ /**
2017
+ * Parse a flat Laravel pagination response into a PaginatedCollection
2018
+ *
2019
+ * @param response - The raw API response object
2020
+ * @param options - The response key name configuration
2021
+ * @returns A typed PaginatedCollection instance
2022
+ */
2023
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2024
+ paginate(response, options) {
2025
+ return new PaginatedCollection(response[options.data], response[options.currentPage], response[options.from], response[options.to], response[options.total], response[options.perPage], response[options.prevPageUrl], response[options.nextPageUrl], response[options.lastPage], response[options.firstPageUrl], response[options.lastPageUrl]);
774
2026
  }
775
2027
  }
776
2028
 
777
- class PaginationService {
778
- _options;
779
- constructor(options = {}) {
780
- this._options = new ResponseOptions(options);
2029
+ /**
2030
+ * Resolve the request strategy instance for the given driver
2031
+ *
2032
+ * @param driver - The pagination driver
2033
+ * @returns The corresponding request strategy
2034
+ */
2035
+ function resolveRequestStrategy$1(driver) {
2036
+ switch (driver) {
2037
+ case DriverEnum.JSON_API:
2038
+ return new JsonApiRequestStrategy();
2039
+ case DriverEnum.NESTJS:
2040
+ return new NestjsRequestStrategy();
2041
+ case DriverEnum.SPATIE:
2042
+ return new SpatieRequestStrategy();
2043
+ case DriverEnum.LARAVEL:
2044
+ return new LaravelRequestStrategy();
781
2045
  }
782
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
783
- paginate(response) {
784
- return new PaginatedCollection(response[this._options.data], response[this._options.currentPage], response[this._options.from], response[this._options.to], response[this._options.total], response[this._options.perPage], response[this._options.prevPageUrl], response[this._options.nextPageUrl], response[this._options.lastPage], response[this._options.firstPageUrl], response[this._options.lastPageUrl]);
2046
+ }
2047
+ /**
2048
+ * Resolve the response strategy instance for the given driver
2049
+ *
2050
+ * @param driver - The pagination driver
2051
+ * @returns The corresponding response strategy
2052
+ */
2053
+ function resolveResponseStrategy$1(driver) {
2054
+ switch (driver) {
2055
+ case DriverEnum.JSON_API:
2056
+ return new JsonApiResponseStrategy();
2057
+ case DriverEnum.NESTJS:
2058
+ return new NestjsResponseStrategy();
2059
+ case DriverEnum.SPATIE:
2060
+ return new SpatieResponseStrategy();
2061
+ case DriverEnum.LARAVEL:
2062
+ return new LaravelResponseStrategy();
785
2063
  }
786
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PaginationService, deps: [{ token: 'RESPONSE_OPTIONS', optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
787
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PaginationService });
788
2064
  }
789
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PaginationService, decorators: [{
790
- type: Injectable
791
- }], ctorParameters: () => [{ type: undefined, decorators: [{
792
- type: Inject,
793
- args: ['RESPONSE_OPTIONS']
794
- }, {
795
- type: Optional
796
- }] }] });
797
-
798
2065
  // @dynamic
799
2066
  class NgQubeeModule {
800
- static forRoot(config = {}) {
2067
+ /**
2068
+ * Configure NgQubee for the root module
2069
+ *
2070
+ * @param config - Configuration object with driver, and optional request and response settings
2071
+ * @returns Module with providers configured for the specified driver
2072
+ */
2073
+ static forRoot(config) {
2074
+ const driver = config.driver;
2075
+ const requestStrategy = resolveRequestStrategy$1(driver);
2076
+ const responseStrategy = resolveResponseStrategy$1(driver);
801
2077
  return {
802
2078
  ngModule: NgQubeeModule,
803
2079
  providers: [
@@ -805,52 +2081,115 @@ class NgQubeeModule {
805
2081
  {
806
2082
  deps: [NestService],
807
2083
  provide: NgQubeeService,
808
- useFactory: (nestService) => new NgQubeeService(nestService, Object.assign({}, config.request))
809
- }, {
2084
+ useFactory: (nestService) => new NgQubeeService(nestService, requestStrategy, driver, Object.assign({}, config.request))
2085
+ },
2086
+ {
810
2087
  provide: PaginationService,
811
- useFactory: () => new PaginationService(Object.assign({}, config.response))
2088
+ useFactory: () => {
2089
+ const responseConfig = Object.assign({}, config.response);
2090
+ if (driver === DriverEnum.JSON_API) {
2091
+ return new PaginationService(responseStrategy, new JsonApiResponseOptions(responseConfig));
2092
+ }
2093
+ return driver === DriverEnum.NESTJS
2094
+ ? new PaginationService(responseStrategy, new NestjsResponseOptions(responseConfig))
2095
+ : new PaginationService(responseStrategy, responseConfig);
2096
+ }
812
2097
  }
813
2098
  ]
814
2099
  };
815
2100
  }
816
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
817
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeModule });
818
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeModule, providers: [{
819
- deps: [NestService],
820
- provide: NgQubeeService,
821
- useFactory: (nestService) => new NgQubeeService(nestService, {})
822
- }] });
2101
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2102
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule });
2103
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule });
823
2104
  }
824
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeModule, decorators: [{
2105
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule, decorators: [{
825
2106
  type: NgModule,
826
- args: [{
827
- providers: [{
828
- deps: [NestService],
829
- provide: NgQubeeService,
830
- useFactory: (nestService) => new NgQubeeService(nestService, {})
831
- }]
832
- }]
2107
+ args: [{}]
833
2108
  }] });
834
2109
 
2110
+ /**
2111
+ * Resolve the request strategy instance for the given driver
2112
+ *
2113
+ * @param driver - The pagination driver
2114
+ * @returns The corresponding request strategy
2115
+ */
2116
+ function resolveRequestStrategy(driver) {
2117
+ switch (driver) {
2118
+ case DriverEnum.JSON_API:
2119
+ return new JsonApiRequestStrategy();
2120
+ case DriverEnum.NESTJS:
2121
+ return new NestjsRequestStrategy();
2122
+ case DriverEnum.SPATIE:
2123
+ return new SpatieRequestStrategy();
2124
+ case DriverEnum.LARAVEL:
2125
+ return new LaravelRequestStrategy();
2126
+ }
2127
+ }
2128
+ /**
2129
+ * Resolve the response strategy instance for the given driver
2130
+ *
2131
+ * @param driver - The pagination driver
2132
+ * @returns The corresponding response strategy
2133
+ */
2134
+ function resolveResponseStrategy(driver) {
2135
+ switch (driver) {
2136
+ case DriverEnum.JSON_API:
2137
+ return new JsonApiResponseStrategy();
2138
+ case DriverEnum.NESTJS:
2139
+ return new NestjsResponseStrategy();
2140
+ case DriverEnum.SPATIE:
2141
+ return new SpatieResponseStrategy();
2142
+ case DriverEnum.LARAVEL:
2143
+ return new LaravelResponseStrategy();
2144
+ }
2145
+ }
835
2146
  /**
836
2147
  * Sets up providers necessary to enable `NgQubee` functionality for the application.
837
2148
  *
838
2149
  * @usageNotes
839
2150
  *
840
- * Basic example of how you can add NgQubee to your application:
2151
+ * Basic example with the Laravel driver:
2152
+ * ```
2153
+ * bootstrapApplication(AppComponent, {
2154
+ * providers: [provideNgQubee({ driver: DriverEnum.LARAVEL })]
2155
+ * });
2156
+ * ```
2157
+ *
2158
+ * Spatie driver example:
2159
+ * ```
2160
+ * import { DriverEnum } from 'ng-qubee';
2161
+ *
2162
+ * bootstrapApplication(AppComponent, {
2163
+ * providers: [provideNgQubee({ driver: DriverEnum.SPATIE })]
2164
+ * });
2165
+ * ```
2166
+ *
2167
+ * JSON:API driver example:
2168
+ * ```
2169
+ * import { DriverEnum } from 'ng-qubee';
2170
+ *
2171
+ * bootstrapApplication(AppComponent, {
2172
+ * providers: [provideNgQubee({ driver: DriverEnum.JSON_API })]
2173
+ * });
2174
+ * ```
2175
+ *
2176
+ * NestJS driver example:
841
2177
  * ```
842
- * const config = {};
2178
+ * import { DriverEnum } from 'ng-qubee';
843
2179
  *
844
2180
  * bootstrapApplication(AppComponent, {
845
- * providers: [provideNgQubee(config)]
2181
+ * providers: [provideNgQubee({ driver: DriverEnum.NESTJS })]
846
2182
  * });
847
2183
  * ```
848
2184
  *
849
2185
  * @publicApi
850
- * @param config Configuration object compliant to the IConfig interface
2186
+ * @param config - Configuration object compliant to the IConfig interface
851
2187
  * @returns A set of providers to setup NgQubee
852
2188
  */
853
- function provideNgQubee(config = {}) {
2189
+ function provideNgQubee(config) {
2190
+ const driver = config.driver;
2191
+ const requestStrategy = resolveRequestStrategy(driver);
2192
+ const responseStrategy = resolveResponseStrategy(driver);
854
2193
  return makeEnvironmentProviders([
855
2194
  {
856
2195
  provide: NestService,
@@ -859,14 +2198,47 @@ function provideNgQubee(config = {}) {
859
2198
  {
860
2199
  deps: [NestService],
861
2200
  provide: NgQubeeService,
862
- useFactory: (nestService) => new NgQubeeService(nestService, Object.assign({}, config.request))
863
- }, {
2201
+ useFactory: (nestService) => new NgQubeeService(nestService, requestStrategy, driver, Object.assign({}, config.request))
2202
+ },
2203
+ {
864
2204
  provide: PaginationService,
865
- useFactory: () => new PaginationService(Object.assign({}, config.response))
2205
+ useFactory: () => {
2206
+ const responseConfig = Object.assign({}, config.response);
2207
+ if (driver === DriverEnum.JSON_API) {
2208
+ return new PaginationService(responseStrategy, new JsonApiResponseOptions(responseConfig));
2209
+ }
2210
+ return driver === DriverEnum.NESTJS
2211
+ ? new PaginationService(responseStrategy, new NestjsResponseOptions(responseConfig))
2212
+ : new PaginationService(responseStrategy, responseConfig);
2213
+ }
866
2214
  }
867
2215
  ]);
868
2216
  }
869
2217
 
2218
+ /**
2219
+ * Enum representing the available filter operators for the NestJS driver
2220
+ *
2221
+ * These operators map to the nestjs-paginate filter syntax:
2222
+ * `filter.field=$operator:value`
2223
+ *
2224
+ * @see https://github.com/ppetzold/nestjs-paginate
2225
+ */
2226
+ var FilterOperatorEnum;
2227
+ (function (FilterOperatorEnum) {
2228
+ FilterOperatorEnum["BTW"] = "$btw";
2229
+ FilterOperatorEnum["CONTAINS"] = "$contains";
2230
+ FilterOperatorEnum["EQ"] = "$eq";
2231
+ FilterOperatorEnum["GT"] = "$gt";
2232
+ FilterOperatorEnum["GTE"] = "$gte";
2233
+ FilterOperatorEnum["ILIKE"] = "$ilike";
2234
+ FilterOperatorEnum["IN"] = "$in";
2235
+ FilterOperatorEnum["LT"] = "$lt";
2236
+ FilterOperatorEnum["LTE"] = "$lte";
2237
+ FilterOperatorEnum["NOT"] = "$not";
2238
+ FilterOperatorEnum["NULL"] = "$null";
2239
+ FilterOperatorEnum["SW"] = "$sw";
2240
+ })(FilterOperatorEnum || (FilterOperatorEnum = {}));
2241
+
870
2242
  /*
871
2243
  * Public API Surface of angular-query-builder
872
2244
  */
@@ -875,5 +2247,5 @@ function provideNgQubee(config = {}) {
875
2247
  * Generated bundle index. Do not edit.
876
2248
  */
877
2249
 
878
- export { InvalidLimitError, InvalidModelNameError, InvalidPageNumberError, KeyNotFoundError, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationService, SortEnum, UnselectableModelError, provideNgQubee };
2250
+ export { DriverEnum, FilterOperatorEnum, InvalidLimitError, InvalidPageNumberError, InvalidResourceNameError, JsonApiRequestStrategy, JsonApiResponseStrategy, KeyNotFoundError, LaravelRequestStrategy, LaravelResponseStrategy, NestjsRequestStrategy, NestjsResponseStrategy, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationService, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, provideNgQubee };
879
2251
  //# sourceMappingURL=ng-qubee.mjs.map