zod-paginate 1.6.0 → 1.7.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 +12 -9
- package/dist/paginate.d.ts +35 -8
- package/dist/paginate.js +14 -17
- package/dist/paginate.js.map +1 -1
- package/dist/select.d.ts +31 -8
- package/dist/select.js +10 -9
- package/dist/select.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ const { queryParamsSchema, validatorSchema, responseSchema } = select({
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
const parsed = queryParamsSchema().parse({ select: "id,name,price" });
|
|
55
|
-
// parsed.select → ["id", "name", "price"]
|
|
55
|
+
// parsed.select.fields → ["id", "name", "price"]
|
|
56
56
|
|
|
57
57
|
// Generic response schema — valid for all possible responses based on config
|
|
58
58
|
responseSchema.parse({
|
|
@@ -194,11 +194,11 @@ const { queryParamsSchema, validatorSchema, responseSchema } = select({
|
|
|
194
194
|
|
|
195
195
|
// select=* expands to all selectable fields
|
|
196
196
|
const parsed = queryParamsSchema().parse({ select: "*" });
|
|
197
|
-
// parsed.select → ["id", "name", "price", "details.weight", "details.color"]
|
|
197
|
+
// parsed.select.fields → ["id", "name", "price", "details.weight", "details.color"]
|
|
198
198
|
|
|
199
199
|
// Specific fields
|
|
200
200
|
const parsed2 = queryParamsSchema().parse({ select: "id,name,details.color" });
|
|
201
|
-
// parsed2.select → ["id", "name", "details.color"]
|
|
201
|
+
// parsed2.select.fields → ["id", "name", "details.color"]
|
|
202
202
|
|
|
203
203
|
// Generic response schema (based on defaultSelect)
|
|
204
204
|
responseSchema.parse({
|
|
@@ -216,7 +216,7 @@ contextSchema.parse({
|
|
|
216
216
|
|
|
217
217
|
// Missing select → uses defaultSelect
|
|
218
218
|
const parsed3 = queryParamsSchema().parse({});
|
|
219
|
-
// parsed3.select → ["id", "name", "price"]
|
|
219
|
+
// parsed3.select.fields → ["id", "name", "price"]
|
|
220
220
|
```
|
|
221
221
|
|
|
222
222
|
Use `SelectResult<TSchema, TSelectable>` instead of `ReturnType<typeof select>` for explicit return types:
|
|
@@ -270,7 +270,7 @@ Returns:
|
|
|
270
270
|
|---|---:|---|
|
|
271
271
|
| `paginationType` | `"LIMIT_OFFSET"` \| `"CURSOR"` | Pagination mode. |
|
|
272
272
|
| `dataSchema` | `z.ZodObject` \| `z.ZodDiscriminatedUnion` \| `z.ZodUnion` | Zod schema for one data item (used for projection + cursor inference). |
|
|
273
|
-
| `selectable
|
|
273
|
+
| `selectable` | `string[]` (typed paths) | **Required.** Allowlist of selectable fields (dot paths). Enables `select`. |
|
|
274
274
|
| `sortable?` | `string[]` (typed paths) | Allowlist of sortable fields. Enables `sortBy`. |
|
|
275
275
|
| `filterable?` | object | Allowlist of filterable fields and allowed operators + field type. |
|
|
276
276
|
| `defaultSortBy?` | `{ property, direction }[]` | Default sort if `sortBy` missing/empty. |
|
|
@@ -335,7 +335,7 @@ function createPaginatorUnion(): PaginateResult<typeof ModelSchema, "id" | "stat
|
|
|
335
335
|
|
|
336
336
|
- **Input:** string or string[] — format: `field:ASC` or `field:DESC`
|
|
337
337
|
- **Output:** `[{ property, direction }]`
|
|
338
|
-
- Properties are matched against the `sortable` allowlist (unknown fields are
|
|
338
|
+
- Properties are matched against the `sortable` allowlist (unknown fields are **rejected**).
|
|
339
339
|
- Falls back to `defaultSortBy` when missing.
|
|
340
340
|
|
|
341
341
|
### `select`
|
|
@@ -371,7 +371,7 @@ responseSchema.parse({
|
|
|
371
371
|
|
|
372
372
|
// Type-safe: z.infer narrows data keys to selectable paths
|
|
373
373
|
type Response = z.infer<typeof responseSchema>;
|
|
374
|
-
// Response["data"][0] → { id?:
|
|
374
|
+
// Response["data"][0] → { id?: number; status?: string; createdAt?: Date; meta?: { score: number } }
|
|
375
375
|
// Response["pagination"].totalItems → number ✓ (no manual narrowing)
|
|
376
376
|
```
|
|
377
377
|
|
|
@@ -648,7 +648,7 @@ const parsed = queryParamsSchema({ search: z.string().optional() }).parse({
|
|
|
648
648
|
select: "id,name",
|
|
649
649
|
search: "widget",
|
|
650
650
|
});
|
|
651
|
-
// parsed.select → ["id", "name"]
|
|
651
|
+
// parsed.select.fields → ["id", "name"]
|
|
652
652
|
// parsed.search → "widget"
|
|
653
653
|
```
|
|
654
654
|
|
|
@@ -716,7 +716,7 @@ responseSchema.parse({
|
|
|
716
716
|
});
|
|
717
717
|
|
|
718
718
|
type Response = z.infer<typeof responseSchema>;
|
|
719
|
-
// Response["data"][0] → { id?:
|
|
719
|
+
// Response["data"][0] → { id?: number; status?: string; createdAt?: Date; meta?: { score: number } }
|
|
720
720
|
// Response["pagination"].totalItems → number ✓
|
|
721
721
|
```
|
|
722
722
|
|
|
@@ -767,6 +767,9 @@ export function select<
|
|
|
767
767
|
| `AllowedPath<TSchema>` | All valid dot-notation paths for a given schema |
|
|
768
768
|
| `SelectConfig<TSchema, TSelectable>` | Configuration for `select()` |
|
|
769
769
|
| `SelectResult<TSchema, TSelectable>` | Return type of `select()` |
|
|
770
|
+
| `SelectQueryPayload<TSchema, TSelectable>` | Parsed output of `select()` — `{ select: { fields, responseType? } }` |
|
|
771
|
+
| `TypedProjectedData<TSchema, TSelect>` | Projected data item with real value types (used in response types) |
|
|
772
|
+
| `ProjectedData<TSchema, TSelect>` | Projected data item with `unknown` values (key autocompletion only) |
|
|
770
773
|
| `PaginateResult<TSchema, TSelectable?, TType?>` | Return type of `paginate()` |
|
|
771
774
|
|
|
772
775
|
## Adapters
|
package/dist/paginate.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { type AllowedPath, type DataSchema, type EnsureDiscriminatorInSelectable, type InferData, type Path, type PathValue, type
|
|
2
|
+
import { type AllowedPath, type DataSchema, type EnsureDiscriminatorInSelectable, type InferData, type Path, type PathValue, type TypedProjectedData, type ZodShape } from './select';
|
|
3
3
|
interface LimitOffsetPaginationConfig {
|
|
4
|
+
/** Pagination mode: classic limit/offset with page numbers. */
|
|
4
5
|
paginationType: 'LIMIT_OFFSET';
|
|
5
6
|
}
|
|
6
7
|
interface CursorPaginationConfig<T> {
|
|
8
|
+
/** Pagination mode: cursor-based (keyset). */
|
|
7
9
|
paginationType: 'CURSOR';
|
|
10
|
+
/** Field used as cursor. Its Zod type determines cursor coercion (number, string, or ISO date). */
|
|
8
11
|
cursorProperty: Path<T>;
|
|
9
12
|
}
|
|
10
13
|
export declare const SortDirectionSchema: z.ZodEnum<{
|
|
@@ -154,19 +157,28 @@ interface FilterableFieldConfig<TKind extends FieldType> {
|
|
|
154
157
|
type: TKind;
|
|
155
158
|
ops: readonly OpsForFieldType<TKind>[];
|
|
156
159
|
}
|
|
160
|
+
/** Configuration shared by all pagination modes. */
|
|
157
161
|
export interface CommonQueryConfigFromSchema<TSchema extends DataSchema, TSelectable extends AllowedPath<TSchema> = AllowedPath<TSchema>> {
|
|
162
|
+
/** Zod schema representing one data item (object, discriminated union, or union). */
|
|
158
163
|
dataSchema: TSchema;
|
|
159
|
-
selectable
|
|
164
|
+
/** Allowlist of selectable fields (dot-notation paths). Enables the `select` query parameter. */
|
|
165
|
+
selectable: readonly TSelectable[];
|
|
166
|
+
/** Allowlist of sortable fields. Enables the `sortBy` query parameter. Unknown sort fields are rejected. */
|
|
160
167
|
sortable?: readonly AllowedPath<TSchema>[];
|
|
168
|
+
/** Map of filterable fields to their allowed type and operators. Enables the `filter.*` query parameters. */
|
|
161
169
|
filterable?: Partial<{
|
|
162
170
|
[P in AllowedPath<TSchema>]: FilterableFieldConfig<FieldTypeFromValue<PathValue<InferData<TSchema>, P>>>;
|
|
163
171
|
}>;
|
|
172
|
+
/** Default sort order applied when `sortBy` is omitted from the query. */
|
|
164
173
|
defaultSortBy?: readonly {
|
|
165
174
|
property: AllowedPath<TSchema>;
|
|
166
175
|
direction: SortDirection;
|
|
167
176
|
}[];
|
|
177
|
+
/** Default number of items per page when `limit` is omitted. */
|
|
168
178
|
defaultLimit: number;
|
|
179
|
+
/** Default fields returned when `select` is omitted. Use `"*"` to select all. */
|
|
169
180
|
defaultSelect: readonly TSelectable[] | '*';
|
|
181
|
+
/** Maximum allowed value for `limit`. Requests exceeding this are rejected. */
|
|
170
182
|
maxLimit: number;
|
|
171
183
|
}
|
|
172
184
|
/**
|
|
@@ -224,11 +236,11 @@ export interface CursorPaginationResponseMeta {
|
|
|
224
236
|
filter?: WhereNode;
|
|
225
237
|
}
|
|
226
238
|
export interface LimitOffsetPaginationResponse<TSchema extends DataSchema, TSelect extends AllowedPath<TSchema> = AllowedPath<TSchema>> {
|
|
227
|
-
data:
|
|
239
|
+
data: TypedProjectedData<TSchema, TSelect>[];
|
|
228
240
|
pagination: LimitOffsetPaginationResponseMeta;
|
|
229
241
|
}
|
|
230
242
|
export interface CursorPaginationResponse<TSchema extends DataSchema, TSelect extends AllowedPath<TSchema> = AllowedPath<TSchema>> {
|
|
231
|
-
data:
|
|
243
|
+
data: TypedProjectedData<TSchema, TSelect>[];
|
|
232
244
|
pagination: CursorPaginationResponseMeta;
|
|
233
245
|
}
|
|
234
246
|
export type PaginationResponse<TSchema extends DataSchema, TSelect extends AllowedPath<TSchema> = AllowedPath<TSchema>, TType extends PaginationType = PaginationType> = TType extends 'LIMIT_OFFSET' ? LimitOffsetPaginationResponse<TSchema, TSelect> : TType extends 'CURSOR' ? CursorPaginationResponse<TSchema, TSelect> : never;
|
|
@@ -276,7 +288,7 @@ export interface PaginateResult<TSchema extends DataSchema, TType extends Pagina
|
|
|
276
288
|
(): z.ZodType<PaginationQueryParams<TSchema, TType>>;
|
|
277
289
|
<TExtraShape extends z.ZodRawShape>(extraShape: TExtraShape): z.ZodType<PaginationQueryParams<TSchema, TType> & z.infer<z.ZodObject<TExtraShape>>>;
|
|
278
290
|
};
|
|
279
|
-
validatorSchema: (parsed?: PaginationPayload<TSchema>) => z.ZodType
|
|
291
|
+
validatorSchema: (parsed?: PaginationPayload<TSchema>) => z.ZodType<PaginationResponse<TSchema, AllowedPath<TSchema>, TType>>;
|
|
280
292
|
responseSchema: z.ZodObject<PaginationResponseSchemaShape<TType>>;
|
|
281
293
|
}
|
|
282
294
|
/**
|
|
@@ -288,37 +300,52 @@ export interface PaginateResult<TSchema extends DataSchema, TType extends Pagina
|
|
|
288
300
|
* - `responseSchema`: A pre-built Zod schema for validating the response (uses defaultSelect or all selectable fields).
|
|
289
301
|
*/
|
|
290
302
|
export declare function paginate<TSchema extends DataSchema, const TSelectable extends readonly AllowedPath<TSchema>[]>(config: Omit<CommonQueryConfigFromSchema<TSchema, TSelectable[number]>, 'selectable' | 'defaultSelect' | 'sortable' | 'defaultSortBy' | 'filterable'> & LimitOffsetPaginationConfig & {
|
|
291
|
-
selectable
|
|
303
|
+
/** Allowlist of selectable fields (dot-notation paths). Enables the `select` query parameter. */
|
|
304
|
+
selectable: EnsureDiscriminatorInSelectable<TSchema, TSelectable>;
|
|
305
|
+
/** Default fields returned when `select` is omitted. Use `"*"` to select all. */
|
|
292
306
|
defaultSelect: readonly NoInfer<TSelectable[number]>[] | '*';
|
|
307
|
+
/** Allowlist of sortable fields. Enables the `sortBy` query parameter. Unknown sort fields are rejected. */
|
|
293
308
|
sortable?: readonly NoInfer<TSelectable[number]>[];
|
|
309
|
+
/** Default sort order applied when `sortBy` is omitted from the query. */
|
|
294
310
|
defaultSortBy?: readonly {
|
|
295
311
|
property: NoInfer<TSelectable[number]>;
|
|
296
312
|
direction: SortDirection;
|
|
297
313
|
}[];
|
|
314
|
+
/** Map of filterable fields to their allowed type and operators. Enables the `filter.*` query parameters. */
|
|
298
315
|
filterable?: Partial<{
|
|
299
316
|
[P in NoInfer<TSelectable[number]>]: FilterableFieldConfig<FieldTypeFromValue<PathValue<InferData<TSchema>, P>>>;
|
|
300
317
|
}>;
|
|
301
318
|
}): PaginateResult<TSchema, 'LIMIT_OFFSET'>;
|
|
302
319
|
export declare function paginate<TSchema extends DataSchema, const TSelectable extends readonly AllowedPath<TSchema>[]>(config: Omit<CommonQueryConfigFromSchema<TSchema, TSelectable[number]>, 'selectable' | 'defaultSelect' | 'sortable' | 'defaultSortBy' | 'filterable'> & CursorPaginationConfig<InferData<TSchema>> & {
|
|
303
|
-
selectable
|
|
320
|
+
/** Allowlist of selectable fields (dot-notation paths). Enables the `select` query parameter. */
|
|
321
|
+
selectable: EnsureDiscriminatorInSelectable<TSchema, TSelectable>;
|
|
322
|
+
/** Default fields returned when `select` is omitted. Use `"*"` to select all. */
|
|
304
323
|
defaultSelect: readonly NoInfer<TSelectable[number]>[] | '*';
|
|
324
|
+
/** Allowlist of sortable fields. Enables the `sortBy` query parameter. Unknown sort fields are rejected. */
|
|
305
325
|
sortable?: readonly NoInfer<TSelectable[number]>[];
|
|
326
|
+
/** Default sort order applied when `sortBy` is omitted from the query. */
|
|
306
327
|
defaultSortBy?: readonly {
|
|
307
328
|
property: NoInfer<TSelectable[number]>;
|
|
308
329
|
direction: SortDirection;
|
|
309
330
|
}[];
|
|
331
|
+
/** Map of filterable fields to their allowed type and operators. Enables the `filter.*` query parameters. */
|
|
310
332
|
filterable?: Partial<{
|
|
311
333
|
[P in NoInfer<TSelectable[number]>]: FilterableFieldConfig<FieldTypeFromValue<PathValue<InferData<TSchema>, P>>>;
|
|
312
334
|
}>;
|
|
313
335
|
}): PaginateResult<TSchema, 'CURSOR'>;
|
|
314
336
|
export declare function paginate<TSchema extends DataSchema, const TSelectable extends readonly AllowedPath<TSchema>[]>(config: Omit<CommonQueryConfigFromSchema<TSchema, TSelectable[number]>, 'selectable' | 'defaultSelect' | 'sortable' | 'defaultSortBy' | 'filterable'> & {
|
|
315
|
-
selectable
|
|
337
|
+
/** Allowlist of selectable fields (dot-notation paths). Enables the `select` query parameter. */
|
|
338
|
+
selectable: EnsureDiscriminatorInSelectable<TSchema, TSelectable>;
|
|
339
|
+
/** Default fields returned when `select` is omitted. Use `"*"` to select all. */
|
|
316
340
|
defaultSelect: readonly NoInfer<TSelectable[number]>[] | '*';
|
|
341
|
+
/** Allowlist of sortable fields. Enables the `sortBy` query parameter. Unknown sort fields are rejected. */
|
|
317
342
|
sortable?: readonly NoInfer<TSelectable[number]>[];
|
|
343
|
+
/** Default sort order applied when `sortBy` is omitted from the query. */
|
|
318
344
|
defaultSortBy?: readonly {
|
|
319
345
|
property: NoInfer<TSelectable[number]>;
|
|
320
346
|
direction: SortDirection;
|
|
321
347
|
}[];
|
|
348
|
+
/** Map of filterable fields to their allowed type and operators. Enables the `filter.*` query parameters. */
|
|
322
349
|
filterable?: Partial<{
|
|
323
350
|
[P in NoInfer<TSelectable[number]>]: FilterableFieldConfig<FieldTypeFromValue<PathValue<InferData<TSchema>, P>>>;
|
|
324
351
|
}>;
|
package/dist/paginate.js
CHANGED
|
@@ -547,12 +547,16 @@ function computeSortBy(sortByRaw, config) {
|
|
|
547
547
|
if (sortByRaw) {
|
|
548
548
|
const cleaned = sortByRaw.map((s) => s.trim()).filter(Boolean);
|
|
549
549
|
if (cleaned.length > 0) {
|
|
550
|
+
const seen = new Set();
|
|
550
551
|
const out = [];
|
|
551
552
|
for (const raw of cleaned) {
|
|
552
553
|
const parsed = parseSortItem(raw);
|
|
553
554
|
const picked = (0, select_1.pickFromAllowlist)(config.sortable, parsed.property);
|
|
554
555
|
if (!picked)
|
|
555
556
|
continue;
|
|
557
|
+
if (seen.has(picked))
|
|
558
|
+
continue;
|
|
559
|
+
seen.add(picked);
|
|
556
560
|
out.push({ property: picked, direction: parsed.direction });
|
|
557
561
|
}
|
|
558
562
|
return out.length > 0 ? out : undefined;
|
|
@@ -719,7 +723,7 @@ function buildCursorResponseSchema(dataItemSchema, parts) {
|
|
|
719
723
|
function paginate(config) {
|
|
720
724
|
const discriminatorKey = (0, select_1.getDiscriminatorKey)(config.dataSchema);
|
|
721
725
|
const nestedDiscriminators = (0, select_1.findNestedDiscriminators)(config.dataSchema);
|
|
722
|
-
const selectableStrings = [...
|
|
726
|
+
const selectableStrings = [...config.selectable];
|
|
723
727
|
const effectiveConfig = {
|
|
724
728
|
selectable: selectableStrings,
|
|
725
729
|
defaultSelect: config.defaultSelect === '*' ? '*' : Array.from(config.defaultSelect, String),
|
|
@@ -787,7 +791,7 @@ function paginate(config) {
|
|
|
787
791
|
example: `${String(config.sortable[0])}:ASC`,
|
|
788
792
|
});
|
|
789
793
|
}
|
|
790
|
-
if (config.selectable
|
|
794
|
+
if (config.selectable.length > 0) {
|
|
791
795
|
const defaultSelectDesc = config.defaultSelect === '*' ? '*' : [...config.defaultSelect].join(', ');
|
|
792
796
|
rootShape.select = zod_1.z
|
|
793
797
|
.string()
|
|
@@ -858,14 +862,6 @@ function paginate(config) {
|
|
|
858
862
|
message: `limit must be <= ${config.maxLimit}`,
|
|
859
863
|
});
|
|
860
864
|
}
|
|
861
|
-
// select forbidden if no selectable configured
|
|
862
|
-
if (val.select && (!config.selectable || config.selectable.length === 0)) {
|
|
863
|
-
ctx.addIssue({
|
|
864
|
-
code: 'custom',
|
|
865
|
-
path: ['select'],
|
|
866
|
-
message: `select is not allowed (no selectable fields configured)`,
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
865
|
// select allowlist + "*" expandability
|
|
870
866
|
const selectForValidation = val.select ??
|
|
871
867
|
(effectiveConfig.defaultSelect === '*' ? ['*'] : effectiveConfig.defaultSelect);
|
|
@@ -923,7 +919,6 @@ function paginate(config) {
|
|
|
923
919
|
}
|
|
924
920
|
}
|
|
925
921
|
// sort allowlist
|
|
926
|
-
const sortItems = computeSortBy(val.sortBy, config);
|
|
927
922
|
if (val.sortBy) {
|
|
928
923
|
if (!config.sortable || config.sortable.length === 0) {
|
|
929
924
|
ctx.addIssue({
|
|
@@ -932,14 +927,16 @@ function paginate(config) {
|
|
|
932
927
|
message: `sortBy is not allowed (no sortable fields configured)`,
|
|
933
928
|
});
|
|
934
929
|
}
|
|
935
|
-
else
|
|
930
|
+
else {
|
|
931
|
+
const cleaned = val.sortBy.map((s) => s.trim()).filter(Boolean);
|
|
936
932
|
let index = 0;
|
|
937
|
-
for (const
|
|
938
|
-
|
|
933
|
+
for (const raw of cleaned) {
|
|
934
|
+
const parsed = parseSortItem(raw);
|
|
935
|
+
if (!allowedSortable.has(parsed.property)) {
|
|
939
936
|
ctx.addIssue({
|
|
940
937
|
code: 'custom',
|
|
941
938
|
path: ['sortBy', index],
|
|
942
|
-
message: `sort property "${
|
|
939
|
+
message: `sort property "${parsed.property}" is not allowed`,
|
|
943
940
|
});
|
|
944
941
|
}
|
|
945
942
|
index += 1;
|
|
@@ -1059,7 +1056,7 @@ function paginate(config) {
|
|
|
1059
1056
|
const LIMIT_OFFSET_META_DESC = 'Pagination metadata for limit/offset mode';
|
|
1060
1057
|
const CURSOR_META_DESC = 'Pagination metadata for cursor mode';
|
|
1061
1058
|
const CURSOR_VALUE_DESC = 'Cursor value pointing to the last item returned';
|
|
1062
|
-
|
|
1059
|
+
function validatorSchema(parsed) {
|
|
1063
1060
|
const effectiveSelect = parsed?.select ?? (0, select_1.computeSelect)(undefined, effectiveConfig) ?? undefined;
|
|
1064
1061
|
const dataItemSchema = effectiveSelect && effectiveSelect.length > 0
|
|
1065
1062
|
? (0, select_1.projectDataSchemaPreservingUnion)(config.dataSchema, effectiveSelect.map(String))
|
|
@@ -1091,7 +1088,7 @@ function paginate(config) {
|
|
|
1091
1088
|
})
|
|
1092
1089
|
.meta({ description: CURSOR_META_DESC }),
|
|
1093
1090
|
});
|
|
1094
|
-
}
|
|
1091
|
+
}
|
|
1095
1092
|
const partialDataItemSchema = selectableStrings.length > 0
|
|
1096
1093
|
? (0, select_1.projectDataSchemaPreservingUnion)(config.dataSchema, selectableStrings, { partial: true })
|
|
1097
1094
|
: (0, select_1.resolveToZodObject)(config.dataSchema);
|