ng-qubee 3.1.0 → 3.3.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 +225 -1
- package/fesm2022/ng-qubee.mjs +2180 -1579
- package/fesm2022/ng-qubee.mjs.map +1 -1
- package/package.json +4 -3
- package/types/ng-qubee.d.ts +835 -265
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ npm i ng-qubee
|
|
|
41
41
|
|
|
42
42
|
## Drivers
|
|
43
43
|
|
|
44
|
-
NgQubee supports
|
|
44
|
+
NgQubee supports five 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
|
|---|---|---|---|
|
|
@@ -49,6 +49,7 @@ NgQubee supports four drivers out of the box. A driver **must** be specified in
|
|
|
49
49
|
| **Laravel** | Plain Laravel pagination | `limit=N&page=N` (pagination only) | Flat: `{ data, current_page, total, ... }` |
|
|
50
50
|
| **Spatie** | Spatie Query Builder | `filter[field]=value`, `sort=-field` | Flat: `{ data, current_page, total, ... }` |
|
|
51
51
|
| **NestJS** | nestjs-paginate | `filter.field=$operator:value`, `sortBy=field:DESC` | Nested: `{ data, meta: {...}, links: {...} }` |
|
|
52
|
+
| **PostgREST** | PostgREST / Supabase | `col=eq.value`, `order=col.asc`, `limit=N&offset=M` | Bare array body + `Content-Range` header for total |
|
|
52
53
|
|
|
53
54
|
## Usage
|
|
54
55
|
|
|
@@ -176,6 +177,141 @@ The NestJS driver generates URIs compatible with [nestjs-paginate](https://githu
|
|
|
176
177
|
- **Limit** is composed as `limit=15`
|
|
177
178
|
- **Page** is composed as `page=1`
|
|
178
179
|
|
|
180
|
+
### PostgREST / Supabase Driver
|
|
181
|
+
|
|
182
|
+
The PostgREST driver generates URIs compatible with [PostgREST](https://postgrest.org/) and anything built on top of it (notably [Supabase](https://supabase.com/)):
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { DriverEnum } from 'ng-qubee';
|
|
186
|
+
|
|
187
|
+
// Standalone approach
|
|
188
|
+
bootstrapApplication(AppComponent, {
|
|
189
|
+
providers: [provideNgQubee({ driver: DriverEnum.POSTGREST })]
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Module approach
|
|
193
|
+
@NgModule({
|
|
194
|
+
imports: [
|
|
195
|
+
NgQubeeModule.forRoot({ driver: DriverEnum.POSTGREST })
|
|
196
|
+
]
|
|
197
|
+
})
|
|
198
|
+
export class AppModule {}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The PostgREST driver supports:
|
|
202
|
+
|
|
203
|
+
- **Filters** (single value) are composed as `col=eq.value`
|
|
204
|
+
- **Filters** (multi-value) are composed as `col=in.(v1,v2,v3)` — PostgREST's native IN-list syntax
|
|
205
|
+
- **Operator filters** cover the full `FilterOperatorEnum` (eq, gt, gte, lt, lte, ilike, in, not, null, btw, sw, contains) plus PostgREST-native full-text search (`fts`, `plfts`, `phfts`, `wfts`) — see the Operator filters section below
|
|
206
|
+
- **Sorts** are composed as `order=col1.asc,col2.desc`
|
|
207
|
+
- **Select** is composed as `select=col1,col2`
|
|
208
|
+
- **Pagination** defaults to `limit=N&offset=M` on the URL and can optionally move to `Range-Unit` + `Range` HTTP headers — see the RANGE-header pagination section below
|
|
209
|
+
|
|
210
|
+
#### Reading totals from the `Content-Range` header
|
|
211
|
+
|
|
212
|
+
PostgREST returns a bare array body and reports the total row count in the `Content-Range` HTTP response header (e.g. `0-9/50`). Opt in by sending `Prefer: count=exact` on the request, then pass the headers to `paginate()`:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
this._http.get<User[]>(uri, {
|
|
216
|
+
observe: 'response',
|
|
217
|
+
headers: { 'Prefer': 'count=exact' }
|
|
218
|
+
}).subscribe(response => {
|
|
219
|
+
const collection = this._paginationService.paginate(response.body, response.headers);
|
|
220
|
+
// collection.total, collection.page, collection.lastPage are populated
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The second argument to `paginate()` accepts Angular's `HttpHeaders`, the native `Headers` class, or a plain `Record<string, string>` — whatever shape your HTTP client emits. If you omit the header (or the server returns `Content-Range: 0-9/*`), the collection still populates `data` and `page` but leaves `total` / `lastPage` undefined — the pagination helpers' conservative defaults then kick in (`hasNextPage()` returns `true`, `isLastPage()` returns `false`).
|
|
225
|
+
|
|
226
|
+
#### Operator filters
|
|
227
|
+
|
|
228
|
+
Every `FilterOperatorEnum` value maps to a PostgREST prefix operator. Call `addFilterOperator(column, operator, ...values)` and the strategy emits the correct wire format:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
qb.addFilterOperator('age', FilterOperatorEnum.GTE, 18); // age=gte.18
|
|
232
|
+
qb.addFilterOperator('id', FilterOperatorEnum.IN, 1, 2, 3); // id=in.(1,2,3)
|
|
233
|
+
qb.addFilterOperator('status', FilterOperatorEnum.NOT, 'deleted'); // status=not.eq.deleted
|
|
234
|
+
qb.addFilterOperator('id', FilterOperatorEnum.NOT, 1, 2); // id=not.in.(1,2)
|
|
235
|
+
qb.addFilterOperator('deletedAt', FilterOperatorEnum.NULL, true); // deletedAt=is.null
|
|
236
|
+
qb.addFilterOperator('deletedAt', FilterOperatorEnum.NULL, false); // deletedAt=is.not.null
|
|
237
|
+
qb.addFilterOperator('price', FilterOperatorEnum.BTW, 10, 100); // price=gte.10&price=lte.100
|
|
238
|
+
qb.addFilterOperator('email', FilterOperatorEnum.SW, 'admin'); // email=like.admin*
|
|
239
|
+
qb.addFilterOperator('name', FilterOperatorEnum.CONTAINS, 'john'); // name=ilike.%john%
|
|
240
|
+
qb.addFilterOperator('description', FilterOperatorEnum.FTS, 'rat'); // description=fts.rat
|
|
241
|
+
qb.addFilterOperator('description', FilterOperatorEnum.PLFTS, 'a b'); // description=plfts.a b
|
|
242
|
+
qb.addFilterOperator('description', FilterOperatorEnum.PHFTS, 'a b'); // description=phfts.a b
|
|
243
|
+
qb.addFilterOperator('description', FilterOperatorEnum.WFTS, 'a -b'); // description=wfts.a -b
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Value shape rules enforced at call time (throw `InvalidFilterOperatorValueError`):
|
|
247
|
+
- `BTW` — exactly 2 values (`[min, max]`).
|
|
248
|
+
- `NULL` — exactly 1 boolean value (`true` → `IS NULL`, `false` → `IS NOT NULL`).
|
|
249
|
+
|
|
250
|
+
All other operators let PostgREST validate server-side.
|
|
251
|
+
|
|
252
|
+
The four `*FTS` operators are PostgREST's full-text search variants (`to_tsquery`, `plainto_tsquery`, `phraseto_tsquery`, `websearch_to_tsquery`). They're column-scoped — pick the column to search, pass the term. Language modifiers (`fts(english).term`) are not supported in this release.
|
|
253
|
+
|
|
254
|
+
#### RANGE-header pagination (alternative to limit/offset)
|
|
255
|
+
|
|
256
|
+
PostgREST also accepts pagination via the `Range` HTTP request header instead of URL query params. Opt in via `IConfig.pagination`:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { DriverEnum, PaginationModeEnum } from 'ng-qubee';
|
|
260
|
+
|
|
261
|
+
provideNgQubee({
|
|
262
|
+
driver: DriverEnum.POSTGREST,
|
|
263
|
+
pagination: PaginationModeEnum.RANGE
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
When `RANGE` is active, `generateUri()` omits `limit` and `offset` from the URL and `NgQubeeService.paginationHeaders()` returns the headers the consumer applies to the HTTP request:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
const uri = await firstValueFrom(qb.generateUri());
|
|
271
|
+
const extraHeaders = qb.paginationHeaders(); // { 'Range-Unit': 'items', 'Range': '0-9' }
|
|
272
|
+
|
|
273
|
+
this._http.get<User[]>(uri, {
|
|
274
|
+
observe: 'response',
|
|
275
|
+
headers: { 'Prefer': 'count=exact', ...extraHeaders }
|
|
276
|
+
}).subscribe(resp => this._pagination.paginate(resp.body, resp.headers));
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`paginationHeaders()` returns `null` for any driver that doesn't use header-based pagination — safe to spread into a headers map unconditionally with the nullish-coalescing pattern above.
|
|
280
|
+
|
|
281
|
+
#### Feature matrix
|
|
282
|
+
|
|
283
|
+
| Method | Supported? | Notes |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `addFilter` / `deleteFilters` | ✓ | Implicit `eq`; multi-value becomes `in.(...)` |
|
|
286
|
+
| `addFilterOperator` / `deleteOperatorFilters` | ✓ | All 16 operators including `FTS`/`PLFTS`/`PHFTS`/`WFTS` |
|
|
287
|
+
| `addSort` / `deleteSorts` | ✓ | Emits `order=col.asc,col.desc` |
|
|
288
|
+
| `addSelect` / `deleteSelect` | ✓ | Flat column selection |
|
|
289
|
+
| `setLimit` / `setPage` | ✓ | `offset` derived from `page` (QUERY mode) or emitted via `Range` header (RANGE mode) |
|
|
290
|
+
| `addFields` / `deleteFields` / `deleteFieldsByModel` | ✗ | Throws `UnsupportedFieldSelectionError`. Per-type field selection is a JSON:API/Spatie concept; use `addSelect` for PostgREST's column pruning. |
|
|
291
|
+
| `addIncludes` / `deleteIncludes` | ✗ | Throws `UnsupportedIncludesError`. PostgREST uses embedded resources via `select=col,rel(*)` — tracked as #66. |
|
|
292
|
+
| `setSearch` / `deleteSearch` | ✗ | Throws `UnsupportedSearchError`. PostgREST's FTS operators are per-column — use `addFilterOperator(col, FilterOperatorEnum.FTS, term)` instead. |
|
|
293
|
+
|
|
294
|
+
### Per-component instances
|
|
295
|
+
|
|
296
|
+
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`:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { Component } from '@angular/core';
|
|
300
|
+
import { NgQubeeService, provideNgQubeeInstance } from 'ng-qubee';
|
|
301
|
+
|
|
302
|
+
@Component({
|
|
303
|
+
selector: 'app-product-list',
|
|
304
|
+
standalone: true,
|
|
305
|
+
providers: [...provideNgQubeeInstance()],
|
|
306
|
+
template: '...'
|
|
307
|
+
})
|
|
308
|
+
export class ProductListComponent {
|
|
309
|
+
constructor(private _qb: NgQubeeService) {}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
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.
|
|
314
|
+
|
|
179
315
|
## Query Builder API
|
|
180
316
|
|
|
181
317
|
For composing queries, the first step is to inject the proper NgQubeeService:
|
|
@@ -337,6 +473,94 @@ Limit validation is driver-scoped — each request strategy enforces its own acc
|
|
|
337
473
|
|
|
338
474
|
Non-integer values, zero, negative numbers (other than `-1` for NestJS), `NaN`, and `Infinity` are all rejected.
|
|
339
475
|
|
|
476
|
+
#### Auto-reset of page on result-set-changing mutations
|
|
477
|
+
|
|
478
|
+
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:
|
|
479
|
+
|
|
480
|
+
| Resets page to 1 | Does NOT reset page |
|
|
481
|
+
|---|---|
|
|
482
|
+
| `setLimit()` | `setBaseUrl()` |
|
|
483
|
+
| `setResource()` | `setPage()` |
|
|
484
|
+
| `setSearch()` / `deleteSearch()` | `addFields()` / `deleteFields()` / `deleteFieldsByModel()` |
|
|
485
|
+
| `addFilter()` / `deleteFilters()` | `addIncludes()` / `deleteIncludes()` |
|
|
486
|
+
| `addFilterOperator()` / `deleteOperatorFilters()` | `addSelect()` / `deleteSelect()` |
|
|
487
|
+
| `addSort()` / `deleteSorts()` | |
|
|
488
|
+
|
|
489
|
+
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.
|
|
490
|
+
|
|
491
|
+
### Pagination navigation
|
|
492
|
+
|
|
493
|
+
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.
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
this._ngQubeeService.nextPage().generateUri().subscribe(uri => /* fire the request */);
|
|
497
|
+
this._ngQubeeService.previousPage();
|
|
498
|
+
this._ngQubeeService.firstPage();
|
|
499
|
+
this._ngQubeeService.lastPage();
|
|
500
|
+
this._ngQubeeService.goToPage(3);
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
#### Auto-sync from `PaginationService.paginate()`
|
|
504
|
+
|
|
505
|
+
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.
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
this._paginationService.paginate(response); // auto-writes page + lastPage
|
|
509
|
+
this._ngQubeeService.lastPage(); // now safe; jumps to the last page
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
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.
|
|
513
|
+
|
|
514
|
+
#### Predicates and accessors
|
|
515
|
+
|
|
516
|
+
Template-safe methods for driving button disable-states and labels:
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
qb.isFirstPage(); // true on page 1
|
|
520
|
+
qb.isLastPage(); // true only when bounds known and page === lastPage
|
|
521
|
+
qb.hasNextPage(); // true when bounds unknown, or page < lastPage
|
|
522
|
+
qb.hasPreviousPage(); // true when page > 1
|
|
523
|
+
qb.currentPage(); // state.page (always safe)
|
|
524
|
+
qb.totalPages(); // state.lastPage (throws if never synced)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Angular template wiring:
|
|
528
|
+
|
|
529
|
+
```html
|
|
530
|
+
<button [disabled]="qb.isFirstPage()" (click)="qb.previousPage()">Prev</button>
|
|
531
|
+
<span>Page {{ qb.currentPage() }} of {{ qb.totalPages() }}</span>
|
|
532
|
+
<button [disabled]="qb.isLastPage()" (click)="qb.nextPage()">Next</button>
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
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.
|
|
536
|
+
|
|
537
|
+
#### Error behavior
|
|
538
|
+
|
|
539
|
+
| Helper | Throws | When |
|
|
540
|
+
|---|---|---|
|
|
541
|
+
| `nextPage()` | — | Never throws. No-op when already at `lastPage` (bounds known). |
|
|
542
|
+
| `previousPage()` | — | Never throws. No-op at page 1. |
|
|
543
|
+
| `firstPage()` | — | Never throws. |
|
|
544
|
+
| `lastPage()` | `PaginationNotSyncedError` | `paginate()` has never run (`state.isLastPageKnown` is `false`). |
|
|
545
|
+
| `goToPage(n)` | `InvalidPageNumberError` | `n` is not a positive integer, or exceeds `lastPage` when bounds are known. |
|
|
546
|
+
| `isFirstPage()` / `isLastPage()` / `hasNextPage()` / `hasPreviousPage()` | — | Template-safe. Conservative defaults when bounds unknown. |
|
|
547
|
+
| `currentPage()` | — | Always safe. |
|
|
548
|
+
| `totalPages()` | `PaginationNotSyncedError` | `paginate()` has never run. Guard with `nest().isLastPageKnown` for a non-throwing check. |
|
|
549
|
+
|
|
550
|
+
#### Guarding the imperative "jump to last" button
|
|
551
|
+
|
|
552
|
+
`lastPage()` and `totalPages()` need a synced response. A safe pattern:
|
|
553
|
+
|
|
554
|
+
```html
|
|
555
|
+
<button
|
|
556
|
+
[disabled]="!qb.nest().isLastPageKnown || qb.isLastPage()"
|
|
557
|
+
(click)="qb.lastPage()">
|
|
558
|
+
Last
|
|
559
|
+
</button>
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
The `isLastPageKnown` read short-circuits before `qb.lastPage()` could throw.
|
|
563
|
+
|
|
340
564
|
### Retrieving data
|
|
341
565
|
URI is generated invoking the _generateUri_ method of the NgQubeeService. An observable is returned and the URI will be emitted:
|
|
342
566
|
|