zodvex 0.2.5 → 0.3.2
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 +319 -7
- package/dist/index.d.ts +177 -66
- package/dist/index.js +122 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -16
- package/src/__type-tests__/infer-returns.ts +154 -0
- package/src/__type-tests__/zodTable-inference.ts +476 -0
- package/src/custom.ts +15 -6
- package/src/ids.ts +8 -8
- package/src/registry.ts +171 -0
- package/src/tables.ts +238 -30
- package/src/types.ts +10 -22
- package/src/wrappers.ts +2 -2
package/src/custom.ts
CHANGED
|
@@ -178,13 +178,15 @@ export function customFnBuilder<
|
|
|
178
178
|
|
|
179
179
|
return function customBuilder(fn: any): any {
|
|
180
180
|
const { args, handler = fn, returns: maybeObject, ...extra } = fn
|
|
181
|
+
const skipConvexValidation = fn.skipConvexValidation ?? false
|
|
181
182
|
|
|
182
183
|
const returns =
|
|
183
184
|
maybeObject && !(maybeObject instanceof z.ZodType) ? z.object(maybeObject) : maybeObject
|
|
185
|
+
// Only generate Convex return validator when not skipping Convex validation
|
|
184
186
|
const returnValidator =
|
|
185
|
-
returns && !
|
|
187
|
+
returns && !skipConvexValidation ? { returns: zodToConvex(returns) } : undefined
|
|
186
188
|
|
|
187
|
-
if (args
|
|
189
|
+
if (args) {
|
|
188
190
|
let argsValidator = args
|
|
189
191
|
let argsSchema: z.ZodObject<any>
|
|
190
192
|
|
|
@@ -202,9 +204,13 @@ export function customFnBuilder<
|
|
|
202
204
|
argsSchema = z.object(argsValidator)
|
|
203
205
|
}
|
|
204
206
|
|
|
205
|
-
|
|
207
|
+
// Only generate Convex args validator when not skipping Convex validation
|
|
208
|
+
const convexArgs = skipConvexValidation
|
|
209
|
+
? inputArgs
|
|
210
|
+
: { ...zodToConvexFields(argsValidator), ...inputArgs }
|
|
211
|
+
|
|
206
212
|
return builder({
|
|
207
|
-
args:
|
|
213
|
+
args: convexArgs,
|
|
208
214
|
...returnValidator,
|
|
209
215
|
handler: async (ctx: Ctx, allArgs: any) => {
|
|
210
216
|
const added: any = await customInput(
|
|
@@ -215,6 +221,7 @@ export function customFnBuilder<
|
|
|
215
221
|
const argKeys = Object.keys(argsValidator)
|
|
216
222
|
const rawArgs = pick(allArgs, argKeys)
|
|
217
223
|
const decoded = fromConvexJS(rawArgs, argsSchema)
|
|
224
|
+
// Always run Zod validation, regardless of skipConvexValidation
|
|
218
225
|
const parsed = argsSchema.safeParse(decoded)
|
|
219
226
|
if (!parsed.success) {
|
|
220
227
|
handleZodValidationError(parsed.error, 'args')
|
|
@@ -224,7 +231,8 @@ export function customFnBuilder<
|
|
|
224
231
|
const addedArgs = (added?.args as Record<string, unknown>) ?? {}
|
|
225
232
|
const finalArgs = { ...baseArgs, ...addedArgs }
|
|
226
233
|
const ret = await handler(finalCtx, finalArgs)
|
|
227
|
-
|
|
234
|
+
// Always run Zod return validation when returns schema is provided
|
|
235
|
+
if (returns) {
|
|
228
236
|
let validated: any
|
|
229
237
|
try {
|
|
230
238
|
validated = (returns as z.ZodTypeAny).parse(ret)
|
|
@@ -261,7 +269,8 @@ export function customFnBuilder<
|
|
|
261
269
|
const addedArgs = (added?.args as Record<string, unknown>) ?? {}
|
|
262
270
|
const finalArgs = { ...baseArgs, ...addedArgs }
|
|
263
271
|
const ret = await handler(finalCtx, finalArgs)
|
|
264
|
-
|
|
272
|
+
// Always run Zod return validation when returns schema is provided
|
|
273
|
+
if (returns) {
|
|
265
274
|
let validated: any
|
|
266
275
|
try {
|
|
267
276
|
validated = (returns as z.ZodTypeAny).parse(ret)
|
package/src/ids.ts
CHANGED
|
@@ -16,23 +16,21 @@ export const registryHelpers = {
|
|
|
16
16
|
/**
|
|
17
17
|
* Create a Zod validator for a Convex Id
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Compatible with AI SDK and other tools that don't support transforms.
|
|
20
|
+
* Uses type-level branding instead of runtime transforms for GenericId<T> compatibility.
|
|
21
|
+
*
|
|
22
|
+
* @param tableName - The Convex table name for this ID
|
|
23
|
+
* @returns A Zod string validator typed as GenericId<TableName>
|
|
21
24
|
*/
|
|
22
25
|
export function zid<TableName extends string>(
|
|
23
26
|
tableName: TableName
|
|
24
27
|
): z.ZodType<GenericId<TableName>> & { _tableName: TableName } {
|
|
25
|
-
//
|
|
28
|
+
// Create base string validator with refinement (no transform or brand)
|
|
26
29
|
const baseSchema = z
|
|
27
30
|
.string()
|
|
28
31
|
.refine(val => typeof val === 'string' && val.length > 0, {
|
|
29
32
|
message: `Invalid ID for table "${tableName}"`
|
|
30
33
|
})
|
|
31
|
-
.transform(val => {
|
|
32
|
-
// Cast to GenericId while keeping the string value
|
|
33
|
-
return val as string & GenericId<TableName>
|
|
34
|
-
})
|
|
35
|
-
.brand(`ConvexId_${tableName}`) // Use native Zod v4 .brand() method
|
|
36
34
|
// Add a human-readable marker for client-side introspection utilities
|
|
37
35
|
// used in apps/native (e.g., to detect relationship fields in dynamic forms).
|
|
38
36
|
.describe(`convexId:${tableName}`)
|
|
@@ -47,6 +45,8 @@ export function zid<TableName extends string>(
|
|
|
47
45
|
const branded = baseSchema as any
|
|
48
46
|
branded._tableName = tableName
|
|
49
47
|
|
|
48
|
+
// Type assertion provides GenericId<TableName> typing without runtime transform
|
|
49
|
+
// This maintains type safety while being compatible with AI SDK and similar tools
|
|
50
50
|
return branded as z.ZodType<GenericId<TableName>> & { _tableName: TableName }
|
|
51
51
|
}
|
|
52
52
|
|
package/src/registry.ts
CHANGED
|
@@ -56,3 +56,174 @@ export function isDateSchema(schema: any): boolean {
|
|
|
56
56
|
|
|
57
57
|
return false
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// JSON Schema Override Support
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Zod's toJSONSchema doesn't support transforms, brands, and other "unrepresentable"
|
|
64
|
+
// types by default. This module provides overrides for zodvex-managed types
|
|
65
|
+
// so they can be used with AI SDKs and other JSON Schema-based tools.
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks if a schema is a zid (Convex ID) schema by looking at its description.
|
|
69
|
+
* zid schemas are marked with "convexId:{tableName}" in their description.
|
|
70
|
+
*/
|
|
71
|
+
export function isZidSchema(schema: z.ZodTypeAny): boolean {
|
|
72
|
+
const description = schema.description
|
|
73
|
+
return typeof description === 'string' && description.startsWith('convexId:')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extracts the table name from a zid schema's description.
|
|
78
|
+
* Returns undefined if not a zid schema.
|
|
79
|
+
*/
|
|
80
|
+
export function getZidTableName(schema: z.ZodTypeAny): string | undefined {
|
|
81
|
+
const description = schema.description
|
|
82
|
+
if (typeof description === 'string' && description.startsWith('convexId:')) {
|
|
83
|
+
return description.slice('convexId:'.length)
|
|
84
|
+
}
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Context object passed to the JSON Schema override function.
|
|
90
|
+
* Uses 'any' types for compatibility with Zod's internal types.
|
|
91
|
+
*/
|
|
92
|
+
export interface JSONSchemaOverrideContext {
|
|
93
|
+
zodSchema: any // Zod's internal $ZodTypes
|
|
94
|
+
jsonSchema: any // Zod's JSONSchema.BaseSchema
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Override function for z.toJSONSchema that handles zodvex-managed types.
|
|
99
|
+
*
|
|
100
|
+
* Handles:
|
|
101
|
+
* - zid schemas: Converts to { type: "string" } with convexId format
|
|
102
|
+
* - z.date(): Converts to { type: "string", format: "date-time" }
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { z } from 'zod'
|
|
107
|
+
* import { zid, zodvexJSONSchemaOverride } from 'zodvex'
|
|
108
|
+
*
|
|
109
|
+
* const schema = z.object({
|
|
110
|
+
* userId: zid('users'),
|
|
111
|
+
* name: z.string()
|
|
112
|
+
* })
|
|
113
|
+
*
|
|
114
|
+
* const jsonSchema = z.toJSONSchema(schema, {
|
|
115
|
+
* unrepresentable: 'any',
|
|
116
|
+
* override: zodvexJSONSchemaOverride
|
|
117
|
+
* })
|
|
118
|
+
* // => { type: "object", properties: { userId: { type: "string" }, name: { type: "string" } } }
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function zodvexJSONSchemaOverride(ctx: JSONSchemaOverrideContext): void {
|
|
122
|
+
const { zodSchema, jsonSchema } = ctx
|
|
123
|
+
|
|
124
|
+
// Handle zid schemas (transforms with convexId description)
|
|
125
|
+
if (isZidSchema(zodSchema)) {
|
|
126
|
+
const tableName = getZidTableName(zodSchema)
|
|
127
|
+
// Set our properties - don't clear existing ones set by user overrides
|
|
128
|
+
// When unrepresentable: 'any', Zod already gives us {} so no clearing needed
|
|
129
|
+
jsonSchema.type = 'string'
|
|
130
|
+
if (tableName) {
|
|
131
|
+
jsonSchema.format = `convex-id:${tableName}`
|
|
132
|
+
}
|
|
133
|
+
// Preserve the description from .describe() - this is what the LLM sees
|
|
134
|
+
if (zodSchema.description) {
|
|
135
|
+
jsonSchema.description = zodSchema.description
|
|
136
|
+
}
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle z.date() - convert to ISO 8601 string format
|
|
141
|
+
// Zod v4 passes real schema instances here (ZodDate has `type === 'date'`).
|
|
142
|
+
if (zodSchema instanceof z.ZodDate || (zodSchema as any).type === 'date') {
|
|
143
|
+
jsonSchema.type = 'string'
|
|
144
|
+
jsonSchema.format = 'date-time'
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Composes multiple JSON Schema override functions into one.
|
|
151
|
+
* Overrides run in order - first override runs first.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* import { composeOverrides, zodvexJSONSchemaOverride } from 'zodvex'
|
|
156
|
+
*
|
|
157
|
+
* const myOverride = (ctx) => {
|
|
158
|
+
* if (ctx.zodSchema.description?.startsWith('myType:')) {
|
|
159
|
+
* ctx.jsonSchema.type = 'string'
|
|
160
|
+
* ctx.jsonSchema.format = 'my-format'
|
|
161
|
+
* }
|
|
162
|
+
* }
|
|
163
|
+
*
|
|
164
|
+
* // User's override runs first, then zodvex's
|
|
165
|
+
* z.toJSONSchema(schema, {
|
|
166
|
+
* unrepresentable: 'any',
|
|
167
|
+
* override: composeOverrides(myOverride, zodvexJSONSchemaOverride)
|
|
168
|
+
* })
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export function composeOverrides(
|
|
172
|
+
...overrides: Array<((ctx: JSONSchemaOverrideContext) => void) | undefined>
|
|
173
|
+
): (ctx: JSONSchemaOverrideContext) => void {
|
|
174
|
+
return (ctx: JSONSchemaOverrideContext) => {
|
|
175
|
+
for (const override of overrides) {
|
|
176
|
+
override?.(ctx)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Options for toJSONSchema, matching Zod's interface.
|
|
183
|
+
*/
|
|
184
|
+
export interface ToJSONSchemaOptions {
|
|
185
|
+
target?: 'draft-4' | 'draft-7' | 'draft-2020-12' | 'openapi-3.0'
|
|
186
|
+
unrepresentable?: 'throw' | 'any'
|
|
187
|
+
cycles?: 'ref' | 'throw'
|
|
188
|
+
reused?: 'ref' | 'inline'
|
|
189
|
+
io?: 'input' | 'output'
|
|
190
|
+
override?: (ctx: JSONSchemaOverrideContext) => void
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Converts a Zod schema to JSON Schema with zodvex-aware overrides.
|
|
195
|
+
*
|
|
196
|
+
* This is a convenience wrapper around z.toJSONSchema that automatically
|
|
197
|
+
* handles zodvex-managed types like zid and dates.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* import { zid, toJSONSchema } from 'zodvex'
|
|
202
|
+
*
|
|
203
|
+
* const schema = z.object({
|
|
204
|
+
* userId: zid('users'),
|
|
205
|
+
* createdAt: z.date(),
|
|
206
|
+
* name: z.string()
|
|
207
|
+
* })
|
|
208
|
+
*
|
|
209
|
+
* const jsonSchema = toJSONSchema(schema)
|
|
210
|
+
* // Works with AI SDK's generateObject, etc.
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function toJSONSchema<T extends z.ZodTypeAny>(
|
|
214
|
+
schema: T,
|
|
215
|
+
options?: ToJSONSchemaOptions
|
|
216
|
+
): Record<string, any> {
|
|
217
|
+
const userOverride = options?.override
|
|
218
|
+
|
|
219
|
+
return z.toJSONSchema(schema, {
|
|
220
|
+
...options,
|
|
221
|
+
// Default to 'any' so transforms don't throw
|
|
222
|
+
unrepresentable: options?.unrepresentable ?? 'any',
|
|
223
|
+
// Chain our override with user's override
|
|
224
|
+
override: ctx => {
|
|
225
|
+
zodvexJSONSchemaOverride(ctx)
|
|
226
|
+
userOverride?.(ctx)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
}
|
package/src/tables.ts
CHANGED
|
@@ -1,8 +1,98 @@
|
|
|
1
|
+
import { defineTable } from 'convex/server'
|
|
1
2
|
import type { GenericId } from 'convex/values'
|
|
2
3
|
import { Table } from 'convex-helpers/server'
|
|
3
4
|
import { z } from 'zod'
|
|
4
5
|
import { zid } from './ids'
|
|
5
|
-
import { type ConvexValidatorFromZodFieldsAuto, zodToConvexFields } from './mapping'
|
|
6
|
+
import { type ConvexValidatorFromZodFieldsAuto, zodToConvex, zodToConvexFields } from './mapping'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Helper type for Convex system fields added to documents
|
|
10
|
+
*/
|
|
11
|
+
type SystemFields<TableName extends string> = {
|
|
12
|
+
_id: ReturnType<typeof zid<TableName>>
|
|
13
|
+
_creationTime: z.ZodNumber
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maps over union options, extending each ZodObject variant with system fields.
|
|
18
|
+
* Non-object variants are preserved as-is.
|
|
19
|
+
*/
|
|
20
|
+
type MapSystemFields<TableName extends string, Options extends readonly z.ZodTypeAny[]> = {
|
|
21
|
+
[K in keyof Options]: Options[K] extends z.ZodObject<infer Shape extends z.ZodRawShape>
|
|
22
|
+
? z.ZodObject<Shape & SystemFields<TableName>>
|
|
23
|
+
: Options[K]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Adds Convex system fields (_id, _creationTime) to a Zod schema.
|
|
28
|
+
*
|
|
29
|
+
* For object schemas: extends with system fields
|
|
30
|
+
* For union schemas: adds system fields to each variant
|
|
31
|
+
*
|
|
32
|
+
* @param tableName - The Convex table name
|
|
33
|
+
* @param schema - The Zod schema (object or union)
|
|
34
|
+
* @returns Schema with system fields added
|
|
35
|
+
*/
|
|
36
|
+
// Overload 1: ZodObject - extends with system fields
|
|
37
|
+
export function addSystemFields<TableName extends string, Shape extends z.ZodRawShape>(
|
|
38
|
+
tableName: TableName,
|
|
39
|
+
schema: z.ZodObject<Shape>
|
|
40
|
+
): z.ZodObject<Shape & SystemFields<TableName>>
|
|
41
|
+
|
|
42
|
+
// Overload 2: ZodUnion - maps system fields to each variant
|
|
43
|
+
export function addSystemFields<TableName extends string, Options extends readonly z.ZodTypeAny[]>(
|
|
44
|
+
tableName: TableName,
|
|
45
|
+
schema: z.ZodUnion<Options>
|
|
46
|
+
): z.ZodUnion<MapSystemFields<TableName, Options>>
|
|
47
|
+
|
|
48
|
+
// Overload 3: ZodDiscriminatedUnion - maps system fields preserving discriminator
|
|
49
|
+
// Note: Zod v4 signature is ZodDiscriminatedUnion<Options, Discriminator>
|
|
50
|
+
export function addSystemFields<
|
|
51
|
+
TableName extends string,
|
|
52
|
+
Options extends readonly z.ZodObject<z.ZodRawShape>[],
|
|
53
|
+
Discriminator extends string
|
|
54
|
+
>(
|
|
55
|
+
tableName: TableName,
|
|
56
|
+
schema: z.ZodDiscriminatedUnion<Options, Discriminator>
|
|
57
|
+
): z.ZodDiscriminatedUnion<MapSystemFields<TableName, Options>, Discriminator>
|
|
58
|
+
|
|
59
|
+
// Overload 4: Fallback for other ZodTypes - returns as-is
|
|
60
|
+
export function addSystemFields<TableName extends string, S extends z.ZodTypeAny>(
|
|
61
|
+
tableName: TableName,
|
|
62
|
+
schema: S
|
|
63
|
+
): S
|
|
64
|
+
|
|
65
|
+
// Implementation
|
|
66
|
+
export function addSystemFields<TableName extends string>(
|
|
67
|
+
tableName: TableName,
|
|
68
|
+
schema: z.ZodTypeAny
|
|
69
|
+
): z.ZodTypeAny {
|
|
70
|
+
// Handle union schemas - add system fields to each variant
|
|
71
|
+
if (schema instanceof z.ZodUnion || schema instanceof z.ZodDiscriminatedUnion) {
|
|
72
|
+
const options = (schema as z.ZodUnion<any>).options.map((variant: z.ZodTypeAny) => {
|
|
73
|
+
if (variant instanceof z.ZodObject) {
|
|
74
|
+
return variant.extend({
|
|
75
|
+
_id: zid(tableName),
|
|
76
|
+
_creationTime: z.number()
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
// Non-object variants are returned as-is (shouldn't happen in practice)
|
|
80
|
+
return variant
|
|
81
|
+
})
|
|
82
|
+
return z.union(options as any)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Handle object schemas
|
|
86
|
+
if (schema instanceof z.ZodObject) {
|
|
87
|
+
return schema.extend({
|
|
88
|
+
_id: zid(tableName),
|
|
89
|
+
_creationTime: z.number()
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fallback: return schema as-is
|
|
94
|
+
return schema
|
|
95
|
+
}
|
|
6
96
|
|
|
7
97
|
// Helper to create a Zod schema for a Convex document
|
|
8
98
|
export function zodDoc<
|
|
@@ -35,21 +125,43 @@ export function zodDocOrNull<
|
|
|
35
125
|
}
|
|
36
126
|
|
|
37
127
|
/**
|
|
38
|
-
*
|
|
128
|
+
* Helper to detect if input is an object shape (plain object with Zod validators)
|
|
129
|
+
*/
|
|
130
|
+
function isObjectShape(input: any): input is Record<string, z.ZodTypeAny> {
|
|
131
|
+
// Check if it's a plain object (not a Zod instance)
|
|
132
|
+
if (!input || typeof input !== 'object') return false
|
|
133
|
+
|
|
134
|
+
// If it's a Zod instance, it's not an object shape
|
|
135
|
+
if (input instanceof z.ZodType) return false
|
|
136
|
+
|
|
137
|
+
// Check if all values are Zod types
|
|
138
|
+
for (const key in input) {
|
|
139
|
+
if (!(input[key] instanceof z.ZodType)) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Defines a Convex table using either:
|
|
149
|
+
* - A raw Zod shape (an object mapping field names to Zod types)
|
|
150
|
+
* - A Zod union schema (for polymorphic tables)
|
|
39
151
|
*
|
|
40
|
-
*
|
|
152
|
+
* For object shapes, this function intentionally accepts a raw shape instead of a ZodObject instance.
|
|
41
153
|
* Accepting raw shapes allows TypeScript to infer field types more accurately and efficiently,
|
|
42
154
|
* leading to better type inference and performance throughout the codebase.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
155
|
+
*
|
|
156
|
+
* For union schemas, this enables polymorphic tables with discriminated unions.
|
|
45
157
|
*
|
|
46
158
|
* Returns the Table definition along with Zod schemas for documents and arrays.
|
|
47
159
|
*
|
|
48
160
|
* @param name - The table name
|
|
49
|
-
* @param
|
|
50
|
-
* @returns A Table with attached shape,
|
|
161
|
+
* @param schemaOrShape - Either a raw object shape or a Zod union schema
|
|
162
|
+
* @returns A Table with attached helpers (shape, schema, zDoc, docArray, withSystemFields)
|
|
51
163
|
*
|
|
52
|
-
* @example
|
|
164
|
+
* @example Object shape
|
|
53
165
|
* ```ts
|
|
54
166
|
* const Users = zodTable('users', {
|
|
55
167
|
* name: z.string(),
|
|
@@ -66,36 +178,132 @@ export function zodDocOrNull<
|
|
|
66
178
|
* { returns: Users.docArray }
|
|
67
179
|
* )
|
|
68
180
|
* ```
|
|
181
|
+
*
|
|
182
|
+
* @example Union schema (polymorphic table)
|
|
183
|
+
* ```ts
|
|
184
|
+
* const shapeSchema = z.union([
|
|
185
|
+
* z.object({ kind: z.literal('circle'), r: z.number() }),
|
|
186
|
+
* z.object({ kind: z.literal('rectangle'), width: z.number() })
|
|
187
|
+
* ])
|
|
188
|
+
*
|
|
189
|
+
* const Shapes = zodTable('shapes', shapeSchema)
|
|
190
|
+
*
|
|
191
|
+
* // Use in schema
|
|
192
|
+
* export default defineSchema({ shapes: Shapes.table })
|
|
193
|
+
*
|
|
194
|
+
* // Use for return types with system fields
|
|
195
|
+
* export const getShapes = zQuery(query, {},
|
|
196
|
+
* async (ctx) => ctx.db.query('shapes').collect(),
|
|
197
|
+
* { returns: Shapes.docArray }
|
|
198
|
+
* )
|
|
199
|
+
* ```
|
|
69
200
|
*/
|
|
201
|
+
// Helper type to compute the result of addSystemFields for use in zodTable return type
|
|
202
|
+
type AddSystemFieldsResult<
|
|
203
|
+
TableName extends string,
|
|
204
|
+
Schema extends z.ZodTypeAny
|
|
205
|
+
> = Schema extends z.ZodObject<infer Shape extends z.ZodRawShape>
|
|
206
|
+
? z.ZodObject<Shape & SystemFields<TableName>>
|
|
207
|
+
: Schema extends z.ZodUnion<infer Options extends readonly z.ZodTypeAny[]>
|
|
208
|
+
? z.ZodUnion<MapSystemFields<TableName, Options>>
|
|
209
|
+
: Schema extends z.ZodDiscriminatedUnion<
|
|
210
|
+
infer Options extends readonly z.ZodObject<z.ZodRawShape>[],
|
|
211
|
+
infer Disc extends string
|
|
212
|
+
>
|
|
213
|
+
? z.ZodDiscriminatedUnion<MapSystemFields<TableName, Options>, Disc>
|
|
214
|
+
: Schema
|
|
215
|
+
|
|
216
|
+
// Overload 1: Object shape (most common case)
|
|
70
217
|
export function zodTable<TableName extends string, Shape extends Record<string, z.ZodTypeAny>>(
|
|
71
218
|
name: TableName,
|
|
72
219
|
shape: Shape
|
|
73
|
-
) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Create docArray helper for return types
|
|
84
|
-
const docArray = z.array(zDoc)
|
|
85
|
-
|
|
86
|
-
// Attach everything for comprehensive usage
|
|
87
|
-
return Object.assign(table, {
|
|
88
|
-
shape,
|
|
89
|
-
zDoc,
|
|
90
|
-
docArray
|
|
91
|
-
}) as typeof table & {
|
|
92
|
-
shape: Shape
|
|
93
|
-
zDoc: z.ZodObject<
|
|
220
|
+
): ReturnType<typeof Table<ConvexValidatorFromZodFieldsAuto<Shape>, TableName>> & {
|
|
221
|
+
shape: Shape
|
|
222
|
+
zDoc: z.ZodObject<
|
|
223
|
+
Shape & {
|
|
224
|
+
_id: ReturnType<typeof zid<TableName>>
|
|
225
|
+
_creationTime: z.ZodNumber
|
|
226
|
+
}
|
|
227
|
+
>
|
|
228
|
+
docArray: z.ZodArray<
|
|
229
|
+
z.ZodObject<
|
|
94
230
|
Shape & {
|
|
95
231
|
_id: ReturnType<typeof zid<TableName>>
|
|
96
232
|
_creationTime: z.ZodNumber
|
|
97
233
|
}
|
|
98
234
|
>
|
|
99
|
-
|
|
235
|
+
>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Overload 2: Union/schema types
|
|
239
|
+
export function zodTable<TableName extends string, Schema extends z.ZodTypeAny>(
|
|
240
|
+
name: TableName,
|
|
241
|
+
schema: Schema
|
|
242
|
+
): {
|
|
243
|
+
table: ReturnType<typeof defineTable>
|
|
244
|
+
tableName: TableName
|
|
245
|
+
validator: ReturnType<typeof zodToConvex<Schema>>
|
|
246
|
+
schema: Schema
|
|
247
|
+
docArray: z.ZodArray<AddSystemFieldsResult<TableName, Schema>>
|
|
248
|
+
withSystemFields: () => AddSystemFieldsResult<TableName, Schema>
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function zodTable<
|
|
252
|
+
TableName extends string,
|
|
253
|
+
SchemaOrShape extends z.ZodTypeAny | Record<string, z.ZodTypeAny>
|
|
254
|
+
>(name: TableName, schemaOrShape: SchemaOrShape): any {
|
|
255
|
+
// Detect if it's an object shape or a schema
|
|
256
|
+
if (isObjectShape(schemaOrShape)) {
|
|
257
|
+
// Original object shape logic
|
|
258
|
+
const shape = schemaOrShape as Record<string, z.ZodTypeAny>
|
|
259
|
+
|
|
260
|
+
// Convert fields with proper types
|
|
261
|
+
const convexFields = zodToConvexFields(shape) as ConvexValidatorFromZodFieldsAuto<typeof shape>
|
|
262
|
+
|
|
263
|
+
// Create the Table from convex-helpers with explicit type
|
|
264
|
+
const table = Table<ConvexValidatorFromZodFieldsAuto<typeof shape>, TableName>(
|
|
265
|
+
name,
|
|
266
|
+
convexFields
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// Create zDoc schema with system fields
|
|
270
|
+
const zDoc = zodDoc(name, z.object(shape))
|
|
271
|
+
|
|
272
|
+
// Create docArray helper for return types
|
|
273
|
+
const docArray = z.array(zDoc)
|
|
274
|
+
|
|
275
|
+
// Attach everything for comprehensive usage
|
|
276
|
+
return Object.assign(table, {
|
|
277
|
+
shape,
|
|
278
|
+
zDoc,
|
|
279
|
+
docArray
|
|
280
|
+
})
|
|
281
|
+
} else {
|
|
282
|
+
// Union or other schema type logic
|
|
283
|
+
const schema = schemaOrShape as z.ZodTypeAny
|
|
284
|
+
|
|
285
|
+
// Convert schema to Convex validator
|
|
286
|
+
const convexValidator = zodToConvex(schema)
|
|
287
|
+
|
|
288
|
+
// For unions, use defineTable directly (not Table helper which expects object fields)
|
|
289
|
+
// Note: TypeScript types don't reflect it, but Convex supports union validators in tables
|
|
290
|
+
const table = defineTable(convexValidator as any)
|
|
291
|
+
|
|
292
|
+
// Create document schema with system fields
|
|
293
|
+
const withFields = addSystemFields(name, schema)
|
|
294
|
+
|
|
295
|
+
// Create docArray helper
|
|
296
|
+
const docArray = z.array(withFields)
|
|
297
|
+
|
|
298
|
+
// Attach helpers for union tables
|
|
299
|
+
// Return structure similar to Table() but without fields-based helpers
|
|
300
|
+
return {
|
|
301
|
+
table,
|
|
302
|
+
tableName: name,
|
|
303
|
+
validator: convexValidator,
|
|
304
|
+
schema,
|
|
305
|
+
docArray,
|
|
306
|
+
withSystemFields: () => addSystemFields(name, schema)
|
|
307
|
+
}
|
|
100
308
|
}
|
|
101
309
|
}
|
package/src/types.ts
CHANGED
|
@@ -15,31 +15,19 @@ export type InferArgs<A> = A extends z.ZodObject<infer S>
|
|
|
15
15
|
? z.infer<A>
|
|
16
16
|
: Record<string, never>
|
|
17
17
|
|
|
18
|
-
// Return type inference
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// Return type inference - uses z.output for Zod schemas
|
|
19
|
+
// Previously had bailouts for unions/custom to avoid TypeScript depth errors,
|
|
20
|
+
// but research (Issue #20) showed convex-helpers handles these without issues.
|
|
21
|
+
// Removing bailouts fixes Issue #19 (Promise<any> return types).
|
|
22
|
+
export type InferReturns<R> = R extends z.ZodType<any, any, any>
|
|
23
|
+
? z.output<R>
|
|
24
|
+
: R extends undefined
|
|
23
25
|
? any
|
|
24
|
-
:
|
|
25
|
-
R extends z.ZodType<any, any, any>
|
|
26
|
-
? z.output<R>
|
|
27
|
-
: // Use z.output for other schemas
|
|
28
|
-
R extends undefined
|
|
29
|
-
? any
|
|
30
|
-
: R
|
|
26
|
+
: R
|
|
31
27
|
|
|
32
28
|
// For handler authoring: what the handler returns before wrapper validation/encoding
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
: // Bail immediately for unions
|
|
36
|
-
R extends z.ZodCustom<any>
|
|
37
|
-
? any
|
|
38
|
-
: // Bail immediately for custom
|
|
39
|
-
R extends z.ZodType<any, any, any>
|
|
40
|
-
? z.input<R>
|
|
41
|
-
: // Use z.input for other schemas
|
|
42
|
-
any
|
|
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
|
|
43
31
|
|
|
44
32
|
/**
|
|
45
33
|
* Extract the visibility type from a Convex builder function
|
package/src/wrappers.ts
CHANGED
|
@@ -39,8 +39,8 @@ function containsCustom(schema: z.ZodTypeAny, maxDepth = 50, currentDepth = 0):
|
|
|
39
39
|
|
|
40
40
|
let result = false
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
if (
|
|
42
|
+
// Zod v4 exports ZodCustom and instances expose `schema.type === "custom"`.
|
|
43
|
+
if (schema instanceof z.ZodCustom) {
|
|
44
44
|
result = true
|
|
45
45
|
} else if (schema instanceof z.ZodUnion) {
|
|
46
46
|
result = (schema.options as z.ZodTypeAny[]).some(opt =>
|