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 +203 -17
- package/fesm2022/ng-qubee.mjs +1035 -398
- package/fesm2022/ng-qubee.mjs.map +1 -1
- package/package.json +1 -2
- package/types/ng-qubee.d.ts +487 -29
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
|
|
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
|
|
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
|
|
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'
|