zodvex 0.2.0 → 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,269 +4,309 @@
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
58
50
 
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
- });
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'
57
+
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
101
+ ```ts
102
+ // convex/schema.ts
103
+ import { z } from 'zod'
104
+ import { zodTable, zid } from 'zodvex'
103
105
 
104
- ### Supported Types
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
+ ```
105
120
 
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).
121
+ ## Building Your Schema
107
122
 
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')`
123
+ Use `zodTable().table` in your Convex schema:
118
124
 
119
- Unsupported or partial (explicitly out-of-scope):
125
+ ```ts
126
+ // convex/schema.ts
127
+ import { defineSchema } from 'convex/server'
128
+ import { Users } from './tables/users'
129
+ import { Teams } from './tables/teams'
120
130
 
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
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
+ ```
125
141
 
126
- Note: `z.bigint()` → `v.int64()` is recognized for validator mapping but currently has no special runtime encode/decode; prefer numbers where possible.
142
+ ## Defining Functions
127
143
 
128
- ## 📚 API Reference
144
+ Use your builders from `util.ts` to create type-safe functions:
129
145
 
130
- ### Mapping Helpers
146
+ ```ts
147
+ import { z } from 'zod'
148
+ import { zid } from 'zodvex'
149
+ import { zq, zm } from './util'
150
+ import { Users } from './tables/users'
151
+
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
+ })
160
+
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
+ })
179
+ ```
131
180
 
132
- Convert Zod schemas to Convex validators:
181
+ ## Working with Subsets
133
182
 
134
- ```ts
135
- import { z } from "zod";
136
- import { zodToConvex, zodToConvexFields, pickShape, safePick } from "zodvex";
183
+ Pick a subset of fields for focused operations:
137
184
 
138
- // Convert a single Zod type to a Convex validator
139
- const validator = zodToConvex(z.string().optional());
140
- // v.optional(v.string())
185
+ ```ts
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
202
+ },
203
+ handler: async (ctx, { id, ...fields }) => {
204
+ await ctx.db.patch(id, fields)
205
+ }
206
+ })
207
+
208
+ // Or inline for simple cases
209
+ export const updateUserName = zm({
210
+ args: {
211
+ id: zid('users'),
212
+ name: z.string()
213
+ },
214
+ handler: async (ctx, { id, name }) => {
215
+ await ctx.db.patch(id, { name })
216
+ }
217
+ })
218
+ ```
141
219
 
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()) }
220
+ ## Form Validation
148
221
 
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.
222
+ Use your schemas with form libraries like react-hook-form:
152
223
 
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
172
- ```
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'
173
231
 
174
- ### Function Wrappers
232
+ // Create form schema from your table schema
233
+ const CreateUserForm = z.object(Users.shape)
234
+ type CreateUserForm = z.infer<typeof CreateUserForm>
175
235
 
176
- Type-safe wrappers for Convex functions:
236
+ function UserForm() {
237
+ const createUser = useMutation(api.users.createUser)
177
238
 
178
- ```ts
179
- import { z } from "zod";
180
- import { query, mutation, action } from "./_generated/server";
181
- import { zQuery, zMutation, zAction } from "zodvex";
239
+ const { register, handleSubmit, formState: { errors } } = useForm<CreateUserForm>({
240
+ resolver: zodResolver(CreateUserForm)
241
+ })
182
242
 
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
- }),
193
- },
194
- );
243
+ const onSubmit = async (data: CreateUserForm) => {
244
+ await createUser(data)
245
+ }
195
246
 
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
212
- },
213
- );
247
+ return (
248
+ <form onSubmit={handleSubmit(onSubmit)}>
249
+ <input {...register('name')} />
250
+ {errors.name && <span>{errors.name.message}</span>}
214
251
 
215
- // Internal functions also supported
216
- import { zInternalQuery, zInternalMutation, zInternalAction } from "zodvex";
252
+ <input {...register('email')} />
253
+ {errors.email && <span>{errors.email.message}</span>}
217
254
 
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.
255
+ <button type="submit">Create User</button>
256
+ </form>
257
+ )
258
+ }
221
259
  ```
222
260
 
223
- #### Return Typing + Helpers
261
+ ## API Reference
224
262
 
225
- Handlers should return domain values shaped by your `returns` schema. zodvex validates and encodes them to Convex Values for you. For common patterns:
263
+ ### Builders
226
264
 
265
+ **Basic builders** - Create type-safe functions without auth:
227
266
  ```ts
228
- import { z } from "zod";
229
- import { zQuery, zodTableWithDocs, returnsAs } from "zodvex";
230
- import { query } from "./_generated/server";
267
+ zQueryBuilder(query) // Creates query builder
268
+ zMutationBuilder(mutation) // Creates mutation builder
269
+ zActionBuilder(action) // Creates action builder
270
+ ```
231
271
 
232
- // Table with doc helpers (use zodTableWithDocs for .docArray)
233
- const UserSchema = z.object({ name: z.string(), createdAt: z.date() });
234
- export const Users = zodTableWithDocs("users", UserSchema);
272
+ **Custom builders** - Add auth or custom context:
273
+ ```ts
274
+ import { customCtx } from 'zodvex'
235
275
 
236
- // 1) Return full docs, strongly typed
237
- export const listUsers = zQuery(
276
+ const authQuery = zCustomQueryBuilder(
238
277
  query,
239
- {},
240
- async (ctx) => {
241
- const rows = await ctx.db.query("users").collect();
242
- // No cast needed — handler returns domain values; wrapper encodes to Convex Values
243
- return rows;
244
- },
245
- { returns: Users.docArray }
246
- );
278
+ customCtx(async (ctx) => {
279
+ const user = await getUserOrThrow(ctx)
280
+ return { user }
281
+ })
282
+ )
283
+
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
+ })
293
+ ```
247
294
 
248
- // 2) Return a custom shape (with Dates)
249
- export const createdBounds = zQuery(
250
- query,
251
- {},
252
- async (ctx) => {
253
- const first = await ctx.db.query("users").order("asc").first();
254
- const last = await ctx.db.query("users").order("desc").first();
255
- return { first: first?.createdAt ?? null, last: last?.createdAt ?? null };
256
- },
257
- { returns: z.object({ first: z.date().nullable(), last: z.date().nullable() }) }
258
- );
295
+ ### Mapping Helpers
259
296
 
260
- // 3) In tricky inference spots, use a typed identity
261
- export const listUsersOk = zQuery(
262
- query,
263
- {},
264
- async (ctx) => {
265
- const rows = await ctx.db.query("users").collect();
266
- return returnsAs<typeof Users.docArray>()(rows);
267
- },
268
- { returns: Users.docArray }
269
- );
297
+ ```ts
298
+ import { zodToConvex, zodToConvexFields } from 'zodvex'
299
+
300
+ // Convert single Zod type to Convex validator
301
+ const validator = zodToConvex(z.string().optional())
302
+ // v.optional(v.string())
303
+
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()) }
270
310
  ```
271
311
 
272
312
  ### Codecs
@@ -274,228 +314,174 @@ export const listUsersOk = zQuery(
274
314
  Convert between Zod-shaped data and Convex-safe JSON:
275
315
 
276
316
  ```ts
277
- import { convexCodec } from "zodvex";
278
- import { z } from "zod";
317
+ import { convexCodec } from 'zodvex'
279
318
 
280
319
  const UserSchema = z.object({
281
320
  name: z.string(),
282
- birthday: z.date().optional(),
283
- metadata: z.record(z.string()),
284
- });
321
+ birthday: z.date().optional()
322
+ })
285
323
 
286
- const codec = convexCodec(UserSchema);
324
+ const codec = convexCodec(UserSchema)
287
325
 
288
- // Get Convex validators for table definition
289
- const validators = codec.toConvexSchema();
290
-
291
- // Encode: Zod data → Convex JSON (Date → timestamp, omit undefined)
326
+ // Encode: Date timestamp, omit undefined
292
327
  const encoded = codec.encode({
293
- name: "Alice",
294
- birthday: new Date("1990-01-01"),
295
- metadata: { role: "admin" },
296
- });
297
- // → { name: 'Alice', birthday: 631152000000, metadata: { role: 'admin' } }
298
-
299
- // Decode: Convex JSON → Zod data (timestamp → Date)
300
- const decoded = codec.decode(encoded);
301
- // → { name: 'Alice', birthday: Date('1990-01-01'), metadata: { role: 'admin' } }
302
-
303
- // Create sub-codecs (ZodObject only)
304
- const nameCodec = codec.pick({ name: true });
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') }
305
336
  ```
306
337
 
307
- ### Table Helpers
338
+ ### Supported Types
308
339
 
309
- Define Convex tables from Zod schemas. `zodTable` accepts a **plain object shape** for best type inference:
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())` |
310
356
 
357
+ **Convex IDs:**
311
358
  ```ts
312
- import { z } from "zod";
313
- import { zodTable, zid } from "zodvex";
314
- import { defineSchema } from "convex/server";
315
-
316
- // Define your schema as a plain object
317
- const postShape = {
318
- title: z.string(),
319
- content: z.string(),
320
- authorId: zid("users"), // Convex ID reference
321
- published: z.boolean().default(false),
322
- tags: z.array(z.string()).optional(),
323
- };
324
-
325
- // Create table helper - pass the plain object shape
326
- export const Posts = zodTable("posts", postShape);
327
-
328
- // Access properties
329
- Posts.table; // → Table definition for defineSchema
330
- Posts.shape; // → Original plain object shape
331
- Posts.zDoc; // → ZodObject with _id and _creationTime system fields
359
+ import { zid } from 'zodvex'
332
360
 
333
- // Use in schema.ts
334
- export default defineSchema({
335
- posts: Posts.table
336
- .index("by_author", ["authorId"])
337
- .index("by_published", ["published"]),
338
- });
361
+ zid('tableName') // v.id('tableName')
362
+ zid('tableName').optional() // → v.optional(v.id('tableName'))
339
363
  ```
340
364
 
341
- #### Working with Large Schemas
342
-
343
- For large schemas (30+ fields), the plain object pattern is recommended:
344
-
345
- ```ts
346
- // Define as plain object for better maintainability
347
- export const users = {
348
- // Meta
349
- tokenId: z.string(),
350
- type: z.literal("member"),
351
- isAdmin: z.boolean(),
352
-
353
- // User info
354
- email: z.string(),
355
- firstName: z.string().optional(),
356
- lastName: z.string().optional(),
357
- phone: z.string().optional(),
358
-
359
- // References
360
- favoriteUsers: z.array(zid("users")).optional(),
361
- activeDancerId: zid("dancers").optional(),
362
- // ... 40+ more fields
363
- };
364
-
365
- // Create table
366
- export const Users = zodTable("users", users);
367
-
368
- // Extract subsets without Zod's .pick() to avoid type complexity
369
- const zClerkFields = z.object(pickShape(users, ["email", "firstName", "lastName", "tokenId"]));
370
- // Use zClerkFields in function wrappers or validators
371
- ```
365
+ ## Advanced Usage
372
366
 
373
- #### Alternative: zodTableWithDocs
367
+ ### Custom Context Builders
374
368
 
375
- If you prefer working with `z.object()` directly, use `zodTableWithDocs`:
369
+ Create builders with injected auth, permissions, or other context:
376
370
 
377
371
  ```ts
378
- const PostSchema = z.object({
379
- title: z.string(),
380
- content: z.string(),
381
- authorId: zid("users"),
382
- });
372
+ import { zCustomQueryBuilder, zCustomMutationBuilder, customCtx } from 'zodvex'
373
+ import { query, mutation } from './_generated/server'
383
374
 
384
- export const Posts = zodTableWithDocs("posts", PostSchema);
385
-
386
- // Different properties available:
387
- Posts.table; // Table definition
388
- Posts.schema; // Original z.object() schema
389
- Posts.docSchema; // Schema with _id and _creationTime (Dates → numbers)
390
- Posts.docArray; // → z.array(docSchema) for return types
375
+ // Add user to all queries
376
+ export const authQuery = zCustomQueryBuilder(
377
+ query,
378
+ customCtx(async (ctx) => {
379
+ const user = await getUserOrThrow(ctx)
380
+ return { user }
381
+ })
382
+ )
383
+
384
+ // Add user + permissions to mutations
385
+ export const authMutation = zCustomMutationBuilder(
386
+ mutation,
387
+ customCtx(async (ctx) => {
388
+ const user = await getUserOrThrow(ctx)
389
+ const permissions = await getPermissions(ctx, user)
390
+ return { user, permissions }
391
+ })
392
+ )
393
+
394
+ // Use them
395
+ export const updateProfile = authMutation({
396
+ args: { name: z.string() },
397
+ returns: z.null(),
398
+ handler: async (ctx, { 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
405
+ }
406
+ })
391
407
  ```
392
408
 
393
- ## 🔧 Advanced Usage
394
-
395
- ### Custom Validators with Zod
396
-
397
- ```ts
398
- import { z } from "zod";
399
- import { zCustomQuery } from "zodvex";
400
- import { customQuery } from "convex-helpers/server/customFunctions";
401
-
402
- // Use with custom function builders
403
- export const authenticatedQuery = zCustomQuery(
404
- customQuery(query, {
405
- args: { sessionId: v.string() },
406
- input: async (ctx, { sessionId }) => {
407
- const user = await getUser(ctx, sessionId);
408
- return { user };
409
- },
410
- }),
411
- { postId: z.string() },
412
- async (ctx, { postId }) => {
413
- // ctx.user is available from custom input
414
- return ctx.db.get(postId);
415
- },
416
- );
417
- ```
409
+ ### Date Handling
418
410
 
419
- ### Working with Dates
411
+ Dates are automatically converted to timestamps:
420
412
 
421
413
  ```ts
422
- // Dates are automatically converted to timestamps
423
414
  const eventShape = {
424
415
  title: z.string(),
425
416
  startDate: z.date(),
426
- endDate: z.date().nullable(),
427
- };
417
+ endDate: z.date().nullable()
418
+ }
428
419
 
429
- const Events = zodTable("events", eventShape);
420
+ export const Events = zodTable('events', eventShape)
430
421
 
431
- // In mutations - Date objects work seamlessly
432
- export const createEvent = zMutation(
433
- mutation,
434
- z.object(eventShape),
435
- async (ctx, event) => {
422
+ export const createEvent = zm({
423
+ args: eventShape,
424
+ handler: async (ctx, event) => {
436
425
  // event.startDate is a Date object
437
- // It's automatically converted to timestamp for storage
438
- return ctx.db.insert("events", event);
439
- },
440
- );
426
+ // Automatically converted to timestamp for storage
427
+ return await ctx.db.insert('events', event)
428
+ }
429
+ })
441
430
  ```
442
431
 
443
- ### Using Convex IDs
432
+ ### Return Type Helpers
444
433
 
445
434
  ```ts
446
- import { zid } from "zodvex";
447
-
448
- const CommentSchema = z.object({
449
- text: z.string(),
450
- postId: zid("posts"), // Reference to posts table
451
- authorId: zid("users"), // Reference to users table
452
- parentId: zid("comments").optional(), // Self-reference
453
- });
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
+ })
454
446
  ```
455
447
 
456
- ## ⚙️ Behavior & Semantics
448
+ ### Working with Large Schemas
457
449
 
458
- ### Type Mappings
450
+ zodvex provides `pickShape` and `safePick` helpers as alternatives to Zod's `.pick()`:
459
451
 
460
- | Zod Type | Convex Validator |
461
- | ----------------- | ------------------------- |
462
- | `z.string()` | `v.string()` |
463
- | `z.number()` | `v.float64()` |
464
- | `z.bigint()` | `v.int64()` |
465
- | `z.boolean()` | `v.boolean()` |
466
- | `z.date()` | `v.float64()` (timestamp) |
467
- | `z.null()` | `v.null()` |
468
- | `z.array(T)` | `v.array(T)` |
469
- | `z.object({...})` | `v.object({...})` |
470
- | `z.record(T)` | `v.record(v.string(), T)` |
471
- | `z.union([...])` | `v.union(...)` |
472
- | `z.literal(x)` | `v.literal(x)` |
473
- | `z.enum([...])` | `v.union(literals...)` |
474
- | `z.optional(T)` | `v.optional(T)` |
475
- | `z.nullable(T)` | `v.union(T, v.null())` |
476
-
477
- ### Important Notes
452
+ ```ts
453
+ import { pickShape, safePick } from 'zodvex'
478
454
 
479
- - **Defaults**: Zod defaults imply optional at the Convex schema level. Apply defaults in your application code.
480
- - **Numbers**: `z.number()` maps to `v.float64()`. For integers, use `z.bigint()` → `v.int64()`.
481
- - **Transforms**: Zod transforms (`.transform()`, `.refine()`) are not supported in schema mapping and fall back to `v.any()`.
482
- - **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 })
483
457
 
484
- ### 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)
485
462
 
486
- 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
+ ```
487
466
 
488
- ## 📝 Compatibility
467
+ ## Why zodvex?
489
468
 
490
- - **Zod**: v4 only (uses public v4 APIs)
491
- - **Convex**: >= 1.27
492
- - **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
493
476
 
494
- ## 🤝 Contributing
477
+ ## Compatibility
495
478
 
496
- 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
497
483
 
498
- ## 📄 License
484
+ ## License
499
485
 
500
486
  MIT
501
487