@zola_do/collection-query 0.2.5 → 0.2.7

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.
Files changed (2) hide show
  1. package/README.md +497 -52
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,21 @@
1
1
  # @zola_do/collection-query
2
2
 
3
- TypeORM query builder for filter, sort, and paginate operations on collections.
3
+ [![npm version](https://img.shields.io/npm/v/@zola_do/collection-query.svg)](https://www.npmjs.com/package/@zola_do/collection-query)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@zola_do/collection-query.svg)](https://www.npmjs.com/package/@zola_do/collection-query)
5
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
6
+
7
+ TypeORM query builder for filter, sort, paginate, and cursor-based operations on entity collections.
8
+
9
+ ## Overview
10
+
11
+ `@zola_do/collection-query` provides a powerful query builder that transforms URL query parameters into TypeORM SelectQueryBuilder calls. It supports:
12
+
13
+ - **Filtering** with 21 operators (equals, between, in, like, etc.)
14
+ - **Sorting** with multiple columns and null handling
15
+ - **Pagination** with both offset and cursor-based approaches
16
+ - **Relation loading** with includes, joins, and eager loading
17
+ - **Fuzzy search** with PostgreSQL similarity
18
+ - **URL encoding** for compact query strings
4
19
 
5
20
  ## Installation
6
21
 
@@ -12,96 +27,526 @@ npm install @zola_do/collection-query
12
27
  npm install @zola_do/nestjs-shared
13
28
  ```
14
29
 
15
- ## Recommended Imports
30
+ ### Peer Dependencies
31
+
32
+ ```bash
33
+ npm install typeorm class-transformer class-validator
34
+ ```
35
+
36
+ Optional for Swagger documentation:
37
+
38
+ ```bash
39
+ npm install @nestjs/swagger
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```typescript
45
+ import {
46
+ decodeCollectionQuery,
47
+ QueryConstructor,
48
+ CollectionResult,
49
+ } from "@zola_do/collection-query";
50
+
51
+ @Controller("products")
52
+ export class ProductsController {
53
+ @Get()
54
+ async findAll(@Query("q") q?: string) {
55
+ const query = decodeCollectionQuery(q);
56
+ const repository = this.productRepository;
57
+
58
+ const dataQuery = QueryConstructor.constructQuery<Product>(
59
+ repository,
60
+ query,
61
+ );
62
+ const result = await dataQuery.getManyAndCount();
63
+
64
+ return {
65
+ total: result[1],
66
+ items: result[0],
67
+ };
68
+ }
69
+ }
70
+ ```
71
+
72
+ ## Query String Format
73
+
74
+ The `q` parameter uses a compact encoding format with these separators:
75
+
76
+ | Part | Separator | Description |
77
+ | ---------- | ----------------------------- | ----------------------------------------- |
78
+ | Where Item | `_:=_:` | Separates column, operator, and value |
79
+ | Where OR | `_,` | Separates items within a group (OR logic) |
80
+ | Where AND | `_\|` | Separates filter groups (AND logic) |
81
+ | Order | `,` between items, `:` inside | `col:desc,col2:asc` |
82
+
83
+ ### Query Parameters
84
+
85
+ | Key | Name | Description | Example |
86
+ | ---- | ---------------- | ----------------------------------- | -------------------- |
87
+ | `s` | Select | Columns to select (comma-separated) | `s=id,name,price` |
88
+ | `w` | Where | Filter conditions | `w=status_:=_:active` |
89
+ | `t` | Take | Number of records to return (limit) | `t=20` |
90
+ | `sk` | Skip | Records to skip (offset) | `sk=0` |
91
+ | `ct` | Cursor Token | Opaque cursor for pagination | `ct=eyJway...` |
92
+ | `cd` | Cursor Direction | `n` (next) or `p` (prev) | `cd=n` |
93
+ | `o` | Order | Sort column and direction | `o=createdAt:desc` |
94
+ | `i` | Includes | Relations to eager-load | `i=category,vendor` |
95
+ | `c` | Count | Return count only | `c=true` |
96
+
97
+ ### Example Queries
98
+
99
+ ```bash
100
+ # Basic filter
101
+ GET /products?q=w=status_:=_:active
102
+
103
+ # With pagination
104
+ GET /products?q=w=status_:=_:active&t=10&sk=0
105
+
106
+ # With sorting
107
+ GET /products?q=w=status_:=_:active&t=10&o=createdAt:desc
108
+
109
+ # With includes
110
+ GET /products?q=i=category,vendor&o=createdAt:desc
111
+
112
+ # ILIKE search (OR within group) - use % for wildcards
113
+ GET /products?q=w=organizationName_:Ilike_:t_,organizationShortName_:Ilike_:t
114
+
115
+ # Multiple filters (AND between groups)
116
+ GET /products?q=w=status_:=_:active_|category_:=_:123
117
+
118
+ # Combined AND + OR
119
+ GET /products?q=w=status_:=_:active_,status_:=_:inactive_|name_:=_:test
120
+ # SQL: WHERE (status = 'active' OR status = 'inactive') AND name = 'test'
121
+
122
+ # Cursor pagination
123
+ GET /products?q=t=10&ct=eyJway...&cd=n
124
+ ```
125
+
126
+ ## Filter Operators Reference
127
+
128
+ The `w` parameter uses these separators:
129
+
130
+ - `_:` separates column, operator, and value
131
+ - `_,` separates items within a group (OR logic)
132
+ - `_|` separates filter groups (AND logic)
133
+
134
+ ### Available Operators
135
+
136
+ | Operator | Symbol | Description | Example |
137
+ | ------------------------ | --------------- | -------------------------------- | ------------------------------------- |
138
+ | **EqualTo** | `=` | Exact match | `w=id_:=_:123` |
139
+ | **NotEqualTo** | `!=` | Not equal | `w=status_:=!:=inactive` |
140
+ | **GreaterThan** | `>` | Greater than | `w=price__:>_100` |
141
+ | **GreaterThanOrEqualTo** | `>=` | Greater than or equal | `w=price__:>=_100` |
142
+ | **LessThan** | `<` | Less than | `w=price__:<_100` |
143
+ | **LessThanOrEqualTo** | `<=` | Less than or equal | `w=price__:<=_100` |
144
+ | **Like** | `LIKE` | Pattern match (case-sensitive) | `w=name_:LIKE_:pro%` |
145
+ | **ILike** | `ILIKE` | Pattern match (case-insensitive) | `w=name_:ILIKE_:pro%` |
146
+ | **In** | `IN` | Value in list | `w=id_:IN_:1,2,3` |
147
+ | **NotIn** | `NotIn` | Value not in list | `w=id_:NotIn_:1,2,3` |
148
+ | **Between** | `BETWEEN` | Range (inclusive) | `w=price_:BETWEEN_:10,100` |
149
+ | **IsNull** | `IsNull` | Is NULL | `w=deletedAt_:IsNull_:true` |
150
+ | **IsNotNull** | `IsNotNull` | Is not NULL | `w=deletedAt_:IsNotNull_:true` |
151
+ | **NotNull** | `NotNull` | Is not NULL | `w=deletedAt_:NotNull_:true` |
152
+ | **Any** | `ANY` | Array contains element | `w=tags_:ANY_:featured` |
153
+ | **ArrayContains** | `ArrayContains` | Array contains value | `w=permissions_:ArrayContains_:admin` |
154
+ | **ArrayFilter** | `ArrayFilter` | Filter array elements | `w=items_:ArrayFilter_:active` |
155
+ | **All** | `All` | Array contains all | `w=roles_:All_:admin,user` |
156
+
157
+ ### Operator Examples
158
+
159
+ ```typescript
160
+ // Equal to
161
+ { column: 'status', value: 'active', operator: FilterOperators.EqualTo }
162
+ // SQL: WHERE status = 'active'
163
+
164
+ // Between
165
+ { column: 'price', value: [10, 100], operator: FilterOperators.Between }
166
+ // SQL: WHERE price BETWEEN 10 AND 100
167
+
168
+ // In
169
+ { column: 'id', value: [1, 2, 3], operator: FilterOperators.In }
170
+ // SQL: WHERE id IN (1, 2, 3)
171
+
172
+ // Like
173
+ { column: 'name', value: 'pro%', operator: FilterOperators.Like }
174
+ // SQL: WHERE name LIKE 'pro%'
175
+
176
+ // IsNull
177
+ { column: 'deletedAt', value: true, operator: FilterOperators.IsNull }
178
+ // SQL: WHERE deletedAt IS NULL
16
179
 
17
- `@zola_do/collection-query` root imports remain fully supported for backward compatibility.
18
- For new code, prefer focused subpath imports:
180
+ // Array contains
181
+ { column: 'permissions', value: 'admin', operator: FilterOperators.ArrayContains }
182
+ // SQL: WHERE permissions @> ARRAY['admin']
183
+ ```
184
+
185
+ ### Complex Filters
186
+
187
+ ```typescript
188
+ // OR within group
189
+ where: [
190
+ [
191
+ { column: "status", value: "active", operator: FilterOperators.EqualTo },
192
+ { column: "featured", value: true, operator: FilterOperators.EqualTo },
193
+ ],
194
+ ];
195
+
196
+ // SQL: WHERE (status = 'active' OR featured = true)
197
+
198
+ // AND between groups
199
+ where: [
200
+ [{ column: "status", value: "active", operator: FilterOperators.EqualTo }],
201
+ [{ column: "categoryId", value: "123", operator: FilterOperators.EqualTo }],
202
+ ];
203
+
204
+ // SQL: WHERE (status = 'active') AND (categoryId = '123')
205
+ ```
206
+
207
+ ## Pagination
208
+
209
+ ### Offset Pagination
19
210
 
20
211
  ```typescript
21
- import { FilterOperators } from '@zola_do/collection-query/filter-operators';
22
- import { CollectionQuery } from '@zola_do/collection-query/query';
23
- import { QueryConstructor } from '@zola_do/collection-query/query-constructor';
24
- import { decodeCollectionQuery } from '@zola_do/collection-query/query-mapper';
212
+ const query = decodeCollectionQuery("t=10&sk=20&o=createdAt:desc");
213
+ // take: 10, skip: 20, orderBy: [{ column: 'createdAt', direction: 'DESC' }]
25
214
  ```
26
215
 
27
- ## Usage
216
+ ### Cursor Pagination
28
217
 
29
- ### Decoding Query Strings
218
+ Cursor pagination is recommended for:
30
219
 
31
- Parse a `q` query parameter from list endpoints into a `CollectionQuery` object:
220
+ - Live data (feeds, activity logs)
221
+ - Deep pagination
222
+ - Data that changes frequently
32
223
 
33
224
  ```typescript
34
- import { decodeCollectionQuery } from '@zola_do/collection-query';
225
+ // First page
226
+ const query1 = decodeCollectionQuery("t=10&o=createdAt:DESC");
227
+ // Set stableSortColumn for best results
228
+ query1.stableSortColumn = "id";
229
+
230
+ // Subsequent pages
231
+ const query2 = decodeCollectionQuery("t=10&ct=<cursor>&cd=n");
232
+
233
+ // Response includes cursors
234
+ interface CollectionResult<T> {
235
+ total: number;
236
+ items: T[];
237
+ nextCursor?: string;
238
+ prevCursor?: string;
239
+ hasNextPage?: boolean;
240
+ }
241
+ ```
242
+
243
+ **Cursor token structure (opaque):**
35
244
 
36
- @Get()
37
- async findAll(@Query('q') q?: string) {
38
- const query = decodeCollectionQuery(q);
39
- return await this.service.findAll(query);
245
+ ```typescript
246
+ interface CursorTokenPayload {
247
+ version: 1;
248
+ orderColumn: string;
249
+ orderDirection: "ASC" | "DESC";
250
+ orderValue: any;
251
+ stableColumn: string;
252
+ stableValue: any;
40
253
  }
41
254
  ```
42
255
 
43
- ### Query Format
256
+ ## Sorting
44
257
 
45
- The `q` parameter supports:
258
+ ### Basic Sorting
46
259
 
47
- - `s` — Select fields (comma-separated)
48
- - `w` — Where conditions (filter operators)
49
- - `t` Take (limit)
50
- - `sk` — Skip (offset)
51
- - `ct` — Cursor token (opaque string)
52
- - `cd` — Cursor direction (`n` or `p`) (`next`/`prev` also accepted)
53
- - `o` Order by
54
- - `i` Includes (relations)
55
- - `c` — Count only
260
+ ```typescript
261
+ // Single column
262
+ query.orderBy = [{ column: "createdAt", direction: "DESC" }];
263
+
264
+ // Multiple columns
265
+ query.orderBy = [
266
+ { column: "category", direction: "ASC" },
267
+ { column: "createdAt", direction: "DESC" },
268
+ ];
269
+ ```
56
270
 
57
- Example: `q=w=column$eq$value&t=10&sk=0&o=createdAt$desc`
271
+ ### Null Handling
58
272
 
59
- Cursor example: `q=t=10&o=createdAt:DESC&ct=<opaque-token>&cd=n`
273
+ ```typescript
274
+ query.orderBy = [
275
+ {
276
+ column: "deletedAt",
277
+ direction: "ASC",
278
+ nulls: "NULLS LAST", // or 'NULLS FIRST'
279
+ },
280
+ ];
281
+ ```
60
282
 
61
- ### Building Queries
283
+ ## Relation Loading
62
284
 
63
- Use `QueryConstructor` to build TypeORM queries from a `CollectionQuery`:
285
+ ### Includes
64
286
 
65
287
  ```typescript
66
- import { QueryConstructor, CollectionQuery } from '@zola_do/collection-query';
288
+ // Simple includes
289
+ query.includes = ["category", "vendor"];
67
290
 
68
- const dataQuery = QueryConstructor.constructQuery<T>(repository, query);
69
- const [result, total] = await dataQuery.getManyAndCount();
291
+ // Nested includes
292
+ query.includes = ["category.parent", "vendor.address"];
70
293
  ```
71
294
 
72
- ### Filter Operators
295
+ ### IncludeAndSelect
296
+
297
+ ```typescript
298
+ query.includeAndSelect = [
299
+ {
300
+ name: "category",
301
+ select: ["id", "name"], // Select specific fields
302
+ },
303
+ ];
304
+ ```
73
305
 
74
- Use `FilterOperators` for where conditions:
306
+ ### Left Join And Map One
75
307
 
76
308
  ```typescript
77
- import { FilterOperators } from '@zola_do/collection-query';
309
+ query.leftJoinAndMapOne = [
310
+ {
311
+ mapToProperty: "vendor",
312
+ property: "vendors",
313
+ condition: "vendors.status = :status",
314
+ parameters: { status: "Approved" },
315
+ },
316
+ ];
317
+ ```
318
+
319
+ ## Fuzzy Search
78
320
 
79
- query.where.push([
80
- { column: 'status', value: 'active', operator: FilterOperators.Equals },
81
- { column: 'createdAt', value: '2024-01-01', operator: FilterOperators.Gte },
82
- ]);
321
+ For PostgreSQL similarity search:
322
+
323
+ ```typescript
324
+ query.fuzzySearch = [
325
+ {
326
+ columns: ["name", "description"],
327
+ value: "search term",
328
+ },
329
+ ];
330
+ // Requires: CREATE EXTENSION IF NOT EXISTS pg_trgm;
83
331
  ```
84
332
 
85
- ### Encoding Queries
333
+ ## Building Queries
86
334
 
87
- Encode a `CollectionQuery` back to a URL string:
335
+ ### Basic Query Construction
88
336
 
89
337
  ```typescript
90
- import { encodeCollectionQuery } from '@zola_do/collection-query';
338
+ import { QueryConstructor, CollectionQuery } from "@zola_do/collection-query";
339
+
340
+ const query = new CollectionQuery();
341
+ query.take = 10;
342
+ query.skip = 0;
343
+ query.where = [[{ column: "status", value: "active", operator: "=" }]];
344
+ query.orderBy = [{ column: "createdAt", direction: "DESC" }];
345
+
346
+ const dataQuery = QueryConstructor.constructQuery<Product>(repository, query);
347
+ const [items, total] = await dataQuery.getManyAndCount();
91
348
 
349
+ return { total, items };
350
+ ```
351
+
352
+ ### With Relations
353
+
354
+ ```typescript
355
+ const query = decodeCollectionQuery(
356
+ "w=status_:=_:active&i=category,vendor&o=name:asc",
357
+ );
358
+ const dataQuery = QueryConstructor.constructQuery<Product>(repository, query);
359
+ ```
360
+
361
+ ### Programmatic Query Building
362
+
363
+ ```typescript
364
+ import { CollectionQuery, FilterOperators } from "@zola_do/collection-query";
365
+
366
+ const query = new CollectionQuery();
367
+
368
+ // Add filters
369
+ query.where = [
370
+ // Group 1: status AND category
371
+ [
372
+ { column: "status", value: "active", operator: FilterOperators.EqualTo },
373
+ {
374
+ column: "categoryId",
375
+ value: categoryId,
376
+ operator: FilterOperators.EqualTo,
377
+ },
378
+ ],
379
+ // Group 2: OR price conditions
380
+ [
381
+ { column: "price", value: [0, 50], operator: FilterOperators.Between },
382
+ { column: "featured", value: true, operator: FilterOperators.EqualTo },
383
+ ],
384
+ ];
385
+
386
+ // Add sorting
387
+ query.orderBy = [
388
+ { column: "featured", direction: "DESC" },
389
+ { column: "createdAt", direction: "DESC", nulls: "NULLS LAST" },
390
+ ];
391
+
392
+ // Add pagination
393
+ query.take = 20;
394
+ query.skip = 0;
395
+
396
+ // Add includes
397
+ query.includes = ["category", "vendor"];
398
+ ```
399
+
400
+ ## API Reference
401
+
402
+ ### Functions
403
+
404
+ #### `decodeCollectionQuery(q?: string): CollectionQuery`
405
+
406
+ Parses a URL query string into a CollectionQuery object.
407
+
408
+ ```typescript
409
+ const query = decodeCollectionQuery("w=status_:=_:active&t=10&o=createdAt:desc");
410
+ // Returns: CollectionQuery instance
411
+ ```
412
+
413
+ #### `encodeCollectionQuery(query: CollectionQuery): string`
414
+
415
+ Serializes a CollectionQuery back to a URL string.
416
+
417
+ ```typescript
92
418
  const queryString = encodeCollectionQuery(query);
419
+ // Returns: 'w=status_:=_:active&t=10&o=createdAt:desc'
420
+ ```
421
+
422
+ ### Classes
423
+
424
+ #### `QueryConstructor`
425
+
426
+ Static class for building TypeORM queries.
427
+
428
+ ```typescript
429
+ QueryConstructor.constructQuery<T>(
430
+ repository: Repository<T>,
431
+ query: CollectionQuery,
432
+ alias?: string
433
+ ): SelectQueryBuilder<T>
93
434
  ```
94
435
 
95
- ## Exports
436
+ #### `CollectionQuery`
437
+
438
+ Query parameters container.
439
+
440
+ **Properties:**
441
+
442
+ | Property | Type | Description |
443
+ | ---------------------- | -------------------------------- | --------------------------------- |
444
+ | `select` | `string[]` | Columns to select |
445
+ | `where` | `Where[][]` | Filter conditions |
446
+ | `take` | `number` | Limit results |
447
+ | `skip` | `number` | Offset |
448
+ | `cursor` | `string` | Cursor token |
449
+ | `cursorDirection` | `'n' \| 'p' \| 'next' \| 'prev'` | Pagination direction |
450
+ | `orderBy` | `Order[]` | Sort columns |
451
+ | `includes` | `string[]` | Relations to load |
452
+ | `includeAndSelect` | `IncludeSelect[]` | Specific relation fields |
453
+ | `leftJoinAndMapOne` | `any[]` | Custom joins |
454
+ | `groupBy` | `string[]` | Group by columns |
455
+ | `having` | `Where[][]` | Having conditions |
456
+ | `count` | `boolean` | Count only |
457
+ | `fuzzySearch` | `Search[]` | Fuzzy search columns |
458
+ | `search` | `Search[]` | Exact search columns |
459
+ | `allowedFilterColumns` | `string[]` | Whitelist filter columns |
460
+ | `allowedSortColumns` | `string[]` | Whitelist sort columns |
461
+ | `stableSortColumn` | `string` | Stable sort for cursor pagination |
462
+ | `maxIncludeDepth` | `number` | Max relation depth |
463
+
464
+ ### Types
465
+
466
+ ```typescript
467
+ interface CollectionResult<T> {
468
+ total: number;
469
+ items: T[];
470
+ nextCursor?: string;
471
+ prevCursor?: string;
472
+ hasNextPage?: boolean;
473
+ }
474
+
475
+ interface Where {
476
+ column: string;
477
+ value: any;
478
+ operator: string;
479
+ }
480
+
481
+ interface Order {
482
+ column: string;
483
+ direction?: "ASC" | "DESC";
484
+ nulls?: "NULLS FIRST" | "NULLS LAST";
485
+ }
486
+
487
+ interface Search {
488
+ columns: string[];
489
+ value: string;
490
+ }
491
+ ```
492
+
493
+ ## Recommended Imports
494
+
495
+ Subpath imports are recommended for tree-shaking:
496
+
497
+ ```typescript
498
+ import { FilterOperators } from "@zola_do/collection-query/filter-operators";
499
+ import { CollectionQuery } from "@zola_do/collection-query/query";
500
+ import { QueryConstructor } from "@zola_do/collection-query/query-constructor";
501
+ import {
502
+ decodeCollectionQuery,
503
+ encodeCollectionQuery,
504
+ } from "@zola_do/collection-query/query-mapper";
505
+ ```
506
+
507
+ Root import is supported for backward compatibility:
508
+
509
+ ```typescript
510
+ import { FilterOperators, CollectionQuery } from "@zola_do/collection-query";
511
+ ```
512
+
513
+ ## Troubleshooting
514
+
515
+ ### Common Issues
516
+
517
+ **Q: Why are my null filters not working?**
518
+
519
+ ```typescript
520
+ // ❌ Wrong
521
+ { column: 'deletedAt', value: null, operator: FilterOperators.IsNull }
522
+
523
+ // ✅ Correct
524
+ { column: 'deletedAt', value: true, operator: FilterOperators.IsNull }
525
+ ```
526
+
527
+ **Q: How do I handle empty values in IN clauses?**
528
+
529
+ ```typescript
530
+ // Empty array returns no results (safe)
531
+ { column: 'id', value: [], operator: FilterOperators.In }
532
+ ```
533
+
534
+ **Q: Cursor pagination is unstable?**
535
+
536
+ ```typescript
537
+ // Always set a stable sort column
538
+ query.stableSortColumn = "id";
539
+ query.orderBy = [{ column: "createdAt", direction: "DESC" }];
540
+ ```
541
+
542
+ ## Related Packages
543
+
544
+ - [@zola_do/crud](../crud) — Uses collection-query for list endpoints
545
+ - [@zola_do/typeorm](../typeorm) — Database configuration
96
546
 
97
- - `decodeCollectionQuery` — Parse query string to CollectionQuery
98
- - `encodeCollectionQuery` — Serialize CollectionQuery to string
99
- - `QueryConstructor` — Build TypeORM queries from CollectionQuery
100
- - `FilterOperators` — Filter operator constants
101
- - `FilterSeparators` — Separator constants for query strings
102
- - Subpath entrypoints: `@zola_do/collection-query/filter-operators`, `@zola_do/collection-query/query`, `@zola_do/collection-query/query-constructor`, `@zola_do/collection-query/query-mapper`
103
- - Root entrypoint: `@zola_do/collection-query` (supported for backward compatibility)
547
+ ## License
104
548
 
549
+ ISC
105
550
 
106
551
  ## Community
107
552
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zola_do/collection-query",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "TypeORM query builder for filter, sort, paginate",
5
5
  "author": "zolaDO",
6
6
  "license": "ISC",