ng-qubee 3.3.0 → 3.4.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
@@ -9,913 +9,70 @@
9
9
  [![npm version](https://badge.fury.io/js/ng-qubee.svg)](https://www.npmjs.com/package/ng-qubee)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
11
 
12
- NgQubee is a Query Builder for Angular. Easily compose your API requests without the hassle of writing the wheel again :)
12
+ NgQubee is a query builder for Angular. Compose your API requests without re-inventing the wheel.
13
13
 
14
- - Easily retrieve URIs with a Service
15
- - Pagination ready
16
- - Reactive, as the results are emitted with a RxJS Observable
17
- - Developed with a test-driven approach
18
- - **Multi-driver support**: JSON:API, Laravel (pagination-only), Spatie Query Builder, and NestJS (nestjs-paginate)
14
+ - Reactive URIs emitted as RxJS observables, state held in Angular Signals
15
+ - Pagination ready — typed `PaginatedCollection`, fluent navigation (`nextPage`, `lastPage`, `goToPage`)
16
+ - Test-driven 495+ specs
17
+ - **Multi-driver support**: JSON:API, Laravel (pagination-only), Spatie Query Builder, NestJS (`nestjs-paginate`), PostgREST / Supabase, and Strapi
19
18
 
20
- ## We love it, we use it ❤️
21
- NgQubee uses some open source projects to work properly:
22
- - [rxjs] - URIs returned via Observables
23
- - [qs] - A querystring parsing and stringifying library with some added security.
19
+ ## 📚 Documentation
24
20
 
25
- And of course NgQubee itself is open source with a [public repository][ng-qubee] on GitHub.
21
+ **Full documentation lives at [ng-qubee.andreatantimonaco.me](https://ng-qubee.andreatantimonaco.me)** driver guides, query-builder API, pagination helpers, auto-generated API reference, and version history.
26
22
 
27
- ## Requirements
23
+ This README is intentionally minimal. For everything beyond install + a five-line example, head to the docs site.
28
24
 
29
- NgQubee requires:
30
- - **Angular**: >=16.0.0 <22.0.0 (supports Angular 16 through 21)
31
- - **RxJS**: ^6.5.0 || ^7.0.0
25
+ ## Requirements
32
26
 
33
- > **Note**: Angular 16+ is required because NgQubee uses Angular Signals for state management.
27
+ - **Angular** 16 (uses Signals)
28
+ - **RxJS** ^6.5.0 || ^7.0.0
34
29
 
35
- ## Installation
36
- Install NgQubee via NPM
30
+ ## Install
37
31
 
38
32
  ```sh
39
- npm i ng-qubee
33
+ npm i ng-qubee
40
34
  ```
41
35
 
42
36
  ## Drivers
43
37
 
44
- NgQubee supports five drivers out of the box. A driver **must** be specified in the configuration:
45
-
46
- | Driver | Backend | Request Format | Response Format |
47
- |---|---|---|---|
48
- | **JSON:API** | Any JSON:API-compliant backend | `filter[field]=value`, `sort=-field`, `page[number]=N&page[size]=N` | Nested: `{ data, meta: {...}, links: {...} }` |
49
- | **Laravel** | Plain Laravel pagination | `limit=N&page=N` (pagination only) | Flat: `{ data, current_page, total, ... }` |
50
- | **Spatie** | Spatie Query Builder | `filter[field]=value`, `sort=-field` | Flat: `{ data, current_page, total, ... }` |
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 |
53
-
54
- ## Usage
55
-
56
- ### JSON:API Driver
57
-
58
- The JSON:API driver generates URIs compatible with any [JSON:API](https://jsonapi.org/format/)-compliant backend (Rails, Django, .NET, Java, Elixir, etc.):
59
-
60
- ```typescript
61
- import { DriverEnum } from 'ng-qubee';
62
-
63
- // Standalone approach
64
- bootstrapApplication(AppComponent, {
65
- providers: [provideNgQubee({ driver: DriverEnum.JSON_API })]
66
- });
67
-
68
- // Module approach
69
- @NgModule({
70
- imports: [
71
- NgQubeeModule.forRoot({ driver: DriverEnum.JSON_API })
72
- ]
73
- })
74
- export class AppModule {}
75
- ```
76
-
77
- The JSON:API driver supports:
78
-
79
- - **Filters** are composed as `filter[field]=value`
80
- - **Fields** are composed as `fields[type]=col1,col2`
81
- - **Includes** are composed as `include=author,comments.author`
82
- - **Sort** is composed as `sort=-created_at,name` (`-` prefix = DESC)
83
- - **Pagination** uses bracket notation: `page[number]=1&page[size]=15`
84
-
85
- ### Laravel Driver (pagination-only)
86
-
87
- The Laravel driver provides basic pagination — limit and page parameters only. No filters, sorts, fields, or includes are supported.
88
-
89
- ```typescript
90
- import { DriverEnum } from 'ng-qubee';
91
-
92
- // Standalone approach
93
- bootstrapApplication(AppComponent, {
94
- providers: [provideNgQubee({ driver: DriverEnum.LARAVEL })]
95
- });
96
-
97
- // Module approach
98
- @NgModule({
99
- imports: [
100
- NgQubeeModule.forRoot({ driver: DriverEnum.LARAVEL })
101
- ]
102
- })
103
- export class AppModule {}
104
- ```
105
-
106
- ### Spatie Driver
107
-
108
- The Spatie driver generates URIs compatible with [Spatie Laravel Query Builder](https://spatie.be/docs/laravel-query-builder):
109
-
110
- ```typescript
111
- import { DriverEnum } from 'ng-qubee';
112
-
113
- // Standalone approach
114
- bootstrapApplication(AppComponent, {
115
- providers: [provideNgQubee({ driver: DriverEnum.SPATIE })]
116
- });
117
-
118
- // Module approach
119
- @NgModule({
120
- imports: [
121
- NgQubeeModule.forRoot({ driver: DriverEnum.SPATIE })
122
- ]
123
- })
124
- export class AppModule {}
125
- ```
126
-
127
- The object given to the _forRoot_ method allows to customize the query param keys. Following, the default behaviour:
128
-
129
- - **Filters** are composed as filter[fieldName]=value / customizable with {request: {filters: 'yourFilterKey'}}
130
- - **Fields** are composed as fields[model]=id,email,username / customizable with {request: {fields: 'yourFieldsKey'}}
131
- - **Includes** are composed as include=modelA, modelB / customizable with {request: {includes: 'yourIncludeKey'}}
132
- - **Limit** is composed as limit=15 / customizable with {request: {limit: 'yourLimitKey'}}
133
- - **Page** is composed as page=1 / customizable with {request: {page: 'yourPageKey'}}
134
- - **Sort** is composed as sort=fieldName / customizable with {request: {sort: 'yourSortKey'}}
135
-
136
- As you can easily imagine, everything that regards the URI composition is placed into the "request" key.
38
+ A driver **must** be specified in the configuration:
137
39
 
138
- ```typescript
139
- NgQubeeModule.forRoot({
140
- driver: DriverEnum.SPATIE,
141
- request: {
142
- filters: 'custom-filter-key',
143
- fields: 'custom-fields-key',
144
- /* and so on... */
145
- }
146
- })
147
- ```
148
-
149
- ### NestJS Driver
150
-
151
- To use the NestJS driver, specify the driver in your configuration:
152
-
153
- ```typescript
154
- import { DriverEnum } from 'ng-qubee';
155
-
156
- // Standalone approach
157
- bootstrapApplication(AppComponent, {
158
- providers: [provideNgQubee({ driver: DriverEnum.NESTJS })]
159
- });
160
-
161
- // Module approach
162
- @NgModule({
163
- imports: [
164
- NgQubeeModule.forRoot({ driver: DriverEnum.NESTJS })
165
- ]
166
- })
167
- export class AppModule {}
168
- ```
169
-
170
- The NestJS driver generates URIs compatible with [nestjs-paginate](https://github.com/ppetzold/nestjs-paginate):
171
-
172
- - **Filters** are composed as `filter.field=value`
173
- - **Filter operators** are composed as `filter.field=$operator:value`
174
- - **Sorts** are composed as `sortBy=field:ASC,field2:DESC`
175
- - **Select** is composed as `select=col1,col2`
176
- - **Search** is composed as `search=term`
177
- - **Limit** is composed as `limit=15`
178
- - **Page** is composed as `page=1`
40
+ | Driver | Backend | Wire format snapshot |
41
+ |---|---|---|
42
+ | **JSON:API** | Any [JSON:API](https://jsonapi.org/format/)-compliant backend | `filter[field]=value`, `sort=-field`, `page[number]=N&page[size]=N` |
43
+ | **Laravel** | Plain Laravel pagination | `limit=N&page=N` (pagination only) |
44
+ | **Spatie** | [Spatie Laravel Query Builder](https://spatie.be/docs/laravel-query-builder) | `filter[field]=value`, `sort=-field` |
45
+ | **NestJS** | [`nestjs-paginate`](https://github.com/ppetzold/nestjs-paginate) | `filter.field=$op:value`, `sortBy=field:DESC` |
46
+ | **PostgREST** | [PostgREST](https://postgrest.org/) / [Supabase](https://supabase.com/) | `col=eq.value`, `order=col.asc`, `limit=N&offset=M` |
47
+ | **Strapi** | [Strapi](https://strapi.io/) v4 / v5 headless CMS | `filters[field][$eq]=value`, `sort[0]=field:asc`, `pagination[page]=N&pagination[pageSize]=N` |
179
48
 
180
- ### PostgREST / Supabase Driver
49
+ Per-driver guides wire format, supported operators, response parsing, customisation — live on the [docs site](https://ng-qubee.andreatantimonaco.me/docs/getting-started).
181
50
 
182
- The PostgREST driver generates URIs compatible with [PostgREST](https://postgrest.org/) and anything built on top of it (notably [Supabase](https://supabase.com/)):
51
+ ## Quick start
183
52
 
184
53
  ```typescript
185
- import { DriverEnum } from 'ng-qubee';
54
+ import { bootstrapApplication } from '@angular/platform-browser';
55
+ import { DriverEnum, NgQubeeService, provideNgQubee, SortEnum } from 'ng-qubee';
186
56
 
187
- // Standalone approach
188
57
  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
58
+ providers: [provideNgQubee({ driver: DriverEnum.STRAPI })]
221
59
  });
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
-
315
- ## Query Builder API
316
-
317
- For composing queries, the first step is to inject the proper NgQubeeService:
318
-
319
- ```typescript
320
- @Injectable()
321
- export class YourService {
322
- constructor(private _ngQubeeService: NgQubeeService) {}
323
- }
324
- ```
325
-
326
- Set the **resource** to run the query against:
327
-
328
- ```typescript
329
- this._ngQubeeService.setResource('users');
330
- ```
331
-
332
- This is necessary to build the prefix of the URI (/users)
333
-
334
-
335
- ### Fields (JSON:API + Spatie)
336
- Fields can be selected as following:
337
-
338
- ```typescript
339
- this._ngQubeeService.addFields('users', ['id', 'email']);
340
- ```
341
-
342
- Will output _/users?fields[users]=id,email_
343
-
344
- ### Select (NestJS only)
345
- Flat field selection for the NestJS driver:
346
-
347
- ```typescript
348
- this._ngQubeeService.addSelect('id', 'name', 'email');
349
- ```
350
-
351
- Will output _/users?select=id,name,email_
352
-
353
- ### Filters (JSON:API + Spatie + NestJS)
354
- Filters are applied as following:
355
-
356
- ```typescript
357
- this._ngQubeeService.addFilter('id', 5);
358
- ```
359
-
360
- Will output:
361
- - Spatie: _/users?filter[id]=5_
362
- - NestJS: _/users?filter.id=5_
363
-
364
- Multiple values are allowed too:
365
-
366
- ```typescript
367
- this._ngQubeeService.addFilter('id', 5, 7, 10);
368
- ```
369
-
370
- Will output:
371
- - Spatie: _/users?filter[id]=5,7,10_
372
- - NestJS: _/users?filter.id=5,7,10_
373
-
374
- ### Filter Operators (NestJS only)
375
- The NestJS driver supports explicit filter operators:
376
-
377
- ```typescript
378
- import { FilterOperatorEnum } from 'ng-qubee';
379
-
380
- // Equality
381
- this._ngQubeeService.addFilterOperator('status', FilterOperatorEnum.EQ, 'active');
382
- // Output: filter.status=$eq:active
383
-
384
- // Greater than or equal
385
- this._ngQubeeService.addFilterOperator('age', FilterOperatorEnum.GTE, 18);
386
- // Output: filter.age=$gte:18
387
-
388
- // In (multiple values)
389
- this._ngQubeeService.addFilterOperator('id', FilterOperatorEnum.IN, 1, 2, 3);
390
- // Output: filter.id=$in:1,2,3
391
-
392
- // Between
393
- this._ngQubeeService.addFilterOperator('price', FilterOperatorEnum.BTW, 10, 100);
394
- // Output: filter.price=$btw:10,100
395
-
396
- // Case-insensitive like
397
- this._ngQubeeService.addFilterOperator('name', FilterOperatorEnum.ILIKE, 'john');
398
- // Output: filter.name=$ilike:john
399
- ```
400
-
401
- **Available operators:** `$eq`, `$not`, `$null`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, `$btw`, `$ilike`, `$sw`, `$contains`
402
-
403
- ### Includes (JSON:API + Spatie)
404
- Ask to include related models with:
405
-
406
- ```typescript
407
- this._ngQubeeService.addIncludes('profile', 'settings');
408
- ```
409
-
410
- Will output _/users?include=profile,settings_
411
-
412
- ### Search (NestJS only)
413
- Full-text search for the NestJS driver:
414
-
415
- ```typescript
416
- this._ngQubeeService.setSearch('john doe');
417
- ```
418
-
419
- Will output _/users?search=john doe_
420
60
 
421
- ### Sort (JSON:API + Spatie + NestJS)
422
- Sort elements as following:
61
+ // In a component or service:
62
+ constructor(private _qb: NgQubeeService) {}
423
63
 
424
- ```typescript
425
- import { SortEnum } from 'ng-qubee';
426
-
427
- this._ngQubeeService.addSort('fieldName', SortEnum.ASC);
428
- ```
429
-
430
- Will output:
431
- - Spatie: _/users?sort=fieldName_ (or _/users?sort=-fieldName_ if DESC)
432
- - NestJS: _/users?sortBy=fieldName:ASC_ (or _/users?sortBy=fieldName:DESC_ if DESC)
433
-
434
- The `SortEnum` provides two ordering options:
435
- - `SortEnum.ASC` - Ascending order
436
- - `SortEnum.DESC` - Descending order
437
-
438
- ### Page and Limit
439
- NgQubee supports paginated queries:
440
-
441
- ```typescript
442
- this._ngQubeeService.setLimit(25);
443
- this._ngQubeeService.setPage(2);
444
- ```
445
-
446
- Will output _/users?limit=25&page=2
447
-
448
- Default values are automatically added to the query:
449
- - **Limit**: 15
450
- - **Page**: 1
451
-
452
- Always expect your query to include _limit=15&page=1_
453
-
454
- #### Fetch all (NestJS only)
455
-
456
- 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`):
457
-
458
- ```typescript
459
- // NestJS driver only
460
- this._ngQubeeService.setLimit(-1);
461
- ```
462
-
463
- JSON:API, Laravel, and Spatie drivers reject `-1` and throw `InvalidLimitError`.
464
-
465
- #### Limit validation
466
-
467
- Limit validation is driver-scoped — each request strategy enforces its own accepted range and invalid values throw `InvalidLimitError` immediately when passed to `setLimit()`:
468
-
469
- | Driver | Accepted limit values |
470
- |---|---|
471
- | NestJS | integer `-1` (fetch all) or `>= 1` |
472
- | JSON:API / Laravel / Spatie | integer `>= 1` |
473
-
474
- Non-integer values, zero, negative numbers (other than `-1` for NestJS), `NaN`, and `Infinity` are all rejected.
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
-
564
- ### Retrieving data
565
- URI is generated invoking the _generateUri_ method of the NgQubeeService. An observable is returned and the URI will be emitted:
566
-
567
- ```typescript
568
- this._ngQubeeService.generateUri().subscribe(uri => console.log(uri));
569
- ```
570
-
571
- ### Deleting State
572
-
573
- All query features have corresponding delete methods:
574
-
575
- ```typescript
576
- // JSON:API + Spatie + NestJS
577
- this._ngQubeeService.deleteFilters('status', 'role');
578
- this._ngQubeeService.deleteSorts('created_at');
579
-
580
- // JSON:API + Spatie only
581
- this._ngQubeeService.deleteFields({ users: ['email'] });
582
- this._ngQubeeService.deleteFieldsByModel('users', 'email');
583
- this._ngQubeeService.deleteIncludes('profile');
584
-
585
- // NestJS only
586
- this._ngQubeeService.deleteOperatorFilters('age');
587
- this._ngQubeeService.deleteSelect('email');
588
- this._ngQubeeService.deleteSearch();
589
- ```
590
-
591
- ### Reset state
592
- Query Builder state can be cleaned with the reset method. This will clean up everything set up previously, including the current resource, filters, includes and so on...
593
-
594
- ```typescript
595
- this._ngQubeeService.reset();
64
+ this._qb
65
+ .setResource('articles')
66
+ .addFilter('status', 'published')
67
+ .addSort('createdAt', SortEnum.DESC)
68
+ .setLimit(10)
69
+ .generateUri()
70
+ .subscribe(uri => console.log(uri));
71
+ // /articles?filters[status][$eq]=published&sort[0]=createdAt:desc&pagination[page]=1&pagination[pageSize]=10
596
72
  ```
597
73
 
598
- ### Driver Validation
599
-
600
- Calling a method that is not supported by the active driver throws a descriptive error immediately:
601
-
602
- | Method | JSON:API | Laravel | Spatie | NestJS |
603
- |---|---|---|---|---|
604
- | `addFilter()` / `deleteFilters()` | supported | throws `UnsupportedFilterError` | supported | supported |
605
- | `addSort()` / `deleteSorts()` | supported | throws `UnsupportedSortError` | supported | supported |
606
- | `addFields()` / `deleteFields()` / `deleteFieldsByModel()` | supported | throws `UnsupportedFieldSelectionError` | supported | throws `UnsupportedFieldSelectionError` |
607
- | `addIncludes()` / `deleteIncludes()` | supported | throws `UnsupportedIncludesError` | supported | throws `UnsupportedIncludesError` |
608
- | `addFilterOperator()` / `deleteOperatorFilters()` | throws `UnsupportedFilterOperatorError` | throws `UnsupportedFilterOperatorError` | throws `UnsupportedFilterOperatorError` | supported |
609
- | `addSelect()` / `deleteSelect()` | throws `UnsupportedSelectError` | throws `UnsupportedSelectError` | throws `UnsupportedSelectError` | supported |
610
- | `setSearch()` / `deleteSearch()` | throws `UnsupportedSearchError` | throws `UnsupportedSearchError` | throws `UnsupportedSearchError` | supported |
611
-
612
- ## Pagination
613
- If you are working with an API that supports pagination, we have got you covered 😉 NgQubee provides:
614
- - A PaginatedCollection class that holds paginated data
615
- - A PaginationService that help to transform the response in a PaginatedCollection
616
-
617
- As a service, you have to inject the PaginationService first:
74
+ The full query-builder API, pagination helpers, per-driver feature matrices, and TypeScript types are documented at [ng-qubee.andreatantimonaco.me](https://ng-qubee.andreatantimonaco.me).
618
75
 
619
- ```typescript
620
- constructor(private _pg: PaginationService) {}
621
- ```
622
-
623
- In the following example, the PaginationService is used to transform the response with the paginate method.
624
-
625
- ```typescript
626
- this._pg.paginate<Model>({ ...response, data: response.data.map(e => new Model(e.id)) })
627
- ```
628
-
629
- The "paginate" method returns a PaginatedCollection that helps handling paginated data. Additionally, if you are dealing with a state library in your application, you can use the "normalize" method of the collection to normalize the data.
630
-
631
- ### Laravel / Spatie Response Format
632
-
633
- When using the Laravel or Spatie driver, the paginated collection will check for the following keys in the response:
634
-
635
- - data - the key that holds the response data
636
- - current_page - requested page for the pagination
637
- - from - Showing items from n
638
- - to - Showing items to n
639
- - total - Count of the items available in the whole pagination
640
- - per_page - Items per page
641
- - prev_page_url - URL to the previous page
642
- - next_page_url - URL to the next page
643
- - last_page - Last page number
644
- - first_page_url - URL to the first page
645
- - last_page_url - URL to the last page
646
-
647
- ### JSON:API Response Format
648
-
649
- When using the JSON:API driver, the PaginationService automatically parses nested responses:
650
-
651
- ```json
652
- {
653
- "data": [...],
654
- "meta": {
655
- "current-page": 1,
656
- "per-page": 10,
657
- "total": 100,
658
- "page-count": 10
659
- },
660
- "links": {
661
- "first": "http://api.com/articles?page[number]=1&page[size]=10",
662
- "prev": null,
663
- "next": "http://api.com/articles?page[number]=2&page[size]=10",
664
- "last": "http://api.com/articles?page[number]=10&page[size]=10"
665
- }
666
- }
667
- ```
668
-
669
- 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.
670
-
671
- ### NestJS Response Format
672
-
673
- When using the NestJS driver, the PaginationService automatically parses nested responses:
674
-
675
- ```json
676
- {
677
- "data": [...],
678
- "meta": {
679
- "currentPage": 1,
680
- "totalItems": 100,
681
- "itemsPerPage": 10,
682
- "totalPages": 10
683
- },
684
- "links": {
685
- "first": "http://api.com/users?page=1",
686
- "previous": null,
687
- "next": "http://api.com/users?page=2",
688
- "last": "http://api.com/users?page=10",
689
- "current": "http://api.com/users?page=1"
690
- }
691
- }
692
- ```
693
-
694
- The `from` and `to` values are computed automatically from `currentPage` and `itemsPerPage` when not present in the response.
695
-
696
- ### Customizing Response Keys
697
-
698
- Just like the query builder, the pagination service supports customizable keys. While invoking the forRoot method of the module, use the response key to look for different keys in the API response:
699
-
700
- ```typescript
701
- // Spatie
702
- NgQubeeModule.forRoot({
703
- driver: DriverEnum.SPATIE,
704
- response: {
705
- currentPage: 'pg'
706
- }
707
- })
708
-
709
- // NestJS (use dot-notation for nested paths)
710
- NgQubeeModule.forRoot({
711
- driver: DriverEnum.NESTJS,
712
- response: {
713
- currentPage: 'pagination.page',
714
- total: 'pagination.total'
715
- }
716
- })
717
- ```
718
-
719
- ## TypeScript Support
720
-
721
- NgQubee is fully typed and exports all public interfaces, enums, and types for TypeScript users.
722
-
723
- ### Available Enums
724
-
725
- ```typescript
726
- import { DriverEnum, FilterOperatorEnum, SortEnum } from 'ng-qubee';
727
-
728
- // Driver options
729
- DriverEnum.JSON_API // 'json-api'
730
- DriverEnum.LARAVEL // 'laravel' (pagination only)
731
- DriverEnum.SPATIE // 'spatie'
732
- DriverEnum.NESTJS // 'nestjs'
733
-
734
- // Sorting options
735
- SortEnum.ASC // 'asc'
736
- SortEnum.DESC // 'desc'
737
-
738
- // Filter operators (NestJS only)
739
- FilterOperatorEnum.EQ // '$eq'
740
- FilterOperatorEnum.NOT // '$not'
741
- FilterOperatorEnum.NULL // '$null'
742
- FilterOperatorEnum.IN // '$in'
743
- FilterOperatorEnum.GT // '$gt'
744
- FilterOperatorEnum.GTE // '$gte'
745
- FilterOperatorEnum.LT // '$lt'
746
- FilterOperatorEnum.LTE // '$lte'
747
- FilterOperatorEnum.BTW // '$btw'
748
- FilterOperatorEnum.ILIKE // '$ilike'
749
- FilterOperatorEnum.SW // '$sw'
750
- FilterOperatorEnum.CONTAINS // '$contains'
751
- ```
752
-
753
- ### Available Interfaces
754
-
755
- NgQubee exports the following interfaces for type-safe development:
756
-
757
- #### Configuration Interfaces
758
-
759
- ```typescript
760
- import {
761
- IConfig,
762
- IQueryBuilderConfig,
763
- IPaginationConfig
764
- } from 'ng-qubee';
765
-
766
- // Main configuration interface (driver is required)
767
- const config: IConfig = {
768
- driver: DriverEnum.NESTJS,
769
- request: {
770
- filters: 'custom-filter-key',
771
- fields: 'custom-fields-key',
772
- includes: 'custom-include-key',
773
- limit: 'custom-limit-key',
774
- page: 'custom-page-key',
775
- sort: 'custom-sort-key'
776
- },
777
- response: {
778
- currentPage: 'pg',
779
- data: 'items',
780
- total: 'count',
781
- perPage: 'itemsPerPage'
782
- }
783
- };
784
- ```
785
-
786
- #### Query Building Interfaces
787
-
788
- ```typescript
789
- import {
790
- IFilters,
791
- IFields,
792
- ISort,
793
- IOperatorFilter
794
- } from 'ng-qubee';
795
-
796
- // Filters interface - key-value pairs with array values
797
- const filters: IFilters = {
798
- id: [1, 2, 3],
799
- status: ['active', 'pending']
800
- };
801
-
802
- // Fields interface - model name with array of field names
803
- const fields: IFields = {
804
- users: ['id', 'email', 'username'],
805
- profile: ['avatar', 'bio']
806
- };
807
-
808
- // Sort interface - field and order
809
- const sort: ISort = {
810
- field: 'created_at',
811
- order: SortEnum.DESC
812
- };
813
-
814
- // Operator filter interface (NestJS only)
815
- const operatorFilter: IOperatorFilter = {
816
- field: 'age',
817
- operator: FilterOperatorEnum.GTE,
818
- values: [18]
819
- };
820
- ```
821
-
822
- #### Strategy Interfaces
823
-
824
- ```typescript
825
- import {
826
- IRequestStrategy,
827
- IResponseStrategy
828
- } from 'ng-qubee';
829
- ```
830
-
831
- ### Spatie Usage Example
832
-
833
- ```typescript
834
- import { Component, OnInit } from '@angular/core';
835
- import {
836
- NgQubeeService,
837
- SortEnum,
838
- IFilters,
839
- IFields
840
- } from 'ng-qubee';
841
-
842
- @Component({
843
- selector: 'app-users',
844
- template: '...'
845
- })
846
- export class UsersComponent implements OnInit {
847
- constructor(private ngQubee: NgQubeeService) {}
848
-
849
- ngOnInit(): void {
850
- // Set up the query with type safety
851
- this.ngQubee.setResource('users');
852
-
853
- // Define fields with type checking
854
- const userFields: IFields = {
855
- users: ['id', 'email', 'username']
856
- };
857
- this.ngQubee.addFields('users', userFields.users);
858
-
859
- // Define filters with type checking
860
- const filters: IFilters = {
861
- status: ['active'],
862
- role: ['admin', 'moderator']
863
- };
864
- this.ngQubee.addFilter('status', ...filters.status);
865
- this.ngQubee.addFilter('role', ...filters.role);
866
-
867
- // Add sorting with enum
868
- this.ngQubee.addSort('created_at', SortEnum.DESC);
869
-
870
- // Generate URI
871
- this.ngQubee.generateUri().subscribe(uri => {
872
- console.log(uri);
873
- // Output: /users?fields[users]=id,email,username&filter[status]=active&filter[role]=admin,moderator&sort=-created_at&limit=15&page=1
874
- });
875
- }
876
- }
877
- ```
878
-
879
- ### NestJS Usage Example
880
-
881
- ```typescript
882
- import { Component, OnInit } from '@angular/core';
883
- import {
884
- NgQubeeService,
885
- PaginationService,
886
- FilterOperatorEnum,
887
- SortEnum
888
- } from 'ng-qubee';
889
-
890
- @Component({
891
- selector: 'app-users',
892
- template: '...'
893
- })
894
- export class UsersComponent implements OnInit {
895
- constructor(
896
- private ngQubee: NgQubeeService,
897
- private pagination: PaginationService
898
- ) {}
899
-
900
- ngOnInit(): void {
901
- this.ngQubee
902
- .setResource('users')
903
- .addFilterOperator('age', FilterOperatorEnum.GTE, 18)
904
- .addFilter('status', 'active')
905
- .addSelect('id', 'name', 'email')
906
- .addSort('name', SortEnum.ASC)
907
- .setSearch('john')
908
- .setLimit(10)
909
- .setPage(1);
910
-
911
- this.ngQubee.generateUri().subscribe(uri => {
912
- console.log(uri);
913
- // Output: /users?filter.status=active&filter.age=$gte:18&sortBy=name:ASC&select=id,name,email&search=john&limit=10&page=1
914
- });
915
- }
916
- }
917
- ```
76
+ ## License
918
77
 
919
- [ng-qubee]: <https://github.com/AndreaAlhena/ng-qubee>
920
- [rxjs]: <https://reactivex.io>
921
- [qs]: <https://github.com/ljharb/qs>
78
+ MIT © [Andrea Tantimonaco](https://www.linkedin.com/in/andrea-tantimonaco/)