@zola_do/collection-query 0.2.4 → 0.2.6
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 +497 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# @zola_do/collection-query
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@zola_do/collection-query)
|
|
4
|
+
[](https://www.npmjs.com/package/@zola_do/collection-query)
|
|
5
|
+
[](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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
216
|
+
### Cursor Pagination
|
|
28
217
|
|
|
29
|
-
|
|
218
|
+
Cursor pagination is recommended for:
|
|
30
219
|
|
|
31
|
-
|
|
220
|
+
- Live data (feeds, activity logs)
|
|
221
|
+
- Deep pagination
|
|
222
|
+
- Data that changes frequently
|
|
32
223
|
|
|
33
224
|
```typescript
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
256
|
+
## Sorting
|
|
44
257
|
|
|
45
|
-
|
|
258
|
+
### Basic Sorting
|
|
46
259
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
271
|
+
### Null Handling
|
|
58
272
|
|
|
59
|
-
|
|
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
|
-
|
|
283
|
+
## Relation Loading
|
|
62
284
|
|
|
63
|
-
|
|
285
|
+
### Includes
|
|
64
286
|
|
|
65
287
|
```typescript
|
|
66
|
-
|
|
288
|
+
// Simple includes
|
|
289
|
+
query.includes = ["category", "vendor"];
|
|
67
290
|
|
|
68
|
-
|
|
69
|
-
|
|
291
|
+
// Nested includes
|
|
292
|
+
query.includes = ["category.parent", "vendor.address"];
|
|
70
293
|
```
|
|
71
294
|
|
|
72
|
-
###
|
|
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
|
-
|
|
306
|
+
### Left Join And Map One
|
|
75
307
|
|
|
76
308
|
```typescript
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
333
|
+
## Building Queries
|
|
86
334
|
|
|
87
|
-
|
|
335
|
+
### Basic Query Construction
|
|
88
336
|
|
|
89
337
|
```typescript
|
|
90
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|