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
package/src/custom.ts CHANGED
@@ -16,7 +16,133 @@ import { z } from 'zod'
16
16
  import { fromConvexJS, toConvexJS } from './codec'
17
17
  import { type ZodValidator, zodToConvex, zodToConvexFields } from './mapping'
18
18
  import type { ExtractCtx, ExtractVisibility } from './types'
19
- import { handleZodValidationError, pick } from './utils'
19
+ import { handleZodValidationError, pick, validateReturns } from './utils'
20
+
21
+ /**
22
+ * Hooks for observing the function execution (side effects, no return value).
23
+ */
24
+ export type CustomizationHooks = {
25
+ /** Called after successful execution with access to ctx, args, and result */
26
+ onSuccess?: (info: {
27
+ ctx: unknown
28
+ args: Record<string, unknown>
29
+ result: unknown
30
+ }) => void | Promise<void>
31
+ }
32
+
33
+ /**
34
+ * Transforms for modifying data in the function flow.
35
+ */
36
+ export type CustomizationTransforms = {
37
+ /** Transform args after validation but before handler receives them */
38
+ input?: (args: unknown, schema: z.ZodTypeAny) => unknown | Promise<unknown>
39
+ /** Transform the output after validation but before wire encoding */
40
+ output?: (result: unknown, schema: z.ZodTypeAny) => unknown | Promise<unknown>
41
+ }
42
+
43
+ /**
44
+ * Result returned from a customization input function.
45
+ * Separates Convex concepts (ctx, args) from hooks (side effects) and transforms (data modifications).
46
+ */
47
+ export type CustomizationResult<
48
+ CustomCtx extends Record<string, any> = Record<string, any>,
49
+ CustomArgs extends Record<string, any> = Record<string, any>
50
+ > = {
51
+ /** Custom context to merge with base context */
52
+ ctx?: CustomCtx
53
+ /** Custom args to merge with parsed args */
54
+ args?: CustomArgs
55
+ /** Hooks for observing execution (side effects) */
56
+ hooks?: CustomizationHooks
57
+ /** Transforms for modifying the data flow */
58
+ transforms?: CustomizationTransforms
59
+ }
60
+
61
+ /**
62
+ * Extended input result that includes hooks and transforms.
63
+ * This is what the input function returns internally.
64
+ */
65
+ export type CustomizationInputResult<
66
+ OutCtx extends Record<string, any>,
67
+ OutArgs extends Record<string, any>
68
+ > = {
69
+ ctx: OutCtx
70
+ args: OutArgs
71
+ hooks?: CustomizationHooks
72
+ transforms?: CustomizationTransforms
73
+ }
74
+
75
+ /**
76
+ * Customization type that supports hooks and transforms.
77
+ * This extends convex-helpers' Customization pattern to include
78
+ * hooks (side effects) and transforms (data modifications).
79
+ *
80
+ * Use customCtxWithHooks() to create instances of this type.
81
+ */
82
+ export type CustomizationWithHooks<
83
+ InCtx extends Record<string, any>,
84
+ OutCtx extends Record<string, any> = Record<string, any>,
85
+ OutArgs extends Record<string, any> = Record<string, any>,
86
+ ExtraArgs extends Record<string, any> = Record<string, any>
87
+ > = {
88
+ args: Record<string, never>
89
+ input: (
90
+ ctx: InCtx,
91
+ args?: Record<string, unknown>,
92
+ extra?: ExtraArgs
93
+ ) =>
94
+ | Promise<CustomizationInputResult<OutCtx, OutArgs>>
95
+ | CustomizationInputResult<OutCtx, OutArgs>
96
+ }
97
+
98
+ /**
99
+ * A helper for defining a Customization with full support for hooks and transforms.
100
+ * Use this instead of customCtx when you need onSuccess, transforms.input, transforms.output, etc.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const secureMutation = zCustomMutationBuilder(
105
+ * mutation,
106
+ * customCtxWithHooks(async (ctx: MutationCtx) => {
107
+ * const securityCtx = await getSecurityContext(ctx)
108
+ * return {
109
+ * ctx: { securityCtx },
110
+ * hooks: {
111
+ * onSuccess: ({ result }) => console.log('Mutation returned:', result),
112
+ * },
113
+ * transforms: {
114
+ * // Transform incoming args (e.g., wire format → runtime objects)
115
+ * input: (args, schema) => transformIncomingArgs(args, securityCtx),
116
+ * // Transform outgoing result (e.g., runtime objects → wire format)
117
+ * output: (result, schema) => transformOutgoingResult(result, securityCtx),
118
+ * },
119
+ * }
120
+ * })
121
+ * )
122
+ * ```
123
+ */
124
+ export function customCtxWithHooks<
125
+ InCtx extends Record<string, any>,
126
+ OutCtx extends Record<string, any> = Record<string, any>,
127
+ OutArgs extends Record<string, any> = Record<string, any>
128
+ >(
129
+ fn: (
130
+ ctx: InCtx
131
+ ) => Promise<CustomizationResult<OutCtx, OutArgs>> | CustomizationResult<OutCtx, OutArgs>
132
+ ): CustomizationWithHooks<InCtx, OutCtx, OutArgs> {
133
+ return {
134
+ args: {},
135
+ input: async (ctx: InCtx): Promise<CustomizationInputResult<OutCtx, OutArgs>> => {
136
+ const result = await fn(ctx)
137
+ return {
138
+ ctx: result.ctx ?? ({} as OutCtx),
139
+ args: result.args ?? ({} as OutArgs),
140
+ hooks: result.hooks,
141
+ transforms: result.transforms
142
+ }
143
+ }
144
+ }
145
+ }
20
146
 
21
147
  // Type helpers for args transformation (from zodV3 example)
22
148
  type OneArgArray<ArgsObject extends DefaultFunctionArgs = DefaultFunctionArgs> = [ArgsObject]
@@ -26,12 +152,14 @@ type NullToUndefinedOrNull<T> = T extends null ? T | undefined | void : T
26
152
  type Returns<T> = Promise<NullToUndefinedOrNull<T>> | NullToUndefinedOrNull<T>
27
153
 
28
154
  // The return value before it's been validated: returned by the handler
155
+ // Uses z.output since the handler produces the internal representation (e.g., Date),
156
+ // which is then encoded to wire format (e.g., string) before sending to the client
29
157
  type ReturnValueInput<ReturnsValidator extends z.ZodTypeAny | ZodValidator | void> = [
30
158
  ReturnsValidator
31
159
  ] extends [z.ZodTypeAny]
32
- ? Returns<z.input<ReturnsValidator>>
160
+ ? Returns<z.output<ReturnsValidator>>
33
161
  : [ReturnsValidator] extends [ZodValidator]
34
- ? Returns<z.input<z.ZodObject<ReturnsValidator>>>
162
+ ? Returns<z.output<z.ZodObject<ReturnsValidator>>>
35
163
  : any
36
164
 
37
165
  // The return value after it's been validated: returned to the client
@@ -171,7 +299,9 @@ export function customFnBuilder<
171
299
  ExtraArgs extends Record<string, any> = Record<string, any>
172
300
  >(
173
301
  builder: Builder,
174
- customization: Customization<Ctx, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs>
302
+ customization:
303
+ | Customization<Ctx, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs>
304
+ | CustomizationWithHooks<Ctx, CustomCtx, CustomMadeArgs, ExtraArgs>
175
305
  ) {
176
306
  const customInput = customization.input ?? NoOp.input
177
307
  const inputArgs = customization.args ?? NoOp.args
@@ -213,6 +343,13 @@ export function customFnBuilder<
213
343
  args: convexArgs,
214
344
  ...returnValidator,
215
345
  handler: async (ctx: Ctx, allArgs: any) => {
346
+ // Cast justification: customInput expects ObjectType<CustomArgsValidator>, but pick()
347
+ // returns Partial<T>. The cast is safe because inputArgs keys are derived from
348
+ // CustomArgsValidator at the type level. The 'added' result is typed as 'any' because
349
+ // it may include hooks/transforms from CustomizationWithHooks which aren't in the
350
+ // convex-helpers Customization type.
351
+ // TODO: Create a type-safe pickArgs<T>() helper that preserves the ObjectType<T>
352
+ // return type when the keys are statically known from the validator.
216
353
  const added: any = await customInput(
217
354
  ctx,
218
355
  pick(allArgs, Object.keys(inputArgs)) as any,
@@ -229,18 +366,30 @@ export function customFnBuilder<
229
366
  const finalCtx = { ...ctx, ...(added?.ctx ?? {}) }
230
367
  const baseArgs = parsed.data as Record<string, unknown>
231
368
  const addedArgs = (added?.args as Record<string, unknown>) ?? {}
232
- const finalArgs = { ...baseArgs, ...addedArgs }
369
+ let finalArgs = { ...baseArgs, ...addedArgs }
370
+
371
+ // Apply input transform if provided (after validation, before handler)
372
+ if (added?.transforms?.input) {
373
+ finalArgs = (await added.transforms.input(finalArgs, argsSchema)) as Record<
374
+ string,
375
+ unknown
376
+ >
377
+ }
378
+
233
379
  const ret = await handler(finalCtx, finalArgs)
234
380
  // Always run Zod return validation when returns schema is provided
235
381
  if (returns) {
236
- let validated: any
237
- try {
238
- validated = (returns as z.ZodTypeAny).parse(ret)
239
- } catch (e) {
240
- handleZodValidationError(e, 'returns')
241
- }
242
- if (added?.onSuccess) {
243
- await added.onSuccess({
382
+ // Apply output transform BEFORE validation (converts internal format → wire format)
383
+ // This allows class instances (e.g., SensitiveField) to be converted to plain objects
384
+ // before validation processes them
385
+ const preTransformed = added?.transforms?.output
386
+ ? await added.transforms.output(ret, returns as z.ZodTypeAny)
387
+ : ret
388
+
389
+ // Validate using encode (for codecs) with fallback to parse (for transforms)
390
+ const validated = validateReturns(returns as z.ZodTypeAny, preTransformed)
391
+ if (added?.hooks?.onSuccess) {
392
+ await added.hooks.onSuccess({
244
393
  ctx,
245
394
  args: parsed.data,
246
395
  result: validated
@@ -248,8 +397,8 @@ export function customFnBuilder<
248
397
  }
249
398
  return toConvexJS(returns as z.ZodTypeAny, validated)
250
399
  }
251
- if (added?.onSuccess) {
252
- await added.onSuccess({ ctx, args: parsed.data, result: ret })
400
+ if (added?.hooks?.onSuccess) {
401
+ await added.hooks.onSuccess({ ctx, args: parsed.data, result: ret })
253
402
  }
254
403
  return ret
255
404
  }
@@ -259,6 +408,9 @@ export function customFnBuilder<
259
408
  args: inputArgs,
260
409
  ...returnValidator,
261
410
  handler: async (ctx: Ctx, allArgs: any) => {
411
+ // Cast justification: Same as above - customInput expects ObjectType<CustomArgsValidator>
412
+ // but pick() returns Partial<T>. Safe because inputArgs keys match CustomArgsValidator.
413
+ // TODO: Create a type-safe pickArgs<T>() helper (see comment in with-args path above).
262
414
  const added: any = await customInput(
263
415
  ctx,
264
416
  pick(allArgs, Object.keys(inputArgs)) as any,
@@ -267,23 +419,36 @@ export function customFnBuilder<
267
419
  const finalCtx = { ...ctx, ...(added?.ctx ?? {}) }
268
420
  const baseArgs = allArgs as Record<string, unknown>
269
421
  const addedArgs = (added?.args as Record<string, unknown>) ?? {}
270
- const finalArgs = { ...baseArgs, ...addedArgs }
422
+ let finalArgs = { ...baseArgs, ...addedArgs }
423
+
424
+ // Apply input transform if provided (even without args schema)
425
+ // Note: schema is z.unknown() in no-args path, transform should handle this
426
+ if (added?.transforms?.input) {
427
+ finalArgs = (await added.transforms.input(finalArgs, z.unknown())) as Record<
428
+ string,
429
+ unknown
430
+ >
431
+ }
432
+
271
433
  const ret = await handler(finalCtx, finalArgs)
272
434
  // Always run Zod return validation when returns schema is provided
273
435
  if (returns) {
274
- let validated: any
275
- try {
276
- validated = (returns as z.ZodTypeAny).parse(ret)
277
- } catch (e) {
278
- handleZodValidationError(e, 'returns')
279
- }
280
- if (added?.onSuccess) {
281
- await added.onSuccess({ ctx, args: allArgs, result: validated })
436
+ // Apply output transform BEFORE validation (converts internal format → wire format)
437
+ // This allows class instances (e.g., SensitiveField) to be converted to plain objects
438
+ // before validation processes them
439
+ const preTransformed = added?.transforms?.output
440
+ ? await added.transforms.output(ret, returns as z.ZodTypeAny)
441
+ : ret
442
+
443
+ // Validate using encode (for codecs) with fallback to parse (for transforms)
444
+ const validated = validateReturns(returns as z.ZodTypeAny, preTransformed)
445
+ if (added?.hooks?.onSuccess) {
446
+ await added.hooks.onSuccess({ ctx, args: allArgs, result: validated })
282
447
  }
283
448
  return toConvexJS(returns as z.ZodTypeAny, validated)
284
449
  }
285
- if (added?.onSuccess) {
286
- await added.onSuccess({ ctx, args: allArgs, result: ret })
450
+ if (added?.hooks?.onSuccess) {
451
+ await added.hooks.onSuccess({ ctx, args: allArgs, result: ret })
287
452
  }
288
453
  return ret
289
454
  }
@@ -343,8 +508,16 @@ export function zCustomQuery<
343
508
  query: QueryBuilder<any, Visibility>,
344
509
  customization: Customization<any, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs>
345
510
  ) {
346
- // Implementation deliberately uses 'any' ctx to preserve overload behavior
347
- // while avoiding a GenericDataModel constraint at the implementation level.
511
+ // Cast justification: This is the TypeScript overload implementation pattern. The function
512
+ // has two overloads (with/without DataModel constraint) that provide precise types to callers.
513
+ // The implementation must satisfy both overloads, which requires a broader signature.
514
+ // The 'as any' casts allow the implementation to delegate to customFnBuilder without
515
+ // TypeScript complaining about the generic parameter differences between overloads.
516
+ // This is type-safe because: (1) callers only see the overload signatures which are strict,
517
+ // (2) the runtime behavior is identical regardless of which overload matched.
518
+ // TODO: Consider using a conditional type or branded types to create a single signature
519
+ // that satisfies both overloads without casts. Alternatively, accept this as idiomatic
520
+ // TypeScript for overloaded functions and keep the casts.
348
521
  return customFnBuilder<
349
522
  any,
350
523
  typeof query,
@@ -388,6 +561,8 @@ export function zCustomMutation<
388
561
  mutation: Builder,
389
562
  customization: Customization<any, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs>
390
563
  ) {
564
+ // Cast justification: Same overload implementation pattern as zCustomQuery.
565
+ // See detailed comment there. Type safety is enforced by the overload signature above.
391
566
  return customFnBuilder<any, Builder, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs>(
392
567
  mutation as any,
393
568
  customization as any
@@ -427,6 +602,8 @@ export function zCustomAction<
427
602
  action: Builder,
428
603
  customization: Customization<any, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs>
429
604
  ) {
605
+ // Cast justification: Same overload implementation pattern as zCustomQuery.
606
+ // See detailed comment there. Type safety is enforced by the overload signature above.
430
607
  return customFnBuilder<any, Builder, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs>(
431
608
  action as any,
432
609
  customization as any
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from './custom'
6
6
  export * from './ids'
7
7
  export * from './mapping'
8
8
  export * from './registry'
9
+ export * from './results'
9
10
  export * from './tables'
10
11
  export * from './types'
11
12
  export * from './utils'
@@ -184,20 +184,32 @@ function zodToConvexInternal<Z extends z.ZodTypeAny>(
184
184
  }
185
185
  case 'transform':
186
186
  case 'pipe': {
187
- // Check for registered codec first
188
- const codec = findBaseCodec(actualValidator)
189
- if (codec) {
190
- convexValidator = codec.toValidator(actualValidator)
191
- } else {
192
- // Check for brand metadata
193
- const metadata = registryHelpers.getMetadata(actualValidator)
194
- if (metadata?.brand && metadata?.originalSchema) {
195
- // For branded types created by our zBrand function, use the original schema
196
- convexValidator = zodToConvexInternal(metadata.originalSchema, visited)
187
+ // Check for native Zod v4 codec first (z.codec())
188
+ // Codecs have def.type='pipe' but are specifically for bidirectional transforms
189
+ // Use the input schema (wire format) for Convex validation
190
+ if (actualValidator instanceof z.ZodCodec) {
191
+ const inputSchema = (actualValidator as any).def?.in
192
+ if (inputSchema && inputSchema instanceof z.ZodType) {
193
+ convexValidator = zodToConvexInternal(inputSchema, visited)
197
194
  } else {
198
- // For non-registered transforms, return v.any()
199
195
  convexValidator = v.any()
200
196
  }
197
+ } else {
198
+ // Check for registered codec from the registry
199
+ const codec = findBaseCodec(actualValidator)
200
+ if (codec) {
201
+ convexValidator = codec.toValidator(actualValidator)
202
+ } else {
203
+ // Check for brand metadata
204
+ const metadata = registryHelpers.getMetadata(actualValidator)
205
+ if (metadata?.brand && metadata?.originalSchema) {
206
+ // For branded types created by our zBrand function, use the original schema
207
+ convexValidator = zodToConvexInternal(metadata.originalSchema, visited)
208
+ } else {
209
+ // For non-registered transforms, return v.any()
210
+ convexValidator = v.any()
211
+ }
212
+ }
201
213
  }
202
214
  break
203
215
  }
@@ -129,7 +129,9 @@ type ConvexValidatorFromZodBase<Z extends z.ZodTypeAny> =
129
129
  ? VAny<'required'>
130
130
  : Z extends z.ZodUnknown
131
131
  ? VAny<'required'>
132
- : VAny<'required'>
132
+ : Z extends z.ZodCodec<infer A extends z.ZodTypeAny, any>
133
+ ? ConvexValidatorFromZodBase<A> // Use input schema (wire format) for Convex
134
+ : VAny<'required'>
133
135
 
134
136
  // Main type mapper with constraint system
135
137
  export type ConvexValidatorFromZod<
@@ -216,7 +218,9 @@ export type ConvexValidatorFromZod<
216
218
  Constraint,
217
219
  string
218
220
  >
219
- : VAny<'required'>
221
+ : Z extends z.ZodCodec<infer A extends z.ZodTypeAny, any>
222
+ ? ConvexValidatorFromZod<A, Constraint> // Use input schema (wire format) for Convex
223
+ : VAny<'required'>
220
224
 
221
225
  type ConvexValidatorFromZodFields<
222
226
  T extends { [key: string]: any },
package/src/results.ts ADDED
@@ -0,0 +1,110 @@
1
+ import { z } from 'zod'
2
+
3
+ // ============================================================================
4
+ // Types
5
+ // ============================================================================
6
+
7
+ /**
8
+ * Result type for mutations that return data on success.
9
+ */
10
+ export type MutationResult<T> = { success: true; data: T } | { success: false; error: string }
11
+
12
+ /**
13
+ * Result type for mutations that don't return data (void operations).
14
+ */
15
+ export type VoidMutationResult = { success: true } | { success: false; error: string }
16
+
17
+ /**
18
+ * Error structure for form validation results.
19
+ */
20
+ export type FormError = {
21
+ formErrors: string[]
22
+ fieldErrors: Record<string, string[]>
23
+ }
24
+
25
+ /**
26
+ * Result type for form submissions with field-level error support.
27
+ * Preserves submitted data on failure for form re-population.
28
+ */
29
+ export type FormResult<TData> =
30
+ | { success: true; data: TData }
31
+ | { success: false; data: TData; error: FormError }
32
+
33
+ // ============================================================================
34
+ // Helper Functions
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Create a success result with data.
39
+ * @example success({ id: '123' })
40
+ */
41
+ export const success = <T>(data: T) => ({ success: true, data }) as const
42
+
43
+ /**
44
+ * Create a failure result with an error message.
45
+ * @example failure('Not found')
46
+ */
47
+ export const failure = (error: string) => ({ success: false, error }) as const
48
+
49
+ /**
50
+ * Create a void success result (no data).
51
+ * @example ok()
52
+ */
53
+ export const ok = () => ({ success: true }) as const
54
+
55
+ /**
56
+ * Create a form success result with data.
57
+ * @example formSuccess({ email: 'user@example.com' })
58
+ */
59
+ export const formSuccess = <T>(data: T) => ({ success: true, data }) as const
60
+
61
+ /**
62
+ * Create a form failure result with data and errors.
63
+ * @example formFailure({ email: 'bad' }, { formErrors: [], fieldErrors: { email: ['Invalid'] } })
64
+ */
65
+ export const formFailure = <T>(data: T, error: FormError) =>
66
+ ({ success: false, data, error }) as const
67
+
68
+ // ============================================================================
69
+ // Zod Schemas for `returns` validation
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Zod schema for MutationResult<T>.
74
+ * Use in `returns` option to validate mutation responses.
75
+ * @example zMutationResult(z.object({ id: zid('users') }))
76
+ */
77
+ export const zMutationResult = <T extends z.ZodTypeAny>(dataSchema: T) =>
78
+ z.discriminatedUnion('success', [
79
+ z.object({ success: z.literal(true), data: dataSchema }),
80
+ z.object({ success: z.literal(false), error: z.string() })
81
+ ])
82
+
83
+ /**
84
+ * Zod schema for VoidMutationResult.
85
+ * Use in `returns` option for void mutations.
86
+ * @example returns: zVoidMutationResult
87
+ */
88
+ export const zVoidMutationResult = z.discriminatedUnion('success', [
89
+ z.object({ success: z.literal(true) }),
90
+ z.object({ success: z.literal(false), error: z.string() })
91
+ ])
92
+
93
+ /**
94
+ * Zod schema for FormError.
95
+ */
96
+ export const zFormError = z.object({
97
+ formErrors: z.array(z.string()),
98
+ fieldErrors: z.record(z.string(), z.array(z.string()))
99
+ })
100
+
101
+ /**
102
+ * Zod schema for FormResult<T>.
103
+ * Use in `returns` option for form submissions.
104
+ * @example zFormResult(z.object({ email: z.string() }))
105
+ */
106
+ export const zFormResult = <T extends z.ZodTypeAny>(dataSchema: T) =>
107
+ z.discriminatedUnion('success', [
108
+ z.object({ success: z.literal(true), data: dataSchema }),
109
+ z.object({ success: z.literal(false), data: dataSchema, error: zFormError })
110
+ ])