ng-qubee 3.2.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,798 +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 four 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
-
53
- ## Usage
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
-
84
- ### Laravel Driver (pagination-only)
85
-
86
- The Laravel driver provides basic pagination — limit and page parameters only. No filters, sorts, fields, or includes are supported.
87
-
88
- ```typescript
89
- import { DriverEnum } from 'ng-qubee';
90
-
91
- // Standalone approach
92
- bootstrapApplication(AppComponent, {
93
- providers: [provideNgQubee({ driver: DriverEnum.LARAVEL })]
94
- });
95
-
96
- // Module approach
97
- @NgModule({
98
- imports: [
99
- NgQubeeModule.forRoot({ driver: DriverEnum.LARAVEL })
100
- ]
101
- })
102
- export class AppModule {}
103
- ```
104
-
105
- ### Spatie Driver
106
-
107
- The Spatie driver generates URIs compatible with [Spatie Laravel Query Builder](https://spatie.be/docs/laravel-query-builder):
108
-
109
- ```typescript
110
- import { DriverEnum } from 'ng-qubee';
111
-
112
- // Standalone approach
113
- bootstrapApplication(AppComponent, {
114
- providers: [provideNgQubee({ driver: DriverEnum.SPATIE })]
115
- });
38
+ A driver **must** be specified in the configuration:
116
39
 
117
- // Module approach
118
- @NgModule({
119
- imports: [
120
- NgQubeeModule.forRoot({ driver: DriverEnum.SPATIE })
121
- ]
122
- })
123
- export class AppModule {}
124
- ```
125
-
126
- The object given to the _forRoot_ method allows to customize the query param keys. Following, the default behaviour:
127
-
128
- - **Filters** are composed as filter[fieldName]=value / customizable with {request: {filters: 'yourFilterKey'}}
129
- - **Fields** are composed as fields[model]=id,email,username / customizable with {request: {fields: 'yourFieldsKey'}}
130
- - **Includes** are composed as include=modelA, modelB / customizable with {request: {includes: 'yourIncludeKey'}}
131
- - **Limit** is composed as limit=15 / customizable with {request: {limit: 'yourLimitKey'}}
132
- - **Page** is composed as page=1 / customizable with {request: {page: 'yourPageKey'}}
133
- - **Sort** is composed as sort=fieldName / customizable with {request: {sort: 'yourSortKey'}}
134
-
135
- As you can easily imagine, everything that regards the URI composition is placed into the "request" key.
136
-
137
- ```typescript
138
- NgQubeeModule.forRoot({
139
- driver: DriverEnum.SPATIE,
140
- request: {
141
- filters: 'custom-filter-key',
142
- fields: 'custom-fields-key',
143
- /* and so on... */
144
- }
145
- })
146
- ```
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` |
147
48
 
148
- ### NestJS 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).
149
50
 
150
- To use the NestJS driver, specify the driver in your configuration:
51
+ ## Quick start
151
52
 
152
53
  ```typescript
153
- import { DriverEnum } from 'ng-qubee';
54
+ import { bootstrapApplication } from '@angular/platform-browser';
55
+ import { DriverEnum, NgQubeeService, provideNgQubee, SortEnum } from 'ng-qubee';
154
56
 
155
- // Standalone approach
156
57
  bootstrapApplication(AppComponent, {
157
- providers: [provideNgQubee({ driver: DriverEnum.NESTJS })]
58
+ providers: [provideNgQubee({ driver: DriverEnum.STRAPI })]
158
59
  });
159
60
 
160
- // Module approach
161
- @NgModule({
162
- imports: [
163
- NgQubeeModule.forRoot({ driver: DriverEnum.NESTJS })
164
- ]
165
- })
166
- export class AppModule {}
167
- ```
168
-
169
- The NestJS driver generates URIs compatible with [nestjs-paginate](https://github.com/ppetzold/nestjs-paginate):
170
-
171
- - **Filters** are composed as `filter.field=value`
172
- - **Filter operators** are composed as `filter.field=$operator:value`
173
- - **Sorts** are composed as `sortBy=field:ASC,field2:DESC`
174
- - **Select** is composed as `select=col1,col2`
175
- - **Search** is composed as `search=term`
176
- - **Limit** is composed as `limit=15`
177
- - **Page** is composed as `page=1`
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
-
200
- ## Query Builder API
201
-
202
- For composing queries, the first step is to inject the proper NgQubeeService:
203
-
204
- ```typescript
205
- @Injectable()
206
- export class YourService {
207
- constructor(private _ngQubeeService: NgQubeeService) {}
208
- }
209
- ```
210
-
211
- Set the **resource** to run the query against:
212
-
213
- ```typescript
214
- this._ngQubeeService.setResource('users');
215
- ```
216
-
217
- This is necessary to build the prefix of the URI (/users)
218
-
219
-
220
- ### Fields (JSON:API + Spatie)
221
- Fields can be selected as following:
222
-
223
- ```typescript
224
- this._ngQubeeService.addFields('users', ['id', 'email']);
225
- ```
226
-
227
- Will output _/users?fields[users]=id,email_
228
-
229
- ### Select (NestJS only)
230
- Flat field selection for the NestJS driver:
231
-
232
- ```typescript
233
- this._ngQubeeService.addSelect('id', 'name', 'email');
234
- ```
235
-
236
- Will output _/users?select=id,name,email_
237
-
238
- ### Filters (JSON:API + Spatie + NestJS)
239
- Filters are applied as following:
240
-
241
- ```typescript
242
- this._ngQubeeService.addFilter('id', 5);
243
- ```
244
-
245
- Will output:
246
- - Spatie: _/users?filter[id]=5_
247
- - NestJS: _/users?filter.id=5_
248
-
249
- Multiple values are allowed too:
250
-
251
- ```typescript
252
- this._ngQubeeService.addFilter('id', 5, 7, 10);
253
- ```
254
-
255
- Will output:
256
- - Spatie: _/users?filter[id]=5,7,10_
257
- - NestJS: _/users?filter.id=5,7,10_
258
-
259
- ### Filter Operators (NestJS only)
260
- The NestJS driver supports explicit filter operators:
261
-
262
- ```typescript
263
- import { FilterOperatorEnum } from 'ng-qubee';
264
-
265
- // Equality
266
- this._ngQubeeService.addFilterOperator('status', FilterOperatorEnum.EQ, 'active');
267
- // Output: filter.status=$eq:active
268
-
269
- // Greater than or equal
270
- this._ngQubeeService.addFilterOperator('age', FilterOperatorEnum.GTE, 18);
271
- // Output: filter.age=$gte:18
272
-
273
- // In (multiple values)
274
- this._ngQubeeService.addFilterOperator('id', FilterOperatorEnum.IN, 1, 2, 3);
275
- // Output: filter.id=$in:1,2,3
276
-
277
- // Between
278
- this._ngQubeeService.addFilterOperator('price', FilterOperatorEnum.BTW, 10, 100);
279
- // Output: filter.price=$btw:10,100
280
-
281
- // Case-insensitive like
282
- this._ngQubeeService.addFilterOperator('name', FilterOperatorEnum.ILIKE, 'john');
283
- // Output: filter.name=$ilike:john
284
- ```
285
-
286
- **Available operators:** `$eq`, `$not`, `$null`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, `$btw`, `$ilike`, `$sw`, `$contains`
287
-
288
- ### Includes (JSON:API + Spatie)
289
- Ask to include related models with:
290
-
291
- ```typescript
292
- this._ngQubeeService.addIncludes('profile', 'settings');
293
- ```
294
-
295
- Will output _/users?include=profile,settings_
296
-
297
- ### Search (NestJS only)
298
- Full-text search for the NestJS driver:
299
-
300
- ```typescript
301
- this._ngQubeeService.setSearch('john doe');
302
- ```
303
-
304
- Will output _/users?search=john doe_
305
-
306
- ### Sort (JSON:API + Spatie + NestJS)
307
- Sort elements as following:
308
-
309
- ```typescript
310
- import { SortEnum } from 'ng-qubee';
311
-
312
- this._ngQubeeService.addSort('fieldName', SortEnum.ASC);
313
- ```
314
-
315
- Will output:
316
- - Spatie: _/users?sort=fieldName_ (or _/users?sort=-fieldName_ if DESC)
317
- - NestJS: _/users?sortBy=fieldName:ASC_ (or _/users?sortBy=fieldName:DESC_ if DESC)
318
-
319
- The `SortEnum` provides two ordering options:
320
- - `SortEnum.ASC` - Ascending order
321
- - `SortEnum.DESC` - Descending order
322
-
323
- ### Page and Limit
324
- NgQubee supports paginated queries:
325
-
326
- ```typescript
327
- this._ngQubeeService.setLimit(25);
328
- this._ngQubeeService.setPage(2);
329
- ```
330
-
331
- Will output _/users?limit=25&page=2
332
-
333
- Default values are automatically added to the query:
334
- - **Limit**: 15
335
- - **Page**: 1
336
-
337
- Always expect your query to include _limit=15&page=1_
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()`:
61
+ // In a component or service:
62
+ constructor(private _qb: NgQubeeService) {}
353
63
 
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
-
449
- ### Retrieving data
450
- URI is generated invoking the _generateUri_ method of the NgQubeeService. An observable is returned and the URI will be emitted:
451
-
452
- ```typescript
453
- this._ngQubeeService.generateUri().subscribe(uri => console.log(uri));
454
- ```
455
-
456
- ### Deleting State
457
-
458
- All query features have corresponding delete methods:
459
-
460
- ```typescript
461
- // JSON:API + Spatie + NestJS
462
- this._ngQubeeService.deleteFilters('status', 'role');
463
- this._ngQubeeService.deleteSorts('created_at');
464
-
465
- // JSON:API + Spatie only
466
- this._ngQubeeService.deleteFields({ users: ['email'] });
467
- this._ngQubeeService.deleteFieldsByModel('users', 'email');
468
- this._ngQubeeService.deleteIncludes('profile');
469
-
470
- // NestJS only
471
- this._ngQubeeService.deleteOperatorFilters('age');
472
- this._ngQubeeService.deleteSelect('email');
473
- this._ngQubeeService.deleteSearch();
474
- ```
475
-
476
- ### Reset state
477
- 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...
478
-
479
- ```typescript
480
- this._ngQubeeService.reset();
481
- ```
482
-
483
- ### Driver Validation
484
-
485
- Calling a method that is not supported by the active driver throws a descriptive error immediately:
486
-
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 |
496
-
497
- ## Pagination
498
- If you are working with an API that supports pagination, we have got you covered 😉 NgQubee provides:
499
- - A PaginatedCollection class that holds paginated data
500
- - A PaginationService that help to transform the response in a PaginatedCollection
501
-
502
- As a service, you have to inject the PaginationService first:
503
-
504
- ```typescript
505
- constructor(private _pg: PaginationService) {}
506
- ```
507
-
508
- In the following example, the PaginationService is used to transform the response with the paginate method.
509
-
510
- ```typescript
511
- this._pg.paginate<Model>({ ...response, data: response.data.map(e => new Model(e.id)) })
512
- ```
513
-
514
- 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.
515
-
516
- ### Laravel / Spatie Response Format
517
-
518
- When using the Laravel or Spatie driver, the paginated collection will check for the following keys in the response:
519
-
520
- - data - the key that holds the response data
521
- - current_page - requested page for the pagination
522
- - from - Showing items from n
523
- - to - Showing items to n
524
- - total - Count of the items available in the whole pagination
525
- - per_page - Items per page
526
- - prev_page_url - URL to the previous page
527
- - next_page_url - URL to the next page
528
- - last_page - Last page number
529
- - first_page_url - URL to the first page
530
- - last_page_url - URL to the last page
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
-
556
- ### NestJS Response Format
557
-
558
- When using the NestJS driver, the PaginationService automatically parses nested responses:
559
-
560
- ```json
561
- {
562
- "data": [...],
563
- "meta": {
564
- "currentPage": 1,
565
- "totalItems": 100,
566
- "itemsPerPage": 10,
567
- "totalPages": 10
568
- },
569
- "links": {
570
- "first": "http://api.com/users?page=1",
571
- "previous": null,
572
- "next": "http://api.com/users?page=2",
573
- "last": "http://api.com/users?page=10",
574
- "current": "http://api.com/users?page=1"
575
- }
576
- }
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
577
72
  ```
578
73
 
579
- The `from` and `to` values are computed automatically from `currentPage` and `itemsPerPage` when not present in the response.
580
-
581
- ### Customizing Response Keys
582
-
583
- 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:
584
-
585
- ```typescript
586
- // Spatie
587
- NgQubeeModule.forRoot({
588
- driver: DriverEnum.SPATIE,
589
- response: {
590
- currentPage: 'pg'
591
- }
592
- })
593
-
594
- // NestJS (use dot-notation for nested paths)
595
- NgQubeeModule.forRoot({
596
- driver: DriverEnum.NESTJS,
597
- response: {
598
- currentPage: 'pagination.page',
599
- total: 'pagination.total'
600
- }
601
- })
602
- ```
603
-
604
- ## TypeScript Support
605
-
606
- NgQubee is fully typed and exports all public interfaces, enums, and types for TypeScript users.
607
-
608
- ### Available Enums
609
-
610
- ```typescript
611
- import { DriverEnum, FilterOperatorEnum, SortEnum } from 'ng-qubee';
612
-
613
- // Driver options
614
- DriverEnum.JSON_API // 'json-api'
615
- DriverEnum.LARAVEL // 'laravel' (pagination only)
616
- DriverEnum.SPATIE // 'spatie'
617
- DriverEnum.NESTJS // 'nestjs'
618
-
619
- // Sorting options
620
- SortEnum.ASC // 'asc'
621
- SortEnum.DESC // 'desc'
622
-
623
- // Filter operators (NestJS only)
624
- FilterOperatorEnum.EQ // '$eq'
625
- FilterOperatorEnum.NOT // '$not'
626
- FilterOperatorEnum.NULL // '$null'
627
- FilterOperatorEnum.IN // '$in'
628
- FilterOperatorEnum.GT // '$gt'
629
- FilterOperatorEnum.GTE // '$gte'
630
- FilterOperatorEnum.LT // '$lt'
631
- FilterOperatorEnum.LTE // '$lte'
632
- FilterOperatorEnum.BTW // '$btw'
633
- FilterOperatorEnum.ILIKE // '$ilike'
634
- FilterOperatorEnum.SW // '$sw'
635
- FilterOperatorEnum.CONTAINS // '$contains'
636
- ```
637
-
638
- ### Available Interfaces
639
-
640
- NgQubee exports the following interfaces for type-safe development:
641
-
642
- #### Configuration Interfaces
643
-
644
- ```typescript
645
- import {
646
- IConfig,
647
- IQueryBuilderConfig,
648
- IPaginationConfig
649
- } from 'ng-qubee';
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).
650
75
 
651
- // Main configuration interface (driver is required)
652
- const config: IConfig = {
653
- driver: DriverEnum.NESTJS,
654
- request: {
655
- filters: 'custom-filter-key',
656
- fields: 'custom-fields-key',
657
- includes: 'custom-include-key',
658
- limit: 'custom-limit-key',
659
- page: 'custom-page-key',
660
- sort: 'custom-sort-key'
661
- },
662
- response: {
663
- currentPage: 'pg',
664
- data: 'items',
665
- total: 'count',
666
- perPage: 'itemsPerPage'
667
- }
668
- };
669
- ```
670
-
671
- #### Query Building Interfaces
672
-
673
- ```typescript
674
- import {
675
- IFilters,
676
- IFields,
677
- ISort,
678
- IOperatorFilter
679
- } from 'ng-qubee';
680
-
681
- // Filters interface - key-value pairs with array values
682
- const filters: IFilters = {
683
- id: [1, 2, 3],
684
- status: ['active', 'pending']
685
- };
686
-
687
- // Fields interface - model name with array of field names
688
- const fields: IFields = {
689
- users: ['id', 'email', 'username'],
690
- profile: ['avatar', 'bio']
691
- };
692
-
693
- // Sort interface - field and order
694
- const sort: ISort = {
695
- field: 'created_at',
696
- order: SortEnum.DESC
697
- };
698
-
699
- // Operator filter interface (NestJS only)
700
- const operatorFilter: IOperatorFilter = {
701
- field: 'age',
702
- operator: FilterOperatorEnum.GTE,
703
- values: [18]
704
- };
705
- ```
706
-
707
- #### Strategy Interfaces
708
-
709
- ```typescript
710
- import {
711
- IRequestStrategy,
712
- IResponseStrategy
713
- } from 'ng-qubee';
714
- ```
715
-
716
- ### Spatie Usage Example
717
-
718
- ```typescript
719
- import { Component, OnInit } from '@angular/core';
720
- import {
721
- NgQubeeService,
722
- SortEnum,
723
- IFilters,
724
- IFields
725
- } from 'ng-qubee';
726
-
727
- @Component({
728
- selector: 'app-users',
729
- template: '...'
730
- })
731
- export class UsersComponent implements OnInit {
732
- constructor(private ngQubee: NgQubeeService) {}
733
-
734
- ngOnInit(): void {
735
- // Set up the query with type safety
736
- this.ngQubee.setResource('users');
737
-
738
- // Define fields with type checking
739
- const userFields: IFields = {
740
- users: ['id', 'email', 'username']
741
- };
742
- this.ngQubee.addFields('users', userFields.users);
743
-
744
- // Define filters with type checking
745
- const filters: IFilters = {
746
- status: ['active'],
747
- role: ['admin', 'moderator']
748
- };
749
- this.ngQubee.addFilter('status', ...filters.status);
750
- this.ngQubee.addFilter('role', ...filters.role);
751
-
752
- // Add sorting with enum
753
- this.ngQubee.addSort('created_at', SortEnum.DESC);
754
-
755
- // Generate URI
756
- this.ngQubee.generateUri().subscribe(uri => {
757
- console.log(uri);
758
- // Output: /users?fields[users]=id,email,username&filter[status]=active&filter[role]=admin,moderator&sort=-created_at&limit=15&page=1
759
- });
760
- }
761
- }
762
- ```
763
-
764
- ### NestJS Usage Example
765
-
766
- ```typescript
767
- import { Component, OnInit } from '@angular/core';
768
- import {
769
- NgQubeeService,
770
- PaginationService,
771
- FilterOperatorEnum,
772
- SortEnum
773
- } from 'ng-qubee';
774
-
775
- @Component({
776
- selector: 'app-users',
777
- template: '...'
778
- })
779
- export class UsersComponent implements OnInit {
780
- constructor(
781
- private ngQubee: NgQubeeService,
782
- private pagination: PaginationService
783
- ) {}
784
-
785
- ngOnInit(): void {
786
- this.ngQubee
787
- .setResource('users')
788
- .addFilterOperator('age', FilterOperatorEnum.GTE, 18)
789
- .addFilter('status', 'active')
790
- .addSelect('id', 'name', 'email')
791
- .addSort('name', SortEnum.ASC)
792
- .setSearch('john')
793
- .setLimit(10)
794
- .setPage(1);
795
-
796
- this.ngQubee.generateUri().subscribe(uri => {
797
- console.log(uri);
798
- // Output: /users?filter.status=active&filter.age=$gte:18&sortBy=name:ASC&select=id,name,email&search=john&limit=10&page=1
799
- });
800
- }
801
- }
802
- ```
76
+ ## License
803
77
 
804
- [ng-qubee]: <https://github.com/AndreaAlhena/ng-qubee>
805
- [rxjs]: <https://reactivex.io>
806
- [qs]: <https://github.com/ljharb/qs>
78
+ MIT © [Andrea Tantimonaco](https://www.linkedin.com/in/andrea-tantimonaco/)