zodvex 0.2.5 → 0.3.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/src/custom.ts CHANGED
@@ -178,13 +178,15 @@ export function customFnBuilder<
178
178
 
179
179
  return function customBuilder(fn: any): any {
180
180
  const { args, handler = fn, returns: maybeObject, ...extra } = fn
181
+ const skipConvexValidation = fn.skipConvexValidation ?? false
181
182
 
182
183
  const returns =
183
184
  maybeObject && !(maybeObject instanceof z.ZodType) ? z.object(maybeObject) : maybeObject
185
+ // Only generate Convex return validator when not skipping Convex validation
184
186
  const returnValidator =
185
- returns && !fn.skipConvexValidation ? { returns: zodToConvex(returns) } : undefined
187
+ returns && !skipConvexValidation ? { returns: zodToConvex(returns) } : undefined
186
188
 
187
- if (args && !fn.skipConvexValidation) {
189
+ if (args) {
188
190
  let argsValidator = args
189
191
  let argsSchema: z.ZodObject<any>
190
192
 
@@ -202,9 +204,13 @@ export function customFnBuilder<
202
204
  argsSchema = z.object(argsValidator)
203
205
  }
204
206
 
205
- const convexValidator = zodToConvexFields(argsValidator)
207
+ // Only generate Convex args validator when not skipping Convex validation
208
+ const convexArgs = skipConvexValidation
209
+ ? inputArgs
210
+ : { ...zodToConvexFields(argsValidator), ...inputArgs }
211
+
206
212
  return builder({
207
- args: { ...convexValidator, ...inputArgs },
213
+ args: convexArgs,
208
214
  ...returnValidator,
209
215
  handler: async (ctx: Ctx, allArgs: any) => {
210
216
  const added: any = await customInput(
@@ -215,6 +221,7 @@ export function customFnBuilder<
215
221
  const argKeys = Object.keys(argsValidator)
216
222
  const rawArgs = pick(allArgs, argKeys)
217
223
  const decoded = fromConvexJS(rawArgs, argsSchema)
224
+ // Always run Zod validation, regardless of skipConvexValidation
218
225
  const parsed = argsSchema.safeParse(decoded)
219
226
  if (!parsed.success) {
220
227
  handleZodValidationError(parsed.error, 'args')
@@ -224,7 +231,8 @@ export function customFnBuilder<
224
231
  const addedArgs = (added?.args as Record<string, unknown>) ?? {}
225
232
  const finalArgs = { ...baseArgs, ...addedArgs }
226
233
  const ret = await handler(finalCtx, finalArgs)
227
- if (returns && !fn.skipConvexValidation) {
234
+ // Always run Zod return validation when returns schema is provided
235
+ if (returns) {
228
236
  let validated: any
229
237
  try {
230
238
  validated = (returns as z.ZodTypeAny).parse(ret)
@@ -261,7 +269,8 @@ export function customFnBuilder<
261
269
  const addedArgs = (added?.args as Record<string, unknown>) ?? {}
262
270
  const finalArgs = { ...baseArgs, ...addedArgs }
263
271
  const ret = await handler(finalCtx, finalArgs)
264
- if (returns && !fn.skipConvexValidation) {
272
+ // Always run Zod return validation when returns schema is provided
273
+ if (returns) {
265
274
  let validated: any
266
275
  try {
267
276
  validated = (returns as z.ZodTypeAny).parse(ret)
package/src/ids.ts CHANGED
@@ -16,23 +16,21 @@ export const registryHelpers = {
16
16
  /**
17
17
  * Create a Zod validator for a Convex Id
18
18
  *
19
- * Uses the string transform brand pattern for proper type narrowing with ctx.db.get()
20
- * This aligns with Zod v4 best practices and matches convex-helpers implementation
19
+ * Compatible with AI SDK and other tools that don't support transforms.
20
+ * Uses type-level branding instead of runtime transforms for GenericId<T> compatibility.
21
+ *
22
+ * @param tableName - The Convex table name for this ID
23
+ * @returns A Zod string validator typed as GenericId<TableName>
21
24
  */
22
25
  export function zid<TableName extends string>(
23
26
  tableName: TableName
24
27
  ): z.ZodType<GenericId<TableName>> & { _tableName: TableName } {
25
- // Use the string transform brand pattern (aligned with Zod v4 best practices)
28
+ // Create base string validator with refinement (no transform or brand)
26
29
  const baseSchema = z
27
30
  .string()
28
31
  .refine(val => typeof val === 'string' && val.length > 0, {
29
32
  message: `Invalid ID for table "${tableName}"`
30
33
  })
31
- .transform(val => {
32
- // Cast to GenericId while keeping the string value
33
- return val as string & GenericId<TableName>
34
- })
35
- .brand(`ConvexId_${tableName}`) // Use native Zod v4 .brand() method
36
34
  // Add a human-readable marker for client-side introspection utilities
37
35
  // used in apps/native (e.g., to detect relationship fields in dynamic forms).
38
36
  .describe(`convexId:${tableName}`)
@@ -47,6 +45,8 @@ export function zid<TableName extends string>(
47
45
  const branded = baseSchema as any
48
46
  branded._tableName = tableName
49
47
 
48
+ // Type assertion provides GenericId<TableName> typing without runtime transform
49
+ // This maintains type safety while being compatible with AI SDK and similar tools
50
50
  return branded as z.ZodType<GenericId<TableName>> & { _tableName: TableName }
51
51
  }
52
52
 
package/src/registry.ts CHANGED
@@ -56,3 +56,174 @@ export function isDateSchema(schema: any): boolean {
56
56
 
57
57
  return false
58
58
  }
59
+
60
+ // ============================================================================
61
+ // JSON Schema Override Support
62
+ // ============================================================================
63
+ // Zod's toJSONSchema doesn't support transforms, brands, and other "unrepresentable"
64
+ // types by default. This module provides overrides for zodvex-managed types
65
+ // so they can be used with AI SDKs and other JSON Schema-based tools.
66
+
67
+ /**
68
+ * Checks if a schema is a zid (Convex ID) schema by looking at its description.
69
+ * zid schemas are marked with "convexId:{tableName}" in their description.
70
+ */
71
+ export function isZidSchema(schema: z.ZodTypeAny): boolean {
72
+ const description = schema.description
73
+ return typeof description === 'string' && description.startsWith('convexId:')
74
+ }
75
+
76
+ /**
77
+ * Extracts the table name from a zid schema's description.
78
+ * Returns undefined if not a zid schema.
79
+ */
80
+ export function getZidTableName(schema: z.ZodTypeAny): string | undefined {
81
+ const description = schema.description
82
+ if (typeof description === 'string' && description.startsWith('convexId:')) {
83
+ return description.slice('convexId:'.length)
84
+ }
85
+ return undefined
86
+ }
87
+
88
+ /**
89
+ * Context object passed to the JSON Schema override function.
90
+ * Uses 'any' types for compatibility with Zod's internal types.
91
+ */
92
+ export interface JSONSchemaOverrideContext {
93
+ zodSchema: any // Zod's internal $ZodTypes
94
+ jsonSchema: any // Zod's JSONSchema.BaseSchema
95
+ }
96
+
97
+ /**
98
+ * Override function for z.toJSONSchema that handles zodvex-managed types.
99
+ *
100
+ * Handles:
101
+ * - zid schemas: Converts to { type: "string" } with convexId format
102
+ * - z.date(): Converts to { type: "string", format: "date-time" }
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * import { z } from 'zod'
107
+ * import { zid, zodvexJSONSchemaOverride } from 'zodvex'
108
+ *
109
+ * const schema = z.object({
110
+ * userId: zid('users'),
111
+ * name: z.string()
112
+ * })
113
+ *
114
+ * const jsonSchema = z.toJSONSchema(schema, {
115
+ * unrepresentable: 'any',
116
+ * override: zodvexJSONSchemaOverride
117
+ * })
118
+ * // => { type: "object", properties: { userId: { type: "string" }, name: { type: "string" } } }
119
+ * ```
120
+ */
121
+ export function zodvexJSONSchemaOverride(ctx: JSONSchemaOverrideContext): void {
122
+ const { zodSchema, jsonSchema } = ctx
123
+
124
+ // Handle zid schemas (transforms with convexId description)
125
+ if (isZidSchema(zodSchema)) {
126
+ const tableName = getZidTableName(zodSchema)
127
+ // Set our properties - don't clear existing ones set by user overrides
128
+ // When unrepresentable: 'any', Zod already gives us {} so no clearing needed
129
+ jsonSchema.type = 'string'
130
+ if (tableName) {
131
+ jsonSchema.format = `convex-id:${tableName}`
132
+ }
133
+ // Preserve the description from .describe() - this is what the LLM sees
134
+ if (zodSchema.description) {
135
+ jsonSchema.description = zodSchema.description
136
+ }
137
+ return
138
+ }
139
+
140
+ // Handle z.date() - convert to ISO 8601 string format
141
+ // Zod v4 passes real schema instances here (ZodDate has `type === 'date'`).
142
+ if (zodSchema instanceof z.ZodDate || (zodSchema as any).type === 'date') {
143
+ jsonSchema.type = 'string'
144
+ jsonSchema.format = 'date-time'
145
+ return
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Composes multiple JSON Schema override functions into one.
151
+ * Overrides run in order - first override runs first.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * import { composeOverrides, zodvexJSONSchemaOverride } from 'zodvex'
156
+ *
157
+ * const myOverride = (ctx) => {
158
+ * if (ctx.zodSchema.description?.startsWith('myType:')) {
159
+ * ctx.jsonSchema.type = 'string'
160
+ * ctx.jsonSchema.format = 'my-format'
161
+ * }
162
+ * }
163
+ *
164
+ * // User's override runs first, then zodvex's
165
+ * z.toJSONSchema(schema, {
166
+ * unrepresentable: 'any',
167
+ * override: composeOverrides(myOverride, zodvexJSONSchemaOverride)
168
+ * })
169
+ * ```
170
+ */
171
+ export function composeOverrides(
172
+ ...overrides: Array<((ctx: JSONSchemaOverrideContext) => void) | undefined>
173
+ ): (ctx: JSONSchemaOverrideContext) => void {
174
+ return (ctx: JSONSchemaOverrideContext) => {
175
+ for (const override of overrides) {
176
+ override?.(ctx)
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Options for toJSONSchema, matching Zod's interface.
183
+ */
184
+ export interface ToJSONSchemaOptions {
185
+ target?: 'draft-4' | 'draft-7' | 'draft-2020-12' | 'openapi-3.0'
186
+ unrepresentable?: 'throw' | 'any'
187
+ cycles?: 'ref' | 'throw'
188
+ reused?: 'ref' | 'inline'
189
+ io?: 'input' | 'output'
190
+ override?: (ctx: JSONSchemaOverrideContext) => void
191
+ }
192
+
193
+ /**
194
+ * Converts a Zod schema to JSON Schema with zodvex-aware overrides.
195
+ *
196
+ * This is a convenience wrapper around z.toJSONSchema that automatically
197
+ * handles zodvex-managed types like zid and dates.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * import { zid, toJSONSchema } from 'zodvex'
202
+ *
203
+ * const schema = z.object({
204
+ * userId: zid('users'),
205
+ * createdAt: z.date(),
206
+ * name: z.string()
207
+ * })
208
+ *
209
+ * const jsonSchema = toJSONSchema(schema)
210
+ * // Works with AI SDK's generateObject, etc.
211
+ * ```
212
+ */
213
+ export function toJSONSchema<T extends z.ZodTypeAny>(
214
+ schema: T,
215
+ options?: ToJSONSchemaOptions
216
+ ): Record<string, any> {
217
+ const userOverride = options?.override
218
+
219
+ return z.toJSONSchema(schema, {
220
+ ...options,
221
+ // Default to 'any' so transforms don't throw
222
+ unrepresentable: options?.unrepresentable ?? 'any',
223
+ // Chain our override with user's override
224
+ override: ctx => {
225
+ zodvexJSONSchemaOverride(ctx)
226
+ userOverride?.(ctx)
227
+ }
228
+ })
229
+ }
package/src/tables.ts CHANGED
@@ -1,8 +1,98 @@
1
+ import { defineTable } from 'convex/server'
1
2
  import type { GenericId } from 'convex/values'
2
3
  import { Table } from 'convex-helpers/server'
3
4
  import { z } from 'zod'
4
5
  import { zid } from './ids'
5
- import { type ConvexValidatorFromZodFieldsAuto, zodToConvexFields } from './mapping'
6
+ import { type ConvexValidatorFromZodFieldsAuto, zodToConvex, zodToConvexFields } from './mapping'
7
+
8
+ /**
9
+ * Helper type for Convex system fields added to documents
10
+ */
11
+ type SystemFields<TableName extends string> = {
12
+ _id: ReturnType<typeof zid<TableName>>
13
+ _creationTime: z.ZodNumber
14
+ }
15
+
16
+ /**
17
+ * Maps over union options, extending each ZodObject variant with system fields.
18
+ * Non-object variants are preserved as-is.
19
+ */
20
+ type MapSystemFields<TableName extends string, Options extends readonly z.ZodTypeAny[]> = {
21
+ [K in keyof Options]: Options[K] extends z.ZodObject<infer Shape extends z.ZodRawShape>
22
+ ? z.ZodObject<Shape & SystemFields<TableName>>
23
+ : Options[K]
24
+ }
25
+
26
+ /**
27
+ * Adds Convex system fields (_id, _creationTime) to a Zod schema.
28
+ *
29
+ * For object schemas: extends with system fields
30
+ * For union schemas: adds system fields to each variant
31
+ *
32
+ * @param tableName - The Convex table name
33
+ * @param schema - The Zod schema (object or union)
34
+ * @returns Schema with system fields added
35
+ */
36
+ // Overload 1: ZodObject - extends with system fields
37
+ export function addSystemFields<TableName extends string, Shape extends z.ZodRawShape>(
38
+ tableName: TableName,
39
+ schema: z.ZodObject<Shape>
40
+ ): z.ZodObject<Shape & SystemFields<TableName>>
41
+
42
+ // Overload 2: ZodUnion - maps system fields to each variant
43
+ export function addSystemFields<TableName extends string, Options extends readonly z.ZodTypeAny[]>(
44
+ tableName: TableName,
45
+ schema: z.ZodUnion<Options>
46
+ ): z.ZodUnion<MapSystemFields<TableName, Options>>
47
+
48
+ // Overload 3: ZodDiscriminatedUnion - maps system fields preserving discriminator
49
+ // Note: Zod v4 signature is ZodDiscriminatedUnion<Options, Discriminator>
50
+ export function addSystemFields<
51
+ TableName extends string,
52
+ Options extends readonly z.ZodObject<z.ZodRawShape>[],
53
+ Discriminator extends string
54
+ >(
55
+ tableName: TableName,
56
+ schema: z.ZodDiscriminatedUnion<Options, Discriminator>
57
+ ): z.ZodDiscriminatedUnion<MapSystemFields<TableName, Options>, Discriminator>
58
+
59
+ // Overload 4: Fallback for other ZodTypes - returns as-is
60
+ export function addSystemFields<TableName extends string, S extends z.ZodTypeAny>(
61
+ tableName: TableName,
62
+ schema: S
63
+ ): S
64
+
65
+ // Implementation
66
+ export function addSystemFields<TableName extends string>(
67
+ tableName: TableName,
68
+ schema: z.ZodTypeAny
69
+ ): z.ZodTypeAny {
70
+ // Handle union schemas - add system fields to each variant
71
+ if (schema instanceof z.ZodUnion || schema instanceof z.ZodDiscriminatedUnion) {
72
+ const options = (schema as z.ZodUnion<any>).options.map((variant: z.ZodTypeAny) => {
73
+ if (variant instanceof z.ZodObject) {
74
+ return variant.extend({
75
+ _id: zid(tableName),
76
+ _creationTime: z.number()
77
+ })
78
+ }
79
+ // Non-object variants are returned as-is (shouldn't happen in practice)
80
+ return variant
81
+ })
82
+ return z.union(options as any)
83
+ }
84
+
85
+ // Handle object schemas
86
+ if (schema instanceof z.ZodObject) {
87
+ return schema.extend({
88
+ _id: zid(tableName),
89
+ _creationTime: z.number()
90
+ })
91
+ }
92
+
93
+ // Fallback: return schema as-is
94
+ return schema
95
+ }
6
96
 
7
97
  // Helper to create a Zod schema for a Convex document
8
98
  export function zodDoc<
@@ -35,21 +125,43 @@ export function zodDocOrNull<
35
125
  }
36
126
 
37
127
  /**
38
- * Defines a Convex table using a raw Zod shape (an object mapping field names to Zod types).
128
+ * Helper to detect if input is an object shape (plain object with Zod validators)
129
+ */
130
+ function isObjectShape(input: any): input is Record<string, z.ZodTypeAny> {
131
+ // Check if it's a plain object (not a Zod instance)
132
+ if (!input || typeof input !== 'object') return false
133
+
134
+ // If it's a Zod instance, it's not an object shape
135
+ if (input instanceof z.ZodType) return false
136
+
137
+ // Check if all values are Zod types
138
+ for (const key in input) {
139
+ if (!(input[key] instanceof z.ZodType)) {
140
+ return false
141
+ }
142
+ }
143
+
144
+ return true
145
+ }
146
+
147
+ /**
148
+ * Defines a Convex table using either:
149
+ * - A raw Zod shape (an object mapping field names to Zod types)
150
+ * - A Zod union schema (for polymorphic tables)
39
151
  *
40
- * This function intentionally accepts a raw shape instead of a ZodObject instance.
152
+ * For object shapes, this function intentionally accepts a raw shape instead of a ZodObject instance.
41
153
  * Accepting raw shapes allows TypeScript to infer field types more accurately and efficiently,
42
154
  * leading to better type inference and performance throughout the codebase.
43
- * This architectural decision is important for projects that rely heavily on type safety and
44
- * developer experience, as it avoids the type erasure that can occur when using ZodObject directly.
155
+ *
156
+ * For union schemas, this enables polymorphic tables with discriminated unions.
45
157
  *
46
158
  * Returns the Table definition along with Zod schemas for documents and arrays.
47
159
  *
48
160
  * @param name - The table name
49
- * @param shape - A raw object mapping field names to Zod validators
50
- * @returns A Table with attached shape, zDoc schema, and docArray helper
161
+ * @param schemaOrShape - Either a raw object shape or a Zod union schema
162
+ * @returns A Table with attached helpers (shape, schema, zDoc, docArray, withSystemFields)
51
163
  *
52
- * @example
164
+ * @example Object shape
53
165
  * ```ts
54
166
  * const Users = zodTable('users', {
55
167
  * name: z.string(),
@@ -66,36 +178,132 @@ export function zodDocOrNull<
66
178
  * { returns: Users.docArray }
67
179
  * )
68
180
  * ```
181
+ *
182
+ * @example Union schema (polymorphic table)
183
+ * ```ts
184
+ * const shapeSchema = z.union([
185
+ * z.object({ kind: z.literal('circle'), r: z.number() }),
186
+ * z.object({ kind: z.literal('rectangle'), width: z.number() })
187
+ * ])
188
+ *
189
+ * const Shapes = zodTable('shapes', shapeSchema)
190
+ *
191
+ * // Use in schema
192
+ * export default defineSchema({ shapes: Shapes.table })
193
+ *
194
+ * // Use for return types with system fields
195
+ * export const getShapes = zQuery(query, {},
196
+ * async (ctx) => ctx.db.query('shapes').collect(),
197
+ * { returns: Shapes.docArray }
198
+ * )
199
+ * ```
69
200
  */
201
+ // Helper type to compute the result of addSystemFields for use in zodTable return type
202
+ type AddSystemFieldsResult<
203
+ TableName extends string,
204
+ Schema extends z.ZodTypeAny
205
+ > = Schema extends z.ZodObject<infer Shape extends z.ZodRawShape>
206
+ ? z.ZodObject<Shape & SystemFields<TableName>>
207
+ : Schema extends z.ZodUnion<infer Options extends readonly z.ZodTypeAny[]>
208
+ ? z.ZodUnion<MapSystemFields<TableName, Options>>
209
+ : Schema extends z.ZodDiscriminatedUnion<
210
+ infer Options extends readonly z.ZodObject<z.ZodRawShape>[],
211
+ infer Disc extends string
212
+ >
213
+ ? z.ZodDiscriminatedUnion<MapSystemFields<TableName, Options>, Disc>
214
+ : Schema
215
+
216
+ // Overload 1: Object shape (most common case)
70
217
  export function zodTable<TableName extends string, Shape extends Record<string, z.ZodTypeAny>>(
71
218
  name: TableName,
72
219
  shape: Shape
73
- ) {
74
- // Convert fields with proper types
75
- const convexFields = zodToConvexFields(shape) as ConvexValidatorFromZodFieldsAuto<Shape>
76
-
77
- // Create the Table from convex-helpers with explicit type
78
- const table = Table<ConvexValidatorFromZodFieldsAuto<Shape>, TableName>(name, convexFields)
79
-
80
- // Create zDoc schema with system fields
81
- const zDoc = zodDoc(name, z.object(shape))
82
-
83
- // Create docArray helper for return types
84
- const docArray = z.array(zDoc)
85
-
86
- // Attach everything for comprehensive usage
87
- return Object.assign(table, {
88
- shape,
89
- zDoc,
90
- docArray
91
- }) as typeof table & {
92
- shape: Shape
93
- zDoc: z.ZodObject<
220
+ ): ReturnType<typeof Table<ConvexValidatorFromZodFieldsAuto<Shape>, TableName>> & {
221
+ shape: Shape
222
+ zDoc: z.ZodObject<
223
+ Shape & {
224
+ _id: ReturnType<typeof zid<TableName>>
225
+ _creationTime: z.ZodNumber
226
+ }
227
+ >
228
+ docArray: z.ZodArray<
229
+ z.ZodObject<
94
230
  Shape & {
95
231
  _id: ReturnType<typeof zid<TableName>>
96
232
  _creationTime: z.ZodNumber
97
233
  }
98
234
  >
99
- docArray: z.ZodArray<typeof zDoc>
235
+ >
236
+ }
237
+
238
+ // Overload 2: Union/schema types
239
+ export function zodTable<TableName extends string, Schema extends z.ZodTypeAny>(
240
+ name: TableName,
241
+ schema: Schema
242
+ ): {
243
+ table: ReturnType<typeof defineTable>
244
+ tableName: TableName
245
+ validator: ReturnType<typeof zodToConvex<Schema>>
246
+ schema: Schema
247
+ docArray: z.ZodArray<AddSystemFieldsResult<TableName, Schema>>
248
+ withSystemFields: () => AddSystemFieldsResult<TableName, Schema>
249
+ }
250
+
251
+ export function zodTable<
252
+ TableName extends string,
253
+ SchemaOrShape extends z.ZodTypeAny | Record<string, z.ZodTypeAny>
254
+ >(name: TableName, schemaOrShape: SchemaOrShape): any {
255
+ // Detect if it's an object shape or a schema
256
+ if (isObjectShape(schemaOrShape)) {
257
+ // Original object shape logic
258
+ const shape = schemaOrShape as Record<string, z.ZodTypeAny>
259
+
260
+ // Convert fields with proper types
261
+ const convexFields = zodToConvexFields(shape) as ConvexValidatorFromZodFieldsAuto<typeof shape>
262
+
263
+ // Create the Table from convex-helpers with explicit type
264
+ const table = Table<ConvexValidatorFromZodFieldsAuto<typeof shape>, TableName>(
265
+ name,
266
+ convexFields
267
+ )
268
+
269
+ // Create zDoc schema with system fields
270
+ const zDoc = zodDoc(name, z.object(shape))
271
+
272
+ // Create docArray helper for return types
273
+ const docArray = z.array(zDoc)
274
+
275
+ // Attach everything for comprehensive usage
276
+ return Object.assign(table, {
277
+ shape,
278
+ zDoc,
279
+ docArray
280
+ })
281
+ } else {
282
+ // Union or other schema type logic
283
+ const schema = schemaOrShape as z.ZodTypeAny
284
+
285
+ // Convert schema to Convex validator
286
+ const convexValidator = zodToConvex(schema)
287
+
288
+ // For unions, use defineTable directly (not Table helper which expects object fields)
289
+ // Note: TypeScript types don't reflect it, but Convex supports union validators in tables
290
+ const table = defineTable(convexValidator as any)
291
+
292
+ // Create document schema with system fields
293
+ const withFields = addSystemFields(name, schema)
294
+
295
+ // Create docArray helper
296
+ const docArray = z.array(withFields)
297
+
298
+ // Attach helpers for union tables
299
+ // Return structure similar to Table() but without fields-based helpers
300
+ return {
301
+ table,
302
+ tableName: name,
303
+ validator: convexValidator,
304
+ schema,
305
+ docArray,
306
+ withSystemFields: () => addSystemFields(name, schema)
307
+ }
100
308
  }
101
309
  }
package/src/types.ts CHANGED
@@ -15,31 +15,19 @@ export type InferArgs<A> = A extends z.ZodObject<infer S>
15
15
  ? z.infer<A>
16
16
  : Record<string, never>
17
17
 
18
- // Return type inference with immediate bailout for unions/custom to avoid depth
19
- export type InferReturns<R> = R extends z.ZodUnion<any>
20
- ? any
21
- : // Bail immediately for unions
22
- R extends z.ZodCustom<any>
18
+ // Return type inference - uses z.output for Zod schemas
19
+ // Previously had bailouts for unions/custom to avoid TypeScript depth errors,
20
+ // but research (Issue #20) showed convex-helpers handles these without issues.
21
+ // Removing bailouts fixes Issue #19 (Promise<any> return types).
22
+ export type InferReturns<R> = R extends z.ZodType<any, any, any>
23
+ ? z.output<R>
24
+ : R extends undefined
23
25
  ? any
24
- : // Bail immediately for custom
25
- R extends z.ZodType<any, any, any>
26
- ? z.output<R>
27
- : // Use z.output for other schemas
28
- R extends undefined
29
- ? any
30
- : R
26
+ : R
31
27
 
32
28
  // For handler authoring: what the handler returns before wrapper validation/encoding
33
- export type InferHandlerReturns<R> = R extends z.ZodUnion<any>
34
- ? any
35
- : // Bail immediately for unions
36
- R extends z.ZodCustom<any>
37
- ? any
38
- : // Bail immediately for custom
39
- R extends z.ZodType<any, any, any>
40
- ? z.input<R>
41
- : // Use z.input for other schemas
42
- any
29
+ // Uses z.input since this is what the handler produces before encoding
30
+ export type InferHandlerReturns<R> = R extends z.ZodType<any, any, any> ? z.input<R> : any
43
31
 
44
32
  /**
45
33
  * Extract the visibility type from a Convex builder function
package/src/wrappers.ts CHANGED
@@ -39,8 +39,8 @@ function containsCustom(schema: z.ZodTypeAny, maxDepth = 50, currentDepth = 0):
39
39
 
40
40
  let result = false
41
41
 
42
- // Use _def.typeName instead of instanceof since ZodCustom is not exported in Zod v4
43
- if ((schema as any)._def?.typeName === 'ZodCustom') {
42
+ // Zod v4 exports ZodCustom and instances expose `schema.type === "custom"`.
43
+ if (schema instanceof z.ZodCustom) {
44
44
  result = true
45
45
  } else if (schema instanceof z.ZodUnion) {
46
46
  result = (schema.options as z.ZodTypeAny[]).some(opt =>