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 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 Zod schema
75
- const UsersSchema = z.object({
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", UsersSchema);
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 PostSchema = z.object({
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", PostSchema);
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; // → Table definition for defineSchema
229
- Posts.schema; // → Original Zod schema
230
- Posts.codec; // → ConvexCodec instance
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
- ### CRUD Operations
352
+ #### Working with Large Schemas
241
353
 
242
- Generate complete CRUD operations from a table:
354
+ For large schemas (30+ fields), the plain object pattern is recommended:
243
355
 
244
356
  ```ts
245
- import { zCrud, zid } from "zodvex";
246
- import { query, mutation } from "./_generated/server";
247
- import { Posts } from "./schemas/posts";
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
- // Generate CRUD operations
250
- export const postsCrud = zCrud(Posts, query, mutation);
384
+ ## 🔧 Advanced Usage
385
+
386
+ ### Builder Pattern for Reusable Function Creators
251
387
 
252
- // Now you have:
253
- // postsCrud.create - Create a new post
254
- // postsCrud.read - Read a post by ID
255
- // postsCrud.paginate - Paginate through posts
256
- // postsCrud.update - Update a post by ID
257
- // postsCrud.destroy - Delete a post by ID
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
- // Each operation is fully typed based on your schema!
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
- ## 🔧 Advanced Usage
415
+ ### Custom Context with Middleware
263
416
 
264
- ### Custom Validators with Zod
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 { z } from "zod";
268
- import { zCustomQuery } from "zodvex";
269
- import { customQuery } from "convex-helpers/server/customFunctions";
270
-
271
- // Use with custom function builders
272
- export const authenticatedQuery = zCustomQuery(
273
- customQuery(query, {
274
- args: { sessionId: v.string() },
275
- input: async (ctx, { sessionId }) => {
276
- const user = await getUser(ctx, sessionId);
277
- return { user };
278
- },
279
- }),
280
- { postId: z.string() },
281
- async (ctx, { postId }) => {
282
- // ctx.user is available from custom input
283
- return ctx.db.get(postId);
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 EventSchema = z.object({
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 Event = zodTable("events", EventSchema);
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
- EventSchema,
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 validation**: When `returns` is specified, the function's return value is validated and encoded (Date → timestamp).
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