ng-qubee 3.1.0 → 3.2.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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, NgModule, makeEnvironmentProviders } from '@angular/core';
2
+ import { signal, computed, Injectable, InjectionToken, Inject, makeEnvironmentProviders, NgModule } from '@angular/core';
3
3
  import { BehaviorSubject, filter, throwError } from 'rxjs';
4
4
  import * as qs from 'qs';
5
5
 
@@ -80,6 +80,37 @@ var DriverEnum;
80
80
  DriverEnum["SPATIE"] = "spatie";
81
81
  })(DriverEnum || (DriverEnum = {}));
82
82
 
83
+ /**
84
+ * Resolved query parameter key names with defaults applied
85
+ *
86
+ * Maps logical query concepts to the actual query parameter names
87
+ * used in the generated URI. Unset values fall back to defaults.
88
+ */
89
+ class QueryBuilderOptions {
90
+ appends;
91
+ fields;
92
+ filters;
93
+ includes;
94
+ limit;
95
+ page;
96
+ search;
97
+ select;
98
+ sort;
99
+ sortBy;
100
+ constructor(options) {
101
+ this.appends = options.appends || 'append';
102
+ this.fields = options.fields || 'fields';
103
+ this.filters = options.filters || 'filter';
104
+ this.includes = options.includes || 'include';
105
+ this.limit = options.limit || 'limit';
106
+ this.page = options.page || 'page';
107
+ this.search = options.search || 'search';
108
+ this.select = options.select || 'select';
109
+ this.sort = options.sort || 'sort';
110
+ this.sortBy = options.sortBy || 'sortBy';
111
+ }
112
+ }
113
+
83
114
  /**
84
115
  * Resolved response field key names with defaults applied
85
116
  *
@@ -172,185 +203,666 @@ class NestjsResponseOptions extends ResponseOptions {
172
203
  }
173
204
 
174
205
  /**
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
206
+ * Error thrown when an invalid resource name is provided
251
207
  *
252
- * Sorts are only supported by the Spatie and NestJS drivers.
208
+ * Resource name must be a non-empty string.
253
209
  */
254
- class UnsupportedSortError extends Error {
255
- constructor() {
256
- super('Sorts are only supported by the Spatie and NestJS drivers.');
257
- this.name = 'UnsupportedSortError';
210
+ class InvalidResourceNameError extends Error {
211
+ constructor(resource) {
212
+ super(`Invalid resource name: Resource name must be a non-empty string. Received: ${JSON.stringify(resource)}`);
213
+ this.name = 'InvalidResourceNameError';
258
214
  }
259
215
  }
260
216
 
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
- */
267
- class QueryBuilderOptions {
268
- appends;
269
- fields;
270
- filters;
271
- includes;
272
- limit;
273
- page;
274
- search;
275
- select;
276
- sort;
277
- sortBy;
278
- constructor(options) {
279
- this.appends = options.appends || 'append';
280
- this.fields = options.fields || 'fields';
281
- this.filters = options.filters || 'filter';
282
- this.includes = options.includes || 'include';
283
- this.limit = options.limit || 'limit';
284
- this.page = options.page || 'page';
285
- this.search = options.search || 'search';
286
- this.select = options.select || 'select';
287
- this.sort = options.sort || 'sort';
288
- this.sortBy = options.sortBy || 'sortBy';
217
+ class InvalidPageNumberError extends Error {
218
+ constructor(page) {
219
+ super(`Invalid page number: Page must be a positive integer greater than 0. Received: ${page}`);
220
+ this.name = 'InvalidPageNumberError';
289
221
  }
290
222
  }
291
223
 
292
- class NgQubeeService {
293
- _nestService;
294
- /**
295
- * The active pagination driver
296
- */
297
- _driver;
298
- /**
299
- * Resolved query parameter key name options
300
- */
301
- _options;
224
+ const INITIAL_STATE = {
225
+ baseUrl: '',
226
+ fields: {},
227
+ filters: {},
228
+ includes: [],
229
+ isLastPageKnown: false,
230
+ lastPage: 1,
231
+ limit: 15,
232
+ operatorFilters: [],
233
+ page: 1,
234
+ resource: '',
235
+ search: '',
236
+ select: [],
237
+ sorts: []
238
+ };
239
+ class NestService {
302
240
  /**
303
- * The request strategy that builds URIs for the active driver
241
+ * Private writable signal that holds the Query Builder state
242
+ *
243
+ * @type {IQueryBuilderState}
304
244
  */
305
- _requestStrategy;
245
+ _nest = signal(this._clone(INITIAL_STATE), ...(ngDevMode ? [{ debugName: "_nest" }] : []));
306
246
  /**
307
- * Internal BehaviorSubject that holds the latest generated URI
247
+ * A computed signal that makes readonly the writable signal _nest
248
+ *
249
+ * @type {Signal<IQueryBuilderState>}
308
250
  */
309
- _uri$ = new BehaviorSubject('');
251
+ nest = computed(() => this._clone(this._nest()), ...(ngDevMode ? [{ debugName: "nest" }] : []));
252
+ constructor() {
253
+ // Nothing to see here
254
+ }
310
255
  /**
311
- * Observable that emits non-empty generated URIs
256
+ * Set the base URL for the API
257
+ *
258
+ * @param {string} baseUrl - The base URL to prepend to generated URIs
259
+ * @example
260
+ * service.baseUrl = 'https://api.example.com';
312
261
  */
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;
262
+ set baseUrl(baseUrl) {
263
+ this._nest.update(nest => ({
264
+ ...nest,
265
+ baseUrl
266
+ }));
319
267
  }
320
268
  /**
321
- * Assert that the active driver is one of the allowed drivers
269
+ * Set the limit for paginated results
322
270
  *
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
271
+ * This setter performs a raw state write. Validation of the value is the
272
+ * responsibility of the active request strategy and is enforced upstream
273
+ * by `NgQubeeService.setLimit()`, because the accepted range depends on
274
+ * the driver (e.g. nestjs-paginate accepts `-1` for "fetch all").
275
+ *
276
+ * @param {number} limit - The number of items per page
277
+ * @example
278
+ * service.limit = 25;
326
279
  */
327
- _assertDriver(allowed, error) {
328
- if (!allowed.includes(this._driver)) {
329
- throw error;
330
- }
280
+ set limit(limit) {
281
+ this._nest.update(nest => ({
282
+ ...nest,
283
+ limit
284
+ }));
331
285
  }
332
286
  /**
333
- * Add fields to the select statement for the given model (JSON:API and Spatie only)
287
+ * Set the page number for pagination
288
+ * Must be a positive integer greater than 0
334
289
  *
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
290
+ * @param {number} page - The page number to fetch
291
+ * @throws {InvalidPageNumberError} If page is not a positive integer
292
+ * @example
293
+ * service.page = 2;
339
294
  */
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;
295
+ set page(page) {
296
+ this._validatePageNumber(page);
297
+ this._nest.update(nest => ({
298
+ ...nest,
299
+ page
300
+ }));
347
301
  }
348
302
  /**
349
- * Add a filter with the given value(s) (JSON:API, NestJS, and Spatie)
350
- *
351
- * Produces: `filter[field]=value` (JSON:API / Spatie) or `filter.field=value` (NestJS)
303
+ * Set the resource name for the query
304
+ * Must be a non-empty string
352
305
  *
353
- * @param {string} field - Name of the field to filter
306
+ * @param {string} resource - The API resource name (e.g., 'users', 'posts')
307
+ * @throws {InvalidResourceNameError} If resource is not a non-empty string
308
+ * @example
309
+ * service.resource = 'users';
310
+ */
311
+ set resource(resource) {
312
+ this._validateResourceName(resource);
313
+ this._nest.update(nest => ({
314
+ ...nest,
315
+ resource
316
+ }));
317
+ }
318
+ _clone(obj) {
319
+ return JSON.parse(JSON.stringify(obj));
320
+ }
321
+ /**
322
+ * Validates that the page number is a positive integer
323
+ *
324
+ * @param {number} page - The page number to validate
325
+ * @throws {InvalidPageNumberError} If page is not a positive integer
326
+ * @private
327
+ */
328
+ _validatePageNumber(page) {
329
+ if (!Number.isInteger(page) || page < 1) {
330
+ throw new InvalidPageNumberError(page);
331
+ }
332
+ }
333
+ /**
334
+ * Validates that the resource name is a non-empty string
335
+ *
336
+ * @param {string} resource - The resource name to validate
337
+ * @throws {InvalidResourceNameError} If resource is not a non-empty string
338
+ * @private
339
+ */
340
+ _validateResourceName(resource) {
341
+ if (!resource || typeof resource !== 'string' || resource.trim().length === 0) {
342
+ throw new InvalidResourceNameError(resource);
343
+ }
344
+ }
345
+ /**
346
+ * Add selectable fields for the given model to the request
347
+ * Automatically prevents duplicate fields for each model
348
+ *
349
+ * @param {IFields} fields - Object mapping model names to arrays of field names
350
+ * @return {void}
351
+ * @example
352
+ * service.addFields({ users: ['id', 'email', 'username'] });
353
+ * service.addFields({ posts: ['title', 'content'] });
354
+ */
355
+ addFields(fields) {
356
+ this._nest.update(nest => {
357
+ const mergedFields = { ...nest.fields };
358
+ Object.keys(fields).forEach(model => {
359
+ const existingFields = mergedFields[model] || [];
360
+ const newFields = fields[model];
361
+ // Use Set to prevent duplicates
362
+ const uniqueFields = Array.from(new Set([...existingFields, ...newFields]));
363
+ mergedFields[model] = uniqueFields;
364
+ });
365
+ return {
366
+ ...nest,
367
+ fields: mergedFields
368
+ };
369
+ });
370
+ }
371
+ /**
372
+ * Add filters to the request
373
+ * Automatically prevents duplicate filter values for each filter key
374
+ *
375
+ * @param {IFilters} filters - Object mapping filter keys to arrays of values
376
+ * @return {void}
377
+ * @example
378
+ * service.addFilters({ id: [1, 2, 3] });
379
+ * service.addFilters({ status: ['active', 'pending'] });
380
+ */
381
+ addFilters(filters) {
382
+ this._nest.update(nest => {
383
+ const mergedFilters = { ...nest.filters };
384
+ Object.keys(filters).forEach(key => {
385
+ const existingValues = mergedFilters[key] || [];
386
+ const newValues = filters[key];
387
+ // Use Set to prevent duplicates
388
+ const uniqueValues = Array.from(new Set([...existingValues, ...newValues]));
389
+ mergedFilters[key] = uniqueValues;
390
+ });
391
+ return {
392
+ ...nest,
393
+ filters: mergedFilters
394
+ };
395
+ });
396
+ }
397
+ /**
398
+ * Add resources to include with the request
399
+ * Automatically prevents duplicate includes
400
+ *
401
+ * @param {string[]} includes - Array of resource names to include in the response
402
+ * @return {void}
403
+ * @example
404
+ * service.addIncludes(['profile', 'posts']);
405
+ * service.addIncludes(['comments']);
406
+ */
407
+ addIncludes(includes) {
408
+ this._nest.update(nest => {
409
+ // Use Set to prevent duplicates
410
+ const uniqueIncludes = Array.from(new Set([...nest.includes, ...includes]));
411
+ return {
412
+ ...nest,
413
+ includes: uniqueIncludes
414
+ };
415
+ });
416
+ }
417
+ /**
418
+ * Add filters with explicit operators (NestJS only)
419
+ * Automatically prevents duplicate operator filters for the same field + operator combination
420
+ *
421
+ * @param {IOperatorFilter[]} filters - Array of operator filter configurations
422
+ * @return {void}
423
+ * @example
424
+ * import { FilterOperatorEnum } from 'ng-qubee';
425
+ * service.addOperatorFilters([{ field: 'age', operator: FilterOperatorEnum.GTE, values: [18] }]);
426
+ */
427
+ addOperatorFilters(filters) {
428
+ this._nest.update(nest => {
429
+ const merged = [...nest.operatorFilters];
430
+ filters.forEach(newFilter => {
431
+ const existingIdx = merged.findIndex(f => f.field === newFilter.field && f.operator === newFilter.operator);
432
+ if (existingIdx > -1) {
433
+ const existingValues = merged[existingIdx].values;
434
+ merged[existingIdx] = {
435
+ ...merged[existingIdx],
436
+ values: Array.from(new Set([...existingValues, ...newFilter.values]))
437
+ };
438
+ }
439
+ else {
440
+ merged.push({ ...newFilter });
441
+ }
442
+ });
443
+ return {
444
+ ...nest,
445
+ operatorFilters: merged
446
+ };
447
+ });
448
+ }
449
+ /**
450
+ * Add flat field selection columns (NestJS only)
451
+ * Automatically prevents duplicate select fields
452
+ *
453
+ * @param {string[]} fields - Array of column names to select
454
+ * @return {void}
455
+ * @example
456
+ * service.addSelect(['id', 'name', 'email']);
457
+ */
458
+ addSelect(fields) {
459
+ this._nest.update(nest => {
460
+ const uniqueSelect = Array.from(new Set([...nest.select, ...fields]));
461
+ return {
462
+ ...nest,
463
+ select: uniqueSelect
464
+ };
465
+ });
466
+ }
467
+ /**
468
+ * Add a field that should be used for sorting data
469
+ *
470
+ * @param {ISort} sort - Sort configuration with field name and order (ASC/DESC)
471
+ * @return {void}
472
+ * @example
473
+ * import { SortEnum } from 'ng-qubee';
474
+ * service.addSort({ field: 'created_at', order: SortEnum.DESC });
475
+ * service.addSort({ field: 'name', order: SortEnum.ASC });
476
+ */
477
+ addSort(sort) {
478
+ this._nest.update(nest => ({
479
+ ...nest,
480
+ sorts: [...nest.sorts, sort]
481
+ }));
482
+ }
483
+ /**
484
+ * Remove fields for the given model
485
+ * Uses deep cloning to prevent mutations to the original state
486
+ *
487
+ * @param {IFields} fields - Object mapping model names to arrays of field names to remove
488
+ * @return {void}
489
+ * @example
490
+ * service.deleteFields({ users: ['email'] });
491
+ * service.deleteFields({ posts: ['content', 'body'] });
492
+ */
493
+ deleteFields(fields) {
494
+ // Deep clone the fields object to prevent mutations
495
+ const f = this._clone(this._nest().fields);
496
+ Object.keys(fields).forEach(k => {
497
+ if (!(k in f)) {
498
+ return;
499
+ }
500
+ f[k] = f[k].filter(v => !fields[k].includes(v));
501
+ });
502
+ this._nest.update(nest => ({
503
+ ...nest,
504
+ fields: f
505
+ }));
506
+ }
507
+ /**
508
+ * Remove filters from the request
509
+ * Uses deep cloning to prevent mutations to the original state
510
+ *
511
+ * @param {...string[]} filters - Filter keys to remove
512
+ * @return {void}
513
+ * @example
514
+ * service.deleteFilters('id');
515
+ * service.deleteFilters('status', 'type');
516
+ */
517
+ deleteFilters(...filters) {
518
+ // Deep clone the filters object to prevent mutations
519
+ const f = this._clone(this._nest().filters);
520
+ filters.forEach(k => delete f[k]);
521
+ this._nest.update(nest => ({
522
+ ...nest,
523
+ filters: f
524
+ }));
525
+ }
526
+ /**
527
+ * Remove includes from the request
528
+ *
529
+ * @param {...string[]} includes - Include names to remove
530
+ * @return {void}
531
+ * @example
532
+ * service.deleteIncludes('profile');
533
+ * service.deleteIncludes('posts', 'comments');
534
+ */
535
+ deleteIncludes(...includes) {
536
+ this._nest.update(nest => ({
537
+ ...nest,
538
+ includes: nest.includes.filter(v => !includes.includes(v))
539
+ }));
540
+ }
541
+ /**
542
+ * Remove operator filters by field name (NestJS only)
543
+ *
544
+ * @param {...string[]} fields - Field names of operator filters to remove
545
+ * @return {void}
546
+ * @example
547
+ * service.deleteOperatorFilters('age');
548
+ * service.deleteOperatorFilters('price', 'quantity');
549
+ */
550
+ deleteOperatorFilters(...fields) {
551
+ this._nest.update(nest => ({
552
+ ...nest,
553
+ operatorFilters: nest.operatorFilters.filter(f => !fields.includes(f.field))
554
+ }));
555
+ }
556
+ /**
557
+ * Remove the search term from the state (NestJS only)
558
+ *
559
+ * @return {void}
560
+ * @example
561
+ * service.deleteSearch();
562
+ */
563
+ deleteSearch() {
564
+ this._nest.update(nest => ({
565
+ ...nest,
566
+ search: ''
567
+ }));
568
+ }
569
+ /**
570
+ * Remove flat field selections from the state (NestJS only)
571
+ *
572
+ * @param {...string[]} fields - Field names to remove from selection
573
+ * @return {void}
574
+ * @example
575
+ * service.deleteSelect('email');
576
+ * service.deleteSelect('name', 'email');
577
+ */
578
+ deleteSelect(...fields) {
579
+ this._nest.update(nest => ({
580
+ ...nest,
581
+ select: nest.select.filter(f => !fields.includes(f))
582
+ }));
583
+ }
584
+ /**
585
+ * Remove sorts from the request by field name
586
+ *
587
+ * @param {...string[]} sorts - Field names of sorts to remove
588
+ * @return {void}
589
+ * @example
590
+ * service.deleteSorts('created_at');
591
+ * service.deleteSorts('name', 'created_at');
592
+ */
593
+ deleteSorts(...sorts) {
594
+ const s = [...this._nest().sorts];
595
+ sorts.forEach(field => {
596
+ const p = s.findIndex(sort => sort.field === field);
597
+ if (p > -1) {
598
+ s.splice(p, 1);
599
+ }
600
+ });
601
+ this._nest.update(nest => ({
602
+ ...nest,
603
+ sorts: s
604
+ }));
605
+ }
606
+ /**
607
+ * Set the full-text search term (NestJS only)
608
+ *
609
+ * @param {string} search - The search term
610
+ * @return {void}
611
+ * @example
612
+ * service.setSearch('john doe');
613
+ */
614
+ setSearch(search) {
615
+ this._nest.update(nest => ({
616
+ ...nest,
617
+ search
618
+ }));
619
+ }
620
+ /**
621
+ * Atomically record the `lastPage` value from a paginated response and
622
+ * flip `isLastPageKnown` to `true`
623
+ *
624
+ * Called exclusively by `PaginationService.paginate()` as part of the
625
+ * auto-sync contract; not intended to be invoked by consumers directly.
626
+ * Keeping the two fields under a single write guarantees they cannot
627
+ * drift out of sync.
628
+ *
629
+ * @param {number} lastPage - The last page number parsed from the most recent paginated response
630
+ * @return {void}
631
+ */
632
+ syncLastPage(lastPage) {
633
+ this._nest.update(nest => ({
634
+ ...nest,
635
+ isLastPageKnown: true,
636
+ lastPage
637
+ }));
638
+ }
639
+ /**
640
+ * Reset the query builder state to initial values
641
+ * Clears all fields, filters, includes, sorts, and resets pagination
642
+ *
643
+ * @return {void}
644
+ * @example
645
+ * service.reset();
646
+ */
647
+ reset() {
648
+ this._nest.update(_ => this._clone(INITIAL_STATE));
649
+ }
650
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
651
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService });
652
+ }
653
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, decorators: [{
654
+ type: Injectable
655
+ }], ctorParameters: () => [] });
656
+
657
+ /**
658
+ * Thrown when a pagination helper that needs `state.lastPage` is called
659
+ * before `PaginationService.paginate()` has ever synced a value.
660
+ *
661
+ * Examples: `NgQubeeService.lastPage()`, `NgQubeeService.totalPages()`.
662
+ *
663
+ * Safe-for-templates predicates (`isLastPage`, `hasNextPage`, etc.) do not
664
+ * throw and return conservative defaults instead.
665
+ */
666
+ class PaginationNotSyncedError extends Error {
667
+ /**
668
+ * @param action - Short imperative describing what the caller was trying
669
+ * to do (e.g. "navigate to last page", "read totalPages"). Surfaced in
670
+ * the error message so the cause is obvious at the call site.
671
+ */
672
+ constructor(action) {
673
+ super(`Cannot ${action}: no paginated response has been synced yet. Call PaginationService.paginate() at least once first.`);
674
+ this.name = 'PaginationNotSyncedError';
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Error thrown when per-model field selection is attempted with a driver that does not support it
680
+ *
681
+ * Per-model field selection is only supported by the Spatie driver.
682
+ * Use `addSelect()` for NestJS flat field selection.
683
+ */
684
+ class UnsupportedFieldSelectionError extends Error {
685
+ constructor() {
686
+ super('Per-model field selection is only supported by the Spatie driver. Use addSelect() for NestJS.');
687
+ this.name = 'UnsupportedFieldSelectionError';
688
+ }
689
+ }
690
+
691
+ /**
692
+ * Error thrown when filters are attempted with a driver that does not support them
693
+ *
694
+ * Filters are only supported by the Spatie and NestJS drivers.
695
+ */
696
+ class UnsupportedFilterError extends Error {
697
+ constructor() {
698
+ super('Filters are only supported by the Spatie and NestJS drivers.');
699
+ this.name = 'UnsupportedFilterError';
700
+ }
701
+ }
702
+
703
+ /**
704
+ * Error thrown when filter operators are attempted with a driver that does not support them
705
+ *
706
+ * Filter operators are only supported by the NestJS driver.
707
+ * Use `addFilter()` for Spatie implicit equality filters.
708
+ */
709
+ class UnsupportedFilterOperatorError extends Error {
710
+ constructor() {
711
+ super('Filter operators are only supported by the NestJS driver. Use addFilter() for Spatie.');
712
+ this.name = 'UnsupportedFilterOperatorError';
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Error thrown when includes are attempted with a driver that does not support them
718
+ *
719
+ * Includes are only supported by the Spatie driver.
720
+ */
721
+ class UnsupportedIncludesError extends Error {
722
+ constructor() {
723
+ super('Includes are only supported by the Spatie driver.');
724
+ this.name = 'UnsupportedIncludesError';
725
+ }
726
+ }
727
+
728
+ /**
729
+ * Error thrown when search is attempted with a driver that does not support it
730
+ *
731
+ * Search is only supported by the NestJS driver.
732
+ */
733
+ class UnsupportedSearchError extends Error {
734
+ constructor() {
735
+ super('Search is only supported by the NestJS driver.');
736
+ this.name = 'UnsupportedSearchError';
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Error thrown when flat field selection is attempted with a driver that does not support it
742
+ *
743
+ * Flat field selection is only supported by the NestJS driver.
744
+ * Use `addFields()` for Spatie per-model field selection.
745
+ */
746
+ class UnsupportedSelectError extends Error {
747
+ constructor() {
748
+ super('Flat field selection is only supported by the NestJS driver. Use addFields() for Spatie.');
749
+ this.name = 'UnsupportedSelectError';
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Error thrown when sorts are attempted with a driver that does not support them
755
+ *
756
+ * Sorts are only supported by the Spatie and NestJS drivers.
757
+ */
758
+ class UnsupportedSortError extends Error {
759
+ constructor() {
760
+ super('Sorts are only supported by the Spatie and NestJS drivers.');
761
+ this.name = 'UnsupportedSortError';
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Injection token for the active pagination driver
767
+ *
768
+ * Provided by `provideNgQubee()` / `NgQubeeModule.forRoot()` from the
769
+ * user-supplied `IConfig.driver`. Services read it to gate driver-specific
770
+ * behavior (e.g. `NgQubeeService._assertDriver`).
771
+ */
772
+ const NG_QUBEE_DRIVER = new InjectionToken('NG_QUBEE_DRIVER');
773
+ /**
774
+ * Injection token for the resolved request URI strategy
775
+ *
776
+ * Provided by `provideNgQubee()` / `NgQubeeModule.forRoot()` based on the
777
+ * active driver. Used by `NgQubeeService` to build request URIs.
778
+ */
779
+ const NG_QUBEE_REQUEST_STRATEGY = new InjectionToken('NG_QUBEE_REQUEST_STRATEGY');
780
+ /**
781
+ * Injection token for the resolved request query-parameter key options
782
+ *
783
+ * Provided as a fully-built `QueryBuilderOptions` instance. `provideNgQubee()`
784
+ * constructs it from `IConfig.request`; consumers don't interact with this
785
+ * token directly.
786
+ */
787
+ const NG_QUBEE_REQUEST_OPTIONS = new InjectionToken('NG_QUBEE_REQUEST_OPTIONS');
788
+ /**
789
+ * Injection token for the resolved response parsing strategy
790
+ *
791
+ * Provided by `provideNgQubee()` / `NgQubeeModule.forRoot()` based on the
792
+ * active driver. Used by `PaginationService` to parse paginated responses.
793
+ */
794
+ const NG_QUBEE_RESPONSE_STRATEGY = new InjectionToken('NG_QUBEE_RESPONSE_STRATEGY');
795
+ /**
796
+ * Injection token for the resolved response field-key options
797
+ *
798
+ * Provided as a fully-built `ResponseOptions` instance (or a driver-specific
799
+ * subclass like `JsonApiResponseOptions` / `NestjsResponseOptions`).
800
+ * `provideNgQubee()` constructs the correct variant from `IConfig.response`.
801
+ */
802
+ const NG_QUBEE_RESPONSE_OPTIONS = new InjectionToken('NG_QUBEE_RESPONSE_OPTIONS');
803
+
804
+ class NgQubeeService {
805
+ _nestService;
806
+ /**
807
+ * The active pagination driver
808
+ */
809
+ _driver;
810
+ /**
811
+ * Resolved query parameter key name options
812
+ */
813
+ _options;
814
+ /**
815
+ * The request strategy that builds URIs for the active driver
816
+ */
817
+ _requestStrategy;
818
+ /**
819
+ * Internal BehaviorSubject that holds the latest generated URI
820
+ */
821
+ _uri$ = new BehaviorSubject('');
822
+ /**
823
+ * Observable that emits non-empty generated URIs
824
+ */
825
+ uri$ = this._uri$.asObservable().pipe(filter(uri => !!uri));
826
+ constructor(_nestService, requestStrategy, driver, options = new QueryBuilderOptions({})) {
827
+ this._nestService = _nestService;
828
+ this._driver = driver;
829
+ this._options = options;
830
+ this._requestStrategy = requestStrategy;
831
+ }
832
+ /**
833
+ * Assert that the active driver is one of the allowed drivers
834
+ *
835
+ * @param allowed - The allowed drivers
836
+ * @param error - The error to throw if the driver is not allowed
837
+ * @throws The provided error if the active driver is not in the allowed list
838
+ */
839
+ _assertDriver(allowed, error) {
840
+ if (!allowed.includes(this._driver)) {
841
+ throw error;
842
+ }
843
+ }
844
+ /**
845
+ * Add fields to the select statement for the given model (JSON:API and Spatie only)
846
+ *
847
+ * @param model - Model that holds the fields
848
+ * @param fields - Fields to select
849
+ * @returns {this}
850
+ * @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
851
+ */
852
+ addFields(model, fields) {
853
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedFieldSelectionError());
854
+ if (!fields.length) {
855
+ return this;
856
+ }
857
+ this._nestService.addFields({ [model]: fields });
858
+ return this;
859
+ }
860
+ /**
861
+ * Add a filter with the given value(s) (JSON:API, NestJS, and Spatie)
862
+ *
863
+ * Produces: `filter[field]=value` (JSON:API / Spatie) or `filter.field=value` (NestJS)
864
+ *
865
+ * @param {string} field - Name of the field to filter
354
866
  * @param {(string | number | boolean)[]} values - The needle(s)
355
867
  * @returns {this}
356
868
  * @throws {UnsupportedFilterError} If the active driver does not support filters
@@ -363,6 +875,7 @@ class NgQubeeService {
363
875
  this._nestService.addFilters({
364
876
  [field]: values
365
877
  });
878
+ this._nestService.page = 1;
366
879
  return this;
367
880
  }
368
881
  /**
@@ -382,6 +895,7 @@ class NgQubeeService {
382
895
  return this;
383
896
  }
384
897
  this._nestService.addOperatorFilters([{ field, operator, values }]);
898
+ this._nestService.page = 1;
385
899
  return this;
386
900
  }
387
901
  /**
@@ -430,8 +944,18 @@ class NgQubeeService {
430
944
  field,
431
945
  order
432
946
  });
947
+ this._nestService.page = 1;
433
948
  return this;
434
949
  }
950
+ /**
951
+ * Get the current page number
952
+ *
953
+ * @remarks Always safe to call. Thin accessor over the internal state's `page` field.
954
+ * @returns The current page number
955
+ */
956
+ currentPage() {
957
+ return this._nestService.nest().page;
958
+ }
435
959
  /**
436
960
  * Delete selected fields for the given models in the current query builder state (JSON:API and Spatie only)
437
961
  *
@@ -486,594 +1010,314 @@ class NgQubeeService {
486
1010
  return this;
487
1011
  }
488
1012
  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
- }));
1013
+ this._nestService.page = 1;
1014
+ return this;
705
1015
  }
706
1016
  /**
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").
1017
+ * Remove selected related models from the query builder state (JSON:API and Spatie only)
713
1018
  *
714
- * @param {number} limit - The number of items per page
715
- * @example
716
- * service.limit = 25;
1019
+ * @param {string[]} includes - Models to remove
1020
+ * @returns {this}
1021
+ * @throws {UnsupportedIncludesError} If the active driver does not support includes
717
1022
  */
718
- set limit(limit) {
719
- this._nest.update(nest => ({
720
- ...nest,
721
- limit
722
- }));
1023
+ deleteIncludes(...includes) {
1024
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedIncludesError());
1025
+ if (!includes.length) {
1026
+ return this;
1027
+ }
1028
+ this._nestService.deleteIncludes(...includes);
1029
+ return this;
723
1030
  }
724
1031
  /**
725
- * Set the page number for pagination
726
- * Must be a positive integer greater than 0
1032
+ * Remove operator filters by field name (NestJS only)
727
1033
  *
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;
1034
+ * @param {string[]} fields - Field names of operator filters to remove
1035
+ * @returns {this}
1036
+ * @throws {UnsupportedFilterOperatorError} If the active driver does not support filter operators
732
1037
  */
733
- set page(page) {
734
- this._validatePageNumber(page);
735
- this._nest.update(nest => ({
736
- ...nest,
737
- page
738
- }));
1038
+ deleteOperatorFilters(...fields) {
1039
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedFilterOperatorError());
1040
+ if (!fields.length) {
1041
+ return this;
1042
+ }
1043
+ this._nestService.deleteOperatorFilters(...fields);
1044
+ this._nestService.page = 1;
1045
+ return this;
739
1046
  }
740
1047
  /**
741
- * Set the resource name for the query
742
- * Must be a non-empty string
1048
+ * Remove search term from the query builder state (NestJS only)
743
1049
  *
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';
1050
+ * @returns {this}
1051
+ * @throws {UnsupportedSearchError} If the active driver does not support search
748
1052
  */
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));
1053
+ deleteSearch() {
1054
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedSearchError());
1055
+ this._nestService.deleteSearch();
1056
+ this._nestService.page = 1;
1057
+ return this;
758
1058
  }
759
1059
  /**
760
- * Validates that the page number is a positive integer
1060
+ * Remove flat field selections from the query builder state (NestJS only)
761
1061
  *
762
- * @param {number} page - The page number to validate
763
- * @throws {InvalidPageNumberError} If page is not a positive integer
764
- * @private
1062
+ * @param {string[]} fields - Fields to remove from selection
1063
+ * @returns {this}
1064
+ * @throws {UnsupportedSelectError} If the active driver does not support flat field selection
765
1065
  */
766
- _validatePageNumber(page) {
767
- if (!Number.isInteger(page) || page < 1) {
768
- throw new InvalidPageNumberError(page);
1066
+ deleteSelect(...fields) {
1067
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedSelectError());
1068
+ if (!fields.length) {
1069
+ return this;
769
1070
  }
1071
+ this._nestService.deleteSelect(...fields);
1072
+ return this;
770
1073
  }
771
1074
  /**
772
- * Validates that the resource name is a non-empty string
1075
+ * Remove sort rules from the query builder state (JSON:API, NestJS, and Spatie)
773
1076
  *
774
- * @param {string} resource - The resource name to validate
775
- * @throws {InvalidResourceNameError} If resource is not a non-empty string
776
- * @private
1077
+ * @param sorts - Fields used for sorting to remove
1078
+ * @returns {this}
1079
+ * @throws {UnsupportedSortError} If the active driver does not support sorts
777
1080
  */
778
- _validateResourceName(resource) {
779
- if (!resource || typeof resource !== 'string' || resource.trim().length === 0) {
780
- throw new InvalidResourceNameError(resource);
781
- }
1081
+ deleteSorts(...sorts) {
1082
+ this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedSortError());
1083
+ this._nestService.deleteSorts(...sorts);
1084
+ this._nestService.page = 1;
1085
+ return this;
782
1086
  }
783
1087
  /**
784
- * Add selectable fields for the given model to the request
785
- * Automatically prevents duplicate fields for each model
1088
+ * Navigate to the first page (page 1)
786
1089
  *
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'] });
1090
+ * @remarks Never throws. Idempotent when already on page 1.
1091
+ * @returns {this}
792
1092
  */
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;
802
- });
803
- return {
804
- ...nest,
805
- fields: mergedFields
806
- };
807
- });
1093
+ firstPage() {
1094
+ this._nestService.page = 1;
1095
+ return this;
808
1096
  }
809
1097
  /**
810
- * Add filters to the request
811
- * Automatically prevents duplicate filter values for each filter key
1098
+ * Generate a URI accordingly to the given data and active driver
812
1099
  *
813
- * @param {IFilters} filters - Object mapping filter keys to arrays of values
814
- * @return {void}
815
- * @example
816
- * service.addFilters({ id: [1, 2, 3] });
817
- * service.addFilters({ status: ['active', 'pending'] });
1100
+ * @returns {Observable<string>} An observable that emits the generated URI
818
1101
  */
819
- addFilters(filters) {
820
- this._nest.update(nest => {
821
- const mergedFilters = { ...nest.filters };
822
- Object.keys(filters).forEach(key => {
823
- const existingValues = mergedFilters[key] || [];
824
- const newValues = filters[key];
825
- // Use Set to prevent duplicates
826
- const uniqueValues = Array.from(new Set([...existingValues, ...newValues]));
827
- mergedFilters[key] = uniqueValues;
828
- });
829
- return {
830
- ...nest,
831
- filters: mergedFilters
832
- };
833
- });
1102
+ generateUri() {
1103
+ try {
1104
+ this._uri$.next(this._requestStrategy.buildUri(this._nestService.nest(), this._options));
1105
+ return this.uri$;
1106
+ }
1107
+ catch (error) {
1108
+ return throwError(() => error);
1109
+ }
834
1110
  }
835
1111
  /**
836
- * Add resources to include with the request
837
- * Automatically prevents duplicate includes
1112
+ * Navigate directly to the specified page
838
1113
  *
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']);
1114
+ * Validates integer/positive via the existing `setPage` path, and
1115
+ * additionally rejects values that exceed `state.lastPage` when
1116
+ * pagination bounds are known.
1117
+ *
1118
+ * @param n - Target page number
1119
+ * @returns {this}
1120
+ * @throws {InvalidPageNumberError} If `n` is not a positive integer, or if `n > state.lastPage` when `state.isLastPageKnown` is true
844
1121
  */
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
- });
1122
+ goToPage(n) {
1123
+ const state = this._nestService.nest();
1124
+ if (state.isLastPageKnown && n > state.lastPage) {
1125
+ throw new InvalidPageNumberError(n);
1126
+ }
1127
+ this._nestService.page = n;
1128
+ return this;
854
1129
  }
855
1130
  /**
856
- * Add filters with explicit operators (NestJS only)
857
- * Automatically prevents duplicate operator filters for the same field + operator combination
1131
+ * Check whether a next page exists
858
1132
  *
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] }]);
1133
+ * @remarks Template-safe. Returns `true` when pagination bounds are unknown (conservative default — keeps a "Next" button enabled before the first `paginate()` call).
1134
+ * @returns `true` if `state.page < state.lastPage` when bounds are known, or `true` when bounds are unknown
864
1135
  */
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
884
- };
885
- });
1136
+ hasNextPage() {
1137
+ const state = this._nestService.nest();
1138
+ return !state.isLastPageKnown || state.page < state.lastPage;
886
1139
  }
887
1140
  /**
888
- * Add flat field selection columns (NestJS only)
889
- * Automatically prevents duplicate select fields
1141
+ * Check whether a previous page exists
890
1142
  *
891
- * @param {string[]} fields - Array of column names to select
892
- * @return {void}
893
- * @example
894
- * service.addSelect(['id', 'name', 'email']);
1143
+ * @remarks Always safe. Does not require a synced paginated response.
1144
+ * @returns `true` if `state.page > 1`
895
1145
  */
896
- addSelect(fields) {
897
- this._nest.update(nest => {
898
- const uniqueSelect = Array.from(new Set([...nest.select, ...fields]));
899
- return {
900
- ...nest,
901
- select: uniqueSelect
902
- };
903
- });
1146
+ hasPreviousPage() {
1147
+ return this._nestService.nest().page > 1;
904
1148
  }
905
1149
  /**
906
- * Add a field that should be used for sorting data
1150
+ * Check whether the current page is the first page
907
1151
  *
908
- * @param {ISort} sort - Sort configuration with field name and order (ASC/DESC)
909
- * @return {void}
910
- * @example
911
- * import { SortEnum } from 'ng-qubee';
912
- * service.addSort({ field: 'created_at', order: SortEnum.DESC });
913
- * service.addSort({ field: 'name', order: SortEnum.ASC });
1152
+ * @remarks Always safe. Does not require a synced paginated response.
1153
+ * @returns `true` if `state.page === 1`
914
1154
  */
915
- addSort(sort) {
916
- this._nest.update(nest => ({
917
- ...nest,
918
- sorts: [...nest.sorts, sort]
919
- }));
1155
+ isFirstPage() {
1156
+ return this._nestService.nest().page === 1;
920
1157
  }
921
1158
  /**
922
- * Remove fields for the given model
923
- * Uses deep cloning to prevent mutations to the original state
1159
+ * Check whether the current page is the last page
924
1160
  *
925
- * @param {IFields} fields - Object mapping model names to arrays of field names to remove
926
- * @return {void}
927
- * @example
928
- * service.deleteFields({ users: ['email'] });
929
- * service.deleteFields({ posts: ['content', 'body'] });
1161
+ * @remarks Template-safe. Returns `false` when pagination bounds are unknown (no paginated response has been synced yet) — keeps "Next" navigation unblocked until the first `paginate()` call syncs.
1162
+ * @returns `true` only when `state.isLastPageKnown` and `state.page === state.lastPage`
930
1163
  */
931
- deleteFields(fields) {
932
- // Deep clone the fields object to prevent mutations
933
- const f = this._clone(this._nest().fields);
934
- Object.keys(fields).forEach(k => {
935
- if (!(k in f)) {
936
- return;
937
- }
938
- f[k] = f[k].filter(v => !fields[k].includes(v));
939
- });
940
- this._nest.update(nest => ({
941
- ...nest,
942
- fields: f
943
- }));
1164
+ isLastPage() {
1165
+ const state = this._nestService.nest();
1166
+ return state.isLastPageKnown && state.page === state.lastPage;
1167
+ }
1168
+ /**
1169
+ * Navigate to the last page known from the most recent paginated response
1170
+ *
1171
+ * @remarks Requires at least one `PaginationService.paginate()` call to have synced `state.lastPage`. Before that, the bound is unknown and this method throws.
1172
+ * @returns {this}
1173
+ * @throws {PaginationNotSyncedError} If `state.isLastPageKnown` is false (no paginated response has been synced yet)
1174
+ */
1175
+ lastPage() {
1176
+ const state = this._nestService.nest();
1177
+ if (!state.isLastPageKnown) {
1178
+ throw new PaginationNotSyncedError('navigate to last page');
1179
+ }
1180
+ this._nestService.page = state.lastPage;
1181
+ return this;
944
1182
  }
945
1183
  /**
946
- * Remove filters from the request
947
- * Uses deep cloning to prevent mutations to the original state
1184
+ * Navigate to the next page
948
1185
  *
949
- * @param {...string[]} filters - Filter keys to remove
950
- * @return {void}
951
- * @example
952
- * service.deleteFilters('id');
953
- * service.deleteFilters('status', 'type');
1186
+ * @remarks Never throws. Idempotent at the known last page (no-op). Pair with `hasNextPage()` for a disable-state binding.
1187
+ * @returns {this}
954
1188
  */
955
- deleteFilters(...filters) {
956
- // Deep clone the filters object to prevent mutations
957
- const f = this._clone(this._nest().filters);
958
- filters.forEach(k => delete f[k]);
959
- this._nest.update(nest => ({
960
- ...nest,
961
- filters: f
962
- }));
1189
+ nextPage() {
1190
+ const state = this._nestService.nest();
1191
+ if (state.isLastPageKnown && state.page >= state.lastPage) {
1192
+ return this;
1193
+ }
1194
+ this._nestService.page = state.page + 1;
1195
+ return this;
963
1196
  }
964
1197
  /**
965
- * Remove includes from the request
1198
+ * Navigate to the previous page
966
1199
  *
967
- * @param {...string[]} includes - Include names to remove
968
- * @return {void}
969
- * @example
970
- * service.deleteIncludes('profile');
971
- * service.deleteIncludes('posts', 'comments');
1200
+ * @remarks Never throws. Idempotent at page 1 (floored). Pair with `hasPreviousPage()` for a disable-state binding.
1201
+ * @returns {this}
972
1202
  */
973
- deleteIncludes(...includes) {
974
- this._nest.update(nest => ({
975
- ...nest,
976
- includes: nest.includes.filter(v => !includes.includes(v))
977
- }));
1203
+ previousPage() {
1204
+ const state = this._nestService.nest();
1205
+ if (state.page <= 1) {
1206
+ return this;
1207
+ }
1208
+ this._nestService.page = state.page - 1;
1209
+ return this;
978
1210
  }
979
1211
  /**
980
- * Remove operator filters by field name (NestJS only)
1212
+ * Clear the current state and reset the Query Builder to a fresh, clean condition
981
1213
  *
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');
1214
+ * @returns {this}
987
1215
  */
988
- deleteOperatorFilters(...fields) {
989
- this._nest.update(nest => ({
990
- ...nest,
991
- operatorFilters: nest.operatorFilters.filter(f => !fields.includes(f.field))
992
- }));
1216
+ reset() {
1217
+ this._nestService.reset();
1218
+ return this;
993
1219
  }
994
1220
  /**
995
- * Remove the search term from the state (NestJS only)
1221
+ * Set the base URL to use for composing the address
996
1222
  *
997
- * @return {void}
998
- * @example
999
- * service.deleteSearch();
1223
+ * @param {string} baseUrl - The base URL
1224
+ * @returns {this}
1000
1225
  */
1001
- deleteSearch() {
1002
- this._nest.update(nest => ({
1003
- ...nest,
1004
- search: ''
1005
- }));
1226
+ setBaseUrl(baseUrl) {
1227
+ this._nestService.baseUrl = baseUrl;
1228
+ return this;
1006
1229
  }
1007
1230
  /**
1008
- * Remove flat field selections from the state (NestJS only)
1231
+ * Set the items per page number
1009
1232
  *
1010
- * @param {...string[]} fields - Field names to remove from selection
1011
- * @return {void}
1012
- * @example
1013
- * service.deleteSelect('email');
1014
- * service.deleteSelect('name', 'email');
1233
+ * Validation is delegated to the active request strategy because the
1234
+ * accepted range is driver-specific: nestjs-paginate additionally accepts
1235
+ * `-1` as a "fetch all" sentinel, while Laravel, Spatie, and JSON:API
1236
+ * require a positive integer.
1237
+ *
1238
+ * @param limit - Number of items per page (or `-1` to fetch all, NestJS only)
1239
+ * @returns {this}
1240
+ * @throws {import('../errors/invalid-limit.error').InvalidLimitError} If the value is not accepted by the active driver
1015
1241
  */
1016
- deleteSelect(...fields) {
1017
- this._nest.update(nest => ({
1018
- ...nest,
1019
- select: nest.select.filter(f => !fields.includes(f))
1020
- }));
1242
+ setLimit(limit) {
1243
+ this._requestStrategy.validateLimit(limit);
1244
+ this._nestService.limit = limit;
1245
+ this._nestService.page = 1;
1246
+ return this;
1021
1247
  }
1022
1248
  /**
1023
- * Remove sorts from the request by field name
1249
+ * Set the page that the backend will use to paginate the result set
1024
1250
  *
1025
- * @param {...string[]} sorts - Field names of sorts to remove
1026
- * @return {void}
1027
- * @example
1028
- * service.deleteSorts('created_at');
1029
- * service.deleteSorts('name', 'created_at');
1251
+ * @param page - Page number
1252
+ * @returns {this}
1030
1253
  */
1031
- deleteSorts(...sorts) {
1032
- const s = [...this._nest().sorts];
1033
- sorts.forEach(field => {
1034
- const p = s.findIndex(sort => sort.field === field);
1035
- if (p > -1) {
1036
- s.splice(p, 1);
1037
- }
1038
- });
1039
- this._nest.update(nest => ({
1040
- ...nest,
1041
- sorts: s
1042
- }));
1254
+ setPage(page) {
1255
+ this._nestService.page = page;
1256
+ return this;
1043
1257
  }
1044
1258
  /**
1045
- * Set the full-text search term (NestJS only)
1259
+ * Set the API resource to run the query against
1260
+ *
1261
+ * @param {string} resource - Resource name (e.g. 'users' produces /users)
1262
+ * @returns {this}
1263
+ */
1264
+ setResource(resource) {
1265
+ this._nestService.resource = resource;
1266
+ this._nestService.page = 1;
1267
+ return this;
1268
+ }
1269
+ /**
1270
+ * Set the search term for full-text search (NestJS only)
1271
+ *
1272
+ * Produces: `search=term`
1046
1273
  *
1047
1274
  * @param {string} search - The search term
1048
- * @return {void}
1049
- * @example
1050
- * service.setSearch('john doe');
1275
+ * @returns {this}
1276
+ * @throws {UnsupportedSearchError} If the active driver does not support search
1051
1277
  */
1052
1278
  setSearch(search) {
1053
- this._nest.update(nest => ({
1054
- ...nest,
1055
- search
1056
- }));
1279
+ this._assertDriver([DriverEnum.NESTJS], new UnsupportedSearchError());
1280
+ this._nestService.setSearch(search);
1281
+ this._nestService.page = 1;
1282
+ return this;
1057
1283
  }
1058
1284
  /**
1059
- * Reset the query builder state to initial values
1060
- * Clears all fields, filters, includes, sorts, and resets pagination
1285
+ * Get the total number of pages reported by the most recent paginated response
1061
1286
  *
1062
- * @return {void}
1063
- * @example
1064
- * service.reset();
1287
+ * @remarks Throws when called before any `paginate()` has synced a value. For a non-throwing read in a template, read `nest().isLastPageKnown` first as a guard.
1288
+ * @returns The last page number
1289
+ * @throws {PaginationNotSyncedError} If `state.isLastPageKnown` is false (no paginated response has been synced yet)
1065
1290
  */
1066
- reset() {
1067
- this._nest.update(_ => this._clone(INITIAL_STATE));
1291
+ totalPages() {
1292
+ const state = this._nestService.nest();
1293
+ if (!state.isLastPageKnown) {
1294
+ throw new PaginationNotSyncedError('read totalPages');
1295
+ }
1296
+ return state.lastPage;
1068
1297
  }
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 });
1298
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService, deps: [{ token: NestService }, { token: NG_QUBEE_REQUEST_STRATEGY }, { token: NG_QUBEE_DRIVER }, { token: NG_QUBEE_REQUEST_OPTIONS }], target: i0.ɵɵFactoryTarget.Injectable });
1299
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService });
1071
1300
  }
1072
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, decorators: [{
1301
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService, decorators: [{
1073
1302
  type: Injectable
1074
- }], ctorParameters: () => [] });
1303
+ }], ctorParameters: () => [{ type: NestService }, { type: undefined, decorators: [{
1304
+ type: Inject,
1305
+ args: [NG_QUBEE_REQUEST_STRATEGY]
1306
+ }] }, { type: DriverEnum, decorators: [{
1307
+ type: Inject,
1308
+ args: [NG_QUBEE_DRIVER]
1309
+ }] }, { type: QueryBuilderOptions, decorators: [{
1310
+ type: Inject,
1311
+ args: [NG_QUBEE_REQUEST_OPTIONS]
1312
+ }] }] });
1075
1313
 
1076
1314
  class PaginationService {
1315
+ /**
1316
+ * The NestService instance that owns the query-builder state for this
1317
+ * PaginationService's scope (environment-level by default, or
1318
+ * component-level when used via `provideNgQubeeInstance()`)
1319
+ */
1320
+ _nestService;
1077
1321
  /**
1078
1322
  * Resolved response key name options
1079
1323
  */
@@ -1082,23 +1326,48 @@ class PaginationService {
1082
1326
  * The response strategy that parses responses for the active driver
1083
1327
  */
1084
1328
  _responseStrategy;
1085
- constructor(responseStrategy, options = {}) {
1086
- this._options = new ResponseOptions(options);
1329
+ constructor(nestService, responseStrategy, options = new ResponseOptions({})) {
1330
+ this._nestService = nestService;
1331
+ this._options = options;
1087
1332
  this._responseStrategy = responseStrategy;
1088
1333
  }
1089
1334
  /**
1090
1335
  * Transform a raw API response into a typed PaginatedCollection
1091
1336
  *
1092
- * Delegates to the active driver's response strategy for parsing.
1337
+ * Delegates to the active driver's response strategy for parsing, then
1338
+ * auto-syncs the parsed `page` and `lastPage` back into `NestService`
1339
+ * so pagination navigation helpers on `NgQubeeService` can operate
1340
+ * against the live server-reported bounds without consumer bookkeeping.
1341
+ *
1342
+ * @remarks
1343
+ * `lastPage` is only synced when the response yields a positive integer.
1344
+ * Server-emitted `0` (empty collection edge case) and absent fields are
1345
+ * treated as "no useful info" and leave `isLastPageKnown: false`.
1093
1346
  *
1094
1347
  * @param response - The raw API response object
1095
1348
  * @returns A typed PaginatedCollection instance
1096
1349
  */
1097
1350
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1098
1351
  paginate(response) {
1099
- return this._responseStrategy.paginate(response, this._options);
1352
+ const collection = this._responseStrategy.paginate(response, this._options);
1353
+ this._nestService.page = collection.page;
1354
+ if (typeof collection.lastPage === 'number' && Number.isInteger(collection.lastPage) && collection.lastPage > 0) {
1355
+ this._nestService.syncLastPage(collection.lastPage);
1356
+ }
1357
+ return collection;
1100
1358
  }
1359
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: PaginationService, deps: [{ token: NestService }, { token: NG_QUBEE_RESPONSE_STRATEGY }, { token: NG_QUBEE_RESPONSE_OPTIONS }], target: i0.ɵɵFactoryTarget.Injectable });
1360
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: PaginationService });
1101
1361
  }
1362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: PaginationService, decorators: [{
1363
+ type: Injectable
1364
+ }], ctorParameters: () => [{ type: NestService }, { type: undefined, decorators: [{
1365
+ type: Inject,
1366
+ args: [NG_QUBEE_RESPONSE_STRATEGY]
1367
+ }] }, { type: ResponseOptions, decorators: [{
1368
+ type: Inject,
1369
+ args: [NG_QUBEE_RESPONSE_OPTIONS]
1370
+ }] }] });
1102
1371
 
1103
1372
  var SortEnum;
1104
1373
  (function (SortEnum) {
@@ -2032,7 +2301,7 @@ class SpatieResponseStrategy {
2032
2301
  * @param driver - The pagination driver
2033
2302
  * @returns The corresponding request strategy
2034
2303
  */
2035
- function resolveRequestStrategy$1(driver) {
2304
+ function resolveRequestStrategy(driver) {
2036
2305
  switch (driver) {
2037
2306
  case DriverEnum.JSON_API:
2038
2307
  return new JsonApiRequestStrategy();
@@ -2050,7 +2319,7 @@ function resolveRequestStrategy$1(driver) {
2050
2319
  * @param driver - The pagination driver
2051
2320
  * @returns The corresponding response strategy
2052
2321
  */
2053
- function resolveResponseStrategy$1(driver) {
2322
+ function resolveResponseStrategy(driver) {
2054
2323
  switch (driver) {
2055
2324
  case DriverEnum.JSON_API:
2056
2325
  return new JsonApiResponseStrategy();
@@ -2062,86 +2331,47 @@ function resolveResponseStrategy$1(driver) {
2062
2331
  return new LaravelResponseStrategy();
2063
2332
  }
2064
2333
  }
2065
- // @dynamic
2066
- class NgQubeeModule {
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);
2077
- return {
2078
- ngModule: NgQubeeModule,
2079
- providers: [
2080
- NestService,
2081
- {
2082
- deps: [NestService],
2083
- provide: NgQubeeService,
2084
- useFactory: (nestService) => new NgQubeeService(nestService, requestStrategy, driver, Object.assign({}, config.request))
2085
- },
2086
- {
2087
- provide: PaginationService,
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
- }
2097
- }
2098
- ]
2099
- };
2100
- }
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 });
2104
- }
2105
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule, decorators: [{
2106
- type: NgModule,
2107
- args: [{}]
2108
- }] });
2109
-
2110
2334
  /**
2111
- * Resolve the request strategy instance for the given driver
2335
+ * Resolve the driver-specific `ResponseOptions` instance
2112
2336
  *
2113
2337
  * @param driver - The pagination driver
2114
- * @returns The corresponding request strategy
2338
+ * @param responseConfig - User-supplied response key overrides
2339
+ * @returns A pre-built ResponseOptions (or driver-specific subclass)
2115
2340
  */
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();
2341
+ function resolveResponseOptions(driver, responseConfig) {
2342
+ if (driver === DriverEnum.JSON_API) {
2343
+ return new JsonApiResponseOptions(responseConfig);
2344
+ }
2345
+ if (driver === DriverEnum.NESTJS) {
2346
+ return new NestjsResponseOptions(responseConfig);
2126
2347
  }
2348
+ return new ResponseOptions(responseConfig);
2127
2349
  }
2128
2350
  /**
2129
- * Resolve the response strategy instance for the given driver
2351
+ * Build the core provider list shared by `provideNgQubee()` and
2352
+ * `NgQubeeModule.forRoot()`
2130
2353
  *
2131
- * @param driver - The pagination driver
2132
- * @returns The corresponding response strategy
2354
+ * Exposes the driver, strategies, and options via injection tokens so that
2355
+ * consumers can request a component-scoped instance of the services through
2356
+ * `provideNgQubeeInstance()`.
2357
+ *
2358
+ * @param config - Configuration object compliant to the IConfig interface
2359
+ * @returns An array of Providers for the environment injector
2133
2360
  */
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
- }
2361
+ function buildNgQubeeProviders(config) {
2362
+ const driver = config.driver;
2363
+ const requestOptions = new QueryBuilderOptions(Object.assign({}, config.request));
2364
+ const responseOptions = resolveResponseOptions(driver, Object.assign({}, config.response));
2365
+ return [
2366
+ { provide: NG_QUBEE_DRIVER, useValue: driver },
2367
+ { provide: NG_QUBEE_REQUEST_STRATEGY, useValue: resolveRequestStrategy(driver) },
2368
+ { provide: NG_QUBEE_REQUEST_OPTIONS, useValue: requestOptions },
2369
+ { provide: NG_QUBEE_RESPONSE_STRATEGY, useValue: resolveResponseStrategy(driver) },
2370
+ { provide: NG_QUBEE_RESPONSE_OPTIONS, useValue: responseOptions },
2371
+ NestService,
2372
+ NgQubeeService,
2373
+ PaginationService
2374
+ ];
2145
2375
  }
2146
2376
  /**
2147
2377
  * Sets up providers necessary to enable `NgQubee` functionality for the application.
@@ -2187,33 +2417,61 @@ function resolveResponseStrategy(driver) {
2187
2417
  * @returns A set of providers to setup NgQubee
2188
2418
  */
2189
2419
  function provideNgQubee(config) {
2190
- const driver = config.driver;
2191
- const requestStrategy = resolveRequestStrategy(driver);
2192
- const responseStrategy = resolveResponseStrategy(driver);
2193
- return makeEnvironmentProviders([
2194
- {
2195
- provide: NestService,
2196
- useClass: NestService
2197
- },
2198
- {
2199
- deps: [NestService],
2200
- provide: NgQubeeService,
2201
- useFactory: (nestService) => new NgQubeeService(nestService, requestStrategy, driver, Object.assign({}, config.request))
2202
- },
2203
- {
2204
- provide: PaginationService,
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
- }
2214
- }
2215
- ]);
2420
+ return makeEnvironmentProviders(buildNgQubeeProviders(config));
2421
+ }
2422
+ /**
2423
+ * Providers for a component-scoped NgQubee instance
2424
+ *
2425
+ * Use this inside a standalone component's `providers: [...]` to get a
2426
+ * dedicated `NgQubeeService` (and its `NestService` / `PaginationService`
2427
+ * collaborators) whose query-builder and pagination state does not bleed
2428
+ * with the app-wide shared instance provided by `provideNgQubee()`.
2429
+ *
2430
+ * @usageNotes
2431
+ *
2432
+ * ```
2433
+ * @Component({
2434
+ * standalone: true,
2435
+ * providers: [...provideNgQubeeInstance()]
2436
+ * })
2437
+ * export class MyFeatureComponent {
2438
+ * constructor(private _qb: NgQubeeService) {}
2439
+ * }
2440
+ * ```
2441
+ *
2442
+ * The driver, strategies, and options are inherited from the environment
2443
+ * injector (`provideNgQubee()` at root), so only the service instances are
2444
+ * re-created at the component level.
2445
+ *
2446
+ * @publicApi
2447
+ * @returns A provider array to spread into a component's `providers`
2448
+ */
2449
+ function provideNgQubeeInstance() {
2450
+ return [NestService, NgQubeeService, PaginationService];
2451
+ }
2452
+
2453
+ // @dynamic
2454
+ class NgQubeeModule {
2455
+ /**
2456
+ * Configure NgQubee for the root module
2457
+ *
2458
+ * @param config - Configuration object with driver, and optional request and response settings
2459
+ * @returns Module with providers configured for the specified driver
2460
+ */
2461
+ static forRoot(config) {
2462
+ return {
2463
+ ngModule: NgQubeeModule,
2464
+ providers: buildNgQubeeProviders(config)
2465
+ };
2466
+ }
2467
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2468
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule });
2469
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule });
2216
2470
  }
2471
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule, decorators: [{
2472
+ type: NgModule,
2473
+ args: [{}]
2474
+ }] });
2217
2475
 
2218
2476
  /**
2219
2477
  * Enum representing the available filter operators for the NestJS driver
@@ -2247,5 +2505,5 @@ var FilterOperatorEnum;
2247
2505
  * Generated bundle index. Do not edit.
2248
2506
  */
2249
2507
 
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 };
2508
+ export { DriverEnum, FilterOperatorEnum, InvalidLimitError, InvalidPageNumberError, InvalidResourceNameError, JsonApiRequestStrategy, JsonApiResponseStrategy, KeyNotFoundError, LaravelRequestStrategy, LaravelResponseStrategy, NG_QUBEE_DRIVER, NG_QUBEE_REQUEST_OPTIONS, NG_QUBEE_REQUEST_STRATEGY, NG_QUBEE_RESPONSE_OPTIONS, NG_QUBEE_RESPONSE_STRATEGY, NestjsRequestStrategy, NestjsResponseStrategy, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationNotSyncedError, PaginationService, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, buildNgQubeeProviders, provideNgQubee, provideNgQubeeInstance };
2251
2509
  //# sourceMappingURL=ng-qubee.mjs.map