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.
- package/README.md +75 -0
- package/dist/__type-tests__/infer-returns.d.ts +2 -0
- package/dist/__type-tests__/infer-returns.d.ts.map +1 -0
- package/dist/__type-tests__/zodTable-inference.d.ts +2 -0
- package/dist/__type-tests__/zodTable-inference.d.ts.map +1 -0
- package/dist/builders.d.ts +173 -0
- package/dist/builders.d.ts.map +1 -0
- package/dist/codec.d.ts +11 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/custom.d.ts +147 -0
- package/dist/custom.d.ts.map +1 -0
- package/dist/ids.d.ts +23 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/index.d.ts +11 -599
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +203 -58
- package/dist/index.js.map +1 -1
- package/dist/mapping/core.d.ts +5 -0
- package/dist/mapping/core.d.ts.map +1 -0
- package/dist/mapping/handlers/enum.d.ts +4 -0
- package/dist/mapping/handlers/enum.d.ts.map +1 -0
- package/dist/mapping/handlers/index.d.ts +5 -0
- package/dist/mapping/handlers/index.d.ts.map +1 -0
- package/dist/mapping/handlers/nullable.d.ts +7 -0
- package/dist/mapping/handlers/nullable.d.ts.map +1 -0
- package/dist/mapping/handlers/record.d.ts +4 -0
- package/dist/mapping/handlers/record.d.ts.map +1 -0
- package/dist/mapping/handlers/union.d.ts +5 -0
- package/dist/mapping/handlers/union.d.ts.map +1 -0
- package/dist/mapping/index.d.ts +4 -0
- package/dist/mapping/index.d.ts.map +1 -0
- package/dist/mapping/types.d.ts +43 -0
- package/dist/mapping/types.d.ts.map +1 -0
- package/dist/mapping/utils.d.ts +6 -0
- package/dist/mapping/utils.d.ts.map +1 -0
- package/dist/registry.d.ts +110 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/results.d.ts +126 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/tables.d.ts +214 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/transform/index.d.ts +26 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +442 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/transform/transform.d.ts +47 -0
- package/dist/transform/transform.d.ts.map +1 -0
- package/dist/transform/traverse.d.ts +62 -0
- package/dist/transform/traverse.d.ts.map +1 -0
- package/dist/transform/types.d.ts +115 -0
- package/dist/transform/types.d.ts.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +55 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/wrappers.d.ts +22 -0
- package/dist/wrappers.d.ts.map +1 -0
- package/package.json +13 -6
- package/src/__type-tests__/infer-returns.ts +24 -0
- package/src/__type-tests__/zodTable-inference.ts +5 -5
- package/src/custom.ts +205 -28
- package/src/index.ts +1 -0
- package/src/mapping/core.ts +23 -11
- package/src/mapping/types.ts +6 -2
- package/src/results.ts +110 -0
- package/src/tables.ts +306 -28
- package/src/transform/index.ts +38 -0
- package/src/transform/transform.ts +409 -0
- package/src/transform/traverse.ts +320 -0
- package/src/transform/types.ts +128 -0
- package/src/types.ts +3 -2
- package/src/utils.ts +35 -0
- 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.
|
|
30
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|