zodvex 0.3.2 → 0.4.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.
Files changed (73) hide show
  1. package/README.md +75 -0
  2. package/dist/__type-tests__/infer-returns.d.ts +2 -0
  3. package/dist/__type-tests__/infer-returns.d.ts.map +1 -0
  4. package/dist/__type-tests__/zodTable-inference.d.ts +2 -0
  5. package/dist/__type-tests__/zodTable-inference.d.ts.map +1 -0
  6. package/dist/builders.d.ts +173 -0
  7. package/dist/builders.d.ts.map +1 -0
  8. package/dist/codec.d.ts +11 -0
  9. package/dist/codec.d.ts.map +1 -0
  10. package/dist/custom.d.ts +147 -0
  11. package/dist/custom.d.ts.map +1 -0
  12. package/dist/ids.d.ts +23 -0
  13. package/dist/ids.d.ts.map +1 -0
  14. package/dist/index.d.ts +11 -599
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +203 -58
  17. package/dist/index.js.map +1 -1
  18. package/dist/mapping/core.d.ts +5 -0
  19. package/dist/mapping/core.d.ts.map +1 -0
  20. package/dist/mapping/handlers/enum.d.ts +4 -0
  21. package/dist/mapping/handlers/enum.d.ts.map +1 -0
  22. package/dist/mapping/handlers/index.d.ts +5 -0
  23. package/dist/mapping/handlers/index.d.ts.map +1 -0
  24. package/dist/mapping/handlers/nullable.d.ts +7 -0
  25. package/dist/mapping/handlers/nullable.d.ts.map +1 -0
  26. package/dist/mapping/handlers/record.d.ts +4 -0
  27. package/dist/mapping/handlers/record.d.ts.map +1 -0
  28. package/dist/mapping/handlers/union.d.ts +5 -0
  29. package/dist/mapping/handlers/union.d.ts.map +1 -0
  30. package/dist/mapping/index.d.ts +4 -0
  31. package/dist/mapping/index.d.ts.map +1 -0
  32. package/dist/mapping/types.d.ts +43 -0
  33. package/dist/mapping/types.d.ts.map +1 -0
  34. package/dist/mapping/utils.d.ts +6 -0
  35. package/dist/mapping/utils.d.ts.map +1 -0
  36. package/dist/registry.d.ts +110 -0
  37. package/dist/registry.d.ts.map +1 -0
  38. package/dist/results.d.ts +126 -0
  39. package/dist/results.d.ts.map +1 -0
  40. package/dist/tables.d.ts +214 -0
  41. package/dist/tables.d.ts.map +1 -0
  42. package/dist/transform/index.d.ts +26 -0
  43. package/dist/transform/index.d.ts.map +1 -0
  44. package/dist/transform/index.js +442 -0
  45. package/dist/transform/index.js.map +1 -0
  46. package/dist/transform/transform.d.ts +47 -0
  47. package/dist/transform/transform.d.ts.map +1 -0
  48. package/dist/transform/traverse.d.ts +62 -0
  49. package/dist/transform/traverse.d.ts.map +1 -0
  50. package/dist/transform/types.d.ts +115 -0
  51. package/dist/transform/types.d.ts.map +1 -0
  52. package/dist/types.d.ts +29 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/utils.d.ts +55 -0
  55. package/dist/utils.d.ts.map +1 -0
  56. package/dist/wrappers.d.ts +22 -0
  57. package/dist/wrappers.d.ts.map +1 -0
  58. package/package.json +13 -6
  59. package/src/__type-tests__/infer-returns.ts +24 -0
  60. package/src/__type-tests__/zodTable-inference.ts +5 -5
  61. package/src/custom.ts +205 -28
  62. package/src/index.ts +1 -0
  63. package/src/mapping/core.ts +23 -11
  64. package/src/mapping/types.ts +6 -2
  65. package/src/results.ts +110 -0
  66. package/src/tables.ts +306 -28
  67. package/src/transform/index.ts +38 -0
  68. package/src/transform/transform.ts +409 -0
  69. package/src/transform/traverse.ts +320 -0
  70. package/src/transform/types.ts +128 -0
  71. package/src/types.ts +3 -2
  72. package/src/utils.ts +35 -0
  73. package/src/wrappers.ts +10 -19
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Schema traversal utilities.
3
+ *
4
+ * General-purpose utilities for walking and inspecting Zod schemas.
5
+ */
6
+
7
+ import type { z } from 'zod'
8
+ import type { FieldInfo, SchemaVisitor, WalkSchemaOptions } from './types'
9
+
10
+ const METADATA_CACHE = new WeakMap<z.ZodTypeAny, Record<string, unknown> | undefined>()
11
+
12
+ /**
13
+ * Metadata key for ZodSensitive wrappers.
14
+ */
15
+ const SENSITIVE_META_KEY = 'zodvex:sensitive'
16
+
17
+ /**
18
+ * Duck-type check for ZodSensitive wrapper.
19
+ */
20
+ function isZodSensitiveDef(schema: any): boolean {
21
+ return schema?._def?.type === 'sensitive' && typeof schema?.unwrap === 'function'
22
+ }
23
+
24
+ /**
25
+ * Get metadata from a Zod schema.
26
+ *
27
+ * Handles both Zod's native .meta() and ZodSensitive wrappers.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const schema = z.string().meta({ encrypted: true })
32
+ * const meta = getMetadata(schema)
33
+ * // => { encrypted: true }
34
+ * ```
35
+ */
36
+ export function getMetadata(schema: z.ZodTypeAny): Record<string, unknown> | undefined {
37
+ if (METADATA_CACHE.has(schema)) {
38
+ return METADATA_CACHE.get(schema)
39
+ }
40
+
41
+ const visited = new Set<z.ZodTypeAny>()
42
+ let current: z.ZodTypeAny | undefined = schema
43
+
44
+ while (current) {
45
+ if (visited.has(current)) return undefined
46
+ visited.add(current)
47
+
48
+ // Fast path: ZodSensitive wrapper stores metadata directly
49
+ if (isZodSensitiveDef(current)) {
50
+ const sensitiveMeta = (current as any)._def.sensitiveMetadata
51
+ const meta = { [SENSITIVE_META_KEY]: sensitiveMeta }
52
+ METADATA_CACHE.set(schema, meta)
53
+ return meta
54
+ }
55
+
56
+ const meta = current.meta?.() as Record<string, unknown> | undefined
57
+ if (meta !== undefined) {
58
+ METADATA_CACHE.set(schema, meta)
59
+ return meta
60
+ }
61
+
62
+ current = unwrapOnce(current)
63
+ }
64
+
65
+ METADATA_CACHE.set(schema, undefined)
66
+ return undefined
67
+ }
68
+
69
+ function unwrapOnce(schema: z.ZodTypeAny): z.ZodTypeAny | undefined {
70
+ const defType = (schema as any)._def?.type as string | undefined
71
+
72
+ switch (defType) {
73
+ case 'sensitive': {
74
+ // ZodSensitive wrapper - unwrap to inner schema
75
+ if (typeof (schema as any).unwrap === 'function') {
76
+ return (schema as any).unwrap()
77
+ }
78
+ return (schema as any)._def?.innerType
79
+ }
80
+
81
+ case 'optional':
82
+ case 'nullable': {
83
+ if (typeof (schema as any).unwrap === 'function') {
84
+ return (schema as any).unwrap()
85
+ }
86
+ return (schema as any)._def?.innerType
87
+ }
88
+
89
+ case 'lazy': {
90
+ const getter = (schema as any)._def?.getter
91
+ if (typeof getter === 'function') {
92
+ return getter()
93
+ }
94
+ return undefined
95
+ }
96
+
97
+ case 'default':
98
+ case 'catch':
99
+ case 'readonly':
100
+ case 'prefault':
101
+ case 'nonoptional': {
102
+ return (schema as any)._def?.innerType
103
+ }
104
+
105
+ case 'pipe': {
106
+ return (schema as any)._def?.in
107
+ }
108
+
109
+ default:
110
+ return undefined
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Check if a schema has metadata matching a predicate.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * const schema = z.string().meta({ sensitive: true })
120
+ * hasMetadata(schema, meta => meta.sensitive === true)
121
+ * // => true
122
+ * ```
123
+ */
124
+ export function hasMetadata(
125
+ schema: z.ZodTypeAny,
126
+ predicate: (meta: Record<string, unknown>) => boolean
127
+ ): boolean {
128
+ const meta = getMetadata(schema)
129
+ return meta !== undefined && predicate(meta)
130
+ }
131
+
132
+ /**
133
+ * Walk a Zod schema, calling visitor functions for each node.
134
+ *
135
+ * Handles: objects, arrays, optionals, nullables, unions, discriminated unions.
136
+ * Prevents infinite recursion on circular schema references.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * walkSchema(userSchema, {
141
+ * onField: (info) => {
142
+ * if (info.meta?.encrypted) {
143
+ * console.log(`Encrypted field: ${info.path}`)
144
+ * }
145
+ * }
146
+ * })
147
+ * ```
148
+ */
149
+ export function walkSchema(
150
+ schema: z.ZodTypeAny,
151
+ visitor: SchemaVisitor,
152
+ options?: WalkSchemaOptions
153
+ ): void {
154
+ const recursionStack = new Set<z.ZodTypeAny>()
155
+ const basePath = options?.path ?? ''
156
+
157
+ function traverse(sch: z.ZodTypeAny, currentPath: string, isOptional: boolean): void {
158
+ // Prevent infinite recursion on circular schema references
159
+ if (recursionStack.has(sch)) return
160
+ recursionStack.add(sch)
161
+
162
+ try {
163
+ const defType = (sch as any)._def?.type as string | undefined
164
+ const meta = getMetadata(sch)
165
+ const info: FieldInfo = { path: currentPath, schema: sch, meta, isOptional }
166
+
167
+ // Call onField for every schema node
168
+ if (visitor.onField) {
169
+ const result = visitor.onField(info)
170
+ if (result === 'skip') return
171
+ }
172
+
173
+ // Dispatch based on schema type
174
+ switch (defType) {
175
+ case 'sensitive': {
176
+ // ZodSensitive wrapper - already reported via onField, now traverse inner
177
+ const inner = (sch as any).unwrap?.() ?? (sch as any)._def?.innerType
178
+ if (inner) {
179
+ traverse(inner, currentPath, isOptional)
180
+ }
181
+ return
182
+ }
183
+
184
+ case 'optional': {
185
+ const inner = (sch as any).unwrap()
186
+ traverse(inner, currentPath, true)
187
+ return
188
+ }
189
+
190
+ case 'nullable': {
191
+ const inner = (sch as any).unwrap()
192
+ traverse(inner, currentPath, isOptional)
193
+ return
194
+ }
195
+
196
+ case 'lazy': {
197
+ const getter = (sch as any)._def?.getter
198
+ if (typeof getter === 'function') {
199
+ const inner = getter()
200
+ traverse(inner, currentPath, isOptional)
201
+ }
202
+ return
203
+ }
204
+
205
+ case 'default':
206
+ case 'catch':
207
+ case 'readonly':
208
+ case 'prefault':
209
+ case 'nonoptional': {
210
+ const inner = (sch as any)._def?.innerType as z.ZodTypeAny | undefined
211
+ if (inner) {
212
+ traverse(inner, currentPath, isOptional)
213
+ }
214
+ return
215
+ }
216
+
217
+ case 'pipe': {
218
+ const inner = (sch as any)._def?.in as z.ZodTypeAny | undefined
219
+ if (inner) {
220
+ traverse(inner, currentPath, isOptional)
221
+ }
222
+ return
223
+ }
224
+
225
+ case 'object': {
226
+ visitor.onObject?.(info)
227
+ const shape = (sch as any).shape
228
+ if (shape) {
229
+ for (const [key, fieldSchema] of Object.entries(shape)) {
230
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key
231
+ traverse(fieldSchema as z.ZodTypeAny, fieldPath, false)
232
+ }
233
+ }
234
+ return
235
+ }
236
+
237
+ case 'array': {
238
+ visitor.onArray?.(info)
239
+ const element = (sch as any).element
240
+ if (element) {
241
+ const arrayPath = currentPath ? `${currentPath}[]` : '[]'
242
+ traverse(element, arrayPath, false)
243
+ }
244
+ return
245
+ }
246
+
247
+ case 'union': {
248
+ const unionOptions = (sch as any)._def.options as z.ZodTypeAny[] | undefined
249
+
250
+ // Get options from either _def.options or _def.optionsMap
251
+ const variantOptions =
252
+ unionOptions ||
253
+ ((sch as any)._def.optionsMap ? Array.from((sch as any)._def.optionsMap.values()) : [])
254
+
255
+ visitor.onUnion?.(info, variantOptions as z.ZodTypeAny[])
256
+
257
+ for (const variant of variantOptions as z.ZodTypeAny[]) {
258
+ traverse(variant, currentPath, isOptional)
259
+ }
260
+ return
261
+ }
262
+ }
263
+
264
+ // Primitives and other types are leaf nodes - nothing more to traverse
265
+ } finally {
266
+ recursionStack.delete(sch)
267
+ }
268
+ }
269
+
270
+ traverse(schema, basePath, false)
271
+ }
272
+
273
+ /**
274
+ * Find all fields in a schema where metadata matches a predicate.
275
+ *
276
+ * @overload Type guard version - returns narrowed meta type
277
+ */
278
+ export function findFieldsWithMeta<TMeta extends Record<string, unknown>>(
279
+ schema: z.ZodTypeAny,
280
+ predicate: (meta: Record<string, unknown> | undefined) => meta is TMeta
281
+ ): Array<FieldInfo & { meta: TMeta }>
282
+
283
+ /**
284
+ * @overload Boolean predicate version
285
+ */
286
+ export function findFieldsWithMeta(
287
+ schema: z.ZodTypeAny,
288
+ predicate: (meta: Record<string, unknown> | undefined) => boolean
289
+ ): FieldInfo[]
290
+
291
+ /**
292
+ * Find all fields in a schema where metadata matches a predicate.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * // Find all fields with 'sensitive' metadata
297
+ * const sensitiveFields = findFieldsWithMeta(
298
+ * userSchema,
299
+ * (meta) => meta?.sensitive === true
300
+ * )
301
+ * // => [{ path: 'email', schema: z.string(), meta: { sensitive: true } }, ...]
302
+ * ```
303
+ */
304
+ export function findFieldsWithMeta(
305
+ schema: z.ZodTypeAny,
306
+ predicate: (meta: Record<string, unknown> | undefined) => boolean
307
+ ): FieldInfo[] {
308
+ const results: FieldInfo[] = []
309
+
310
+ walkSchema(schema, {
311
+ onField: info => {
312
+ if (predicate(info.meta)) {
313
+ results.push(info)
314
+ return 'skip' // Don't recurse into matching fields
315
+ }
316
+ }
317
+ })
318
+
319
+ return results
320
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Transform layer type definitions.
3
+ *
4
+ * General-purpose types for schema traversal and value transformation.
5
+ */
6
+
7
+ import type { z } from 'zod'
8
+
9
+ /**
10
+ * Information about a field during schema traversal.
11
+ */
12
+ export type FieldInfo = {
13
+ /** Dot-notation path (e.g., 'profile.email', 'contacts[].email') */
14
+ path: string
15
+ /** The Zod schema for this field */
16
+ schema: z.ZodTypeAny
17
+ /** Metadata from schema.meta() if present */
18
+ meta: Record<string, unknown> | undefined
19
+ /** Whether the field is wrapped in optional/nullable */
20
+ isOptional: boolean
21
+ }
22
+
23
+ /**
24
+ * Visitor functions for walkSchema().
25
+ */
26
+ export type SchemaVisitor = {
27
+ /** Called for every field. Return 'skip' to skip children. */
28
+ onField?: (info: FieldInfo) => void | 'skip'
29
+ /** Called when entering an object schema */
30
+ onObject?: (info: FieldInfo) => void
31
+ /** Called when entering an array schema */
32
+ onArray?: (info: FieldInfo) => void
33
+ /** Called when entering a union schema */
34
+ onUnion?: (info: FieldInfo, variants: z.ZodTypeAny[]) => void
35
+ }
36
+
37
+ /**
38
+ * Options for walkSchema().
39
+ */
40
+ export type WalkSchemaOptions = {
41
+ /** Starting path prefix */
42
+ path?: string
43
+ }
44
+
45
+ /**
46
+ * Context passed to transform functions.
47
+ */
48
+ export type TransformContext<TCtx = unknown> = {
49
+ /** Current field path */
50
+ path: string
51
+ /** The Zod schema for this field */
52
+ schema: z.ZodTypeAny
53
+ /** Metadata from schema.meta() if present */
54
+ meta: Record<string, unknown> | undefined
55
+ /** User-provided context */
56
+ ctx: TCtx
57
+ }
58
+
59
+ /**
60
+ * Synchronous transform function signature.
61
+ */
62
+ export type TransformFn<TCtx = unknown> = (
63
+ value: unknown,
64
+ context: TransformContext<TCtx>
65
+ ) => unknown
66
+
67
+ /**
68
+ * Async transform function signature.
69
+ */
70
+ export type AsyncTransformFn<TCtx = unknown> = (
71
+ value: unknown,
72
+ context: TransformContext<TCtx>
73
+ ) => unknown | Promise<unknown>
74
+
75
+ /**
76
+ * Options for transformBySchema().
77
+ */
78
+ export type TransformOptions = {
79
+ /** Starting path prefix */
80
+ path?: string
81
+ /**
82
+ * How to handle values that don't match any union variant.
83
+ * - 'passthrough': Return value unchanged (default)
84
+ * - 'error': Throw an error
85
+ * - 'null': Replace with null (fail-closed for security)
86
+ */
87
+ unmatchedUnion?: 'passthrough' | 'error' | 'null'
88
+ /** Callback when a union doesn't match */
89
+ onUnmatchedUnion?: (path: string) => void
90
+ /**
91
+ * Fast predicate to check if a schema needs transformation.
92
+ *
93
+ * When provided, this predicate is called before the transform callback.
94
+ * If it returns false, the transform callback is skipped for this schema
95
+ * (but recursion into children continues).
96
+ *
97
+ * This optimization avoids callback overhead for schemas that don't need
98
+ * transformation, which is useful when only a small subset of fields
99
+ * require processing (e.g., only sensitive fields).
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * // Only call transform for schemas with sensitive metadata
104
+ * transformBySchema(value, schema, ctx, transform, {
105
+ * shouldTransform: (sch) => getSensitiveMetadata(sch) !== undefined
106
+ * })
107
+ * ```
108
+ */
109
+ shouldTransform?: (schema: z.ZodTypeAny) => boolean
110
+ /**
111
+ * Process array elements in parallel (async only).
112
+ *
113
+ * When true, array elements are processed with Promise.all() instead of
114
+ * sequentially. This can significantly improve performance for large arrays
115
+ * when transforms involve async operations like entitlement checks.
116
+ *
117
+ * Default: false (sequential processing for backwards compatibility)
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * // Process user entitlements for all items in parallel
122
+ * const result = await transformBySchemaAsync(docs, schema, ctx, transform, {
123
+ * parallel: true
124
+ * })
125
+ * ```
126
+ */
127
+ parallel?: boolean
128
+ }
package/src/types.ts CHANGED
@@ -26,8 +26,9 @@ export type InferReturns<R> = R extends z.ZodType<any, any, any>
26
26
  : R
27
27
 
28
28
  // For handler authoring: what the handler returns before wrapper validation/encoding
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
29
+ // Uses z.output since the handler produces the internal representation (e.g., Date),
30
+ // which is then encoded to wire format (e.g., string) before sending to the client
31
+ export type InferHandlerReturns<R> = R extends z.ZodType<any, any, any> ? z.output<R> : any
31
32
 
32
33
  /**
33
34
  * Extract the visibility type from a Convex builder function
package/src/utils.ts CHANGED
@@ -48,6 +48,41 @@ export function handleZodValidationError(
48
48
  throw e
49
49
  }
50
50
 
51
+ /**
52
+ * Validates a return value against a Zod schema, supporting both codecs and regular schemas.
53
+ *
54
+ * Tries z.encode() first (for codec support), then falls back to .parse() if the schema
55
+ * contains unidirectional transforms (which don't support encoding).
56
+ *
57
+ * For codecs: returns the encoded wire format (z.input<T>)
58
+ * For transforms: returns the transformed output (z.output<T>)
59
+ * For plain schemas: returns the validated value
60
+ *
61
+ * @param schema - The Zod schema to validate against
62
+ * @param value - The value to validate
63
+ * @returns The validated/encoded value
64
+ * @throws Calls handleZodValidationError on validation failure
65
+ */
66
+ export function validateReturns(schema: z.ZodTypeAny, value: unknown): unknown {
67
+ try {
68
+ // Try encode first - works for codecs and plain schemas
69
+ return z.encode(schema, value)
70
+ } catch (e: any) {
71
+ // If it's a unidirectional transform error, fall back to parse
72
+ if (e?.message?.includes('unidirectional transform')) {
73
+ try {
74
+ return schema.parse(value)
75
+ } catch (parseError) {
76
+ handleZodValidationError(parseError, 'returns')
77
+ }
78
+ }
79
+ // For any other error, handle it normally
80
+ handleZodValidationError(e, 'returns')
81
+ }
82
+ // TypeScript can't infer that handleZodValidationError always throws
83
+ throw new Error('Unreachable')
84
+ }
85
+
51
86
  // Helper: standard Convex paginate() result schema
52
87
  export function zPaginated<T extends z.ZodTypeAny>(item: T) {
53
88
  return z.object({
package/src/wrappers.ts CHANGED
@@ -16,7 +16,7 @@ import type {
16
16
  InferReturns,
17
17
  ZodToConvexArgs
18
18
  } from './types'
19
- import { handleZodValidationError } from './utils'
19
+ import { handleZodValidationError, validateReturns } from './utils'
20
20
 
21
21
  // Cache to avoid re-checking the same schema
22
22
  const customCheckCache = new WeakMap<z.ZodTypeAny, boolean>()
@@ -103,12 +103,9 @@ export function zQuery<
103
103
  }
104
104
  const raw = await handler(ctx, parsed)
105
105
  if (options?.returns) {
106
- try {
107
- const validated = (options.returns as z.ZodTypeAny).parse(raw)
108
- return toConvexJS(options.returns as z.ZodTypeAny, validated)
109
- } catch (e) {
110
- handleZodValidationError(e, 'returns')
111
- }
106
+ // Validate using encode (for codecs) with fallback to parse (for transforms)
107
+ const validated = validateReturns(options.returns as z.ZodTypeAny, raw)
108
+ return toConvexJS(options.returns as z.ZodTypeAny, validated)
112
109
  }
113
110
  // Fallback: ensure Convex-safe return values (e.g., Date → timestamp)
114
111
  return toConvexJS(raw) as any
@@ -177,12 +174,9 @@ export function zMutation<
177
174
  }
178
175
  const raw = await handler(ctx, parsed)
179
176
  if (options?.returns) {
180
- try {
181
- const validated = (options.returns as z.ZodTypeAny).parse(raw)
182
- return toConvexJS(options.returns as z.ZodTypeAny, validated)
183
- } catch (e) {
184
- handleZodValidationError(e, 'returns')
185
- }
177
+ // Validate using encode (for codecs) with fallback to parse (for transforms)
178
+ const validated = validateReturns(options.returns as z.ZodTypeAny, raw)
179
+ return toConvexJS(options.returns as z.ZodTypeAny, validated)
186
180
  }
187
181
  // Fallback: ensure Convex-safe return values (e.g., Date → timestamp)
188
182
  return toConvexJS(raw) as any
@@ -251,12 +245,9 @@ export function zAction<
251
245
  }
252
246
  const raw = await handler(ctx, parsed)
253
247
  if (options?.returns) {
254
- try {
255
- const validated = (options.returns as z.ZodTypeAny).parse(raw)
256
- return toConvexJS(options.returns as z.ZodTypeAny, validated)
257
- } catch (e) {
258
- handleZodValidationError(e, 'returns')
259
- }
248
+ // Validate using encode (for codecs) with fallback to parse (for transforms)
249
+ const validated = validateReturns(options.returns as z.ZodTypeAny, raw)
250
+ return toConvexJS(options.returns as z.ZodTypeAny, validated)
260
251
  }
261
252
  // Fallback: ensure Convex-safe return values (e.g., Date → timestamp)
262
253
  return toConvexJS(raw) as any