ng-qubee 2.0.5 → 3.0.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
@@ -3,12 +3,19 @@
3
3
  </p>
4
4
 
5
5
  # Your next Angular Query Builder 🐝
6
+
7
+ [![CI](https://github.com/AndreaAlhena/ng-qubee/workflows/CI/badge.svg)](https://github.com/AndreaAlhena/ng-qubee/actions)
8
+ [![codecov](https://codecov.io/gh/AndreaAlhena/ng-qubee/branch/master/graph/badge.svg)](https://codecov.io/gh/AndreaAlhena/ng-qubee)
9
+ [![npm version](https://badge.fury.io/js/ng-qubee.svg)](https://www.npmjs.com/package/ng-qubee)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+
6
12
  NgQubee is a Query Builder for Angular. Easily compose your API requests without the hassle of writing the wheel again :)
7
13
 
8
14
  - Easily retrieve URIs with a Service
9
15
  - Pagination ready
10
16
  - Reactive, as the results are emitted with a RxJS Observable
11
17
  - Developed with a test-driven approach
18
+ - **Multi-driver support**: Laravel (pagination-only), Spatie Query Builder, and NestJS (nestjs-paginate)
12
19
 
13
20
  ## We love it, we use it ❤️
14
21
  NgQubee uses some open source projects to work properly:
@@ -32,25 +39,58 @@ Install NgQubee via NPM
32
39
  npm i ng-qubee
33
40
  ```
34
41
 
42
+ ## Drivers
43
+
44
+ NgQubee supports three drivers out of the box. A driver **must** be specified in the configuration:
45
+
46
+ | Driver | Backend | Request Format | Response Format |
47
+ |---|---|---|---|
48
+ | **Laravel** | Plain Laravel pagination | `limit=N&page=N` (pagination only) | Flat: `{ data, current_page, total, ... }` |
49
+ | **Spatie** | Spatie Query Builder | `filter[field]=value`, `sort=-field` | Flat: `{ data, current_page, total, ... }` |
50
+ | **NestJS** | nestjs-paginate | `filter.field=$operator:value`, `sortBy=field:DESC` | Nested: `{ data, meta: {...}, links: {...} }` |
51
+
35
52
  ## Usage
36
- Import the module in your Angular app:
53
+
54
+ ### Laravel Driver (pagination-only)
55
+
56
+ The Laravel driver provides basic pagination — limit and page parameters only. No filters, sorts, fields, or includes are supported.
37
57
 
38
58
  ```typescript
59
+ import { DriverEnum } from 'ng-qubee';
60
+
61
+ // Standalone approach
62
+ bootstrapApplication(AppComponent, {
63
+ providers: [provideNgQubee({ driver: DriverEnum.LARAVEL })]
64
+ });
65
+
66
+ // Module approach
39
67
  @NgModule({
40
68
  imports: [
41
- NgQubeeModule.forRoot({}) // You can omit the empty object as it is an optional argument
69
+ NgQubeeModule.forRoot({ driver: DriverEnum.LARAVEL })
42
70
  ]
43
71
  })
44
- export class AppModule {}
72
+ export class AppModule {}
45
73
  ```
46
74
 
47
- Or if you are working with Angular 15 or greater, use the provide function:
75
+ ### Spatie Driver
76
+
77
+ The Spatie driver generates URIs compatible with [Spatie Laravel Query Builder](https://spatie.be/docs/laravel-query-builder):
78
+
48
79
  ```typescript
49
- const config = {};
80
+ import { DriverEnum } from 'ng-qubee';
50
81
 
82
+ // Standalone approach
51
83
  bootstrapApplication(AppComponent, {
52
- providers: [provideNgQubee(config)]
84
+ providers: [provideNgQubee({ driver: DriverEnum.SPATIE })]
53
85
  });
86
+
87
+ // Module approach
88
+ @NgModule({
89
+ imports: [
90
+ NgQubeeModule.forRoot({ driver: DriverEnum.SPATIE })
91
+ ]
92
+ })
93
+ export class AppModule {}
54
94
  ```
55
95
 
56
96
  The object given to the _forRoot_ method allows to customize the query param keys. Following, the default behaviour:
@@ -66,6 +106,7 @@ As you can easily imagine, everything that regards the URI composition is placed
66
106
 
67
107
  ```typescript
68
108
  NgQubeeModule.forRoot({
109
+ driver: DriverEnum.SPATIE,
69
110
  request: {
70
111
  filters: 'custom-filter-key',
71
112
  fields: 'custom-fields-key',
@@ -74,6 +115,39 @@ NgQubeeModule.forRoot({
74
115
  })
75
116
  ```
76
117
 
118
+ ### NestJS Driver
119
+
120
+ To use the NestJS driver, specify the driver in your configuration:
121
+
122
+ ```typescript
123
+ import { DriverEnum } from 'ng-qubee';
124
+
125
+ // Standalone approach
126
+ bootstrapApplication(AppComponent, {
127
+ providers: [provideNgQubee({ driver: DriverEnum.NESTJS })]
128
+ });
129
+
130
+ // Module approach
131
+ @NgModule({
132
+ imports: [
133
+ NgQubeeModule.forRoot({ driver: DriverEnum.NESTJS })
134
+ ]
135
+ })
136
+ export class AppModule {}
137
+ ```
138
+
139
+ The NestJS driver generates URIs compatible with [nestjs-paginate](https://github.com/ppetzold/nestjs-paginate):
140
+
141
+ - **Filters** are composed as `filter.field=value`
142
+ - **Filter operators** are composed as `filter.field=$operator:value`
143
+ - **Sorts** are composed as `sortBy=field:ASC,field2:DESC`
144
+ - **Select** is composed as `select=col1,col2`
145
+ - **Search** is composed as `search=term`
146
+ - **Limit** is composed as `limit=15`
147
+ - **Page** is composed as `page=1`
148
+
149
+ ## Query Builder API
150
+
77
151
  For composing queries, the first step is to inject the proper NgQubeeService:
78
152
 
79
153
  ```typescript
@@ -83,16 +157,16 @@ export class YourService {
83
157
  }
84
158
  ```
85
159
 
86
- Set the **model** to run the query against:
160
+ Set the **resource** to run the query against:
87
161
 
88
162
  ```typescript
89
- this._ngQubeeService.setModel('users');
163
+ this._ngQubeeService.setResource('users');
90
164
  ```
91
165
 
92
166
  This is necessary to build the prefix of the URI (/users)
93
167
 
94
168
 
95
- ### Fields
169
+ ### Fields (Spatie only)
96
170
  Fields can be selected as following:
97
171
 
98
172
  ```typescript
@@ -101,14 +175,25 @@ this._ngQubeeService.addFields('users', ['id', 'email']);
101
175
 
102
176
  Will output _/users?fields[users]=id,email_
103
177
 
104
- ### Filters
178
+ ### Select (NestJS only)
179
+ Flat field selection for the NestJS driver:
180
+
181
+ ```typescript
182
+ this._ngQubeeService.addSelect('id', 'name', 'email');
183
+ ```
184
+
185
+ Will output _/users?select=id,name,email_
186
+
187
+ ### Filters (Spatie + NestJS)
105
188
  Filters are applied as following:
106
189
 
107
190
  ```typescript
108
191
  this._ngQubeeService.addFilter('id', 5);
109
192
  ```
110
193
 
111
- Will output _/users?filter[id]=5_
194
+ Will output:
195
+ - Spatie: _/users?filter[id]=5_
196
+ - NestJS: _/users?filter.id=5_
112
197
 
113
198
  Multiple values are allowed too:
114
199
 
@@ -116,11 +201,40 @@ Multiple values are allowed too:
116
201
  this._ngQubeeService.addFilter('id', 5, 7, 10);
117
202
  ```
118
203
 
119
- Will output _/users?filter[id]=5,7,10_
204
+ Will output:
205
+ - Spatie: _/users?filter[id]=5,7,10_
206
+ - NestJS: _/users?filter.id=5,7,10_
207
+
208
+ ### Filter Operators (NestJS only)
209
+ The NestJS driver supports explicit filter operators:
210
+
211
+ ```typescript
212
+ import { FilterOperatorEnum } from 'ng-qubee';
213
+
214
+ // Equality
215
+ this._ngQubeeService.addFilterOperator('status', FilterOperatorEnum.EQ, 'active');
216
+ // Output: filter.status=$eq:active
217
+
218
+ // Greater than or equal
219
+ this._ngQubeeService.addFilterOperator('age', FilterOperatorEnum.GTE, 18);
220
+ // Output: filter.age=$gte:18
120
221
 
121
-
222
+ // In (multiple values)
223
+ this._ngQubeeService.addFilterOperator('id', FilterOperatorEnum.IN, 1, 2, 3);
224
+ // Output: filter.id=$in:1,2,3
122
225
 
123
- ### Includes
226
+ // Between
227
+ this._ngQubeeService.addFilterOperator('price', FilterOperatorEnum.BTW, 10, 100);
228
+ // Output: filter.price=$btw:10,100
229
+
230
+ // Case-insensitive like
231
+ this._ngQubeeService.addFilterOperator('name', FilterOperatorEnum.ILIKE, 'john');
232
+ // Output: filter.name=$ilike:john
233
+ ```
234
+
235
+ **Available operators:** `$eq`, `$not`, `$null`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, `$btw`, `$ilike`, `$sw`, `$contains`
236
+
237
+ ### Includes (Spatie only)
124
238
  Ask to include related models with:
125
239
 
126
240
  ```typescript
@@ -129,14 +243,31 @@ this._ngQubeeService.addIncludes('profile', 'settings');
129
243
 
130
244
  Will output _/users?include=profile,settings_
131
245
 
132
- ### Sort
246
+ ### Search (NestJS only)
247
+ Full-text search for the NestJS driver:
248
+
249
+ ```typescript
250
+ this._ngQubeeService.setSearch('john doe');
251
+ ```
252
+
253
+ Will output _/users?search=john doe_
254
+
255
+ ### Sort (Spatie + NestJS)
133
256
  Sort elements as following:
134
257
 
135
258
  ```typescript
259
+ import { SortEnum } from 'ng-qubee';
260
+
136
261
  this._ngQubeeService.addSort('fieldName', SortEnum.ASC);
137
262
  ```
138
263
 
139
- Will output _/users?sort=fieldName_ (or _/users?sort=-fieldName_ if DESC)
264
+ Will output:
265
+ - Spatie: _/users?sort=fieldName_ (or _/users?sort=-fieldName_ if DESC)
266
+ - NestJS: _/users?sortBy=fieldName:ASC_ (or _/users?sortBy=fieldName:DESC_ if DESC)
267
+
268
+ The `SortEnum` provides two ordering options:
269
+ - `SortEnum.ASC` - Ascending order
270
+ - `SortEnum.DESC` - Descending order
140
271
 
141
272
  ### Page and Limit
142
273
  NgQubee supports paginated queries:
@@ -161,13 +292,47 @@ URI is generated invoking the _generateUri_ method of the NgQubeeService. An obs
161
292
  this._ngQubeeService.generateUri().subscribe(uri => console.log(uri));
162
293
  ```
163
294
 
295
+ ### Deleting State
296
+
297
+ All query features have corresponding delete methods:
298
+
299
+ ```typescript
300
+ // Spatie + NestJS
301
+ this._ngQubeeService.deleteFilters('status', 'role');
302
+ this._ngQubeeService.deleteSorts('created_at');
303
+
304
+ // Spatie only
305
+ this._ngQubeeService.deleteFields({ users: ['email'] });
306
+ this._ngQubeeService.deleteFieldsByModel('users', 'email');
307
+ this._ngQubeeService.deleteIncludes('profile');
308
+
309
+ // NestJS only
310
+ this._ngQubeeService.deleteOperatorFilters('age');
311
+ this._ngQubeeService.deleteSelect('email');
312
+ this._ngQubeeService.deleteSearch();
313
+ ```
314
+
164
315
  ### Reset state
165
- Query Builder state can be cleaned with the reset method. This will clean up everything set up previously, including the current model, filters, includes and so on...
316
+ 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...
166
317
 
167
318
  ```typescript
168
319
  this._ngQubeeService.reset();
169
320
  ```
170
321
 
322
+ ### Driver Validation
323
+
324
+ Calling a method that is not supported by the active driver throws a descriptive error immediately:
325
+
326
+ | Method | Laravel | Spatie | NestJS |
327
+ |---|---|---|---|
328
+ | `addFilter()` / `deleteFilters()` | throws `UnsupportedFilterError` | supported | supported |
329
+ | `addSort()` / `deleteSorts()` | throws `UnsupportedSortError` | supported | supported |
330
+ | `addFields()` / `deleteFields()` / `deleteFieldsByModel()` | throws `UnsupportedFieldSelectionError` | supported | throws `UnsupportedFieldSelectionError` |
331
+ | `addIncludes()` / `deleteIncludes()` | throws `UnsupportedIncludesError` | supported | throws `UnsupportedIncludesError` |
332
+ | `addFilterOperator()` / `deleteOperatorFilters()` | throws `UnsupportedFilterOperatorError` | throws `UnsupportedFilterOperatorError` | supported |
333
+ | `addSelect()` / `deleteSelect()` | throws `UnsupportedSelectError` | throws `UnsupportedSelectError` | supported |
334
+ | `setSearch()` / `deleteSearch()` | throws `UnsupportedSearchError` | throws `UnsupportedSearchError` | supported |
335
+
171
336
  ## Pagination
172
337
  If you are working with an API that supports pagination, we have got you covered 😉 NgQubee provides:
173
338
  - A PaginatedCollection class that holds paginated data
@@ -187,31 +352,268 @@ this._pg.paginate<Model>({ ...response, data: response.data.map(e => new Mo
187
352
 
188
353
  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.
189
354
 
190
- As you can see from the example, the paginate method requires a generic type: put there your model and you'll be provided with a PaginatedCollection<Model>. By default, the paginated collection will check for the following keys in the response:
355
+ ### Laravel / Spatie Response Format
356
+
357
+ When using the Laravel or Spatie driver, the paginated collection will check for the following keys in the response:
191
358
 
192
359
  - data - the key that holds the response data
193
- - currentPage - requested page for the pagination
194
- - from - Showing items from n (where n is a number)
195
- - to - Showing items from n (where n is a number)
196
- - total - Count of the items available in thw whole pagination
197
- - perPage - Items per page
198
- - prevPageUrl - Url to the previous page
199
- - nextPageUrl - Url to the next page
200
- - lastPage - Last page number
201
- - firstPageUrl - Url to the first page
202
- - lastPageUrl - Url to the last page
360
+ - current_page - requested page for the pagination
361
+ - from - Showing items from n
362
+ - to - Showing items to n
363
+ - total - Count of the items available in the whole pagination
364
+ - per_page - Items per page
365
+ - prev_page_url - URL to the previous page
366
+ - next_page_url - URL to the next page
367
+ - last_page - Last page number
368
+ - first_page_url - URL to the first page
369
+ - last_page_url - URL to the last page
370
+
371
+ ### NestJS Response Format
372
+
373
+ When using the NestJS driver, the PaginationService automatically parses nested responses:
374
+
375
+ ```json
376
+ {
377
+ "data": [...],
378
+ "meta": {
379
+ "currentPage": 1,
380
+ "totalItems": 100,
381
+ "itemsPerPage": 10,
382
+ "totalPages": 10
383
+ },
384
+ "links": {
385
+ "first": "http://api.com/users?page=1",
386
+ "previous": null,
387
+ "next": "http://api.com/users?page=2",
388
+ "last": "http://api.com/users?page=10",
389
+ "current": "http://api.com/users?page=1"
390
+ }
391
+ }
392
+ ```
203
393
 
204
- 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. Let's assume that the "currentPage" key is named "pg" in your API responseL your forRoot configuration will look as following:
394
+ The `from` and `to` values are computed automatically from `currentPage` and `itemsPerPage` when not present in the response.
395
+
396
+ ### Customizing Response Keys
397
+
398
+ 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:
205
399
 
206
400
  ```typescript
401
+ // Spatie
207
402
  NgQubeeModule.forRoot({
403
+ driver: DriverEnum.SPATIE,
208
404
  response: {
209
405
  currentPage: 'pg'
210
406
  }
211
407
  })
408
+
409
+ // NestJS (use dot-notation for nested paths)
410
+ NgQubeeModule.forRoot({
411
+ driver: DriverEnum.NESTJS,
412
+ response: {
413
+ currentPage: 'pagination.page',
414
+ total: 'pagination.total'
415
+ }
416
+ })
417
+ ```
418
+
419
+ ## TypeScript Support
420
+
421
+ NgQubee is fully typed and exports all public interfaces, enums, and types for TypeScript users.
422
+
423
+ ### Available Enums
424
+
425
+ ```typescript
426
+ import { DriverEnum, FilterOperatorEnum, SortEnum } from 'ng-qubee';
427
+
428
+ // Driver options
429
+ DriverEnum.LARAVEL // 'laravel' (pagination only)
430
+ DriverEnum.SPATIE // 'spatie'
431
+ DriverEnum.NESTJS // 'nestjs'
432
+
433
+ // Sorting options
434
+ SortEnum.ASC // 'asc'
435
+ SortEnum.DESC // 'desc'
436
+
437
+ // Filter operators (NestJS only)
438
+ FilterOperatorEnum.EQ // '$eq'
439
+ FilterOperatorEnum.NOT // '$not'
440
+ FilterOperatorEnum.NULL // '$null'
441
+ FilterOperatorEnum.IN // '$in'
442
+ FilterOperatorEnum.GT // '$gt'
443
+ FilterOperatorEnum.GTE // '$gte'
444
+ FilterOperatorEnum.LT // '$lt'
445
+ FilterOperatorEnum.LTE // '$lte'
446
+ FilterOperatorEnum.BTW // '$btw'
447
+ FilterOperatorEnum.ILIKE // '$ilike'
448
+ FilterOperatorEnum.SW // '$sw'
449
+ FilterOperatorEnum.CONTAINS // '$contains'
212
450
  ```
213
451
 
214
- Feel free to customize your PaginationService as you need, using the keys shown in the upper list.
452
+ ### Available Interfaces
453
+
454
+ NgQubee exports the following interfaces for type-safe development:
455
+
456
+ #### Configuration Interfaces
457
+
458
+ ```typescript
459
+ import {
460
+ IConfig,
461
+ IQueryBuilderConfig,
462
+ IPaginationConfig
463
+ } from 'ng-qubee';
464
+
465
+ // Main configuration interface (driver is required)
466
+ const config: IConfig = {
467
+ driver: DriverEnum.NESTJS,
468
+ request: {
469
+ filters: 'custom-filter-key',
470
+ fields: 'custom-fields-key',
471
+ includes: 'custom-include-key',
472
+ limit: 'custom-limit-key',
473
+ page: 'custom-page-key',
474
+ sort: 'custom-sort-key'
475
+ },
476
+ response: {
477
+ currentPage: 'pg',
478
+ data: 'items',
479
+ total: 'count',
480
+ perPage: 'itemsPerPage'
481
+ }
482
+ };
483
+ ```
484
+
485
+ #### Query Building Interfaces
486
+
487
+ ```typescript
488
+ import {
489
+ IFilters,
490
+ IFields,
491
+ ISort,
492
+ IOperatorFilter
493
+ } from 'ng-qubee';
494
+
495
+ // Filters interface - key-value pairs with array values
496
+ const filters: IFilters = {
497
+ id: [1, 2, 3],
498
+ status: ['active', 'pending']
499
+ };
500
+
501
+ // Fields interface - model name with array of field names
502
+ const fields: IFields = {
503
+ users: ['id', 'email', 'username'],
504
+ profile: ['avatar', 'bio']
505
+ };
506
+
507
+ // Sort interface - field and order
508
+ const sort: ISort = {
509
+ field: 'created_at',
510
+ order: SortEnum.DESC
511
+ };
512
+
513
+ // Operator filter interface (NestJS only)
514
+ const operatorFilter: IOperatorFilter = {
515
+ field: 'age',
516
+ operator: FilterOperatorEnum.GTE,
517
+ values: [18]
518
+ };
519
+ ```
520
+
521
+ #### Strategy Interfaces
522
+
523
+ ```typescript
524
+ import {
525
+ IRequestStrategy,
526
+ IResponseStrategy
527
+ } from 'ng-qubee';
528
+ ```
529
+
530
+ ### Spatie Usage Example
531
+
532
+ ```typescript
533
+ import { Component, OnInit } from '@angular/core';
534
+ import {
535
+ NgQubeeService,
536
+ SortEnum,
537
+ IFilters,
538
+ IFields
539
+ } from 'ng-qubee';
540
+
541
+ @Component({
542
+ selector: 'app-users',
543
+ template: '...'
544
+ })
545
+ export class UsersComponent implements OnInit {
546
+ constructor(private ngQubee: NgQubeeService) {}
547
+
548
+ ngOnInit(): void {
549
+ // Set up the query with type safety
550
+ this.ngQubee.setResource('users');
551
+
552
+ // Define fields with type checking
553
+ const userFields: IFields = {
554
+ users: ['id', 'email', 'username']
555
+ };
556
+ this.ngQubee.addFields('users', userFields.users);
557
+
558
+ // Define filters with type checking
559
+ const filters: IFilters = {
560
+ status: ['active'],
561
+ role: ['admin', 'moderator']
562
+ };
563
+ this.ngQubee.addFilter('status', ...filters.status);
564
+ this.ngQubee.addFilter('role', ...filters.role);
565
+
566
+ // Add sorting with enum
567
+ this.ngQubee.addSort('created_at', SortEnum.DESC);
568
+
569
+ // Generate URI
570
+ this.ngQubee.generateUri().subscribe(uri => {
571
+ console.log(uri);
572
+ // Output: /users?fields[users]=id,email,username&filter[status]=active&filter[role]=admin,moderator&sort=-created_at&limit=15&page=1
573
+ });
574
+ }
575
+ }
576
+ ```
577
+
578
+ ### NestJS Usage Example
579
+
580
+ ```typescript
581
+ import { Component, OnInit } from '@angular/core';
582
+ import {
583
+ NgQubeeService,
584
+ PaginationService,
585
+ FilterOperatorEnum,
586
+ SortEnum
587
+ } from 'ng-qubee';
588
+
589
+ @Component({
590
+ selector: 'app-users',
591
+ template: '...'
592
+ })
593
+ export class UsersComponent implements OnInit {
594
+ constructor(
595
+ private ngQubee: NgQubeeService,
596
+ private pagination: PaginationService
597
+ ) {}
598
+
599
+ ngOnInit(): void {
600
+ this.ngQubee
601
+ .setResource('users')
602
+ .addFilterOperator('age', FilterOperatorEnum.GTE, 18)
603
+ .addFilter('status', 'active')
604
+ .addSelect('id', 'name', 'email')
605
+ .addSort('name', SortEnum.ASC)
606
+ .setSearch('john')
607
+ .setLimit(10)
608
+ .setPage(1);
609
+
610
+ this.ngQubee.generateUri().subscribe(uri => {
611
+ console.log(uri);
612
+ // Output: /users?filter.status=active&filter.age=$gte:18&sortBy=name:ASC&select=id,name,email&search=john&limit=10&page=1
613
+ });
614
+ }
615
+ }
616
+ ```
215
617
 
216
618
  [ng-qubee]: <https://github.com/AndreaAlhena/ng-qubee>
217
619
  [rxjs]: <https://reactivex.io>