zod-paginate 1.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/LICENSE.txt +674 -0
- package/README.md +427 -0
- package/dist/main.d.ts +211 -0
- package/dist/main.js +1130 -0
- package/dist/main.js.map +1 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# zod-paginate
|
|
2
|
+
|
|
3
|
+
A small utility to **parse and validate pagination + select + sort + filters** from querystring-like objects using **Zod v4**, and to generate a **response validator** that automatically projects your `dataSchema` based on the requested `select`.
|
|
4
|
+
|
|
5
|
+
It is designed for Node.js HTTP stacks where query parameters arrive as strings (or string arrays). It outputs a **typed, normalized** structure you can map to your ORM/query builder.
|
|
6
|
+
|
|
7
|
+
- Supports **LIMIT/OFFSET pagination** (`limit` + `page`).
|
|
8
|
+
- Supports **CURSOR pagination** with cursor coercion based on `cursorProperty` (number / string / ISO date string).
|
|
9
|
+
- Supports **field projection** using `select` (CSV), including wildcard expansion (`*`) when enabled.
|
|
10
|
+
- Supports **sorting** with an allowlist of sortable fields.
|
|
11
|
+
- Supports a **filter DSL** with `$` operators and **nested AND/OR grouping**.
|
|
12
|
+
- Provides a **response validator** (`validatorSchema`) to validate API responses against the projected schema.
|
|
13
|
+
|
|
14
|
+
> This library does **not** bind DB queries automatically.
|
|
15
|
+
> It gives you a safe parsed structure; you decide how to map it to your data layer.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm i zod-paginate
|
|
21
|
+
# or
|
|
22
|
+
pnpm add zod-paginate
|
|
23
|
+
# or
|
|
24
|
+
yarn add zod-paginate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
import { paginate } from "zod-paginate";
|
|
32
|
+
|
|
33
|
+
const ModelSchema = z.object({
|
|
34
|
+
id: z.number(),
|
|
35
|
+
status: z.string(),
|
|
36
|
+
createdAt: z.date(),
|
|
37
|
+
meta: z.object({
|
|
38
|
+
score: z.number(),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const { queryParamsSchema, validatorSchema } = paginate({
|
|
43
|
+
paginationType: "LIMIT_OFFSET",
|
|
44
|
+
dataSchema: ModelSchema,
|
|
45
|
+
|
|
46
|
+
selectable: ["id", "status", "createdAt", "meta.score"],
|
|
47
|
+
sortable: ["createdAt", "id"],
|
|
48
|
+
filterable: {
|
|
49
|
+
status: { type: "string", ops: ["$eq", "$ilike"] },
|
|
50
|
+
createdAt: { type: "date", ops: ["$btw", "$null", "$eq", "$gt", "$lte"] },
|
|
51
|
+
id: { type: "number", ops: ["$gt", "$in", "$eq"] },
|
|
52
|
+
"meta.score": { type: "number", ops: ["$gte", "$lte"] },
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
defaultSortBy: [{ property: "createdAt", direction: "DESC" }],
|
|
56
|
+
defaultLimit: 20,
|
|
57
|
+
maxLimit: 100,
|
|
58
|
+
defaultSelect: ["*"],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Example querystring-like input
|
|
62
|
+
const parsed = queryParamsSchema.parse({
|
|
63
|
+
limit: "10",
|
|
64
|
+
page: "2",
|
|
65
|
+
sortBy: "createdAt:DESC",
|
|
66
|
+
select: "id,status",
|
|
67
|
+
"filter.status": "$ilike:act",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(parsed.pagination);
|
|
71
|
+
|
|
72
|
+
// Build the response validator from the request context
|
|
73
|
+
const responseSchema = validatorSchema(parsed);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## API
|
|
77
|
+
|
|
78
|
+
### `paginate(config)`
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
|
|
82
|
+
- `queryParamsSchema`: Zod schema to parse query objects (strings / string arrays).
|
|
83
|
+
- `validatorSchema(parsed?)`: function returning a Zod schema to validate the response payload.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
export function paginate<TSchema extends DataSchema>(
|
|
87
|
+
config: QueryConfigFromSchema<TSchema>,
|
|
88
|
+
): {
|
|
89
|
+
queryParamsSchema: z.ZodType<PaginationQueryParams<TSchema>>;
|
|
90
|
+
validatorSchema: (parsed?: PaginationQueryParams<TSchema>) => z.ZodType;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Configuration (`paginate({...})`)
|
|
95
|
+
|
|
96
|
+
| Option | Type | Description |
|
|
97
|
+
|---|---:|---|
|
|
98
|
+
| `paginationType` | `"LIMIT_OFFSET"` \| `"CURSOR"` | Select pagination mode. |
|
|
99
|
+
| `dataSchema` | `z.ZodObject` | Zod schema representing one **data item** returned by your API (used for projection + cursor inference). |
|
|
100
|
+
| `selectable?` | `string[]` (typed paths) | Allowlist of selectable fields (dot paths supported). Enables `select`. |
|
|
101
|
+
| `sortable?` | `string[]` (typed paths) | Allowlist of sortable fields. Enables `sortBy`. |
|
|
102
|
+
| `filterable?` | object | Allowlist of filterable fields and allowed operators + field type. |
|
|
103
|
+
| `defaultSortBy?` | `{ property, direction }[]` | Default sort if `sortBy` missing/empty. |
|
|
104
|
+
| `defaultLimit?` | `number` | Default limit if `limit` missing. |
|
|
105
|
+
| `maxLimit?` | `number` | Rejects `limit` values above this. |
|
|
106
|
+
| `defaultSelect?` | `("*" \| field)[]` | Default select if `select` missing. `["*"]` expands to `selectable`. |
|
|
107
|
+
| `cursorProperty` | (CURSOR only) typed path | The field used for cursor paging. Cursor type is inferred from `dataSchema` at that path and the query input cursor is coerced accordingly. |
|
|
108
|
+
|
|
109
|
+
## Query input shape
|
|
110
|
+
|
|
111
|
+
`queryParamsSchema` accepts any record-like input:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
Record<string, unknown>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Typical querystring parsers produce values like:
|
|
118
|
+
|
|
119
|
+
- `"10"` (string)
|
|
120
|
+
- `["a", "b"]` (repeated query params)
|
|
121
|
+
- everything else is ignored / treated as undefined
|
|
122
|
+
|
|
123
|
+
## Query parameters
|
|
124
|
+
|
|
125
|
+
### `limit`
|
|
126
|
+
|
|
127
|
+
- Input: string numeric (e.g. `"10"`)
|
|
128
|
+
- Output: number
|
|
129
|
+
- Rules
|
|
130
|
+
- Must be a numeric string
|
|
131
|
+
- Must be `<= maxLimit` if configured
|
|
132
|
+
- Falls back to `defaultLimit` when missing
|
|
133
|
+
|
|
134
|
+
### `page` (LIMIT_OFFSET only)
|
|
135
|
+
|
|
136
|
+
- Input: string numeric (e.g. `"2"`)
|
|
137
|
+
- Output: number
|
|
138
|
+
- Rules
|
|
139
|
+
- Only valid when `paginationType: "LIMIT_OFFSET"`
|
|
140
|
+
- Forbidden in CURSOR mode
|
|
141
|
+
|
|
142
|
+
### `cursor` (CURSOR only)
|
|
143
|
+
|
|
144
|
+
- Input: string (querystring input is always string)
|
|
145
|
+
- Output: `number | string` (coerced)
|
|
146
|
+
- Rules
|
|
147
|
+
- Only valid when `paginationType: "CURSOR"`
|
|
148
|
+
- Forbidden in LIMIT_OFFSET mode
|
|
149
|
+
- If provided, it is coerced based on the Zod type of `cursorProperty` in `dataSchema`:
|
|
150
|
+
- `z.number()` field → `"123"` becomes `123` (integer-only)
|
|
151
|
+
- `z.string()` field → `"abc"` stays `"abc"`
|
|
152
|
+
- `z.date()` field → must be ISO date or ISO datetime, stays a string (`"2022-01-01"` or `"2022-01-01T12:00:00Z"`)
|
|
153
|
+
|
|
154
|
+
### `sortBy`
|
|
155
|
+
|
|
156
|
+
- Input: string or string[]
|
|
157
|
+
- Output: `[{ property, direction }]`
|
|
158
|
+
- Rules
|
|
159
|
+
- Requires `sortable` in config
|
|
160
|
+
- Format: `field:ASC` or `field:DESC`
|
|
161
|
+
- Empty items are ignored
|
|
162
|
+
- If missing (or becomes empty after cleanup), falls back to `defaultSortBy` if configured
|
|
163
|
+
- Properties are matched against the allowlist (unknown fields are dropped)
|
|
164
|
+
|
|
165
|
+
### `select`
|
|
166
|
+
|
|
167
|
+
- Input: CSV string
|
|
168
|
+
- Output: string[] (typed paths)
|
|
169
|
+
- Rules
|
|
170
|
+
- Requires `selectable` in config
|
|
171
|
+
- CSV is split by `,`, trimmed, empty items removed
|
|
172
|
+
- `*` expands to the configured `selectable` allowlist
|
|
173
|
+
- If missing, falls back to `defaultSelect` if configured
|
|
174
|
+
- `select=` (empty) is rejected
|
|
175
|
+
- Unknown fields are rejected at parse-time (strict allowlist)
|
|
176
|
+
|
|
177
|
+
## Filters
|
|
178
|
+
|
|
179
|
+
Filters are passed as query keys with this pattern:
|
|
180
|
+
|
|
181
|
+
```txt
|
|
182
|
+
filter.<field>=<dsl>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Where `<field>` is a dot-path field (example: `meta.score`).
|
|
186
|
+
|
|
187
|
+
You configure which fields are filterable and which operators are allowed via `filterable`.
|
|
188
|
+
|
|
189
|
+
### Operators
|
|
190
|
+
|
|
191
|
+
| Operator | Meaning | Value format |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `$eq` | equals | number / string / ISO date depending on field type |
|
|
194
|
+
| `$null` | is null | no value |
|
|
195
|
+
| `$in` | in list | `a,b,c` (comma-separated) |
|
|
196
|
+
| `$contains` | contains values | `a,b,c` (comma-separated) |
|
|
197
|
+
| `$gt` `$gte` `$lt` `$lte` | comparisons | number or ISO date |
|
|
198
|
+
| `$btw` | between | `a,b` where both are numbers OR both are ISO dates |
|
|
199
|
+
| `$ilike` | case-insensitive contains (string) | string |
|
|
200
|
+
| `$sw` | starts with (string) | string |
|
|
201
|
+
|
|
202
|
+
Runtime validation enforces:
|
|
203
|
+
|
|
204
|
+
1) field allowlist (`filterable`)
|
|
205
|
+
2) operator allowlist per field (`ops`)
|
|
206
|
+
3) value type compatibility (number vs date vs string)
|
|
207
|
+
|
|
208
|
+
### Default operator: `$eq`
|
|
209
|
+
|
|
210
|
+
If the filter does **not** start with `$`, it is interpreted as `$eq:<value>`.
|
|
211
|
+
|
|
212
|
+
### Negation: `$not`
|
|
213
|
+
|
|
214
|
+
Prefix any operator with `$not:` to negate the condition.
|
|
215
|
+
|
|
216
|
+
Examples:
|
|
217
|
+
|
|
218
|
+
```txt
|
|
219
|
+
filter.createdAt=$not:$null
|
|
220
|
+
filter.status=$not:$eq:active
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Multiple conditions for the same field
|
|
224
|
+
|
|
225
|
+
Use repeated query params:
|
|
226
|
+
|
|
227
|
+
```txt
|
|
228
|
+
filter.id=$gt:10&filter.id=$lt:100
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Or in object form:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
{
|
|
235
|
+
"filter.id": ["$gt:10", "$lt:100"]
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Groups
|
|
240
|
+
|
|
241
|
+
Groups let you build nested AND/OR boolean logic.
|
|
242
|
+
|
|
243
|
+
There are two layers:
|
|
244
|
+
|
|
245
|
+
1) Combine multiple conditions inside the same group
|
|
246
|
+
2) Build a group tree (attach groups as children of other groups)
|
|
247
|
+
|
|
248
|
+
### Put a condition into a group: `$g:<id>`
|
|
249
|
+
|
|
250
|
+
Prefix any filter DSL with:
|
|
251
|
+
|
|
252
|
+
```txt
|
|
253
|
+
$g:<groupId>:
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Combine conditions inside a group: `$and` / `$or`
|
|
257
|
+
|
|
258
|
+
Within a group, the **first** condition cannot have `$and`/`$or`. All following conditions may be prefixed with `$and` or `$or`.
|
|
259
|
+
|
|
260
|
+
### Group tree definitions: `group.<id>.*`
|
|
261
|
+
|
|
262
|
+
To nest groups, define these query keys:
|
|
263
|
+
|
|
264
|
+
- `group.<id>.parent` — parent group id (integer string)
|
|
265
|
+
- `group.<id>.join` — how this group is joined to its parent (`$and` or `$or`)
|
|
266
|
+
- `group.<id>.op` — default join used when combining this group's children (optional)
|
|
267
|
+
|
|
268
|
+
Rules:
|
|
269
|
+
|
|
270
|
+
- Root group id is always `"0"`.
|
|
271
|
+
- `group.0.parent` and `group.0.join` are forbidden.
|
|
272
|
+
- Cycles are rejected.
|
|
273
|
+
- Child groups are resolved in numeric order (deterministic).
|
|
274
|
+
|
|
275
|
+
## Validating responses with `validatorSchema()`
|
|
276
|
+
|
|
277
|
+
`validatorSchema(parsed)` returns a Zod schema you can use to validate your API response.
|
|
278
|
+
|
|
279
|
+
What it does:
|
|
280
|
+
|
|
281
|
+
- Uses the effective `select` (explicit `select`, else `defaultSelect`, else full schema) to project the item schema.
|
|
282
|
+
- Validates cursor type (CURSOR mode) based on `cursorProperty`.
|
|
283
|
+
- Enforces mode-specific pagination metadata shape.
|
|
284
|
+
|
|
285
|
+
### What `validatorSchema(parsed)` expects
|
|
286
|
+
|
|
287
|
+
**LIMIT/OFFSET mode**:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
{
|
|
291
|
+
data: Array<ProjectedItem>,
|
|
292
|
+
pagination: {
|
|
293
|
+
itemsPerPage: number,
|
|
294
|
+
totalItems: number,
|
|
295
|
+
currentPage: number,
|
|
296
|
+
totalPages: number,
|
|
297
|
+
sortBy?: Array<{ property: string, direction: "ASC" | "DESC" }>,
|
|
298
|
+
filter?: WhereNode
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**CURSOR mode**:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
{
|
|
307
|
+
data: Array<ProjectedItem>,
|
|
308
|
+
pagination: {
|
|
309
|
+
itemsPerPage: number,
|
|
310
|
+
cursor: number | string | Date,
|
|
311
|
+
sortBy?: Array<{ property: string, direction: "ASC" | "DESC" }>,
|
|
312
|
+
filter?: WhereNode
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Notes:
|
|
318
|
+
|
|
319
|
+
- `ProjectedItem` is computed from `dataSchema` + the effective `select`.
|
|
320
|
+
- If `cursorProperty` points to a `z.number()` field, `pagination.cursor` must be a number.
|
|
321
|
+
- If `cursorProperty` points to a `z.string()` field, `pagination.cursor` must be a string.
|
|
322
|
+
- If `cursorProperty` points to a `z.date()` field, this library accepts an ISO string or a `Date` (depending on implementation).
|
|
323
|
+
|
|
324
|
+
You can call `validatorSchema()` without arguments to build a validator based on defaults (`defaultSelect`, `cursorProperty`, etc.).
|
|
325
|
+
|
|
326
|
+
## End-to-end examples
|
|
327
|
+
|
|
328
|
+
### Example 1 — LIMIT/OFFSET
|
|
329
|
+
|
|
330
|
+
HTTP query:
|
|
331
|
+
|
|
332
|
+
```txt
|
|
333
|
+
?limit=20&page=1&select=id,status,createdAt&sortBy=createdAt:DESC&filter.status=$ilike:act&filter.id=$gt:10
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Parsing:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
const parsed = queryParamsSchema.parse({
|
|
340
|
+
limit: "20",
|
|
341
|
+
page: "1",
|
|
342
|
+
select: "id,status,createdAt",
|
|
343
|
+
sortBy: "createdAt:DESC",
|
|
344
|
+
"filter.status": "$ilike:act",
|
|
345
|
+
"filter.id": "$gt:10",
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// parsed.pagination
|
|
349
|
+
// {
|
|
350
|
+
// type: "LIMIT_OFFSET",
|
|
351
|
+
// limit: 20,
|
|
352
|
+
// page: 1,
|
|
353
|
+
// select: ["id", "status", "createdAt"],
|
|
354
|
+
// sortBy: [{ property: "createdAt", direction: "DESC" }],
|
|
355
|
+
// filters: { type: "and", items: [...] } // WhereNode AST
|
|
356
|
+
// }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Example 2 — CURSOR + coercion
|
|
360
|
+
|
|
361
|
+
Config:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
const { queryParamsSchema } = paginate({
|
|
365
|
+
paginationType: "CURSOR",
|
|
366
|
+
dataSchema: ModelSchema,
|
|
367
|
+
cursorProperty: "id", // id is z.number()
|
|
368
|
+
selectable: ["id", "status", "createdAt"],
|
|
369
|
+
defaultSelect: ["id", "createdAt"],
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Parsing:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
const parsed = queryParamsSchema.parse({ cursor: "123", limit: "10" });
|
|
377
|
+
|
|
378
|
+
// parsed.pagination
|
|
379
|
+
// {
|
|
380
|
+
// type: "CURSOR",
|
|
381
|
+
// limit: 10,
|
|
382
|
+
// cursor: 123, // <- coerced from "123" because cursorProperty is a number
|
|
383
|
+
// cursorProperty: "id",
|
|
384
|
+
// select: ["id", "createdAt"],
|
|
385
|
+
// filters: { type: "and", items: [] }
|
|
386
|
+
// }
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Example 3 — groups
|
|
390
|
+
|
|
391
|
+
Goal: `(status == active OR status == postponed) AND (id > 10)`
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
const parsed = queryParamsSchema.parse({
|
|
395
|
+
"filter.status": ["$g:1:$eq:active", "$g:1:$or:$eq:postponed"],
|
|
396
|
+
"filter.id": "$g:2:$gt:10",
|
|
397
|
+
|
|
398
|
+
"group.1.parent": "0",
|
|
399
|
+
"group.2.parent": "0",
|
|
400
|
+
"group.2.join": "$and",
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// parsed.pagination.filters
|
|
404
|
+
// {
|
|
405
|
+
// type: "and",
|
|
406
|
+
// items: [
|
|
407
|
+
// { type: "or", items: [ ...status filters... ] },
|
|
408
|
+
// { type: "filter", field: "id", condition: { op: "$gt", value: 10, ... } }
|
|
409
|
+
// ]
|
|
410
|
+
// }
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Example 4 — validating your response
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const parsed = queryParamsSchema.parse({ select: "id,status", limit: "10", page: "1" });
|
|
417
|
+
const responseSchema = validatorSchema(parsed);
|
|
418
|
+
|
|
419
|
+
// responseSchema expects:
|
|
420
|
+
// - data items shaped like { id: number, status: string }
|
|
421
|
+
// - pagination metadata for LIMIT/OFFSET
|
|
422
|
+
|
|
423
|
+
responseSchema.parse({
|
|
424
|
+
data: [{ id: 1, status: "active" }],
|
|
425
|
+
pagination: { itemsPerPage: 10, totalItems: 1, currentPage: 1, totalPages: 1 },
|
|
426
|
+
});
|
|
427
|
+
```
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Primitive types that we consider as leaves in the Path type. Arrays are also considered leaves, since we don't want to generate paths like "arrayField.0.someProp".
|
|
4
|
+
*/
|
|
5
|
+
type Primitive = string | number | boolean | bigint | symbol | null | undefined | Date;
|
|
6
|
+
/**
|
|
7
|
+
* Join two path segments K and P with a dot, if both are strings. Otherwise, return never.
|
|
8
|
+
*/
|
|
9
|
+
type Join<K, P> = K extends string ? (P extends string ? `${K}.${P}` : never) : never;
|
|
10
|
+
/**
|
|
11
|
+
* Generate dot notation paths for a given type T, up to a certain depth D (default 5).
|
|
12
|
+
* For example, for { a: { b: string }, c: number }, we would generate "a", "a.b", and "c". We stop recursion at depth 0 to prevent infinite types.
|
|
13
|
+
*/
|
|
14
|
+
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
15
|
+
/**
|
|
16
|
+
* Generate dot notation paths for a given type T. For example, for { a: { b: string }, c: number }, we would generate "a", "a.b", and "c".
|
|
17
|
+
*/
|
|
18
|
+
type Path<T, D extends number = 5> = D extends 0 ? never : T extends Primitive ? never : T extends readonly unknown[] ? never : {
|
|
19
|
+
[K in Extract<keyof T, string>]: T[K] extends Primitive | readonly unknown[] ? K : K | Join<K, Path<T[K], Prev[D]>>;
|
|
20
|
+
}[Extract<keyof T, string>];
|
|
21
|
+
/**
|
|
22
|
+
* Given a type T and a dot notation path P, resolve the type at that path.
|
|
23
|
+
* For example, for T = { a: { b: string }, c: number } and P = "a.b", we would get string.
|
|
24
|
+
*/
|
|
25
|
+
type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<T[K], Rest> : never : P extends keyof T ? T[P] : never;
|
|
26
|
+
interface LimitOffsetPaginationConfig {
|
|
27
|
+
paginationType: 'LIMIT_OFFSET';
|
|
28
|
+
}
|
|
29
|
+
interface CursorPaginationConfig<T> {
|
|
30
|
+
paginationType: 'CURSOR';
|
|
31
|
+
cursorProperty: Path<T>;
|
|
32
|
+
}
|
|
33
|
+
declare const SortDirectionSchema: z.ZodEnum<{
|
|
34
|
+
ASC: "ASC";
|
|
35
|
+
DESC: "DESC";
|
|
36
|
+
}>;
|
|
37
|
+
type SortDirection = z.infer<typeof SortDirectionSchema>;
|
|
38
|
+
/**
|
|
39
|
+
* Supported operators.
|
|
40
|
+
* $eq: equality (for strings, numbers, dates)
|
|
41
|
+
* $null: checks for null (ignores the value)
|
|
42
|
+
* $in: checks if the field value is in the provided array (for strings, numbers, dates)
|
|
43
|
+
* $gt, $gte, $lt, $lte: comparison operators (for numbers and dates)
|
|
44
|
+
* $btw: checks if the field value is between two values (for numbers and dates)
|
|
45
|
+
* $ilike: case-insensitive substring match (for strings)
|
|
46
|
+
* $sw: case-insensitive starts-with match (for strings)
|
|
47
|
+
* $contains: checks if the field value contains the provided value (for strings)
|
|
48
|
+
*/
|
|
49
|
+
declare const OperatorSchema: z.ZodEnum<{
|
|
50
|
+
$eq: "$eq";
|
|
51
|
+
$null: "$null";
|
|
52
|
+
$in: "$in";
|
|
53
|
+
$gt: "$gt";
|
|
54
|
+
$gte: "$gte";
|
|
55
|
+
$lt: "$lt";
|
|
56
|
+
$lte: "$lte";
|
|
57
|
+
$btw: "$btw";
|
|
58
|
+
$ilike: "$ilike";
|
|
59
|
+
$sw: "$sw";
|
|
60
|
+
$contains: "$contains";
|
|
61
|
+
}>;
|
|
62
|
+
type Operator = z.infer<typeof OperatorSchema>;
|
|
63
|
+
type FieldType = 'string' | 'number' | 'date' | 'any';
|
|
64
|
+
type FieldTypeFromValue<V> = V extends Date ? 'date' : V extends number ? 'number' : V extends string ? 'string' : 'any';
|
|
65
|
+
type CommonOps = '$eq' | '$null' | '$in' | '$contains';
|
|
66
|
+
type StringOnlyOps = '$ilike' | '$sw';
|
|
67
|
+
type ComparableOps = '$gt' | '$gte' | '$lt' | '$lte' | '$btw';
|
|
68
|
+
type OpsForFieldType<TKind extends FieldType> = TKind extends 'string' ? CommonOps | StringOnlyOps : TKind extends 'number' ? CommonOps | ComparableOps : TKind extends 'date' ? CommonOps | ComparableOps : Operator;
|
|
69
|
+
declare const ConditionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
70
|
+
group: z.ZodString;
|
|
71
|
+
combinator: z.ZodOptional<z.ZodEnum<{
|
|
72
|
+
$and: "$and";
|
|
73
|
+
$or: "$or";
|
|
74
|
+
}>>;
|
|
75
|
+
op: z.ZodLiteral<"$null">;
|
|
76
|
+
not: z.ZodOptional<z.ZodLiteral<true>>;
|
|
77
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
78
|
+
group: z.ZodString;
|
|
79
|
+
combinator: z.ZodOptional<z.ZodEnum<{
|
|
80
|
+
$and: "$and";
|
|
81
|
+
$or: "$or";
|
|
82
|
+
}>>;
|
|
83
|
+
op: z.ZodLiteral<"$eq">;
|
|
84
|
+
not: z.ZodOptional<z.ZodLiteral<true>>;
|
|
85
|
+
value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
|
86
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
87
|
+
group: z.ZodString;
|
|
88
|
+
combinator: z.ZodOptional<z.ZodEnum<{
|
|
89
|
+
$and: "$and";
|
|
90
|
+
$or: "$or";
|
|
91
|
+
}>>;
|
|
92
|
+
op: z.ZodEnum<{
|
|
93
|
+
$ilike: "$ilike";
|
|
94
|
+
$sw: "$sw";
|
|
95
|
+
}>;
|
|
96
|
+
not: z.ZodOptional<z.ZodLiteral<true>>;
|
|
97
|
+
value: z.ZodString;
|
|
98
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
99
|
+
group: z.ZodString;
|
|
100
|
+
combinator: z.ZodOptional<z.ZodEnum<{
|
|
101
|
+
$and: "$and";
|
|
102
|
+
$or: "$or";
|
|
103
|
+
}>>;
|
|
104
|
+
op: z.ZodEnum<{
|
|
105
|
+
$in: "$in";
|
|
106
|
+
$contains: "$contains";
|
|
107
|
+
}>;
|
|
108
|
+
not: z.ZodOptional<z.ZodLiteral<true>>;
|
|
109
|
+
value: z.ZodArray<z.ZodString>;
|
|
110
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
111
|
+
group: z.ZodString;
|
|
112
|
+
combinator: z.ZodOptional<z.ZodEnum<{
|
|
113
|
+
$and: "$and";
|
|
114
|
+
$or: "$or";
|
|
115
|
+
}>>;
|
|
116
|
+
op: z.ZodEnum<{
|
|
117
|
+
$gt: "$gt";
|
|
118
|
+
$gte: "$gte";
|
|
119
|
+
$lt: "$lt";
|
|
120
|
+
$lte: "$lte";
|
|
121
|
+
}>;
|
|
122
|
+
not: z.ZodOptional<z.ZodLiteral<true>>;
|
|
123
|
+
value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
|
124
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
125
|
+
group: z.ZodString;
|
|
126
|
+
combinator: z.ZodOptional<z.ZodEnum<{
|
|
127
|
+
$and: "$and";
|
|
128
|
+
$or: "$or";
|
|
129
|
+
}>>;
|
|
130
|
+
op: z.ZodLiteral<"$btw">;
|
|
131
|
+
not: z.ZodOptional<z.ZodLiteral<true>>;
|
|
132
|
+
value: z.ZodTuple<[z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>], null>;
|
|
133
|
+
}, z.core.$strip>], "op">;
|
|
134
|
+
type Condition = z.infer<typeof ConditionSchema>;
|
|
135
|
+
interface WhereFilter {
|
|
136
|
+
type: 'filter';
|
|
137
|
+
field: string;
|
|
138
|
+
condition: Condition;
|
|
139
|
+
}
|
|
140
|
+
interface WhereAnd {
|
|
141
|
+
type: 'and';
|
|
142
|
+
items: WhereNode[];
|
|
143
|
+
}
|
|
144
|
+
interface WhereOr {
|
|
145
|
+
type: 'or';
|
|
146
|
+
items: WhereNode[];
|
|
147
|
+
}
|
|
148
|
+
type WhereNode = WhereFilter | WhereAnd | WhereOr;
|
|
149
|
+
export type DataSchema = z.ZodObject<z.ZodRawShape>;
|
|
150
|
+
export type InferData<TSchema extends DataSchema> = z.infer<TSchema>;
|
|
151
|
+
export type AllowedPath<TSchema extends DataSchema> = Path<InferData<TSchema>>;
|
|
152
|
+
interface FilterableFieldConfig<TKind extends FieldType> {
|
|
153
|
+
type: TKind;
|
|
154
|
+
ops: readonly OpsForFieldType<TKind>[];
|
|
155
|
+
}
|
|
156
|
+
export interface CommonQueryConfigFromSchema<TSchema extends DataSchema> {
|
|
157
|
+
dataSchema: TSchema;
|
|
158
|
+
selectable?: readonly AllowedPath<TSchema>[];
|
|
159
|
+
sortable?: readonly AllowedPath<TSchema>[];
|
|
160
|
+
filterable?: Partial<{
|
|
161
|
+
[P in AllowedPath<TSchema>]: FilterableFieldConfig<FieldTypeFromValue<PathValue<InferData<TSchema>, P>>>;
|
|
162
|
+
}>;
|
|
163
|
+
defaultSortBy?: readonly {
|
|
164
|
+
property: AllowedPath<TSchema>;
|
|
165
|
+
direction: SortDirection;
|
|
166
|
+
}[];
|
|
167
|
+
defaultLimit?: number;
|
|
168
|
+
defaultSelect?: readonly (AllowedPath<TSchema> | '*')[];
|
|
169
|
+
maxLimit?: number;
|
|
170
|
+
}
|
|
171
|
+
export type QueryConfigFromSchema<TSchema extends DataSchema> = CommonQueryConfigFromSchema<TSchema> & (LimitOffsetPaginationConfig | CursorPaginationConfig<InferData<TSchema>>);
|
|
172
|
+
export interface SortItemTyped<TSchema extends DataSchema> {
|
|
173
|
+
property: AllowedPath<TSchema>;
|
|
174
|
+
direction: SortDirection;
|
|
175
|
+
}
|
|
176
|
+
export interface LimitOffsetPaginationPayload<TSchema extends DataSchema> {
|
|
177
|
+
type: 'LIMIT_OFFSET';
|
|
178
|
+
limit?: number;
|
|
179
|
+
page?: number;
|
|
180
|
+
sortBy?: SortItemTyped<TSchema>[];
|
|
181
|
+
select?: AllowedPath<TSchema>[];
|
|
182
|
+
filters?: WhereNode;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Cursor is always a string in the query input, BUT we coerce it at parse-time
|
|
186
|
+
* to match the type of cursorProperty (number / string / ISO date string).
|
|
187
|
+
*/
|
|
188
|
+
export interface CursorPaginationPayload<TSchema extends DataSchema> {
|
|
189
|
+
type: 'CURSOR';
|
|
190
|
+
limit?: number;
|
|
191
|
+
cursor?: number | string;
|
|
192
|
+
cursorProperty: AllowedPath<TSchema>;
|
|
193
|
+
sortBy?: SortItemTyped<TSchema>[];
|
|
194
|
+
select?: AllowedPath<TSchema>[];
|
|
195
|
+
filters?: WhereNode;
|
|
196
|
+
}
|
|
197
|
+
export interface PaginationQueryParams<TSchema extends DataSchema> {
|
|
198
|
+
pagination: LimitOffsetPaginationPayload<TSchema> | CursorPaginationPayload<TSchema>;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Generate Zod schemas and runtime validators for pagination query parameters, based on a config object.
|
|
202
|
+
* @param config The configuration object defining the pagination behavior and allowed fields.
|
|
203
|
+
* @returns An object containing:
|
|
204
|
+
* - `queryParamsSchema`: A Zod schema for validating and parsing the raw query parameters.
|
|
205
|
+
* - `validatorSchema`: A function that takes the already-parsed query parameters and returns a Zod schema for further validation (e.g. filters).
|
|
206
|
+
*/
|
|
207
|
+
export declare function paginate<TSchema extends DataSchema>(config: QueryConfigFromSchema<TSchema>): {
|
|
208
|
+
queryParamsSchema: z.ZodType<PaginationQueryParams<TSchema>>;
|
|
209
|
+
validatorSchema: (parsed?: PaginationQueryParams<TSchema>) => z.ZodType;
|
|
210
|
+
};
|
|
211
|
+
export {};
|