zodvex 0.2.1 → 0.2.2

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
@@ -4,544 +4,484 @@
4
4
 
5
5
  Type-safe Convex functions with Zod schemas. Preserve Convex's optional/nullable semantics while leveraging Zod's powerful validation.
6
6
 
7
- > Heavily inspired by [convex-helpers](https://github.com/get-convex/convex-helpers). Built on top of their excellent utilities.
7
+ > Built on top of [convex-helpers](https://github.com/get-convex/convex-helpers)
8
8
 
9
- ## 📦 Installation
9
+ ## Table of Contents
10
10
 
11
- ```bash
12
- npm install zodvex zod convex convex-helpers
13
- ```
14
-
15
- ```bash
16
- pnpm add zodvex zod convex convex-helpers
17
- ```
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [Defining Schemas](#defining-schemas)
14
+ - [Table Definitions](#table-definitions)
15
+ - [Building Your Schema](#building-your-schema)
16
+ - [Defining Functions](#defining-functions)
17
+ - [Working with Subsets](#working-with-subsets)
18
+ - [Form Validation](#form-validation)
19
+ - [API Reference](#api-reference)
20
+ - [Advanced Usage](#advanced-usage)
18
21
 
19
- ```bash
20
- yarn add zodvex zod convex convex-helpers
21
- ```
22
+ ## Installation
22
23
 
23
24
  ```bash
24
- bun add zodvex zod convex convex-helpers
25
+ npm install zodvex zod@^4.1.0 convex convex-helpers
25
26
  ```
26
27
 
27
28
  **Peer dependencies:**
29
+ - `zod` (^4.1.0 or later)
30
+ - `convex` (>= 1.27.0)
31
+ - `convex-helpers` (>= 0.1.104)
28
32
 
29
- - `zod` (v4)
30
- - `convex` (>= 1.27)
31
- - `convex-helpers` (>= 0.1.101-alpha.1)
33
+ ## Quick Start
32
34
 
33
- ## 🚀 Quick Start
35
+ ### 1. Set up your builders
34
36
 
35
- ### 1. Define a Zod schema and create type-safe Convex functions
37
+ Create a `convex/util.ts` file with reusable builders ([copy full example](./examples/queries.ts)):
36
38
 
37
39
  ```ts
38
- // convex/users.ts
39
- import { z } from "zod";
40
- import { query, mutation } from "./_generated/server";
41
- import { zQuery, zMutation } from "zodvex";
40
+ // convex/util.ts
41
+ import { query, mutation, action } from './_generated/server'
42
+ import { zQueryBuilder, zMutationBuilder, zActionBuilder } from 'zodvex'
42
43
 
43
- // Define your schema
44
- const UserInput = z.object({
45
- name: z.string().min(1),
46
- email: z.string().email(),
47
- age: z.number().optional(),
48
- });
44
+ export const zq = zQueryBuilder(query)
45
+ export const zm = zMutationBuilder(mutation)
46
+ export const za = zActionBuilder(action)
47
+ ```
49
48
 
50
- // Create type-safe queries
51
- export const getUser = zQuery(
52
- query,
53
- { id: z.string() },
54
- async (ctx, { id }) => {
55
- return await ctx.db.get(id);
56
- },
57
- );
49
+ ### 2. Use builders to create type-safe functions
50
+
51
+ ```ts
52
+ // convex/users.ts
53
+ import { z } from 'zod'
54
+ import { zid } from 'zodvex'
55
+ import { zq, zm } from './util'
56
+ import { Users } from './schemas/users'
58
57
 
59
- // Create type-safe mutations
60
- export const createUser = zMutation(mutation, UserInput, async (ctx, user) => {
61
- // `user` is fully typed and validated!
62
- return await ctx.db.insert("users", user);
63
- });
58
+ export const getUser = zq({
59
+ args: { id: zid('users') },
60
+ returns: Users.zDoc.nullable(),
61
+ handler: async (ctx, { id }) => {
62
+ return await ctx.db.get(id)
63
+ }
64
+ })
65
+
66
+ export const createUser = zm({
67
+ args: Users.shape,
68
+ returns: zid('users'),
69
+ handler: async (ctx, user) => {
70
+ // user is fully typed and validated
71
+ return await ctx.db.insert('users', user)
72
+ }
73
+ })
64
74
  ```
65
75
 
66
- ### 2. Use with Convex schemas
76
+ ## Defining Schemas
77
+
78
+ Define your Zod schemas as plain objects for best type inference:
67
79
 
68
80
  ```ts
69
- // convex/schema.ts
70
- import { defineSchema } from "convex/server";
71
- import { z } from "zod";
72
- import { zodTable } from "zodvex";
81
+ import { z } from 'zod'
82
+ import { zid } from 'zodvex'
73
83
 
74
- // Define a table from a plain object shape
75
- const usersShape = {
84
+ // Plain object shape - recommended
85
+ export const userShape = {
76
86
  name: z.string(),
77
87
  email: z.string().email(),
78
- age: z.number().optional(), // → v.optional(v.float64())
79
- deletedAt: z.date().nullable(), // → v.union(v.float64(), v.null())
80
- };
81
-
82
- export const Users = zodTable("users", usersShape);
88
+ age: z.number().optional(),
89
+ avatarUrl: z.string().url().nullable(),
90
+ teamId: zid('teams').optional() // Convex ID reference
91
+ }
83
92
 
84
- // Use in your Convex schema
85
- export default defineSchema({
86
- users: Users.table.index("by_email", ["email"]).searchIndex("search_name", {
87
- searchField: "name",
88
- }),
89
- });
93
+ // Can also use z.object() if preferred
94
+ export const User = z.object(userShape)
90
95
  ```
91
96
 
92
- ## Features
97
+ ## Table Definitions
93
98
 
94
- ### Why zodvex?
99
+ Use `zodTable` as a drop-in replacement for Convex's `Table`:
95
100
 
96
- - **Correct optional/nullable semantics**: Preserves Convex's distinction between optional and nullable fields
97
- - `.optional()` → `v.optional(T)` (field can be omitted)
98
- - `.nullable()` `v.union(T, v.null())` (field required but can be null)
99
- - `.optional().nullable()` → `v.optional(v.union(T, v.null()))` (can be omitted or null)
100
- - **Type-safe function wrappers**: Full TypeScript inference for inputs and outputs
101
- - **Date handling**: Automatic conversion between JavaScript `Date` objects and Convex timestamps
102
- - **CRUD scaffolding**: Generate complete CRUD operations from a single schema
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).
101
+ ```ts
102
+ // convex/schema.ts
103
+ import { z } from 'zod'
104
+ import { zodTable, zid } from 'zodvex'
107
105
 
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')`
106
+ export const Users = zodTable('users', {
107
+ name: z.string(),
108
+ email: z.string().email(),
109
+ age: z.number().optional(), // → v.optional(v.float64())
110
+ deletedAt: z.date().nullable(), // → v.union(v.float64(), v.null())
111
+ teamId: zid('teams').optional()
112
+ })
113
+
114
+ // Access the underlying table
115
+ Users.table // Convex table definition
116
+ Users.shape // Original Zod shape
117
+ Users.zDoc // Zod schema with _id and _creationTime
118
+ Users.docArray // z.array(zDoc) for return types
119
+ ```
118
120
 
119
- Unsupported or partial (explicitly out-of-scope):
121
+ ## Building Your Schema
120
122
 
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
123
+ Use `zodTable().table` in your Convex schema:
125
124
 
126
- Note: `z.bigint()` → `v.int64()` is recognized for validator mapping but currently has no special runtime encode/decode; prefer numbers where possible.
125
+ ```ts
126
+ // convex/schema.ts
127
+ import { defineSchema } from 'convex/server'
128
+ import { Users } from './tables/users'
129
+ import { Teams } from './tables/teams'
127
130
 
128
- ## 📚 API Reference
131
+ export default defineSchema({
132
+ users: Users.table
133
+ .index('by_email', ['email'])
134
+ .index('by_team', ['teamId'])
135
+ .searchIndex('search_name', { searchField: 'name' }),
136
+
137
+ teams: Teams.table
138
+ .index('by_created', ['_creationTime'])
139
+ })
140
+ ```
129
141
 
130
- ### Mapping Helpers
142
+ ## Defining Functions
131
143
 
132
- Convert Zod schemas to Convex validators:
144
+ Use your builders from `util.ts` to create type-safe functions:
133
145
 
134
146
  ```ts
135
- import { z } from "zod";
136
- import { zodToConvex, zodToConvexFields, pickShape, safePick } from "zodvex";
137
-
138
- // Convert a single Zod type to a Convex validator
139
- const validator = zodToConvex(z.string().optional());
140
- // → v.optional(v.string())
141
-
142
- // Convert a Zod object shape to Convex field validators
143
- const fields = zodToConvexFields({
144
- name: z.string(),
145
- age: z.number().nullable(),
146
- });
147
- // → { name: v.string(), age: v.union(v.float64(), v.null()) }
147
+ import { z } from 'zod'
148
+ import { zid } from 'zodvex'
149
+ import { zq, zm } from './util'
150
+ import { Users } from './tables/users'
148
151
 
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
+ // Query with return type validation
153
+ export const listUsers = zq({
154
+ args: {},
155
+ returns: Users.docArray,
156
+ handler: async (ctx) => {
157
+ return await ctx.db.query('users').collect()
158
+ }
159
+ })
152
160
 
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
161
+ // Mutation with Convex ID
162
+ export const deleteUser = zm({
163
+ args: { id: zid('users') },
164
+ returns: z.null(),
165
+ handler: async (ctx, { id }) => {
166
+ await ctx.db.delete(id)
167
+ return null
168
+ }
169
+ })
170
+
171
+ // Using the full schema
172
+ export const createUser = zm({
173
+ args: Users.shape,
174
+ returns: zid('users'),
175
+ handler: async (ctx, user) => {
176
+ return await ctx.db.insert('users', user)
177
+ }
178
+ })
172
179
  ```
173
180
 
174
- ### Function Wrappers
181
+ ## Working with Subsets
175
182
 
176
- Type-safe wrappers for Convex functions:
183
+ Pick a subset of fields for focused operations:
177
184
 
178
185
  ```ts
179
- import { z } from "zod";
180
- import { query, mutation, action } from "./_generated/server";
181
- import { zQuery, zMutation, zAction } from "zodvex";
182
-
183
- // Query with validated input and optional return validation
184
- export const getById = zQuery(
185
- query,
186
- { id: z.string() },
187
- async (ctx, { id }) => ctx.db.get(id),
188
- {
189
- returns: z.object({
190
- name: z.string(),
191
- createdAt: z.date(),
192
- }),
186
+ import { z } from 'zod'
187
+ import { zid } from 'zodvex'
188
+ import { zm } from './util'
189
+ import { Users, zUsers } from './tables/users'
190
+
191
+ // Use Zod's .pick() to select fields
192
+ const UpdateFields = zUsers.pick({
193
+ firstName: true,
194
+ lastName: true,
195
+ email: true
196
+ })
197
+
198
+ export const updateUserProfile = zm({
199
+ args: {
200
+ id: zid('users'),
201
+ ...UpdateFields.shape
193
202
  },
194
- );
203
+ handler: async (ctx, { id, ...fields }) => {
204
+ await ctx.db.patch(id, fields)
205
+ }
206
+ })
195
207
 
196
- // Mutation with Zod object
197
- export const updateUser = zMutation(
198
- mutation,
199
- z.object({
200
- id: z.string(),
201
- name: z.string().min(1),
202
- }),
203
- async (ctx, { id, name }) => ctx.db.patch(id, { name }),
204
- );
205
-
206
- // Action with single value (normalizes to { value })
207
- export const sendEmail = zAction(
208
- action,
209
- z.string().email(),
210
- async (ctx, { value: email }) => {
211
- // Send email to the address
208
+ // Or inline for simple cases
209
+ export const updateUserName = zm({
210
+ args: {
211
+ id: zid('users'),
212
+ name: z.string()
212
213
  },
213
- );
214
+ handler: async (ctx, { id, name }) => {
215
+ await ctx.db.patch(id, { name })
216
+ }
217
+ })
218
+ ```
214
219
 
215
- // Internal functions also supported
216
- import { zInternalQuery, zInternalMutation, zInternalAction } from "zodvex";
220
+ ## Form Validation
217
221
 
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
+ Use your schemas with form libraries like react-hook-form:
222
223
 
223
- #### Return Typing + Helpers
224
+ ```tsx
225
+ import { useForm } from 'react-hook-form'
226
+ import { zodResolver } from '@hookform/resolvers/zod'
227
+ import { z } from 'zod'
228
+ import { useMutation } from 'convex/react'
229
+ import { api } from '../convex/_generated/api'
230
+ import { Users } from '../convex/tables/users'
224
231
 
225
- Handlers should return domain values shaped by your `returns` schema. zodvex validates and encodes them to Convex Values for you. For common patterns:
232
+ // Create form schema from your table schema
233
+ const CreateUserForm = z.object(Users.shape)
234
+ type CreateUserForm = z.infer<typeof CreateUserForm>
226
235
 
227
- ```ts
228
- import { z } from "zod";
229
- import { zQuery, zodTable, returnsAs } from "zodvex";
230
- import { query } from "./_generated/server";
236
+ function UserForm() {
237
+ const createUser = useMutation(api.users.createUser)
231
238
 
232
- // Table with doc helpers
233
- export const Users = zodTable("users", {
234
- name: z.string(),
235
- createdAt: z.date()
236
- });
239
+ const { register, handleSubmit, formState: { errors } } = useForm<CreateUserForm>({
240
+ resolver: zodResolver(CreateUserForm)
241
+ })
237
242
 
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
- );
243
+ const onSubmit = async (data: CreateUserForm) => {
244
+ await createUser(data)
245
+ }
249
246
 
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
- );
247
+ return (
248
+ <form onSubmit={handleSubmit(onSubmit)}>
249
+ <input {...register('name')} />
250
+ {errors.name && <span>{errors.name.message}</span>}
261
251
 
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
- );
252
+ <input {...register('email')} />
253
+ {errors.email && <span>{errors.email.message}</span>}
254
+
255
+ <button type="submit">Create User</button>
256
+ </form>
257
+ )
258
+ }
272
259
  ```
273
260
 
274
- ### Codecs
261
+ ## API Reference
275
262
 
276
- Convert between Zod-shaped data and Convex-safe JSON:
263
+ ### Builders
277
264
 
265
+ **Basic builders** - Create type-safe functions without auth:
278
266
  ```ts
279
- import { convexCodec } from "zodvex";
280
- import { z } from "zod";
281
-
282
- const UserSchema = z.object({
283
- name: z.string(),
284
- birthday: z.date().optional(),
285
- metadata: z.record(z.string()),
286
- });
267
+ zQueryBuilder(query) // Creates query builder
268
+ zMutationBuilder(mutation) // Creates mutation builder
269
+ zActionBuilder(action) // Creates action builder
270
+ ```
287
271
 
288
- const codec = convexCodec(UserSchema);
272
+ **Custom builders** - Add auth or custom context:
273
+ ```ts
274
+ import { customCtx } from 'zodvex'
289
275
 
290
- // Get Convex validators for table definition
291
- const validators = codec.toConvexSchema();
276
+ const authQuery = zCustomQueryBuilder(
277
+ query,
278
+ customCtx(async (ctx) => {
279
+ const user = await getUserOrThrow(ctx)
280
+ return { user }
281
+ })
282
+ )
292
283
 
293
- // Encode: Zod data Convex JSON (Date → timestamp, omit undefined)
294
- const encoded = codec.encode({
295
- name: "Alice",
296
- birthday: new Date("1990-01-01"),
297
- metadata: { role: "admin" },
298
- });
299
- // → { name: 'Alice', birthday: 631152000000, metadata: { role: 'admin' } }
300
-
301
- // Decode: Convex JSON → Zod data (timestamp → Date)
302
- const decoded = codec.decode(encoded);
303
- // → { name: 'Alice', birthday: Date('1990-01-01'), metadata: { role: 'admin' } }
304
-
305
- // Create sub-codecs (ZodObject only)
306
- const nameCodec = codec.pick({ name: true });
284
+ // Use with automatic context injection
285
+ export const getMyProfile = authQuery({
286
+ args: {},
287
+ returns: Users.zDoc.nullable(),
288
+ handler: async (ctx) => {
289
+ if (!ctx.user) return null
290
+ return ctx.db.get(ctx.user._id)
291
+ }
292
+ })
307
293
  ```
308
294
 
309
- ### Table Helpers
310
-
311
- Define Convex tables from Zod schemas. `zodTable` accepts a **plain object shape** for best type inference:
295
+ ### Mapping Helpers
312
296
 
313
297
  ```ts
314
- import { z } from "zod";
315
- import { zodTable, zid } from "zodvex";
316
- import { defineSchema } from "convex/server";
298
+ import { zodToConvex, zodToConvexFields } from 'zodvex'
317
299
 
318
- // Define your schema as a plain object
319
- const postShape = {
320
- title: z.string(),
321
- content: z.string(),
322
- authorId: zid("users"), // Convex ID reference
323
- published: z.boolean().default(false),
324
- tags: z.array(z.string()).optional(),
325
- };
326
-
327
- // Create table helper - pass the plain object shape
328
- export const Posts = zodTable("posts", postShape);
329
-
330
- // Access properties
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
335
-
336
- // Use in schema.ts
337
- export default defineSchema({
338
- posts: Posts.table
339
- .index("by_author", ["authorId"])
340
- .index("by_published", ["published"]),
341
- });
300
+ // Convert single Zod type to Convex validator
301
+ const validator = zodToConvex(z.string().optional())
302
+ // → v.optional(v.string())
342
303
 
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
- );
304
+ // Convert object shape to Convex field validators
305
+ const fields = zodToConvexFields({
306
+ name: z.string(),
307
+ age: z.number().nullable()
308
+ })
309
+ // → { name: v.string(), age: v.union(v.float64(), v.null()) }
350
310
  ```
351
311
 
352
- #### Working with Large Schemas
312
+ ### Codecs
353
313
 
354
- For large schemas (30+ fields), the plain object pattern is recommended:
314
+ Convert between Zod-shaped data and Convex-safe JSON:
355
315
 
356
316
  ```ts
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
- ```
317
+ import { convexCodec } from 'zodvex'
383
318
 
384
- ## 🔧 Advanced Usage
319
+ const UserSchema = z.object({
320
+ name: z.string(),
321
+ birthday: z.date().optional()
322
+ })
385
323
 
386
- ### Builder Pattern for Reusable Function Creators
324
+ const codec = convexCodec(UserSchema)
387
325
 
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:
326
+ // Encode: Date timestamp, omit undefined
327
+ const encoded = codec.encode({
328
+ name: 'Alice',
329
+ birthday: new Date('1990-01-01')
330
+ })
331
+ // → { name: 'Alice', birthday: 631152000000 }
332
+
333
+ // Decode: timestamp → Date
334
+ const decoded = codec.decode(encoded)
335
+ // → { name: 'Alice', birthday: Date('1990-01-01') }
336
+ ```
390
337
 
391
- ```ts
392
- import { query, mutation } from "./_generated/server";
393
- import { zQueryBuilder, zMutationBuilder } from "zodvex";
338
+ ### Supported Types
394
339
 
395
- // Create reusable builders
396
- export const zq = zQueryBuilder(query);
397
- export const zm = zMutationBuilder(mutation);
340
+ | Zod Type | Convex Validator |
341
+ | ----------------- | ------------------------- |
342
+ | `z.string()` | `v.string()` |
343
+ | `z.number()` | `v.float64()` |
344
+ | `z.bigint()` | `v.int64()` |
345
+ | `z.boolean()` | `v.boolean()` |
346
+ | `z.date()` | `v.float64()` (timestamp) |
347
+ | `z.null()` | `v.null()` |
348
+ | `z.array(T)` | `v.array(T)` |
349
+ | `z.object({...})` | `v.object({...})` |
350
+ | `z.record(T)` | `v.record(v.string(), T)` |
351
+ | `z.union([...])` | `v.union(...)` |
352
+ | `z.literal(x)` | `v.literal(x)` |
353
+ | `z.enum([...])` | `v.union(literals...)` |
354
+ | `z.optional(T)` | `v.optional(T)` |
355
+ | `z.nullable(T)` | `v.union(T, v.null())` |
398
356
 
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
- });
357
+ **Convex IDs:**
358
+ ```ts
359
+ import { zid } from 'zodvex'
406
360
 
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
- });
361
+ zid('tableName') // v.id('tableName')
362
+ zid('tableName').optional() // → v.optional(v.id('tableName'))
413
363
  ```
414
364
 
415
- ### Custom Context with Middleware
365
+ ## Advanced Usage
416
366
 
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:
367
+ ### Custom Context Builders
368
+
369
+ Create builders with injected auth, permissions, or other context:
419
370
 
420
371
  ```ts
421
- import { query, mutation } from "./_generated/server";
422
- import { zCustomQueryBuilder, zCustomMutationBuilder, customCtx } from "zodvex";
372
+ import { zCustomQueryBuilder, zCustomMutationBuilder, customCtx } from 'zodvex'
373
+ import { query, mutation } from './_generated/server'
423
374
 
424
- // Create authenticated query builder
375
+ // Add user to all queries
425
376
  export const authQuery = zCustomQueryBuilder(
426
377
  query,
427
378
  customCtx(async (ctx) => {
428
- const user = await getUserOrThrow(ctx);
429
- return { user };
379
+ const user = await getUserOrThrow(ctx)
380
+ return { user }
430
381
  })
431
- );
382
+ )
432
383
 
433
- // Create authenticated mutation builder
384
+ // Add user + permissions to mutations
434
385
  export const authMutation = zCustomMutationBuilder(
435
386
  mutation,
436
387
  customCtx(async (ctx) => {
437
- const user = await getUserOrThrow(ctx);
438
- return { user };
388
+ const user = await getUserOrThrow(ctx)
389
+ const permissions = await getPermissions(ctx, user)
390
+ return { user, permissions }
439
391
  })
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
- });
392
+ )
450
393
 
394
+ // Use them
451
395
  export const updateProfile = authMutation({
452
396
  args: { name: z.string() },
397
+ returns: z.null(),
453
398
  handler: async (ctx, { name }) => {
454
- // ctx.user is automatically available and typed!
455
- return ctx.db.patch(ctx.user._id, { name });
399
+ // ctx.user and ctx.permissions are available
400
+ if (!ctx.permissions.canEdit) {
401
+ throw new Error('No permission')
402
+ }
403
+ await ctx.db.patch(ctx.user._id, { name })
404
+ return null
456
405
  }
457
- });
406
+ })
458
407
  ```
459
408
 
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
409
+ ### Date Handling
464
410
 
465
- ### Working with Dates
411
+ Dates are automatically converted to timestamps:
466
412
 
467
413
  ```ts
468
- // Dates are automatically converted to timestamps
469
414
  const eventShape = {
470
415
  title: z.string(),
471
416
  startDate: z.date(),
472
- endDate: z.date().nullable(),
473
- };
417
+ endDate: z.date().nullable()
418
+ }
474
419
 
475
- const Events = zodTable("events", eventShape);
420
+ export const Events = zodTable('events', eventShape)
476
421
 
477
- // In mutations - Date objects work seamlessly
478
- export const createEvent = zMutation(
479
- mutation,
480
- z.object(eventShape),
481
- async (ctx, event) => {
422
+ export const createEvent = zm({
423
+ args: eventShape,
424
+ handler: async (ctx, event) => {
482
425
  // event.startDate is a Date object
483
- // It's automatically converted to timestamp for storage
484
- return ctx.db.insert("events", event);
485
- },
486
- );
426
+ // Automatically converted to timestamp for storage
427
+ return await ctx.db.insert('events', event)
428
+ }
429
+ })
487
430
  ```
488
431
 
489
- ### Using Convex IDs
432
+ ### Return Type Helpers
490
433
 
491
434
  ```ts
492
- import { zid } from "zodvex";
493
-
494
- const CommentSchema = z.object({
495
- text: z.string(),
496
- postId: zid("posts"), // Reference to posts table
497
- authorId: zid("users"), // Reference to users table
498
- parentId: zid("comments").optional(), // Self-reference
499
- });
435
+ import { returnsAs } from 'zodvex'
436
+
437
+ export const listUsers = zq({
438
+ args: {},
439
+ handler: async (ctx) => {
440
+ const rows = await ctx.db.query('users').collect()
441
+ // Use returnsAs for type hint in tricky inference spots
442
+ return returnsAs<typeof Users.docArray>()(rows)
443
+ },
444
+ returns: Users.docArray
445
+ })
500
446
  ```
501
447
 
502
- ## ⚙️ Behavior & Semantics
448
+ ### Working with Large Schemas
503
449
 
504
- ### Type Mappings
450
+ zodvex provides `pickShape` and `safePick` helpers as alternatives to Zod's `.pick()`:
505
451
 
506
- | Zod Type | Convex Validator |
507
- | ----------------- | ------------------------- |
508
- | `z.string()` | `v.string()` |
509
- | `z.number()` | `v.float64()` |
510
- | `z.bigint()` | `v.int64()` |
511
- | `z.boolean()` | `v.boolean()` |
512
- | `z.date()` | `v.float64()` (timestamp) |
513
- | `z.null()` | `v.null()` |
514
- | `z.array(T)` | `v.array(T)` |
515
- | `z.object({...})` | `v.object({...})` |
516
- | `z.record(T)` | `v.record(v.string(), T)` |
517
- | `z.union([...])` | `v.union(...)` |
518
- | `z.literal(x)` | `v.literal(x)` |
519
- | `z.enum([...])` | `v.union(literals...)` |
520
- | `z.optional(T)` | `v.optional(T)` |
521
- | `z.nullable(T)` | `v.union(T, v.null())` |
522
-
523
- ### Important Notes
452
+ ```ts
453
+ import { pickShape, safePick } from 'zodvex'
524
454
 
525
- - **Defaults**: Zod defaults imply optional at the Convex schema level. Apply defaults in your application code.
526
- - **Numbers**: `z.number()` maps to `v.float64()`. For integers, use `z.bigint()` → `v.int64()`.
527
- - **Transforms**: Zod transforms (`.transform()`, `.refine()`) are not supported in schema mapping and fall back to `v.any()`.
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.
455
+ // Standard Zod .pick() works great for most schemas
456
+ const UserUpdate = User.pick({ email: true, firstName: true, lastName: true })
529
457
 
530
- ### Runtime Conversion Consistency
458
+ // If you hit TypeScript instantiation depth limits (rare, 100+ fields),
459
+ // use pickShape or safePick:
460
+ const userShape = pickShape(User, ['email', 'firstName', 'lastName'])
461
+ const UserUpdate = z.object(userShape)
531
462
 
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.
463
+ // Or use safePick (convenience wrapper that does the same thing)
464
+ const UserUpdate = safePick(User, { email: true, firstName: true, lastName: true })
465
+ ```
533
466
 
534
- ## 📝 Compatibility
467
+ ## Why zodvex?
535
468
 
536
- - **Zod**: v4 only (uses public v4 APIs)
537
- - **Convex**: >= 1.27
538
- - **TypeScript**: Full type inference support
469
+ - **Correct optional/nullable semantics** - Preserves Convex's distinction
470
+ - `.optional()` `v.optional(T)` (field can be omitted)
471
+ - `.nullable()` `v.union(T, v.null())` (required but can be null)
472
+ - Both → `v.optional(v.union(T, v.null()))`
473
+ - **Superior type safety** - Builders provide better type inference than wrapper functions
474
+ - **Date handling** - Automatic `Date` ↔ timestamp conversion
475
+ - **End-to-end validation** - Same schema from database to frontend forms
539
476
 
540
- ## 🤝 Contributing
477
+ ## Compatibility
541
478
 
542
- Contributions are welcome! Please feel free to submit a Pull Request.
479
+ - **Zod**: ^4.1.0 or later
480
+ - **Convex**: >= 1.27.0
481
+ - **convex-helpers**: >= 0.1.104
482
+ - **TypeScript**: Full type inference support
543
483
 
544
- ## 📄 License
484
+ ## License
545
485
 
546
486
  MIT
547
487