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,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value transformation utilities.
|
|
3
|
+
*
|
|
4
|
+
* General-purpose utilities for recursively transforming values based on their schema structure.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { z } from 'zod'
|
|
8
|
+
import { getMetadata } from './traverse'
|
|
9
|
+
import type { AsyncTransformFn, TransformContext, TransformFn, TransformOptions } from './types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Recursively transform a value based on its schema structure.
|
|
13
|
+
*
|
|
14
|
+
* The transform function is called for each value/schema pair during traversal.
|
|
15
|
+
* If the transform returns a different value (val !== transformed), that value
|
|
16
|
+
* is used and recursion into that subtree stops. If the same value is returned,
|
|
17
|
+
* recursion continues.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* // Mask all fields with 'pii' metadata for logging
|
|
22
|
+
* const safeForLogs = transformBySchema(userData, userSchema, null, (value, ctx) => {
|
|
23
|
+
* if (ctx.meta?.pii) {
|
|
24
|
+
* return '[REDACTED]'
|
|
25
|
+
* }
|
|
26
|
+
* return value
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function transformBySchema<T, TCtx>(
|
|
31
|
+
value: T,
|
|
32
|
+
schema: z.ZodTypeAny,
|
|
33
|
+
ctx: TCtx,
|
|
34
|
+
transform: TransformFn<TCtx>,
|
|
35
|
+
options?: TransformOptions
|
|
36
|
+
): T {
|
|
37
|
+
const basePath = options?.path ?? ''
|
|
38
|
+
|
|
39
|
+
function recurse(val: unknown, sch: z.ZodTypeAny, currentPath: string): unknown {
|
|
40
|
+
// Pass through null/undefined unchanged
|
|
41
|
+
if (val === undefined || val === null) {
|
|
42
|
+
return val
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const defType = (sch as any)._def?.type as string | undefined
|
|
46
|
+
|
|
47
|
+
// Check shouldTransform predicate - if false, skip callback but continue recursion
|
|
48
|
+
const shouldCall = !options?.shouldTransform || options.shouldTransform(sch)
|
|
49
|
+
|
|
50
|
+
if (shouldCall) {
|
|
51
|
+
const meta = getMetadata(sch)
|
|
52
|
+
|
|
53
|
+
// Call transform for this value/schema pair
|
|
54
|
+
const context: TransformContext<TCtx> = { path: currentPath, schema: sch, meta, ctx }
|
|
55
|
+
const transformed = transform(val, context)
|
|
56
|
+
|
|
57
|
+
// If transform returned something different, use it (don't recurse)
|
|
58
|
+
if (transformed !== val) {
|
|
59
|
+
return transformed
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Dispatch based on schema type
|
|
64
|
+
switch (defType) {
|
|
65
|
+
case 'sensitive': {
|
|
66
|
+
// ZodSensitive wrapper - transform was already called above, now recurse into inner
|
|
67
|
+
const inner = (sch as any).unwrap?.() ?? (sch as any)._def?.innerType
|
|
68
|
+
if (inner) {
|
|
69
|
+
return recurse(val, inner, currentPath)
|
|
70
|
+
}
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case 'optional':
|
|
75
|
+
case 'nullable': {
|
|
76
|
+
if (val === null) return null
|
|
77
|
+
const inner = (sch as any).unwrap()
|
|
78
|
+
return recurse(val, inner, currentPath)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'lazy': {
|
|
82
|
+
const getter = (sch as any)._def?.getter
|
|
83
|
+
if (typeof getter === 'function') {
|
|
84
|
+
const inner = getter()
|
|
85
|
+
return recurse(val, inner, currentPath)
|
|
86
|
+
}
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'default':
|
|
91
|
+
case 'catch':
|
|
92
|
+
case 'readonly':
|
|
93
|
+
case 'prefault':
|
|
94
|
+
case 'nonoptional': {
|
|
95
|
+
const inner = (sch as any)._def?.innerType as z.ZodTypeAny | undefined
|
|
96
|
+
if (inner) {
|
|
97
|
+
return recurse(val, inner, currentPath)
|
|
98
|
+
}
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'pipe': {
|
|
103
|
+
const inner = (sch as any)._def?.in as z.ZodTypeAny | undefined
|
|
104
|
+
if (inner) {
|
|
105
|
+
return recurse(val, inner, currentPath)
|
|
106
|
+
}
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'object': {
|
|
111
|
+
if (typeof val === 'object' && val !== null) {
|
|
112
|
+
const shape = (sch as any).shape
|
|
113
|
+
if (shape) {
|
|
114
|
+
const result: Record<string, unknown> = {}
|
|
115
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
116
|
+
const fieldPath = currentPath ? `${currentPath}.${key}` : key
|
|
117
|
+
const fieldValue = (val as Record<string, unknown>)[key]
|
|
118
|
+
result[key] = recurse(fieldValue, fieldSchema as z.ZodTypeAny, fieldPath)
|
|
119
|
+
}
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'array': {
|
|
127
|
+
if (Array.isArray(val)) {
|
|
128
|
+
const element = (sch as any).element
|
|
129
|
+
return val.map((item, i) => {
|
|
130
|
+
const itemPath = `${currentPath}[${i}]`
|
|
131
|
+
return recurse(item, element, itemPath)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'union':
|
|
138
|
+
return handleUnion(val, sch, currentPath, recurse, options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return val
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return recurse(value, schema, basePath) as T
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Async version of transformBySchema.
|
|
149
|
+
*
|
|
150
|
+
* Supports async transform functions for operations like policy resolution
|
|
151
|
+
* or encryption with async key lookup.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* // Apply security policies (async entitlement checks)
|
|
156
|
+
* const limited = await transformBySchemaAsync(doc, schema, ctx, async (value, info) => {
|
|
157
|
+
* if (isSensitive(info.meta)) {
|
|
158
|
+
* const decision = await resolvePolicy(info, ctx)
|
|
159
|
+
* return applyDecision(value, decision)
|
|
160
|
+
* }
|
|
161
|
+
* return value
|
|
162
|
+
* })
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export async function transformBySchemaAsync<T, TCtx>(
|
|
166
|
+
value: T,
|
|
167
|
+
schema: z.ZodTypeAny,
|
|
168
|
+
ctx: TCtx,
|
|
169
|
+
transform: AsyncTransformFn<TCtx>,
|
|
170
|
+
options?: TransformOptions
|
|
171
|
+
): Promise<T> {
|
|
172
|
+
const basePath = options?.path ?? ''
|
|
173
|
+
|
|
174
|
+
async function recurse(val: unknown, sch: z.ZodTypeAny, currentPath: string): Promise<unknown> {
|
|
175
|
+
// Pass through null/undefined unchanged
|
|
176
|
+
if (val === undefined || val === null) {
|
|
177
|
+
return val
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const defType = (sch as any)._def?.type as string | undefined
|
|
181
|
+
|
|
182
|
+
// Check shouldTransform predicate - if false, skip callback but continue recursion
|
|
183
|
+
const shouldCall = !options?.shouldTransform || options.shouldTransform(sch)
|
|
184
|
+
|
|
185
|
+
if (shouldCall) {
|
|
186
|
+
const meta = getMetadata(sch)
|
|
187
|
+
|
|
188
|
+
// Call transform for this value/schema pair
|
|
189
|
+
const context: TransformContext<TCtx> = { path: currentPath, schema: sch, meta, ctx }
|
|
190
|
+
const transformed = await transform(val, context)
|
|
191
|
+
|
|
192
|
+
// If transform returned something different, use it
|
|
193
|
+
if (transformed !== val) {
|
|
194
|
+
return transformed
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Dispatch based on schema type
|
|
199
|
+
switch (defType) {
|
|
200
|
+
case 'sensitive': {
|
|
201
|
+
// ZodSensitive wrapper - transform was already called above, now recurse into inner
|
|
202
|
+
const inner = (sch as any).unwrap?.() ?? (sch as any)._def?.innerType
|
|
203
|
+
if (inner) {
|
|
204
|
+
return recurse(val, inner, currentPath)
|
|
205
|
+
}
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case 'optional':
|
|
210
|
+
case 'nullable': {
|
|
211
|
+
if (val === null) return null
|
|
212
|
+
const inner = (sch as any).unwrap()
|
|
213
|
+
return recurse(val, inner, currentPath)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'lazy': {
|
|
217
|
+
const getter = (sch as any)._def?.getter
|
|
218
|
+
if (typeof getter === 'function') {
|
|
219
|
+
const inner = getter()
|
|
220
|
+
return recurse(val, inner, currentPath)
|
|
221
|
+
}
|
|
222
|
+
break
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'default':
|
|
226
|
+
case 'catch':
|
|
227
|
+
case 'readonly':
|
|
228
|
+
case 'prefault':
|
|
229
|
+
case 'nonoptional': {
|
|
230
|
+
const inner = (sch as any)._def?.innerType as z.ZodTypeAny | undefined
|
|
231
|
+
if (inner) {
|
|
232
|
+
return recurse(val, inner, currentPath)
|
|
233
|
+
}
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case 'pipe': {
|
|
238
|
+
const inner = (sch as any)._def?.in as z.ZodTypeAny | undefined
|
|
239
|
+
if (inner) {
|
|
240
|
+
return recurse(val, inner, currentPath)
|
|
241
|
+
}
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
case 'object': {
|
|
246
|
+
if (typeof val === 'object' && val !== null) {
|
|
247
|
+
const shape = (sch as any).shape
|
|
248
|
+
if (shape) {
|
|
249
|
+
const result: Record<string, unknown> = {}
|
|
250
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
251
|
+
const fieldPath = currentPath ? `${currentPath}.${key}` : key
|
|
252
|
+
const fieldValue = (val as Record<string, unknown>)[key]
|
|
253
|
+
result[key] = await recurse(fieldValue, fieldSchema as z.ZodTypeAny, fieldPath)
|
|
254
|
+
}
|
|
255
|
+
return result
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
case 'array': {
|
|
262
|
+
if (Array.isArray(val)) {
|
|
263
|
+
const element = (sch as any).element
|
|
264
|
+
if (options?.parallel) {
|
|
265
|
+
// Parallel processing with Promise.all
|
|
266
|
+
return Promise.all(
|
|
267
|
+
val.map((item, i) => {
|
|
268
|
+
const itemPath = `${currentPath}[${i}]`
|
|
269
|
+
return recurse(item, element, itemPath)
|
|
270
|
+
})
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
// Sequential processing (default)
|
|
274
|
+
const results: unknown[] = []
|
|
275
|
+
for (let i = 0; i < val.length; i++) {
|
|
276
|
+
const itemPath = `${currentPath}[${i}]`
|
|
277
|
+
results.push(await recurse(val[i], element, itemPath))
|
|
278
|
+
}
|
|
279
|
+
return results
|
|
280
|
+
}
|
|
281
|
+
break
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case 'union':
|
|
285
|
+
return handleUnionAsync(val, sch, currentPath, recurse, options)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return val
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return recurse(value, schema, basePath) as Promise<T>
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Handle union matching for sync transforms.
|
|
296
|
+
*/
|
|
297
|
+
function handleUnion(
|
|
298
|
+
val: unknown,
|
|
299
|
+
sch: z.ZodTypeAny,
|
|
300
|
+
currentPath: string,
|
|
301
|
+
recurse: (v: unknown, s: z.ZodTypeAny, p: string) => unknown,
|
|
302
|
+
options?: TransformOptions
|
|
303
|
+
): unknown {
|
|
304
|
+
const unionOptions = (sch as any)._def.options as z.ZodTypeAny[] | undefined
|
|
305
|
+
const discriminator = (sch as any)._def?.discriminator
|
|
306
|
+
|
|
307
|
+
// Discriminated union - find matching variant by discriminator value
|
|
308
|
+
if (discriminator && typeof val === 'object' && val !== null && unionOptions) {
|
|
309
|
+
const discValue = (val as Record<string, unknown>)[discriminator]
|
|
310
|
+
|
|
311
|
+
for (const variant of unionOptions) {
|
|
312
|
+
const variantShape = (variant as any).shape
|
|
313
|
+
if (variantShape) {
|
|
314
|
+
const discField = variantShape[discriminator]
|
|
315
|
+
if ((discField as any)?._def?.type === 'literal') {
|
|
316
|
+
// Zod v4 stores literal values in _def.values array
|
|
317
|
+
const literalValues = (discField as any)._def.values as unknown[]
|
|
318
|
+
if (literalValues?.includes(discValue)) {
|
|
319
|
+
return recurse(val, variant, currentPath)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// No variant matched - handle according to options
|
|
326
|
+
return handleUnmatchedUnion(val, currentPath, options)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Regular union - try each variant
|
|
330
|
+
if (unionOptions) {
|
|
331
|
+
for (const variant of unionOptions) {
|
|
332
|
+
try {
|
|
333
|
+
const result = recurse(val, variant, currentPath)
|
|
334
|
+
// If we got a non-null result, use it
|
|
335
|
+
if (result !== null) return result
|
|
336
|
+
} catch {
|
|
337
|
+
// This variant didn't work, try next
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// No variant matched for regular union
|
|
343
|
+
return handleUnmatchedUnion(val, currentPath, options)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Handle union matching for async transforms.
|
|
348
|
+
*/
|
|
349
|
+
async function handleUnionAsync(
|
|
350
|
+
val: unknown,
|
|
351
|
+
sch: z.ZodTypeAny,
|
|
352
|
+
currentPath: string,
|
|
353
|
+
recurse: (v: unknown, s: z.ZodTypeAny, p: string) => Promise<unknown>,
|
|
354
|
+
options?: TransformOptions
|
|
355
|
+
): Promise<unknown> {
|
|
356
|
+
const unionOptions = (sch as any)._def.options as z.ZodTypeAny[] | undefined
|
|
357
|
+
const discriminator = (sch as any)._def?.discriminator
|
|
358
|
+
|
|
359
|
+
// Discriminated union - find matching variant by discriminator value
|
|
360
|
+
if (discriminator && typeof val === 'object' && val !== null && unionOptions) {
|
|
361
|
+
const discValue = (val as Record<string, unknown>)[discriminator]
|
|
362
|
+
|
|
363
|
+
for (const variant of unionOptions) {
|
|
364
|
+
const variantShape = (variant as any).shape
|
|
365
|
+
if (variantShape) {
|
|
366
|
+
const discField = variantShape[discriminator]
|
|
367
|
+
if ((discField as any)?._def?.type === 'literal') {
|
|
368
|
+
const literalValues = (discField as any)._def.values as unknown[]
|
|
369
|
+
if (literalValues?.includes(discValue)) {
|
|
370
|
+
return recurse(val, variant, currentPath)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return handleUnmatchedUnion(val, currentPath, options)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Regular union - try each variant
|
|
380
|
+
if (unionOptions) {
|
|
381
|
+
for (const variant of unionOptions) {
|
|
382
|
+
try {
|
|
383
|
+
const result = await recurse(val, variant, currentPath)
|
|
384
|
+
if (result !== null) return result
|
|
385
|
+
} catch {
|
|
386
|
+
// Try next variant
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return handleUnmatchedUnion(val, currentPath, options)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Handle unmatched union according to options.
|
|
396
|
+
*/
|
|
397
|
+
function handleUnmatchedUnion(val: unknown, path: string, options?: TransformOptions): unknown {
|
|
398
|
+
options?.onUnmatchedUnion?.(path)
|
|
399
|
+
|
|
400
|
+
switch (options?.unmatchedUnion) {
|
|
401
|
+
case 'error':
|
|
402
|
+
throw new Error(`No union variant matched at path: ${path}`)
|
|
403
|
+
case 'null':
|
|
404
|
+
return null
|
|
405
|
+
case 'passthrough':
|
|
406
|
+
default:
|
|
407
|
+
return val
|
|
408
|
+
}
|
|
409
|
+
}
|