zodvex 0.1.1 → 0.2.1
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 +236 -55
- package/dist/index.d.mts +462 -95
- package/dist/index.d.ts +462 -95
- package/dist/index.js +880 -352
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +856 -350
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -6
- package/src/builders.ts +310 -0
- package/src/codec.ts +183 -103
- package/src/custom.ts +341 -75
- package/src/ids.ts +53 -0
- package/src/index.ts +12 -0
- package/src/mapping/core.ts +330 -0
- package/src/mapping/handlers/enum.ts +30 -0
- package/src/mapping/handlers/index.ts +4 -0
- package/src/mapping/handlers/nullable.ts +35 -0
- package/src/mapping/handlers/record.ts +72 -0
- package/src/mapping/handlers/union.ts +64 -0
- package/src/mapping/index.ts +7 -0
- package/src/mapping/types.ts +188 -0
- package/src/mapping/utils.ts +32 -0
- package/src/registry.ts +58 -0
- package/src/tables.ts +89 -46
- package/src/types.ts +60 -39
- package/src/utils.ts +125 -0
- package/src/wrappers.ts +169 -71
- package/src/mapping.ts +0 -190
- package/src/zodV4Compat.ts +0 -36
package/README.md
CHANGED
|
@@ -71,15 +71,15 @@ import { defineSchema } from "convex/server";
|
|
|
71
71
|
import { z } from "zod";
|
|
72
72
|
import { zodTable } from "zodvex";
|
|
73
73
|
|
|
74
|
-
// Define a table from a
|
|
75
|
-
const
|
|
74
|
+
// Define a table from a plain object shape
|
|
75
|
+
const usersShape = {
|
|
76
76
|
name: z.string(),
|
|
77
77
|
email: z.string().email(),
|
|
78
78
|
age: z.number().optional(), // → v.optional(v.float64())
|
|
79
79
|
deletedAt: z.date().nullable(), // → v.union(v.float64(), v.null())
|
|
80
|
-
}
|
|
80
|
+
};
|
|
81
81
|
|
|
82
|
-
export const Users = zodTable("users",
|
|
82
|
+
export const Users = zodTable("users", usersShape);
|
|
83
83
|
|
|
84
84
|
// Use in your Convex schema
|
|
85
85
|
export default defineSchema({
|
|
@@ -101,6 +101,30 @@ export default defineSchema({
|
|
|
101
101
|
- **Date handling**: Automatic conversion between JavaScript `Date` objects and Convex timestamps
|
|
102
102
|
- **CRUD scaffolding**: Generate complete CRUD operations from a single schema
|
|
103
103
|
|
|
104
|
+
### Supported Types
|
|
105
|
+
|
|
106
|
+
This library intentionally supports only Zod types that map cleanly to Convex validators. Anything outside this list is unsupported (or best-effort with caveats).
|
|
107
|
+
|
|
108
|
+
- Primitives: `z.string()`, `z.number()` → `v.float64()`, `z.boolean()`, `z.null()`
|
|
109
|
+
- Date: `z.date()` → `v.float64()` (timestamp), encoded/decoded automatically
|
|
110
|
+
- Literals: `z.literal(x)` → `v.literal(x)`
|
|
111
|
+
- Enums: `z.enum([...])` → `v.union(v.literal(...))`
|
|
112
|
+
- Arrays: `z.array(T)` → `v.array(T')`
|
|
113
|
+
- Objects: `z.object({...})` → `v.object({...})`
|
|
114
|
+
- Records: `z.record(T)` or `z.record(z.string(), T)` → `v.record(v.string(), T')` (string keys only)
|
|
115
|
+
- Unions: `z.union([...])` (members must be supported types)
|
|
116
|
+
- Optional/nullable wrappers: `.optional()` → `v.optional(T')`, `.nullable()` → `v.union(T', v.null())`
|
|
117
|
+
- Convex IDs: `zid('table')` → `v.id('table')`
|
|
118
|
+
|
|
119
|
+
Unsupported or partial (explicitly out-of-scope):
|
|
120
|
+
|
|
121
|
+
- Tuples (fixed-length) — Convex has no fixed-length tuple validator; mapping would be lossy
|
|
122
|
+
- Intersections — combining object shapes widens overlapping fields; not equivalent to true intersection
|
|
123
|
+
- Transforms/effects/pipelines — not used for validator mapping; if you use them, conversions happen at runtime only
|
|
124
|
+
- Lazy, function, promise, set, map, symbol, branded/readonly, NaN/catch — unsupported
|
|
125
|
+
|
|
126
|
+
Note: `z.bigint()` → `v.int64()` is recognized for validator mapping but currently has no special runtime encode/decode; prefer numbers where possible.
|
|
127
|
+
|
|
104
128
|
## 📚 API Reference
|
|
105
129
|
|
|
106
130
|
### Mapping Helpers
|
|
@@ -109,7 +133,7 @@ Convert Zod schemas to Convex validators:
|
|
|
109
133
|
|
|
110
134
|
```ts
|
|
111
135
|
import { z } from "zod";
|
|
112
|
-
import { zodToConvex, zodToConvexFields } from "zodvex";
|
|
136
|
+
import { zodToConvex, zodToConvexFields, pickShape, safePick } from "zodvex";
|
|
113
137
|
|
|
114
138
|
// Convert a single Zod type to a Convex validator
|
|
115
139
|
const validator = zodToConvex(z.string().optional());
|
|
@@ -121,6 +145,30 @@ const fields = zodToConvexFields({
|
|
|
121
145
|
age: z.number().nullable(),
|
|
122
146
|
});
|
|
123
147
|
// → { name: v.string(), age: v.union(v.float64(), v.null()) }
|
|
148
|
+
|
|
149
|
+
// Safe picking from large schemas
|
|
150
|
+
// IMPORTANT: Avoid Zod's .pick() on large schemas (30+ fields) as it can cause
|
|
151
|
+
// TypeScript instantiation depth errors. Use pickShape/safePick instead.
|
|
152
|
+
|
|
153
|
+
const User = z.object({
|
|
154
|
+
id: z.string(),
|
|
155
|
+
email: z.string().email(),
|
|
156
|
+
firstName: z.string().optional(),
|
|
157
|
+
lastName: z.string().optional(),
|
|
158
|
+
createdAt: z.date().nullable(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ❌ DON'T use Zod's .pick() on large schemas (type instantiation issues)
|
|
162
|
+
// const Clerk = User.pick({ email: true, firstName: true, lastName: true });
|
|
163
|
+
|
|
164
|
+
// ✅ DO use pickShape to extract a plain object shape
|
|
165
|
+
const clerkShape = pickShape(User, ["email", "firstName", "lastName"]);
|
|
166
|
+
// Use directly in wrappers or build a new z.object()
|
|
167
|
+
const Clerk = z.object(clerkShape);
|
|
168
|
+
|
|
169
|
+
// Alternative: safePick builds the z.object() for you
|
|
170
|
+
const ClerkAlt = safePick(User, { email: true, firstName: true, lastName: true });
|
|
171
|
+
// Both Clerk and ClerkAlt are equivalent - use whichever is more readable
|
|
124
172
|
```
|
|
125
173
|
|
|
126
174
|
### Function Wrappers
|
|
@@ -166,6 +214,61 @@ export const sendEmail = zAction(
|
|
|
166
214
|
|
|
167
215
|
// Internal functions also supported
|
|
168
216
|
import { zInternalQuery, zInternalMutation, zInternalAction } from "zodvex";
|
|
217
|
+
|
|
218
|
+
// Handler return typing
|
|
219
|
+
// When `returns` is provided, your handler must return z.input<typeof returns>
|
|
220
|
+
// (domain values like Date). zodvex validates and encodes to Convex Values for you.
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Return Typing + Helpers
|
|
224
|
+
|
|
225
|
+
Handlers should return domain values shaped by your `returns` schema. zodvex validates and encodes them to Convex Values for you. For common patterns:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { z } from "zod";
|
|
229
|
+
import { zQuery, zodTable, returnsAs } from "zodvex";
|
|
230
|
+
import { query } from "./_generated/server";
|
|
231
|
+
|
|
232
|
+
// Table with doc helpers
|
|
233
|
+
export const Users = zodTable("users", {
|
|
234
|
+
name: z.string(),
|
|
235
|
+
createdAt: z.date()
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 1) Return full docs, strongly typed
|
|
239
|
+
export const listUsers = zQuery(
|
|
240
|
+
query,
|
|
241
|
+
{},
|
|
242
|
+
async (ctx) => {
|
|
243
|
+
const rows = await ctx.db.query("users").collect();
|
|
244
|
+
// No cast needed — handler returns domain values; wrapper encodes to Convex Values
|
|
245
|
+
return rows;
|
|
246
|
+
},
|
|
247
|
+
{ returns: Users.docArray }
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// 2) Return a custom shape (with Dates)
|
|
251
|
+
export const createdBounds = zQuery(
|
|
252
|
+
query,
|
|
253
|
+
{},
|
|
254
|
+
async (ctx) => {
|
|
255
|
+
const first = await ctx.db.query("users").order("asc").first();
|
|
256
|
+
const last = await ctx.db.query("users").order("desc").first();
|
|
257
|
+
return { first: first?.createdAt ?? null, last: last?.createdAt ?? null };
|
|
258
|
+
},
|
|
259
|
+
{ returns: z.object({ first: z.date().nullable(), last: z.date().nullable() }) }
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// 3) In tricky inference spots, use a typed identity
|
|
263
|
+
export const listUsersOk = zQuery(
|
|
264
|
+
query,
|
|
265
|
+
{},
|
|
266
|
+
async (ctx) => {
|
|
267
|
+
const rows = await ctx.db.query("users").collect();
|
|
268
|
+
return returnsAs<typeof Users.docArray>()(rows);
|
|
269
|
+
},
|
|
270
|
+
{ returns: Users.docArray }
|
|
271
|
+
);
|
|
169
272
|
```
|
|
170
273
|
|
|
171
274
|
### Codecs
|
|
@@ -205,29 +308,30 @@ const nameCodec = codec.pick({ name: true });
|
|
|
205
308
|
|
|
206
309
|
### Table Helpers
|
|
207
310
|
|
|
208
|
-
Define Convex tables from Zod schemas:
|
|
311
|
+
Define Convex tables from Zod schemas. `zodTable` accepts a **plain object shape** for best type inference:
|
|
209
312
|
|
|
210
313
|
```ts
|
|
211
314
|
import { z } from "zod";
|
|
212
|
-
import { zodTable } from "zodvex";
|
|
315
|
+
import { zodTable, zid } from "zodvex";
|
|
213
316
|
import { defineSchema } from "convex/server";
|
|
214
317
|
|
|
215
|
-
// Define your schema
|
|
216
|
-
const
|
|
318
|
+
// Define your schema as a plain object
|
|
319
|
+
const postShape = {
|
|
217
320
|
title: z.string(),
|
|
218
321
|
content: z.string(),
|
|
219
322
|
authorId: zid("users"), // Convex ID reference
|
|
220
323
|
published: z.boolean().default(false),
|
|
221
324
|
tags: z.array(z.string()).optional(),
|
|
222
|
-
}
|
|
325
|
+
};
|
|
223
326
|
|
|
224
|
-
// Create table helper
|
|
225
|
-
export const Posts = zodTable("posts",
|
|
327
|
+
// Create table helper - pass the plain object shape
|
|
328
|
+
export const Posts = zodTable("posts", postShape);
|
|
226
329
|
|
|
227
330
|
// Access properties
|
|
228
|
-
Posts.table;
|
|
229
|
-
Posts.
|
|
230
|
-
Posts.
|
|
331
|
+
Posts.table; // → Table definition for defineSchema
|
|
332
|
+
Posts.shape; // → Original plain object shape
|
|
333
|
+
Posts.zDoc; // → ZodObject with _id and _creationTime system fields
|
|
334
|
+
Posts.docArray; // → z.array(zDoc) for return type validation
|
|
231
335
|
|
|
232
336
|
// Use in schema.ts
|
|
233
337
|
export default defineSchema({
|
|
@@ -235,72 +339,145 @@ export default defineSchema({
|
|
|
235
339
|
.index("by_author", ["authorId"])
|
|
236
340
|
.index("by_published", ["published"]),
|
|
237
341
|
});
|
|
342
|
+
|
|
343
|
+
// Use docArray for return types
|
|
344
|
+
export const listPosts = zQuery(
|
|
345
|
+
query,
|
|
346
|
+
{},
|
|
347
|
+
async (ctx) => ctx.db.query("posts").collect(),
|
|
348
|
+
{ returns: Posts.docArray }
|
|
349
|
+
);
|
|
238
350
|
```
|
|
239
351
|
|
|
240
|
-
|
|
352
|
+
#### Working with Large Schemas
|
|
241
353
|
|
|
242
|
-
|
|
354
|
+
For large schemas (30+ fields), the plain object pattern is recommended:
|
|
243
355
|
|
|
244
356
|
```ts
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
357
|
+
// Define as plain object for better maintainability
|
|
358
|
+
export const users = {
|
|
359
|
+
// Meta
|
|
360
|
+
tokenId: z.string(),
|
|
361
|
+
type: z.literal("member"),
|
|
362
|
+
isAdmin: z.boolean(),
|
|
363
|
+
|
|
364
|
+
// User info
|
|
365
|
+
email: z.string(),
|
|
366
|
+
firstName: z.string().optional(),
|
|
367
|
+
lastName: z.string().optional(),
|
|
368
|
+
phone: z.string().optional(),
|
|
369
|
+
|
|
370
|
+
// References
|
|
371
|
+
favoriteUsers: z.array(zid("users")).optional(),
|
|
372
|
+
activeDancerId: zid("dancers").optional(),
|
|
373
|
+
// ... 40+ more fields
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Create table
|
|
377
|
+
export const Users = zodTable("users", users);
|
|
378
|
+
|
|
379
|
+
// Extract subsets without Zod's .pick() to avoid type complexity
|
|
380
|
+
const zClerkFields = z.object(pickShape(users, ["email", "firstName", "lastName", "tokenId"]));
|
|
381
|
+
// Use zClerkFields in function wrappers or validators
|
|
382
|
+
```
|
|
248
383
|
|
|
249
|
-
|
|
250
|
-
|
|
384
|
+
## 🔧 Advanced Usage
|
|
385
|
+
|
|
386
|
+
### Builder Pattern for Reusable Function Creators
|
|
251
387
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
388
|
+
For projects with many functions, create reusable builders instead of repeating the base builder.
|
|
389
|
+
Builders use Convex's native `{ args, handler, returns }` object syntax:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
import { query, mutation } from "./_generated/server";
|
|
393
|
+
import { zQueryBuilder, zMutationBuilder } from "zodvex";
|
|
394
|
+
|
|
395
|
+
// Create reusable builders
|
|
396
|
+
export const zq = zQueryBuilder(query);
|
|
397
|
+
export const zm = zMutationBuilder(mutation);
|
|
398
|
+
|
|
399
|
+
// Use them with Convex-style object syntax
|
|
400
|
+
export const getUser = zq({
|
|
401
|
+
args: { id: z.string() },
|
|
402
|
+
handler: async (ctx, { id }) => {
|
|
403
|
+
return ctx.db.get(id);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
258
406
|
|
|
259
|
-
|
|
407
|
+
export const updateUser = zm({
|
|
408
|
+
args: { id: z.string(), name: z.string() },
|
|
409
|
+
handler: async (ctx, { id, name }) => {
|
|
410
|
+
return ctx.db.patch(id, { name });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
260
413
|
```
|
|
261
414
|
|
|
262
|
-
|
|
415
|
+
### Custom Context with Middleware
|
|
263
416
|
|
|
264
|
-
|
|
417
|
+
Use custom builders to inject auth, permissions, or other context into your functions.
|
|
418
|
+
zodvex provides `customCtx` (re-exported from convex-helpers) for convenience:
|
|
265
419
|
|
|
266
420
|
```ts
|
|
267
|
-
import {
|
|
268
|
-
import {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
421
|
+
import { query, mutation } from "./_generated/server";
|
|
422
|
+
import { zCustomQueryBuilder, zCustomMutationBuilder, customCtx } from "zodvex";
|
|
423
|
+
|
|
424
|
+
// Create authenticated query builder
|
|
425
|
+
export const authQuery = zCustomQueryBuilder(
|
|
426
|
+
query,
|
|
427
|
+
customCtx(async (ctx) => {
|
|
428
|
+
const user = await getUserOrThrow(ctx);
|
|
429
|
+
return { user };
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Create authenticated mutation builder
|
|
434
|
+
export const authMutation = zCustomMutationBuilder(
|
|
435
|
+
mutation,
|
|
436
|
+
customCtx(async (ctx) => {
|
|
437
|
+
const user = await getUserOrThrow(ctx);
|
|
438
|
+
return { user };
|
|
439
|
+
})
|
|
285
440
|
);
|
|
441
|
+
|
|
442
|
+
// Use them with automatic type-safe context
|
|
443
|
+
export const getProfile = authQuery({
|
|
444
|
+
args: {},
|
|
445
|
+
handler: async (ctx) => {
|
|
446
|
+
// ctx.user is automatically available and typed!
|
|
447
|
+
return ctx.db.query("users").filter(q => q.eq(q.field("_id"), ctx.user._id)).first();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
export const updateProfile = authMutation({
|
|
452
|
+
args: { name: z.string() },
|
|
453
|
+
handler: async (ctx, { name }) => {
|
|
454
|
+
// ctx.user is automatically available and typed!
|
|
455
|
+
return ctx.db.patch(ctx.user._id, { name });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
286
458
|
```
|
|
287
459
|
|
|
460
|
+
**Available custom builders:**
|
|
461
|
+
- `zCustomQueryBuilder` - for queries with custom context
|
|
462
|
+
- `zCustomMutationBuilder` - for mutations with custom context
|
|
463
|
+
- `zCustomActionBuilder` - for actions with custom context
|
|
464
|
+
|
|
288
465
|
### Working with Dates
|
|
289
466
|
|
|
290
467
|
```ts
|
|
291
|
-
// Dates are automatically converted
|
|
292
|
-
const
|
|
468
|
+
// Dates are automatically converted to timestamps
|
|
469
|
+
const eventShape = {
|
|
293
470
|
title: z.string(),
|
|
294
471
|
startDate: z.date(),
|
|
295
472
|
endDate: z.date().nullable(),
|
|
296
|
-
}
|
|
473
|
+
};
|
|
297
474
|
|
|
298
|
-
const
|
|
475
|
+
const Events = zodTable("events", eventShape);
|
|
299
476
|
|
|
300
477
|
// In mutations - Date objects work seamlessly
|
|
301
478
|
export const createEvent = zMutation(
|
|
302
479
|
mutation,
|
|
303
|
-
|
|
480
|
+
z.object(eventShape),
|
|
304
481
|
async (ctx, event) => {
|
|
305
482
|
// event.startDate is a Date object
|
|
306
483
|
// It's automatically converted to timestamp for storage
|
|
@@ -348,7 +525,11 @@ const CommentSchema = z.object({
|
|
|
348
525
|
- **Defaults**: Zod defaults imply optional at the Convex schema level. Apply defaults in your application code.
|
|
349
526
|
- **Numbers**: `z.number()` maps to `v.float64()`. For integers, use `z.bigint()` → `v.int64()`.
|
|
350
527
|
- **Transforms**: Zod transforms (`.transform()`, `.refine()`) are not supported in schema mapping and fall back to `v.any()`.
|
|
351
|
-
- **Return
|
|
528
|
+
- **Return encoding**: Return values are always encoded to Convex Values. When `returns` is specified, values are validated and then encoded according to the schema; without `returns`, values are still encoded (e.g., Date → timestamp) for runtime safety.
|
|
529
|
+
|
|
530
|
+
### Runtime Conversion Consistency
|
|
531
|
+
|
|
532
|
+
zodvex uses an internal base-type codec registry to keep validator mapping and runtime value conversion aligned (e.g., `Date` ↔ timestamp). Composite types (arrays, objects, records, unions, optional/nullable) are composed from these base entries.
|
|
352
533
|
|
|
353
534
|
## 📝 Compatibility
|
|
354
535
|
|