ng-qubee 3.0.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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ NgQubee is a Query Builder for Angular. Easily compose your API requests without
15
15
  - Pagination ready
16
16
  - Reactive, as the results are emitted with a RxJS Observable
17
17
  - Developed with a test-driven approach
18
- - **Multi-driver support**: Laravel (pagination-only), Spatie Query Builder, and NestJS (nestjs-paginate)
18
+ - **Multi-driver support**: JSON:API, Laravel (pagination-only), Spatie Query Builder, and NestJS (nestjs-paginate)
19
19
 
20
20
  ## We love it, we use it ❤️
21
21
  NgQubee uses some open source projects to work properly:
@@ -41,16 +41,46 @@ npm i ng-qubee
41
41
 
42
42
  ## Drivers
43
43
 
44
- NgQubee supports three drivers out of the box. A driver **must** be specified in the configuration:
44
+ NgQubee supports four drivers out of the box. A driver **must** be specified in the configuration:
45
45
 
46
46
  | Driver | Backend | Request Format | Response Format |
47
47
  |---|---|---|---|
48
+ | **JSON:API** | Any JSON:API-compliant backend | `filter[field]=value`, `sort=-field`, `page[number]=N&page[size]=N` | Nested: `{ data, meta: {...}, links: {...} }` |
48
49
  | **Laravel** | Plain Laravel pagination | `limit=N&page=N` (pagination only) | Flat: `{ data, current_page, total, ... }` |
49
50
  | **Spatie** | Spatie Query Builder | `filter[field]=value`, `sort=-field` | Flat: `{ data, current_page, total, ... }` |
50
51
  | **NestJS** | nestjs-paginate | `filter.field=$operator:value`, `sortBy=field:DESC` | Nested: `{ data, meta: {...}, links: {...} }` |
51
52
 
52
53
  ## Usage
53
54
 
55
+ ### JSON:API Driver
56
+
57
+ The JSON:API driver generates URIs compatible with any [JSON:API](https://jsonapi.org/format/)-compliant backend (Rails, Django, .NET, Java, Elixir, etc.):
58
+
59
+ ```typescript
60
+ import { DriverEnum } from 'ng-qubee';
61
+
62
+ // Standalone approach
63
+ bootstrapApplication(AppComponent, {
64
+ providers: [provideNgQubee({ driver: DriverEnum.JSON_API })]
65
+ });
66
+
67
+ // Module approach
68
+ @NgModule({
69
+ imports: [
70
+ NgQubeeModule.forRoot({ driver: DriverEnum.JSON_API })
71
+ ]
72
+ })
73
+ export class AppModule {}
74
+ ```
75
+
76
+ The JSON:API driver supports:
77
+
78
+ - **Filters** are composed as `filter[field]=value`
79
+ - **Fields** are composed as `fields[type]=col1,col2`
80
+ - **Includes** are composed as `include=author,comments.author`
81
+ - **Sort** is composed as `sort=-created_at,name` (`-` prefix = DESC)
82
+ - **Pagination** uses bracket notation: `page[number]=1&page[size]=15`
83
+
54
84
  ### Laravel Driver (pagination-only)
55
85
 
56
86
  The Laravel driver provides basic pagination — limit and page parameters only. No filters, sorts, fields, or includes are supported.
@@ -146,6 +176,27 @@ The NestJS driver generates URIs compatible with [nestjs-paginate](https://githu
146
176
  - **Limit** is composed as `limit=15`
147
177
  - **Page** is composed as `page=1`
148
178
 
179
+ ### Per-component instances
180
+
181
+ By default, `provideNgQubee()` / `NgQubeeModule.forRoot()` register `NgQubeeService` at the environment injector, so every component that injects it shares the same query-builder state. If you need a dedicated instance — e.g. a feature component whose filters and pagination must not bleed into the app-wide one — spread `provideNgQubeeInstance()` into the component's `providers`:
182
+
183
+ ```typescript
184
+ import { Component } from '@angular/core';
185
+ import { NgQubeeService, provideNgQubeeInstance } from 'ng-qubee';
186
+
187
+ @Component({
188
+ selector: 'app-product-list',
189
+ standalone: true,
190
+ providers: [...provideNgQubeeInstance()],
191
+ template: '...'
192
+ })
193
+ export class ProductListComponent {
194
+ constructor(private _qb: NgQubeeService) {}
195
+ }
196
+ ```
197
+
198
+ The component gets its own `NgQubeeService`, `NestService`, and `PaginationService`. The driver, strategies, and options are inherited from the environment injector configured by `provideNgQubee()` — you still configure the library once at bootstrap.
199
+
149
200
  ## Query Builder API
150
201
 
151
202
  For composing queries, the first step is to inject the proper NgQubeeService:
@@ -166,7 +217,7 @@ this._ngQubeeService.setResource('users');
166
217
  This is necessary to build the prefix of the URI (/users)
167
218
 
168
219
 
169
- ### Fields (Spatie only)
220
+ ### Fields (JSON:API + Spatie)
170
221
  Fields can be selected as following:
171
222
 
172
223
  ```typescript
@@ -184,7 +235,7 @@ this._ngQubeeService.addSelect('id', 'name', 'email');
184
235
 
185
236
  Will output _/users?select=id,name,email_
186
237
 
187
- ### Filters (Spatie + NestJS)
238
+ ### Filters (JSON:API + Spatie + NestJS)
188
239
  Filters are applied as following:
189
240
 
190
241
  ```typescript
@@ -234,7 +285,7 @@ this._ngQubeeService.addFilterOperator('name', FilterOperatorEnum.ILIKE, 'john')
234
285
 
235
286
  **Available operators:** `$eq`, `$not`, `$null`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, `$btw`, `$ilike`, `$sw`, `$contains`
236
287
 
237
- ### Includes (Spatie only)
288
+ ### Includes (JSON:API + Spatie)
238
289
  Ask to include related models with:
239
290
 
240
291
  ```typescript
@@ -252,7 +303,7 @@ this._ngQubeeService.setSearch('john doe');
252
303
 
253
304
  Will output _/users?search=john doe_
254
305
 
255
- ### Sort (Spatie + NestJS)
306
+ ### Sort (JSON:API + Spatie + NestJS)
256
307
  Sort elements as following:
257
308
 
258
309
  ```typescript
@@ -285,6 +336,116 @@ Default values are automatically added to the query:
285
336
 
286
337
  Always expect your query to include _limit=15&page=1_
287
338
 
339
+ #### Fetch all (NestJS only)
340
+
341
+ When the active driver is NestJS, `setLimit(-1)` is accepted as a "fetch all items" sentinel, following the [nestjs-paginate](https://github.com/ppetzold/nestjs-paginate) convention (the server must opt in via `maxLimit: -1`):
342
+
343
+ ```typescript
344
+ // NestJS driver only
345
+ this._ngQubeeService.setLimit(-1);
346
+ ```
347
+
348
+ JSON:API, Laravel, and Spatie drivers reject `-1` and throw `InvalidLimitError`.
349
+
350
+ #### Limit validation
351
+
352
+ Limit validation is driver-scoped — each request strategy enforces its own accepted range and invalid values throw `InvalidLimitError` immediately when passed to `setLimit()`:
353
+
354
+ | Driver | Accepted limit values |
355
+ |---|---|
356
+ | NestJS | integer `-1` (fetch all) or `>= 1` |
357
+ | JSON:API / Laravel / Spatie | integer `>= 1` |
358
+
359
+ Non-integer values, zero, negative numbers (other than `-1` for NestJS), `NaN`, and `Infinity` are all rejected.
360
+
361
+ #### Auto-reset of page on result-set-changing mutations
362
+
363
+ Any mutation that changes *which records* the server would return also resets `state.page` to `1` automatically. Staying on page 5 of an old result set after changing filters is almost always a bug, so the library makes the reset explicit:
364
+
365
+ | Resets page to 1 | Does NOT reset page |
366
+ |---|---|
367
+ | `setLimit()` | `setBaseUrl()` |
368
+ | `setResource()` | `setPage()` |
369
+ | `setSearch()` / `deleteSearch()` | `addFields()` / `deleteFields()` / `deleteFieldsByModel()` |
370
+ | `addFilter()` / `deleteFilters()` | `addIncludes()` / `deleteIncludes()` |
371
+ | `addFilterOperator()` / `deleteOperatorFilters()` | `addSelect()` / `deleteSelect()` |
372
+ | `addSort()` / `deleteSorts()` | |
373
+
374
+ Rule of thumb: if a mutation changes the record *set* (filters, sort, search, limit, resource), page resets. If it only changes the record *shape* (fields, includes, select), page stays. If you intentionally want to keep the previous page number, call `setPage(n)` again after the mutation.
375
+
376
+ ### Pagination navigation
377
+
378
+ The service exposes a fluent navigation surface so you can wire a standard paginator UI (Prev / N of M / Next) with no manual bookkeeping. All navigation methods return `this` and can be chained.
379
+
380
+ ```typescript
381
+ this._ngQubeeService.nextPage().generateUri().subscribe(uri => /* fire the request */);
382
+ this._ngQubeeService.previousPage();
383
+ this._ngQubeeService.firstPage();
384
+ this._ngQubeeService.lastPage();
385
+ this._ngQubeeService.goToPage(3);
386
+ ```
387
+
388
+ #### Auto-sync from `PaginationService.paginate()`
389
+
390
+ When you hand a paginated response to `PaginationService.paginate()`, the library automatically copies the response's `page` and `lastPage` back into the query-builder state. That means `lastPage()`, `goToPage(n)` bounds checks, and the predicates below become accurate immediately — you don't thread the collection's `lastPage` back in yourself.
391
+
392
+ ```typescript
393
+ this._paginationService.paginate(response); // auto-writes page + lastPage
394
+ this._ngQubeeService.lastPage(); // now safe; jumps to the last page
395
+ ```
396
+
397
+ The auto-sync only flips `isLastPageKnown` to `true` when the response carries a **positive integer** `lastPage`. Server-emitted `0` (empty collection) and absent fields leave the flag `false` — the helpers fall back to their conservative defaults.
398
+
399
+ #### Predicates and accessors
400
+
401
+ Template-safe methods for driving button disable-states and labels:
402
+
403
+ ```typescript
404
+ qb.isFirstPage(); // true on page 1
405
+ qb.isLastPage(); // true only when bounds known and page === lastPage
406
+ qb.hasNextPage(); // true when bounds unknown, or page < lastPage
407
+ qb.hasPreviousPage(); // true when page > 1
408
+ qb.currentPage(); // state.page (always safe)
409
+ qb.totalPages(); // state.lastPage (throws if never synced)
410
+ ```
411
+
412
+ Angular template wiring:
413
+
414
+ ```html
415
+ <button [disabled]="qb.isFirstPage()" (click)="qb.previousPage()">Prev</button>
416
+ <span>Page {{ qb.currentPage() }} of {{ qb.totalPages() }}</span>
417
+ <button [disabled]="qb.isLastPage()" (click)="qb.nextPage()">Next</button>
418
+ ```
419
+
420
+ For the `qb.totalPages()` template usage above, either call `paginate()` at least once before the template renders, or guard the display with `*ngIf="qb.hasNextPage() || !qb.isFirstPage()"` / by reading `qb.nest().isLastPageKnown` directly.
421
+
422
+ #### Error behavior
423
+
424
+ | Helper | Throws | When |
425
+ |---|---|---|
426
+ | `nextPage()` | — | Never throws. No-op when already at `lastPage` (bounds known). |
427
+ | `previousPage()` | — | Never throws. No-op at page 1. |
428
+ | `firstPage()` | — | Never throws. |
429
+ | `lastPage()` | `PaginationNotSyncedError` | `paginate()` has never run (`state.isLastPageKnown` is `false`). |
430
+ | `goToPage(n)` | `InvalidPageNumberError` | `n` is not a positive integer, or exceeds `lastPage` when bounds are known. |
431
+ | `isFirstPage()` / `isLastPage()` / `hasNextPage()` / `hasPreviousPage()` | — | Template-safe. Conservative defaults when bounds unknown. |
432
+ | `currentPage()` | — | Always safe. |
433
+ | `totalPages()` | `PaginationNotSyncedError` | `paginate()` has never run. Guard with `nest().isLastPageKnown` for a non-throwing check. |
434
+
435
+ #### Guarding the imperative "jump to last" button
436
+
437
+ `lastPage()` and `totalPages()` need a synced response. A safe pattern:
438
+
439
+ ```html
440
+ <button
441
+ [disabled]="!qb.nest().isLastPageKnown || qb.isLastPage()"
442
+ (click)="qb.lastPage()">
443
+ Last
444
+ </button>
445
+ ```
446
+
447
+ The `isLastPageKnown` read short-circuits before `qb.lastPage()` could throw.
448
+
288
449
  ### Retrieving data
289
450
  URI is generated invoking the _generateUri_ method of the NgQubeeService. An observable is returned and the URI will be emitted:
290
451
 
@@ -297,11 +458,11 @@ this._ngQubeeService.generateUri().subscribe(uri => console.log(uri));
297
458
  All query features have corresponding delete methods:
298
459
 
299
460
  ```typescript
300
- // Spatie + NestJS
461
+ // JSON:API + Spatie + NestJS
301
462
  this._ngQubeeService.deleteFilters('status', 'role');
302
463
  this._ngQubeeService.deleteSorts('created_at');
303
464
 
304
- // Spatie only
465
+ // JSON:API + Spatie only
305
466
  this._ngQubeeService.deleteFields({ users: ['email'] });
306
467
  this._ngQubeeService.deleteFieldsByModel('users', 'email');
307
468
  this._ngQubeeService.deleteIncludes('profile');
@@ -323,15 +484,15 @@ this._ngQubeeService.reset();
323
484
 
324
485
  Calling a method that is not supported by the active driver throws a descriptive error immediately:
325
486
 
326
- | Method | Laravel | Spatie | NestJS |
327
- |---|---|---|---|
328
- | `addFilter()` / `deleteFilters()` | throws `UnsupportedFilterError` | supported | supported |
329
- | `addSort()` / `deleteSorts()` | throws `UnsupportedSortError` | supported | supported |
330
- | `addFields()` / `deleteFields()` / `deleteFieldsByModel()` | throws `UnsupportedFieldSelectionError` | supported | throws `UnsupportedFieldSelectionError` |
331
- | `addIncludes()` / `deleteIncludes()` | throws `UnsupportedIncludesError` | supported | throws `UnsupportedIncludesError` |
332
- | `addFilterOperator()` / `deleteOperatorFilters()` | throws `UnsupportedFilterOperatorError` | throws `UnsupportedFilterOperatorError` | supported |
333
- | `addSelect()` / `deleteSelect()` | throws `UnsupportedSelectError` | throws `UnsupportedSelectError` | supported |
334
- | `setSearch()` / `deleteSearch()` | throws `UnsupportedSearchError` | throws `UnsupportedSearchError` | supported |
487
+ | Method | JSON:API | Laravel | Spatie | NestJS |
488
+ |---|---|---|---|---|
489
+ | `addFilter()` / `deleteFilters()` | supported | throws `UnsupportedFilterError` | supported | supported |
490
+ | `addSort()` / `deleteSorts()` | supported | throws `UnsupportedSortError` | supported | supported |
491
+ | `addFields()` / `deleteFields()` / `deleteFieldsByModel()` | supported | throws `UnsupportedFieldSelectionError` | supported | throws `UnsupportedFieldSelectionError` |
492
+ | `addIncludes()` / `deleteIncludes()` | supported | throws `UnsupportedIncludesError` | supported | throws `UnsupportedIncludesError` |
493
+ | `addFilterOperator()` / `deleteOperatorFilters()` | throws `UnsupportedFilterOperatorError` | throws `UnsupportedFilterOperatorError` | throws `UnsupportedFilterOperatorError` | supported |
494
+ | `addSelect()` / `deleteSelect()` | throws `UnsupportedSelectError` | throws `UnsupportedSelectError` | throws `UnsupportedSelectError` | supported |
495
+ | `setSearch()` / `deleteSearch()` | throws `UnsupportedSearchError` | throws `UnsupportedSearchError` | throws `UnsupportedSearchError` | supported |
335
496
 
336
497
  ## Pagination
337
498
  If you are working with an API that supports pagination, we have got you covered 😉 NgQubee provides:
@@ -368,6 +529,30 @@ When using the Laravel or Spatie driver, the paginated collection will check for
368
529
  - first_page_url - URL to the first page
369
530
  - last_page_url - URL to the last page
370
531
 
532
+ ### JSON:API Response Format
533
+
534
+ When using the JSON:API driver, the PaginationService automatically parses nested responses:
535
+
536
+ ```json
537
+ {
538
+ "data": [...],
539
+ "meta": {
540
+ "current-page": 1,
541
+ "per-page": 10,
542
+ "total": 100,
543
+ "page-count": 10
544
+ },
545
+ "links": {
546
+ "first": "http://api.com/articles?page[number]=1&page[size]=10",
547
+ "prev": null,
548
+ "next": "http://api.com/articles?page[number]=2&page[size]=10",
549
+ "last": "http://api.com/articles?page[number]=10&page[size]=10"
550
+ }
551
+ }
552
+ ```
553
+
554
+ The `from` and `to` values are computed automatically from `current-page` and `per-page` when not present in the response. JSON:API meta key names vary by implementation; defaults can be fully customised via response configuration.
555
+
371
556
  ### NestJS Response Format
372
557
 
373
558
  When using the NestJS driver, the PaginationService automatically parses nested responses:
@@ -426,6 +611,7 @@ NgQubee is fully typed and exports all public interfaces, enums, and types for T
426
611
  import { DriverEnum, FilterOperatorEnum, SortEnum } from 'ng-qubee';
427
612
 
428
613
  // Driver options
614
+ DriverEnum.JSON_API // 'json-api'
429
615
  DriverEnum.LARAVEL // 'laravel' (pagination only)
430
616
  DriverEnum.SPATIE // 'spatie'
431
617
  DriverEnum.NESTJS // 'nestjs'