ng-qubee 3.0.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.
- package/README.md +94 -17
- package/fesm2022/ng-qubee.mjs +944 -565
- package/fesm2022/ng-qubee.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ng-qubee.d.ts +269 -27
package/fesm2022/ng-qubee.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, computed, Injectable,
|
|
2
|
+
import { signal, computed, Injectable, NgModule, makeEnvironmentProviders } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject, filter, throwError } from 'rxjs';
|
|
4
4
|
import * as qs from 'qs';
|
|
5
5
|
|
|
@@ -74,6 +74,7 @@ class PaginatedCollection {
|
|
|
74
74
|
*/
|
|
75
75
|
var DriverEnum;
|
|
76
76
|
(function (DriverEnum) {
|
|
77
|
+
DriverEnum["JSON_API"] = "json-api";
|
|
77
78
|
DriverEnum["LARAVEL"] = "laravel";
|
|
78
79
|
DriverEnum["NESTJS"] = "nestjs";
|
|
79
80
|
DriverEnum["SPATIE"] = "spatie";
|
|
@@ -121,6 +122,31 @@ class ResponseOptions {
|
|
|
121
122
|
this.total = options.total || 'total';
|
|
122
123
|
}
|
|
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
|
+
}
|
|
124
150
|
/**
|
|
125
151
|
* Pre-configured ResponseOptions for the NestJS driver
|
|
126
152
|
*
|
|
@@ -263,203 +289,539 @@ class QueryBuilderOptions {
|
|
|
263
289
|
}
|
|
264
290
|
}
|
|
265
291
|
|
|
266
|
-
class
|
|
267
|
-
|
|
268
|
-
super(`Invalid limit value: Limit must be a positive integer greater than 0. Received: ${limit}`);
|
|
269
|
-
this.name = 'InvalidLimitError';
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Error thrown when an invalid resource name is provided
|
|
275
|
-
*
|
|
276
|
-
* Resource name must be a non-empty string.
|
|
277
|
-
*/
|
|
278
|
-
class InvalidResourceNameError extends Error {
|
|
279
|
-
constructor(resource) {
|
|
280
|
-
super(`Invalid resource name: Resource name must be a non-empty string. Received: ${JSON.stringify(resource)}`);
|
|
281
|
-
this.name = 'InvalidResourceNameError';
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
class InvalidPageNumberError extends Error {
|
|
286
|
-
constructor(page) {
|
|
287
|
-
super(`Invalid page number: Page must be a positive integer greater than 0. Received: ${page}`);
|
|
288
|
-
this.name = 'InvalidPageNumberError';
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const INITIAL_STATE = {
|
|
293
|
-
baseUrl: '',
|
|
294
|
-
fields: {},
|
|
295
|
-
filters: {},
|
|
296
|
-
includes: [],
|
|
297
|
-
limit: 15,
|
|
298
|
-
operatorFilters: [],
|
|
299
|
-
page: 1,
|
|
300
|
-
resource: '',
|
|
301
|
-
search: '',
|
|
302
|
-
select: [],
|
|
303
|
-
sorts: []
|
|
304
|
-
};
|
|
305
|
-
class NestService {
|
|
292
|
+
class NgQubeeService {
|
|
293
|
+
_nestService;
|
|
306
294
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* @type {IQueryBuilderState}
|
|
295
|
+
* The active pagination driver
|
|
310
296
|
*/
|
|
311
|
-
|
|
297
|
+
_driver;
|
|
312
298
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
* @type {Signal<IQueryBuilderState>}
|
|
299
|
+
* Resolved query parameter key name options
|
|
316
300
|
*/
|
|
317
|
-
|
|
318
|
-
constructor() {
|
|
319
|
-
// Nothing to see here
|
|
320
|
-
}
|
|
301
|
+
_options;
|
|
321
302
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* @param {string} baseUrl - The base URL to prepend to generated URIs
|
|
325
|
-
* @example
|
|
326
|
-
* service.baseUrl = 'https://api.example.com';
|
|
303
|
+
* The request strategy that builds URIs for the active driver
|
|
327
304
|
*/
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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;
|
|
333
319
|
}
|
|
334
320
|
/**
|
|
335
|
-
*
|
|
336
|
-
* Must be a positive integer greater than 0
|
|
321
|
+
* Assert that the active driver is one of the allowed drivers
|
|
337
322
|
*
|
|
338
|
-
* @param
|
|
339
|
-
* @
|
|
340
|
-
* @
|
|
341
|
-
* 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
|
|
342
326
|
*/
|
|
343
|
-
|
|
344
|
-
this.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
limit
|
|
348
|
-
}));
|
|
327
|
+
_assertDriver(allowed, error) {
|
|
328
|
+
if (!allowed.includes(this._driver)) {
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
349
331
|
}
|
|
350
332
|
/**
|
|
351
|
-
*
|
|
352
|
-
* Must be a positive integer greater than 0
|
|
333
|
+
* Add fields to the select statement for the given model (JSON:API and Spatie only)
|
|
353
334
|
*
|
|
354
|
-
* @param
|
|
355
|
-
* @
|
|
356
|
-
* @
|
|
357
|
-
*
|
|
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
|
|
358
339
|
*/
|
|
359
|
-
|
|
360
|
-
this.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
})
|
|
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;
|
|
365
347
|
}
|
|
366
348
|
/**
|
|
367
|
-
*
|
|
368
|
-
* Must be a non-empty string
|
|
349
|
+
* Add a filter with the given value(s) (JSON:API, NestJS, and Spatie)
|
|
369
350
|
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
* @
|
|
373
|
-
*
|
|
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
|
|
374
357
|
*/
|
|
375
|
-
|
|
376
|
-
this.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return
|
|
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;
|
|
384
367
|
}
|
|
385
368
|
/**
|
|
386
|
-
*
|
|
369
|
+
* Add a filter with an explicit operator (NestJS only)
|
|
387
370
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* @
|
|
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
|
|
391
378
|
*/
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
379
|
+
addFilterOperator(field, operator, ...values) {
|
|
380
|
+
this._assertDriver([DriverEnum.NESTJS], new UnsupportedFilterOperatorError());
|
|
381
|
+
if (!values.length) {
|
|
382
|
+
return this;
|
|
395
383
|
}
|
|
384
|
+
this._nestService.addOperatorFilters([{ field, operator, values }]);
|
|
385
|
+
return this;
|
|
396
386
|
}
|
|
397
387
|
/**
|
|
398
|
-
*
|
|
388
|
+
* Add related entities to include in the request (JSON:API and Spatie only)
|
|
399
389
|
*
|
|
400
|
-
* @param {
|
|
401
|
-
* @
|
|
402
|
-
* @
|
|
390
|
+
* @param {string[]} models - Models to include
|
|
391
|
+
* @returns {this}
|
|
392
|
+
* @throws {UnsupportedIncludesError} If the active driver does not support includes
|
|
403
393
|
*/
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
394
|
+
addIncludes(...models) {
|
|
395
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedIncludesError());
|
|
396
|
+
if (!models.length) {
|
|
397
|
+
return this;
|
|
407
398
|
}
|
|
399
|
+
this._nestService.addIncludes(models);
|
|
400
|
+
return this;
|
|
408
401
|
}
|
|
409
402
|
/**
|
|
410
|
-
*
|
|
403
|
+
* Add flat field selection (NestJS only)
|
|
411
404
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
* @
|
|
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
|
|
415
410
|
*/
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
411
|
+
addSelect(...fields) {
|
|
412
|
+
this._assertDriver([DriverEnum.NESTJS], new UnsupportedSelectError());
|
|
413
|
+
if (!fields.length) {
|
|
414
|
+
return this;
|
|
419
415
|
}
|
|
416
|
+
this._nestService.addSelect(fields);
|
|
417
|
+
return this;
|
|
420
418
|
}
|
|
421
419
|
/**
|
|
422
|
-
* Add
|
|
423
|
-
* Automatically prevents duplicate fields for each model
|
|
420
|
+
* Add a field with a sort criteria (JSON:API, NestJS, and Spatie)
|
|
424
421
|
*
|
|
425
|
-
* @param
|
|
426
|
-
* @
|
|
427
|
-
* @
|
|
428
|
-
*
|
|
429
|
-
* 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
|
|
430
426
|
*/
|
|
431
|
-
|
|
432
|
-
this.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const newFields = fields[model];
|
|
437
|
-
// Use Set to prevent duplicates
|
|
438
|
-
const uniqueFields = Array.from(new Set([...existingFields, ...newFields]));
|
|
439
|
-
mergedFields[model] = uniqueFields;
|
|
440
|
-
});
|
|
441
|
-
return {
|
|
442
|
-
...nest,
|
|
443
|
-
fields: mergedFields
|
|
444
|
-
};
|
|
427
|
+
addSort(field, order) {
|
|
428
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedSortError());
|
|
429
|
+
this._nestService.addSort({
|
|
430
|
+
field,
|
|
431
|
+
order
|
|
445
432
|
});
|
|
433
|
+
return this;
|
|
446
434
|
}
|
|
447
435
|
/**
|
|
448
|
-
*
|
|
449
|
-
* Automatically prevents duplicate filter values for each filter key
|
|
436
|
+
* Delete selected fields for the given models in the current query builder state (JSON:API and Spatie only)
|
|
450
437
|
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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;
|
|
802
|
+
});
|
|
803
|
+
return {
|
|
804
|
+
...nest,
|
|
805
|
+
fields: mergedFields
|
|
806
|
+
};
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Add filters to the request
|
|
811
|
+
* Automatically prevents duplicate filter values for each filter key
|
|
812
|
+
*
|
|
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'] });
|
|
818
|
+
*/
|
|
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];
|
|
463
825
|
// Use Set to prevent duplicates
|
|
464
826
|
const uniqueValues = Array.from(new Set([...existingValues, ...newValues]));
|
|
465
827
|
mergedFilters[key] = uniqueValues;
|
|
@@ -635,483 +997,444 @@ class NestService {
|
|
|
635
997
|
* @return {void}
|
|
636
998
|
* @example
|
|
637
999
|
* service.deleteSearch();
|
|
638
|
-
*/
|
|
639
|
-
deleteSearch() {
|
|
640
|
-
this._nest.update(nest => ({
|
|
641
|
-
...nest,
|
|
642
|
-
search: ''
|
|
643
|
-
}));
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Remove flat field selections from the state (NestJS only)
|
|
647
|
-
*
|
|
648
|
-
* @param {...string[]} fields - Field names to remove from selection
|
|
649
|
-
* @return {void}
|
|
650
|
-
* @example
|
|
651
|
-
* service.deleteSelect('email');
|
|
652
|
-
* service.deleteSelect('name', 'email');
|
|
653
|
-
*/
|
|
654
|
-
deleteSelect(...fields) {
|
|
655
|
-
this._nest.update(nest => ({
|
|
656
|
-
...nest,
|
|
657
|
-
select: nest.select.filter(f => !fields.includes(f))
|
|
658
|
-
}));
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* Remove sorts from the request by field name
|
|
662
|
-
*
|
|
663
|
-
* @param {...string[]} sorts - Field names of sorts to remove
|
|
664
|
-
* @return {void}
|
|
665
|
-
* @example
|
|
666
|
-
* service.deleteSorts('created_at');
|
|
667
|
-
* service.deleteSorts('name', 'created_at');
|
|
668
|
-
*/
|
|
669
|
-
deleteSorts(...sorts) {
|
|
670
|
-
const s = [...this._nest().sorts];
|
|
671
|
-
sorts.forEach(field => {
|
|
672
|
-
const p = s.findIndex(sort => sort.field === field);
|
|
673
|
-
if (p > -1) {
|
|
674
|
-
s.splice(p, 1);
|
|
675
|
-
}
|
|
676
|
-
});
|
|
677
|
-
this._nest.update(nest => ({
|
|
678
|
-
...nest,
|
|
679
|
-
sorts: s
|
|
680
|
-
}));
|
|
681
|
-
}
|
|
682
|
-
/**
|
|
683
|
-
* Set the full-text search term (NestJS only)
|
|
684
|
-
*
|
|
685
|
-
* @param {string} search - The search term
|
|
686
|
-
* @return {void}
|
|
687
|
-
* @example
|
|
688
|
-
* service.setSearch('john doe');
|
|
689
|
-
*/
|
|
690
|
-
setSearch(search) {
|
|
691
|
-
this._nest.update(nest => ({
|
|
692
|
-
...nest,
|
|
693
|
-
search
|
|
694
|
-
}));
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* Reset the query builder state to initial values
|
|
698
|
-
* Clears all fields, filters, includes, sorts, and resets pagination
|
|
699
|
-
*
|
|
700
|
-
* @return {void}
|
|
701
|
-
* @example
|
|
702
|
-
* service.reset();
|
|
703
|
-
*/
|
|
704
|
-
reset() {
|
|
705
|
-
this._nest.update(_ => this._clone(INITIAL_STATE));
|
|
706
|
-
}
|
|
707
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
708
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService });
|
|
709
|
-
}
|
|
710
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, decorators: [{
|
|
711
|
-
type: Injectable
|
|
712
|
-
}], ctorParameters: () => [] });
|
|
713
|
-
|
|
714
|
-
class NgQubeeService {
|
|
715
|
-
_nestService;
|
|
716
|
-
/**
|
|
717
|
-
* The active pagination driver
|
|
718
|
-
*/
|
|
719
|
-
_driver;
|
|
720
|
-
/**
|
|
721
|
-
* Resolved query parameter key name options
|
|
722
|
-
*/
|
|
723
|
-
_options;
|
|
724
|
-
/**
|
|
725
|
-
* The request strategy that builds URIs for the active driver
|
|
726
|
-
*/
|
|
727
|
-
_requestStrategy;
|
|
728
|
-
/**
|
|
729
|
-
* Internal BehaviorSubject that holds the latest generated URI
|
|
730
|
-
*/
|
|
731
|
-
_uri$ = new BehaviorSubject('');
|
|
732
|
-
/**
|
|
733
|
-
* Observable that emits non-empty generated URIs
|
|
734
|
-
*/
|
|
735
|
-
uri$ = this._uri$.asObservable().pipe(filter(uri => !!uri));
|
|
736
|
-
constructor(_nestService, requestStrategy, driver, options = {}) {
|
|
737
|
-
this._nestService = _nestService;
|
|
738
|
-
this._driver = driver;
|
|
739
|
-
this._options = new QueryBuilderOptions(options);
|
|
740
|
-
this._requestStrategy = requestStrategy;
|
|
741
|
-
}
|
|
742
|
-
/**
|
|
743
|
-
* Assert that the active driver is one of the allowed drivers
|
|
744
|
-
*
|
|
745
|
-
* @param allowed - The allowed drivers
|
|
746
|
-
* @param error - The error to throw if the driver is not allowed
|
|
747
|
-
* @throws The provided error if the active driver is not in the allowed list
|
|
748
|
-
*/
|
|
749
|
-
_assertDriver(allowed, error) {
|
|
750
|
-
if (!allowed.includes(this._driver)) {
|
|
751
|
-
throw error;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
/**
|
|
755
|
-
* Add fields to the select statement for the given model (Spatie only)
|
|
756
|
-
*
|
|
757
|
-
* @param model - Model that holds the fields
|
|
758
|
-
* @param fields - Fields to select
|
|
759
|
-
* @returns {this}
|
|
760
|
-
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
761
|
-
*/
|
|
762
|
-
addFields(model, fields) {
|
|
763
|
-
this._assertDriver([DriverEnum.SPATIE], new UnsupportedFieldSelectionError());
|
|
764
|
-
if (!fields.length) {
|
|
765
|
-
return this;
|
|
766
|
-
}
|
|
767
|
-
this._nestService.addFields({ [model]: fields });
|
|
768
|
-
return this;
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Add a filter with the given value(s) (Spatie and NestJS only)
|
|
772
|
-
*
|
|
773
|
-
* Produces: `filter[field]=value` (Spatie) or `filter.field=value` (NestJS)
|
|
774
|
-
*
|
|
775
|
-
* @param {string} field - Name of the field to filter
|
|
776
|
-
* @param {(string | number | boolean)[]} values - The needle(s)
|
|
777
|
-
* @returns {this}
|
|
778
|
-
* @throws {UnsupportedFilterError} If the active driver does not support filters
|
|
779
|
-
*/
|
|
780
|
-
addFilter(field, ...values) {
|
|
781
|
-
this._assertDriver([DriverEnum.SPATIE, DriverEnum.NESTJS], new UnsupportedFilterError());
|
|
782
|
-
if (!values.length) {
|
|
783
|
-
return this;
|
|
784
|
-
}
|
|
785
|
-
this._nestService.addFilters({
|
|
786
|
-
[field]: values
|
|
787
|
-
});
|
|
788
|
-
return this;
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Add a filter with an explicit operator (NestJS only)
|
|
792
|
-
*
|
|
793
|
-
* Produces: `filter.field=$operator:value`
|
|
794
|
-
*
|
|
795
|
-
* @param {string} field - Name of the field to filter
|
|
796
|
-
* @param {FilterOperatorEnum} operator - The filter operator to apply
|
|
797
|
-
* @param {(string | number | boolean)[]} values - The value(s) for the filter
|
|
798
|
-
* @returns {this}
|
|
799
|
-
* @throws {UnsupportedFilterOperatorError} If the active driver does not support filter operators
|
|
800
|
-
*/
|
|
801
|
-
addFilterOperator(field, operator, ...values) {
|
|
802
|
-
this._assertDriver([DriverEnum.NESTJS], new UnsupportedFilterOperatorError());
|
|
803
|
-
if (!values.length) {
|
|
804
|
-
return this;
|
|
805
|
-
}
|
|
806
|
-
this._nestService.addOperatorFilters([{ field, operator, values }]);
|
|
807
|
-
return this;
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* Add related entities to include in the request (Spatie only)
|
|
811
|
-
*
|
|
812
|
-
* @param {string[]} models - Models to include
|
|
813
|
-
* @returns {this}
|
|
814
|
-
* @throws {UnsupportedIncludesError} If the active driver does not support includes
|
|
815
|
-
*/
|
|
816
|
-
addIncludes(...models) {
|
|
817
|
-
this._assertDriver([DriverEnum.SPATIE], new UnsupportedIncludesError());
|
|
818
|
-
if (!models.length) {
|
|
819
|
-
return this;
|
|
820
|
-
}
|
|
821
|
-
this._nestService.addIncludes(models);
|
|
822
|
-
return this;
|
|
1000
|
+
*/
|
|
1001
|
+
deleteSearch() {
|
|
1002
|
+
this._nest.update(nest => ({
|
|
1003
|
+
...nest,
|
|
1004
|
+
search: ''
|
|
1005
|
+
}));
|
|
823
1006
|
}
|
|
824
1007
|
/**
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
* Produces: `select=col1,col2`
|
|
1008
|
+
* Remove flat field selections from the state (NestJS only)
|
|
828
1009
|
*
|
|
829
|
-
* @param {string[]} fields -
|
|
830
|
-
* @
|
|
831
|
-
* @
|
|
1010
|
+
* @param {...string[]} fields - Field names to remove from selection
|
|
1011
|
+
* @return {void}
|
|
1012
|
+
* @example
|
|
1013
|
+
* service.deleteSelect('email');
|
|
1014
|
+
* service.deleteSelect('name', 'email');
|
|
832
1015
|
*/
|
|
833
|
-
|
|
834
|
-
this.
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
this._nestService.addSelect(fields);
|
|
839
|
-
return this;
|
|
1016
|
+
deleteSelect(...fields) {
|
|
1017
|
+
this._nest.update(nest => ({
|
|
1018
|
+
...nest,
|
|
1019
|
+
select: nest.select.filter(f => !fields.includes(f))
|
|
1020
|
+
}));
|
|
840
1021
|
}
|
|
841
1022
|
/**
|
|
842
|
-
*
|
|
1023
|
+
* Remove sorts from the request by field name
|
|
843
1024
|
*
|
|
844
|
-
* @param
|
|
845
|
-
* @
|
|
846
|
-
* @
|
|
847
|
-
*
|
|
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');
|
|
848
1030
|
*/
|
|
849
|
-
|
|
850
|
-
this.
|
|
851
|
-
|
|
852
|
-
field
|
|
853
|
-
|
|
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
|
+
}
|
|
854
1038
|
});
|
|
855
|
-
|
|
1039
|
+
this._nest.update(nest => ({
|
|
1040
|
+
...nest,
|
|
1041
|
+
sorts: s
|
|
1042
|
+
}));
|
|
856
1043
|
}
|
|
857
1044
|
/**
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
* ```
|
|
861
|
-
* ngQubeeService.deleteFields({
|
|
862
|
-
* users: ['email', 'password'],
|
|
863
|
-
* address: ['zipcode']
|
|
864
|
-
* });
|
|
865
|
-
* ```
|
|
1045
|
+
* Set the full-text search term (NestJS only)
|
|
866
1046
|
*
|
|
867
|
-
* @param {
|
|
868
|
-
* @
|
|
869
|
-
* @
|
|
1047
|
+
* @param {string} search - The search term
|
|
1048
|
+
* @return {void}
|
|
1049
|
+
* @example
|
|
1050
|
+
* service.setSearch('john doe');
|
|
870
1051
|
*/
|
|
871
|
-
|
|
872
|
-
this.
|
|
873
|
-
|
|
874
|
-
|
|
1052
|
+
setSearch(search) {
|
|
1053
|
+
this._nest.update(nest => ({
|
|
1054
|
+
...nest,
|
|
1055
|
+
search
|
|
1056
|
+
}));
|
|
875
1057
|
}
|
|
876
1058
|
/**
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
* ```
|
|
880
|
-
* ngQubeeService.deleteFieldsByModel('users', 'email', 'password');
|
|
881
|
-
* ```
|
|
1059
|
+
* Reset the query builder state to initial values
|
|
1060
|
+
* Clears all fields, filters, includes, sorts, and resets pagination
|
|
882
1061
|
*
|
|
883
|
-
* @
|
|
884
|
-
* @
|
|
885
|
-
*
|
|
886
|
-
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
1062
|
+
* @return {void}
|
|
1063
|
+
* @example
|
|
1064
|
+
* service.reset();
|
|
887
1065
|
*/
|
|
888
|
-
|
|
889
|
-
this.
|
|
890
|
-
if (!fields.length) {
|
|
891
|
-
return this;
|
|
892
|
-
}
|
|
893
|
-
this._nestService.deleteFields({
|
|
894
|
-
[model]: fields
|
|
895
|
-
});
|
|
896
|
-
return this;
|
|
1066
|
+
reset() {
|
|
1067
|
+
this._nest.update(_ => this._clone(INITIAL_STATE));
|
|
897
1068
|
}
|
|
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 });
|
|
1071
|
+
}
|
|
1072
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, decorators: [{
|
|
1073
|
+
type: Injectable
|
|
1074
|
+
}], ctorParameters: () => [] });
|
|
1075
|
+
|
|
1076
|
+
class PaginationService {
|
|
898
1077
|
/**
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
* @param {string[]} filters - Filters to remove
|
|
902
|
-
* @returns {this}
|
|
903
|
-
* @throws {UnsupportedFilterError} If the active driver does not support filters
|
|
1078
|
+
* Resolved response key name options
|
|
904
1079
|
*/
|
|
905
|
-
|
|
906
|
-
this._assertDriver([DriverEnum.SPATIE, DriverEnum.NESTJS], new UnsupportedFilterError());
|
|
907
|
-
if (!filters.length) {
|
|
908
|
-
return this;
|
|
909
|
-
}
|
|
910
|
-
this._nestService.deleteFilters(...filters);
|
|
911
|
-
return this;
|
|
912
|
-
}
|
|
1080
|
+
_options;
|
|
913
1081
|
/**
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
* @param {string[]} includes - Models to remove
|
|
917
|
-
* @returns {this}
|
|
918
|
-
* @throws {UnsupportedIncludesError} If the active driver does not support includes
|
|
1082
|
+
* The response strategy that parses responses for the active driver
|
|
919
1083
|
*/
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
this._nestService.deleteIncludes(...includes);
|
|
926
|
-
return this;
|
|
1084
|
+
_responseStrategy;
|
|
1085
|
+
constructor(responseStrategy, options = {}) {
|
|
1086
|
+
this._options = new ResponseOptions(options);
|
|
1087
|
+
this._responseStrategy = responseStrategy;
|
|
927
1088
|
}
|
|
928
1089
|
/**
|
|
929
|
-
*
|
|
1090
|
+
* Transform a raw API response into a typed PaginatedCollection
|
|
930
1091
|
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
* @
|
|
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
|
|
934
1096
|
*/
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return this;
|
|
939
|
-
}
|
|
940
|
-
this._nestService.deleteOperatorFilters(...fields);
|
|
941
|
-
return this;
|
|
1097
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1098
|
+
paginate(response) {
|
|
1099
|
+
return this._responseStrategy.paginate(response, this._options);
|
|
942
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 {
|
|
943
1118
|
/**
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
* @returns {this}
|
|
947
|
-
* @throws {UnsupportedSearchError} If the active driver does not support search
|
|
1119
|
+
* @param limit - The rejected limit value
|
|
1120
|
+
* @param allowFetchAll - Whether the active driver accepts `-1` (fetch all)
|
|
948
1121
|
*/
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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.`);
|
|
953
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 {
|
|
954
1150
|
/**
|
|
955
|
-
*
|
|
1151
|
+
* Accumulator for composing the URI string
|
|
1152
|
+
*/
|
|
1153
|
+
_uri = '';
|
|
1154
|
+
/**
|
|
1155
|
+
* Build a URI string from the given state using the JSON:API format
|
|
956
1156
|
*
|
|
957
|
-
* @param
|
|
958
|
-
* @
|
|
959
|
-
* @
|
|
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
|
|
960
1161
|
*/
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
return this;
|
|
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');
|
|
965
1165
|
}
|
|
966
|
-
this.
|
|
967
|
-
|
|
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;
|
|
968
1173
|
}
|
|
969
1174
|
/**
|
|
970
|
-
*
|
|
1175
|
+
* Validate that the given limit is accepted by the JSON:API driver
|
|
971
1176
|
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
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
|
|
975
1183
|
*/
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1184
|
+
validateLimit(limit) {
|
|
1185
|
+
if (Number.isInteger(limit) && limit >= 1) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
throw new InvalidLimitError(limit);
|
|
980
1189
|
}
|
|
981
1190
|
/**
|
|
982
|
-
*
|
|
1191
|
+
* Parse and append field selection parameters
|
|
983
1192
|
*
|
|
984
|
-
*
|
|
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
|
|
985
1201
|
*/
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
this._uri
|
|
989
|
-
return this.uri$;
|
|
1202
|
+
_parseFields(state, options) {
|
|
1203
|
+
if (!Object.keys(state.fields).length) {
|
|
1204
|
+
return this._uri;
|
|
990
1205
|
}
|
|
991
|
-
|
|
992
|
-
|
|
1206
|
+
if (!state.resource) {
|
|
1207
|
+
throw new Error('While selecting fields, the -> resource <- is required');
|
|
1208
|
+
}
|
|
1209
|
+
if (!(state.resource in state.fields)) {
|
|
1210
|
+
throw new Error(`Key ${state.resource} is missing in the fields object`);
|
|
1211
|
+
}
|
|
1212
|
+
const f = {};
|
|
1213
|
+
for (const k in state.fields) {
|
|
1214
|
+
if (state.fields.hasOwnProperty(k)) {
|
|
1215
|
+
if (k !== state.resource && !state.includes.includes(k)) {
|
|
1216
|
+
throw new UnselectableModelError(k);
|
|
1217
|
+
}
|
|
1218
|
+
Object.assign(f, { [`${options.fields}[${k}]`]: state.fields[k].join(',') });
|
|
1219
|
+
}
|
|
993
1220
|
}
|
|
1221
|
+
const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
|
|
1222
|
+
this._uri += param;
|
|
1223
|
+
return param;
|
|
994
1224
|
}
|
|
995
1225
|
/**
|
|
996
|
-
*
|
|
1226
|
+
* Parse and append filter parameters
|
|
997
1227
|
*
|
|
998
|
-
*
|
|
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
|
|
999
1233
|
*/
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1234
|
+
_parseFilters(state, options) {
|
|
1235
|
+
const keys = Object.keys(state.filters);
|
|
1236
|
+
if (!keys.length) {
|
|
1237
|
+
return this._uri;
|
|
1238
|
+
}
|
|
1239
|
+
const f = {
|
|
1240
|
+
[`${options.filters}`]: keys.reduce((acc, key) => {
|
|
1241
|
+
return Object.assign(acc, { [key]: state.filters[key].join(',') });
|
|
1242
|
+
}, {})
|
|
1243
|
+
};
|
|
1244
|
+
const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
|
|
1245
|
+
this._uri += param;
|
|
1246
|
+
return param;
|
|
1003
1247
|
}
|
|
1004
1248
|
/**
|
|
1005
|
-
*
|
|
1249
|
+
* Parse and append include parameters
|
|
1006
1250
|
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
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
|
|
1009
1256
|
*/
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1257
|
+
_parseIncludes(state, options) {
|
|
1258
|
+
if (!state.includes.length) {
|
|
1259
|
+
return this._uri;
|
|
1260
|
+
}
|
|
1261
|
+
const param = `${this._prepend(state)}${options.includes}=${state.includes}`;
|
|
1262
|
+
this._uri += param;
|
|
1263
|
+
return param;
|
|
1013
1264
|
}
|
|
1014
1265
|
/**
|
|
1015
|
-
*
|
|
1266
|
+
* Parse and append pagination parameters in JSON:API bracket notation
|
|
1016
1267
|
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
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
|
|
1019
1273
|
*/
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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}`;
|
|
1277
|
+
this._uri += param;
|
|
1278
|
+
return param;
|
|
1023
1279
|
}
|
|
1024
1280
|
/**
|
|
1025
|
-
*
|
|
1281
|
+
* Parse and append sort parameters
|
|
1026
1282
|
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
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
|
|
1029
1288
|
*/
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1289
|
+
_parseSort(state, options) {
|
|
1290
|
+
let param = '';
|
|
1291
|
+
if (!state.sorts.length) {
|
|
1292
|
+
return param;
|
|
1293
|
+
}
|
|
1294
|
+
param = `${this._prepend(state)}${options.sort}=`;
|
|
1295
|
+
state.sorts.forEach((sort, idx) => {
|
|
1296
|
+
param += `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`;
|
|
1297
|
+
if (idx < state.sorts.length - 1) {
|
|
1298
|
+
param += ',';
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
this._uri += param;
|
|
1302
|
+
return param;
|
|
1033
1303
|
}
|
|
1034
1304
|
/**
|
|
1035
|
-
*
|
|
1305
|
+
* Determine the appropriate URI prefix based on the current accumulator state
|
|
1036
1306
|
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
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
|
|
1039
1312
|
*/
|
|
1040
|
-
|
|
1041
|
-
this.
|
|
1042
|
-
|
|
1313
|
+
_prepend(state) {
|
|
1314
|
+
if (this._uri) {
|
|
1315
|
+
return '&';
|
|
1316
|
+
}
|
|
1317
|
+
return state.baseUrl ? `${state.baseUrl}/${state.resource}?` : `/${state.resource}?`;
|
|
1043
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 {
|
|
1044
1348
|
/**
|
|
1045
|
-
*
|
|
1349
|
+
* Parse a JSON:API pagination response into a PaginatedCollection
|
|
1046
1350
|
*
|
|
1047
|
-
*
|
|
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.
|
|
1048
1354
|
*
|
|
1049
|
-
* @param
|
|
1050
|
-
* @
|
|
1051
|
-
* @
|
|
1355
|
+
* @param response - The raw API response object
|
|
1356
|
+
* @param options - The response key name configuration
|
|
1357
|
+
* @returns A typed PaginatedCollection instance
|
|
1052
1358
|
*/
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
this.
|
|
1056
|
-
|
|
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);
|
|
1057
1374
|
}
|
|
1058
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService, deps: [{ token: NestService }, { token: 'REQUEST_STRATEGY' }, { token: 'DRIVER' }, { token: 'QUERY_PARAMS_CONFIG', optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1059
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService });
|
|
1060
|
-
}
|
|
1061
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService, decorators: [{
|
|
1062
|
-
type: Injectable
|
|
1063
|
-
}], ctorParameters: () => [{ type: NestService }, { type: undefined, decorators: [{
|
|
1064
|
-
type: Inject,
|
|
1065
|
-
args: ['REQUEST_STRATEGY']
|
|
1066
|
-
}] }, { type: DriverEnum, decorators: [{
|
|
1067
|
-
type: Inject,
|
|
1068
|
-
args: ['DRIVER']
|
|
1069
|
-
}] }, { type: undefined, decorators: [{
|
|
1070
|
-
type: Inject,
|
|
1071
|
-
args: ['QUERY_PARAMS_CONFIG']
|
|
1072
|
-
}, {
|
|
1073
|
-
type: Optional
|
|
1074
|
-
}] }] });
|
|
1075
|
-
|
|
1076
|
-
class PaginationService {
|
|
1077
1375
|
/**
|
|
1078
|
-
*
|
|
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
|
|
1079
1383
|
*/
|
|
1080
|
-
|
|
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
|
+
}
|
|
1081
1388
|
/**
|
|
1082
|
-
*
|
|
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
|
|
1083
1400
|
*/
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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;
|
|
1088
1411
|
}
|
|
1089
1412
|
/**
|
|
1090
|
-
*
|
|
1413
|
+
* Resolve the "to" index value
|
|
1091
1414
|
*
|
|
1092
|
-
*
|
|
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)`
|
|
1093
1418
|
*
|
|
1094
|
-
* @param response - The raw
|
|
1095
|
-
* @
|
|
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
|
|
1096
1425
|
*/
|
|
1097
1426
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1098
|
-
|
|
1099
|
-
|
|
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;
|
|
1100
1436
|
}
|
|
1101
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: PaginationService, deps: [{ token: 'RESPONSE_STRATEGY' }, { token: 'RESPONSE_OPTIONS', optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1102
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: PaginationService });
|
|
1103
1437
|
}
|
|
1104
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: PaginationService, decorators: [{
|
|
1105
|
-
type: Injectable
|
|
1106
|
-
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
1107
|
-
type: Inject,
|
|
1108
|
-
args: ['RESPONSE_STRATEGY']
|
|
1109
|
-
}] }, { type: undefined, decorators: [{
|
|
1110
|
-
type: Inject,
|
|
1111
|
-
args: ['RESPONSE_OPTIONS']
|
|
1112
|
-
}, {
|
|
1113
|
-
type: Optional
|
|
1114
|
-
}] }] });
|
|
1115
1438
|
|
|
1116
1439
|
/**
|
|
1117
1440
|
* Request strategy for the Laravel (pagination-only) driver
|
|
@@ -1137,6 +1460,21 @@ class LaravelRequestStrategy {
|
|
|
1137
1460
|
const base = state.baseUrl ? `${state.baseUrl}/${state.resource}` : `/${state.resource}`;
|
|
1138
1461
|
return `${base}?${options.limit}=${state.limit}&${options.page}=${state.page}`;
|
|
1139
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
|
+
}
|
|
1140
1478
|
}
|
|
1141
1479
|
|
|
1142
1480
|
/**
|
|
@@ -1169,12 +1507,6 @@ class LaravelResponseStrategy {
|
|
|
1169
1507
|
}
|
|
1170
1508
|
}
|
|
1171
1509
|
|
|
1172
|
-
var SortEnum;
|
|
1173
|
-
(function (SortEnum) {
|
|
1174
|
-
SortEnum["ASC"] = "asc";
|
|
1175
|
-
SortEnum["DESC"] = "desc";
|
|
1176
|
-
})(SortEnum || (SortEnum = {}));
|
|
1177
|
-
|
|
1178
1510
|
/**
|
|
1179
1511
|
* Request strategy for the NestJS (nestjs-paginate) driver
|
|
1180
1512
|
*
|
|
@@ -1215,6 +1547,21 @@ class NestjsRequestStrategy {
|
|
|
1215
1547
|
this._parsePage(state, options);
|
|
1216
1548
|
return this._uri;
|
|
1217
1549
|
}
|
|
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
|
+
}
|
|
1218
1565
|
/**
|
|
1219
1566
|
* Parse and append simple filter parameters
|
|
1220
1567
|
*
|
|
@@ -1454,12 +1801,6 @@ class NestjsResponseStrategy {
|
|
|
1454
1801
|
}
|
|
1455
1802
|
}
|
|
1456
1803
|
|
|
1457
|
-
class UnselectableModelError extends Error {
|
|
1458
|
-
constructor(model) {
|
|
1459
|
-
super(`Unselectable Model: the selected model (${model}) is not present neither in the "model" property, nor in the includes object.`);
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
1804
|
/**
|
|
1464
1805
|
* Request strategy for the Spatie Query Builder driver
|
|
1465
1806
|
*
|
|
@@ -1498,6 +1839,21 @@ class SpatieRequestStrategy {
|
|
|
1498
1839
|
this._parseSort(state, options);
|
|
1499
1840
|
return this._uri;
|
|
1500
1841
|
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Validate that the given limit is accepted by the Spatie driver
|
|
1844
|
+
*
|
|
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
|
|
1850
|
+
*/
|
|
1851
|
+
validateLimit(limit) {
|
|
1852
|
+
if (Number.isInteger(limit) && limit >= 1) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
throw new InvalidLimitError(limit);
|
|
1856
|
+
}
|
|
1501
1857
|
/**
|
|
1502
1858
|
* Parse and append field selection parameters
|
|
1503
1859
|
*
|
|
@@ -1678,6 +2034,8 @@ class SpatieResponseStrategy {
|
|
|
1678
2034
|
*/
|
|
1679
2035
|
function resolveRequestStrategy$1(driver) {
|
|
1680
2036
|
switch (driver) {
|
|
2037
|
+
case DriverEnum.JSON_API:
|
|
2038
|
+
return new JsonApiRequestStrategy();
|
|
1681
2039
|
case DriverEnum.NESTJS:
|
|
1682
2040
|
return new NestjsRequestStrategy();
|
|
1683
2041
|
case DriverEnum.SPATIE:
|
|
@@ -1694,6 +2052,8 @@ function resolveRequestStrategy$1(driver) {
|
|
|
1694
2052
|
*/
|
|
1695
2053
|
function resolveResponseStrategy$1(driver) {
|
|
1696
2054
|
switch (driver) {
|
|
2055
|
+
case DriverEnum.JSON_API:
|
|
2056
|
+
return new JsonApiResponseStrategy();
|
|
1697
2057
|
case DriverEnum.NESTJS:
|
|
1698
2058
|
return new NestjsResponseStrategy();
|
|
1699
2059
|
case DriverEnum.SPATIE:
|
|
@@ -1727,6 +2087,9 @@ class NgQubeeModule {
|
|
|
1727
2087
|
provide: PaginationService,
|
|
1728
2088
|
useFactory: () => {
|
|
1729
2089
|
const responseConfig = Object.assign({}, config.response);
|
|
2090
|
+
if (driver === DriverEnum.JSON_API) {
|
|
2091
|
+
return new PaginationService(responseStrategy, new JsonApiResponseOptions(responseConfig));
|
|
2092
|
+
}
|
|
1730
2093
|
return driver === DriverEnum.NESTJS
|
|
1731
2094
|
? new PaginationService(responseStrategy, new NestjsResponseOptions(responseConfig))
|
|
1732
2095
|
: new PaginationService(responseStrategy, responseConfig);
|
|
@@ -1752,6 +2115,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImpor
|
|
|
1752
2115
|
*/
|
|
1753
2116
|
function resolveRequestStrategy(driver) {
|
|
1754
2117
|
switch (driver) {
|
|
2118
|
+
case DriverEnum.JSON_API:
|
|
2119
|
+
return new JsonApiRequestStrategy();
|
|
1755
2120
|
case DriverEnum.NESTJS:
|
|
1756
2121
|
return new NestjsRequestStrategy();
|
|
1757
2122
|
case DriverEnum.SPATIE:
|
|
@@ -1768,6 +2133,8 @@ function resolveRequestStrategy(driver) {
|
|
|
1768
2133
|
*/
|
|
1769
2134
|
function resolveResponseStrategy(driver) {
|
|
1770
2135
|
switch (driver) {
|
|
2136
|
+
case DriverEnum.JSON_API:
|
|
2137
|
+
return new JsonApiResponseStrategy();
|
|
1771
2138
|
case DriverEnum.NESTJS:
|
|
1772
2139
|
return new NestjsResponseStrategy();
|
|
1773
2140
|
case DriverEnum.SPATIE:
|
|
@@ -1797,6 +2164,15 @@ function resolveResponseStrategy(driver) {
|
|
|
1797
2164
|
* });
|
|
1798
2165
|
* ```
|
|
1799
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
|
+
*
|
|
1800
2176
|
* NestJS driver example:
|
|
1801
2177
|
* ```
|
|
1802
2178
|
* import { DriverEnum } from 'ng-qubee';
|
|
@@ -1828,6 +2204,9 @@ function provideNgQubee(config) {
|
|
|
1828
2204
|
provide: PaginationService,
|
|
1829
2205
|
useFactory: () => {
|
|
1830
2206
|
const responseConfig = Object.assign({}, config.response);
|
|
2207
|
+
if (driver === DriverEnum.JSON_API) {
|
|
2208
|
+
return new PaginationService(responseStrategy, new JsonApiResponseOptions(responseConfig));
|
|
2209
|
+
}
|
|
1831
2210
|
return driver === DriverEnum.NESTJS
|
|
1832
2211
|
? new PaginationService(responseStrategy, new NestjsResponseOptions(responseConfig))
|
|
1833
2212
|
: new PaginationService(responseStrategy, responseConfig);
|
|
@@ -1868,5 +2247,5 @@ var FilterOperatorEnum;
|
|
|
1868
2247
|
* Generated bundle index. Do not edit.
|
|
1869
2248
|
*/
|
|
1870
2249
|
|
|
1871
|
-
export { DriverEnum, FilterOperatorEnum, InvalidLimitError, InvalidPageNumberError, InvalidResourceNameError, KeyNotFoundError, LaravelRequestStrategy, LaravelResponseStrategy, NestjsRequestStrategy, NestjsResponseStrategy, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationService, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, 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 };
|
|
1872
2251
|
//# sourceMappingURL=ng-qubee.mjs.map
|