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 +40 -768
- package/fesm2022/ng-qubee.mjs +2376 -1732
- package/fesm2022/ng-qubee.mjs.map +1 -1
- package/package.json +17 -8
- package/types/ng-qubee.d.ts +784 -261
package/README.md
CHANGED
|
@@ -9,798 +9,70 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/ng-qubee)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
|
|
12
|
-
NgQubee is a
|
|
12
|
+
NgQubee is a query builder for Angular. Compose your API requests without re-inventing the wheel.
|
|
13
13
|
|
|
14
|
-
-
|
|
15
|
-
- Pagination ready
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
This README is intentionally minimal. For everything beyond install + a five-line example, head to the docs site.
|
|
28
24
|
|
|
29
|
-
|
|
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
|
-
|
|
27
|
+
- **Angular** ≥ 16 (uses Signals)
|
|
28
|
+
- **RxJS** ^6.5.0 || ^7.0.0
|
|
34
29
|
|
|
35
|
-
##
|
|
36
|
-
Install NgQubee via NPM
|
|
30
|
+
## Install
|
|
37
31
|
|
|
38
32
|
```sh
|
|
39
|
-
npm
|
|
33
|
+
npm i ng-qubee
|
|
40
34
|
```
|
|
41
35
|
|
|
42
36
|
## Drivers
|
|
43
37
|
|
|
44
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
## Quick start
|
|
151
52
|
|
|
152
53
|
```typescript
|
|
153
|
-
import {
|
|
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.
|
|
58
|
+
providers: [provideNgQubee({ driver: DriverEnum.STRAPI })]
|
|
158
59
|
});
|
|
159
60
|
|
|
160
|
-
//
|
|
161
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
[
|
|
805
|
-
[rxjs]: <https://reactivex.io>
|
|
806
|
-
[qs]: <https://github.com/ljharb/qs>
|
|
78
|
+
MIT © [Andrea Tantimonaco](https://www.linkedin.com/in/andrea-tantimonaco/)
|