zod-paginate 1.0.1 → 1.1.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
@@ -6,10 +6,11 @@ It is designed for Node.js HTTP stacks where query parameters arrive as strings
6
6
 
7
7
  - Supports **LIMIT/OFFSET pagination** (`limit` + `page`).
8
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.
9
+ - Supports **field projection** using `select`, including wildcard expansion (`*`) when enabled.
10
10
  - Supports **sorting** with an allowlist of sortable fields.
11
11
  - Supports a **filter DSL** with `$` operators and **nested AND/OR grouping**.
12
12
  - Provides a **response validator** (`validatorSchema`) to validate API responses against the projected schema.
13
+ - Also exports a lightweight **`select()`** utility for field-projection-only use cases.
13
14
 
14
15
  > This library does **not** bind DB queries automatically.
15
16
  > It gives you a safe parsed structure; you decide how to map it to your data layer.
@@ -164,11 +165,11 @@ Typical querystring parsers produce values like:
164
165
 
165
166
  ### `select`
166
167
 
167
- - Input: CSV string
168
+ - Input: string
168
169
  - Output: string[] (typed paths)
169
170
  - Rules
170
171
  - Requires `selectable` in config
171
- - CSV is split by `,`, trimmed, empty items removed
172
+ - string is split by `,`, trimmed, empty items removed
172
173
  - `*` expands to the configured `selectable` allowlist
173
174
  - If missing, falls back to `defaultSelect` if configured
174
175
  - `select=` (empty) is rejected
@@ -194,11 +195,103 @@ You configure which fields are filterable and which operators are allowed via `f
194
195
  | `$null` | is null | no value |
195
196
  | `$in` | in list | `a,b,c` (comma-separated) |
196
197
  | `$contains` | contains values | `a,b,c` (comma-separated) |
197
- | `$gt` `$gte` `$lt` `$lte` | comparisons | number or ISO date |
198
+ | `$gt` | greater than | number or ISO date |
199
+ | `$gte` | greater than or equal | number or ISO date |
200
+ | `$lt` | less than | number or ISO date |
201
+ | `$lte` | less than or equal | number or ISO date |
198
202
  | `$btw` | between | `a,b` where both are numbers OR both are ISO dates |
199
203
  | `$ilike` | case-insensitive contains (string) | string |
200
204
  | `$sw` | starts with (string) | string |
201
205
 
206
+ #### `$eq` — equals
207
+
208
+ Matches rows where the field is exactly equal to the given value. The value type must match the field type (number, string, or ISO date).
209
+
210
+ ```txt
211
+ filter.status=$eq:active
212
+ filter.id=$eq:42
213
+ filter.createdAt=$eq:2025-01-15
214
+ ```
215
+
216
+ #### `$null` — is null
217
+
218
+ Matches rows where the field is `NULL`. No value is required after the operator.
219
+
220
+ ```txt
221
+ filter.deletedAt=$null
222
+ ```
223
+
224
+ To match rows where the field is **not** null, combine with `$not`:
225
+
226
+ ```txt
227
+ filter.deletedAt=$not:$null
228
+ ```
229
+
230
+ #### `$in` — in list
231
+
232
+ Matches rows where the field value is one of the provided comma-separated values.
233
+
234
+ ```txt
235
+ filter.status=$in:active,pending,review
236
+ filter.id=$in:1,2,3,10
237
+ ```
238
+
239
+ #### `$contains` — contains values
240
+
241
+ Matches rows where the field (typically an array column) contains all the provided comma-separated values.
242
+
243
+ ```txt
244
+ filter.tags=$contains:typescript,zod
245
+ filter.roles=$contains:admin
246
+ ```
247
+
248
+ #### `$gt` / `$gte` / `$lt` / `$lte` — comparisons
249
+
250
+ Standard comparison operators: greater than, greater than or equal, less than, less than or equal. Works with numbers and ISO dates.
251
+
252
+ ```txt
253
+ filter.id=$gt:100
254
+ filter.id=$lte:500
255
+ filter.createdAt=$gte:2025-01-01
256
+ filter.createdAt=$lt:2025-06-01T00:00:00Z
257
+ ```
258
+
259
+ Combine multiple comparisons to build ranges:
260
+
261
+ ```txt
262
+ filter.id=$gt:10&filter.id=$lt:100
263
+ ```
264
+
265
+ #### `$btw` — between
266
+
267
+ Matches rows where the field value falls between two bounds (inclusive). Both bounds must be the same type — either both numbers or both ISO dates.
268
+
269
+ ```txt
270
+ filter.id=$btw:10,100
271
+ filter.createdAt=$btw:2025-01-01,2025-12-31
272
+ filter.createdAt=$btw:2025-01-01T00:00:00Z,2025-06-30T23:59:59Z
273
+ ```
274
+
275
+ #### `$ilike` — case-insensitive contains
276
+
277
+ Matches rows where the string field contains the given substring, ignoring case. Useful for search-style filtering.
278
+
279
+ ```txt
280
+ filter.status=$ilike:act
281
+ filter.name=$ilike:john
282
+ filter.email=$ilike:@example.com
283
+ ```
284
+
285
+ #### `$sw` — starts with
286
+
287
+ Matches rows where the string field starts with the given prefix.
288
+
289
+ ```txt
290
+ filter.name=$sw:Jon
291
+ filter.email=$sw:admin@
292
+ filter.path=$sw:/api/v2
293
+ ```
294
+
202
295
  Runtime validation enforces:
203
296
 
204
297
  1) field allowlist (`filterable`)
@@ -381,8 +474,7 @@ const parsed = queryParamsSchema.parse({ cursor: "123", limit: "10" });
381
474
  // limit: 10,
382
475
  // cursor: 123, // <- coerced from "123" because cursorProperty is a number
383
476
  // cursorProperty: "id",
384
- // select: ["id", "createdAt"],
385
- // filters: { type: "and", items: [] }
477
+ // select: ["id", "createdAt"]
386
478
  // }
387
479
  ```
388
480
 
@@ -425,3 +517,77 @@ responseSchema.parse({
425
517
  pagination: { itemsPerPage: 10, totalItems: 1, currentPage: 1, totalPages: 1 },
426
518
  });
427
519
  ```
520
+
521
+ ## `select()` — standalone field projection
522
+
523
+ If you only need **field projection** without pagination, sorting, or filters, you can use the `select()` utility directly.
524
+
525
+ ### API
526
+
527
+ ```ts
528
+ import { select } from "zod-paginate";
529
+
530
+ export function select<TSchema extends DataSchema>(
531
+ config: SelectConfig<TSchema>,
532
+ ): {
533
+ queryParamsSchema: z.ZodType<SelectQueryParams<TSchema>>;
534
+ validatorSchema: (parsed?: SelectQueryParams<TSchema>) => z.ZodType;
535
+ }
536
+ ```
537
+
538
+ #### `SelectConfig`
539
+
540
+ | Option | Type | Description |
541
+ |---|---:|---|
542
+ | `dataSchema` | `z.ZodObject` | Zod schema representing one data item. |
543
+ | `selectable` | `string[]` (typed paths) | Allowlist of selectable fields (dot paths supported). |
544
+ | `defaultSelect?` | `("*" \| field)[]` | Default select if `select` is missing. `["*"]` expands to `selectable`. |
545
+
546
+ #### Output
547
+
548
+ - `queryParamsSchema` parses `{ select: "id,name" }` into `{ select: ["id", "name"] }`.
549
+ - `validatorSchema(parsed?)` returns a Zod schema expecting `{ data: Array<ProjectedItem> }`.
550
+
551
+ #### Example
552
+
553
+ ```ts
554
+ import { z } from "zod";
555
+ import { select } from "zod-paginate";
556
+
557
+ const ProductSchema = z.object({
558
+ id: z.number(),
559
+ name: z.string(),
560
+ price: z.number(),
561
+ details: z.object({
562
+ weight: z.number(),
563
+ color: z.string(),
564
+ }),
565
+ });
566
+
567
+ const { queryParamsSchema, validatorSchema } = select({
568
+ dataSchema: ProductSchema,
569
+ selectable: ["id", "name", "price", "details.weight", "details.color"],
570
+ defaultSelect: ["id", "name", "price"],
571
+ });
572
+
573
+ // select=* expands to all selectable fields
574
+ const parsed = queryParamsSchema.parse({ select: "*" });
575
+ // parsed.select → ["id", "name", "price", "details.weight", "details.color"]
576
+
577
+ // With specific fields
578
+ const parsed2 = queryParamsSchema.parse({ select: "id,name,details.color" });
579
+ // parsed2.select → ["id", "name", "details.color"]
580
+
581
+ // Validate response shape
582
+ const responseSchema = validatorSchema(parsed2);
583
+ responseSchema.parse({
584
+ data: [
585
+ { id: 1, name: "Widget", details: { color: "red" } },
586
+ { id: 2, name: "Gadget", details: { color: "blue" } },
587
+ ],
588
+ });
589
+
590
+ // Missing select → uses defaultSelect
591
+ const parsed3 = queryParamsSchema.parse({});
592
+ // parsed3.select → ["id", "name", "price"]
593
+ ```
package/dist/main.d.ts CHANGED
@@ -1,238 +1,2 @@
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
- export declare const SortDirectionSchema: z.ZodEnum<{
34
- ASC: "ASC";
35
- DESC: "DESC";
36
- }>;
37
- export type SortDirection = z.infer<typeof SortDirectionSchema>;
38
- export declare const SortItemSchema: z.ZodObject<{
39
- property: z.ZodString;
40
- direction: z.ZodEnum<{
41
- ASC: "ASC";
42
- DESC: "DESC";
43
- }>;
44
- }, z.core.$strip>;
45
- export type SortItem = z.infer<typeof SortItemSchema>;
46
- /**
47
- * Supported operators.
48
- * $eq: equality (for strings, numbers, dates)
49
- * $null: checks for null (ignores the value)
50
- * $in: checks if the field value is in the provided array (for strings, numbers, dates)
51
- * $gt, $gte, $lt, $lte: comparison operators (for numbers and dates)
52
- * $btw: checks if the field value is between two values (for numbers and dates)
53
- * $ilike: case-insensitive substring match (for strings)
54
- * $sw: case-insensitive starts-with match (for strings)
55
- * $contains: checks if the field value contains the provided value (for strings)
56
- */
57
- export declare const OperatorSchema: z.ZodEnum<{
58
- $eq: "$eq";
59
- $null: "$null";
60
- $in: "$in";
61
- $gt: "$gt";
62
- $gte: "$gte";
63
- $lt: "$lt";
64
- $lte: "$lte";
65
- $btw: "$btw";
66
- $ilike: "$ilike";
67
- $sw: "$sw";
68
- $contains: "$contains";
69
- }>;
70
- export type Operator = z.infer<typeof OperatorSchema>;
71
- /**
72
- * Logical combinators for grouping conditions. $and and $or can be used to combine multiple conditions within the same group.
73
- */
74
- export declare const CombinatorSchema: z.ZodEnum<{
75
- $and: "$and";
76
- $or: "$or";
77
- }>;
78
- export type Combinator = z.infer<typeof CombinatorSchema>;
79
- export declare const IntegerStringSchema: z.ZodString;
80
- /**
81
- * Regex for validating ISO date strings (YYYY-MM-DD).
82
- */
83
- export declare const ISO_DATE_RE: RegExp;
84
- /**
85
- * Regex for validating ISO datetime strings (YYYY-MM-DDTHH:mm:ss.sssZ or with timezone offset).
86
- */
87
- export declare const ISO_DATETIME_RE: RegExp;
88
- export declare const NumericStringSchema: z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>;
89
- export declare const NumOrDateSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
90
- type FieldType = 'string' | 'number' | 'date' | 'any';
91
- type FieldTypeFromValue<V> = V extends Date ? 'date' : V extends number ? 'number' : V extends string ? 'string' : 'any';
92
- type CommonOps = '$eq' | '$null' | '$in' | '$contains';
93
- type StringOnlyOps = '$ilike' | '$sw';
94
- type ComparableOps = '$gt' | '$gte' | '$lt' | '$lte' | '$btw';
95
- type OpsForFieldType<TKind extends FieldType> = TKind extends 'string' ? CommonOps | StringOnlyOps : TKind extends 'number' ? CommonOps | ComparableOps : TKind extends 'date' ? CommonOps | ComparableOps : Operator;
96
- export declare const ConditionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
97
- group: z.ZodString;
98
- combinator: z.ZodOptional<z.ZodEnum<{
99
- $and: "$and";
100
- $or: "$or";
101
- }>>;
102
- op: z.ZodLiteral<"$null">;
103
- not: z.ZodOptional<z.ZodLiteral<true>>;
104
- }, z.core.$strip>, z.ZodObject<{
105
- group: z.ZodString;
106
- combinator: z.ZodOptional<z.ZodEnum<{
107
- $and: "$and";
108
- $or: "$or";
109
- }>>;
110
- op: z.ZodLiteral<"$eq">;
111
- not: z.ZodOptional<z.ZodLiteral<true>>;
112
- value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
113
- }, z.core.$strip>, z.ZodObject<{
114
- group: z.ZodString;
115
- combinator: z.ZodOptional<z.ZodEnum<{
116
- $and: "$and";
117
- $or: "$or";
118
- }>>;
119
- op: z.ZodEnum<{
120
- $ilike: "$ilike";
121
- $sw: "$sw";
122
- }>;
123
- not: z.ZodOptional<z.ZodLiteral<true>>;
124
- value: z.ZodString;
125
- }, z.core.$strip>, z.ZodObject<{
126
- group: z.ZodString;
127
- combinator: z.ZodOptional<z.ZodEnum<{
128
- $and: "$and";
129
- $or: "$or";
130
- }>>;
131
- op: z.ZodEnum<{
132
- $in: "$in";
133
- $contains: "$contains";
134
- }>;
135
- not: z.ZodOptional<z.ZodLiteral<true>>;
136
- value: z.ZodArray<z.ZodString>;
137
- }, z.core.$strip>, z.ZodObject<{
138
- group: z.ZodString;
139
- combinator: z.ZodOptional<z.ZodEnum<{
140
- $and: "$and";
141
- $or: "$or";
142
- }>>;
143
- op: z.ZodEnum<{
144
- $gt: "$gt";
145
- $gte: "$gte";
146
- $lt: "$lt";
147
- $lte: "$lte";
148
- }>;
149
- not: z.ZodOptional<z.ZodLiteral<true>>;
150
- value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
151
- }, z.core.$strip>, z.ZodObject<{
152
- group: z.ZodString;
153
- combinator: z.ZodOptional<z.ZodEnum<{
154
- $and: "$and";
155
- $or: "$or";
156
- }>>;
157
- op: z.ZodLiteral<"$btw">;
158
- not: z.ZodOptional<z.ZodLiteral<true>>;
159
- value: z.ZodTuple<[z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>], null>;
160
- }, z.core.$strip>], "op">;
161
- export type Condition = z.infer<typeof ConditionSchema>;
162
- export interface WhereFilter {
163
- type: 'filter';
164
- field: string;
165
- condition: Condition;
166
- }
167
- export interface WhereAnd {
168
- type: 'and';
169
- items: WhereNode[];
170
- }
171
- export interface WhereOr {
172
- type: 'or';
173
- items: WhereNode[];
174
- }
175
- export type WhereNode = WhereFilter | WhereAnd | WhereOr;
176
- export type DataSchema = z.ZodObject<z.ZodRawShape>;
177
- export type InferData<TSchema extends DataSchema> = z.infer<TSchema>;
178
- export type AllowedPath<TSchema extends DataSchema> = Path<InferData<TSchema>>;
179
- interface FilterableFieldConfig<TKind extends FieldType> {
180
- type: TKind;
181
- ops: readonly OpsForFieldType<TKind>[];
182
- }
183
- export interface CommonQueryConfigFromSchema<TSchema extends DataSchema> {
184
- dataSchema: TSchema;
185
- selectable?: readonly AllowedPath<TSchema>[];
186
- sortable?: readonly AllowedPath<TSchema>[];
187
- filterable?: Partial<{
188
- [P in AllowedPath<TSchema>]: FilterableFieldConfig<FieldTypeFromValue<PathValue<InferData<TSchema>, P>>>;
189
- }>;
190
- defaultSortBy?: readonly {
191
- property: AllowedPath<TSchema>;
192
- direction: SortDirection;
193
- }[];
194
- defaultLimit?: number;
195
- defaultSelect?: readonly (AllowedPath<TSchema> | '*')[];
196
- maxLimit?: number;
197
- }
198
- export type QueryConfigFromSchema<TSchema extends DataSchema> = CommonQueryConfigFromSchema<TSchema> & (LimitOffsetPaginationConfig | CursorPaginationConfig<InferData<TSchema>>);
199
- export interface SortItemTyped<TSchema extends DataSchema> {
200
- property: AllowedPath<TSchema>;
201
- direction: SortDirection;
202
- }
203
- export interface LimitOffsetPaginationPayload<TSchema extends DataSchema> {
204
- type: 'LIMIT_OFFSET';
205
- limit?: number;
206
- page?: number;
207
- sortBy?: SortItemTyped<TSchema>[];
208
- select?: AllowedPath<TSchema>[];
209
- filters?: WhereNode;
210
- }
211
- /**
212
- * Cursor is always a string in the query input, BUT we coerce it at parse-time
213
- * to match the type of cursorProperty (number / string / ISO date string).
214
- */
215
- export interface CursorPaginationPayload<TSchema extends DataSchema> {
216
- type: 'CURSOR';
217
- limit?: number;
218
- cursor?: number | string;
219
- cursorProperty: AllowedPath<TSchema>;
220
- sortBy?: SortItemTyped<TSchema>[];
221
- select?: AllowedPath<TSchema>[];
222
- filters?: WhereNode;
223
- }
224
- export interface PaginationQueryParams<TSchema extends DataSchema> {
225
- pagination: LimitOffsetPaginationPayload<TSchema> | CursorPaginationPayload<TSchema>;
226
- }
227
- /**
228
- * Generate Zod schemas and runtime validators for pagination query parameters, based on a config object.
229
- * @param config The configuration object defining the pagination behavior and allowed fields.
230
- * @returns An object containing:
231
- * - `queryParamsSchema`: A Zod schema for validating and parsing the raw query parameters.
232
- * - `validatorSchema`: A function that takes the already-parsed query parameters and returns a Zod schema for further validation (e.g. filters).
233
- */
234
- export declare function paginate<TSchema extends DataSchema>(config: QueryConfigFromSchema<TSchema>): {
235
- queryParamsSchema: z.ZodType<PaginationQueryParams<TSchema>>;
236
- validatorSchema: (parsed?: PaginationQueryParams<TSchema>) => z.ZodType;
237
- };
238
- export {};
1
+ export * from './paginate';
2
+ export * from './select';