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,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
+ }