import { ConvexError, v } from "convex/values"; import type { GenericId, GenericValidator, ObjectType, OptionalProperty, PropertyValidators, Validator, Value, VAny, VArray, VBoolean, VBytes, VFloat64, VId, VInt64, VLiteral, VNull, VObject, VOptional, VRecord, VString, VUnion, } from "convex/values"; import * as zCore from "zod/v4/core"; import * as z from "zod/v4"; import type { ActionBuilder, ArgsArrayToObject, DefaultFunctionArgs, FunctionVisibility, GenericActionCtx, GenericDataModel, GenericMutationCtx, GenericQueryCtx, MutationBuilder, QueryBuilder, TableNamesInDataModel, } from "convex/server"; import { pick, type Expand } from "../index.js"; import type { Customization, Registration } from "./customFunctions.js"; import { NoOp } from "./customFunctions.js"; import { addFieldsToValidator, vRequired, type VRequired, } from "../validators.js"; // #region Convex function definition with Zod /** * zCustomQuery is like customQuery, but allows validation via zod. * You can define custom behavior on top of `query` or `internalQuery` * by passing a function that modifies the ctx and args. Or NoOp to do nothing. * * Example usage: * ```ts * const myQueryBuilder = zCustomQuery(query, { * args: { sessionId: v.id("sessions") }, * input: async (ctx, args) => { * const user = await getUserOrNull(ctx); * const session = await db.get(sessionId); * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules); * return { ctx: { db, user, session }, args: {} }; * }, * }); * * // Using the custom builder * export const getSomeData = myQueryBuilder({ * args: { someArg: z.string() }, * handler: async (ctx, args) => { * const { db, user, session, scheduler } = ctx; * const { someArg } = args; * // ... * } * }); * ``` * * Simple usage only modifying ctx: * ```ts * const myInternalQuery = zCustomQuery( * internalQuery, * customCtx(async (ctx) => { * return { * // Throws an exception if the user isn't logged in * user: await getUserByTokenIdentifier(ctx), * }; * }) * ); * * // Using it * export const getUser = myInternalQuery({ * args: { email: z.string().email() }, * handler: async (ctx, args) => { * console.log(args.email); * return ctx.user; * }, * }); * * @param query The query to be modified. Usually `query` or `internalQuery` * from `_generated/server`. * @param customization The customization to be applied to the query, changing ctx and args. * @returns A new query builder using zod validation to define queries. */ export function zCustomQuery< CustomArgsValidator extends PropertyValidators, CustomCtx extends Record, CustomMadeArgs extends Record, Visibility extends FunctionVisibility, DataModel extends GenericDataModel, ExtraArgs extends Record = object, >( query: QueryBuilder, customization: Customization< GenericQueryCtx, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs >, ) { return customFnBuilder(query, customization) as CustomBuilder< "query", CustomArgsValidator, CustomCtx, CustomMadeArgs, GenericQueryCtx, Visibility, ExtraArgs >; } /** * zCustomMutation is like customMutation, but allows validation via zod. * You can define custom behavior on top of `mutation` or `internalMutation` * by passing a function that modifies the ctx and args. Or NoOp to do nothing. * * Example usage: * ```ts * const myMutationBuilder = zCustomMutation(mutation, { * args: { sessionId: v.id("sessions") }, * input: async (ctx, args) => { * const user = await getUserOrNull(ctx); * const session = await db.get(sessionId); * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules); * return { ctx: { db, user, session }, args: {} }; * }, * }); * * // Using the custom builder * export const getSomeData = myMutationBuilder({ * args: { someArg: z.string() }, * handler: async (ctx, args) => { * const { db, user, session, scheduler } = ctx; * const { someArg } = args; * // ... * } * }); * ``` * * Simple usage only modifying ctx: * ```ts * const myInternalMutation = zCustomMutation( * internalMutation, * customCtx(async (ctx) => { * return { * // Throws an exception if the user isn't logged in * user: await getUserByTokenIdentifier(ctx), * }; * }) * ); * * // Using it * export const getUser = myInternalMutation({ * args: { email: z.string().email() }, * handler: async (ctx, args) => { * console.log(args.email); * return ctx.user; * }, * }); * * @param mutation The mutation to be modified. Usually `mutation` or `internalMutation` * from `_generated/server`. * @param customization The customization to be applied to the mutation, changing ctx and args. * @returns A new mutation builder using zod validation to define queries. */ export function zCustomMutation< CustomArgsValidator extends PropertyValidators, CustomCtx extends Record, CustomMadeArgs extends Record, Visibility extends FunctionVisibility, DataModel extends GenericDataModel, ExtraArgs extends Record = object, >( mutation: MutationBuilder, customization: Customization< GenericMutationCtx, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs >, ) { return customFnBuilder(mutation, customization) as CustomBuilder< "mutation", CustomArgsValidator, CustomCtx, CustomMadeArgs, GenericMutationCtx, Visibility, ExtraArgs >; } /** * zCustomAction is like customAction, but allows validation via zod. * You can define custom behavior on top of `action` or `internalAction` * by passing a function that modifies the ctx and args. Or NoOp to do nothing. * * Example usage: * ```ts * const myActionBuilder = zCustomAction(action, { * args: { sessionId: v.id("sessions") }, * input: async (ctx, args) => { * const user = await getUserOrNull(ctx); * const session = await db.get(sessionId); * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules); * return { ctx: { db, user, session }, args: {} }; * }, * }); * * // Using the custom builder * export const getSomeData = myActionBuilder({ * args: { someArg: z.string() }, * handler: async (ctx, args) => { * const { db, user, session, scheduler } = ctx; * const { someArg } = args; * // ... * } * }); * ``` * * Simple usage only modifying ctx: * ```ts * const myInternalAction = zCustomAction( * internalAction, * customCtx(async (ctx) => { * return { * // Throws an exception if the user isn't logged in * user: await getUserByTokenIdentifier(ctx), * }; * }) * ); * * // Using it * export const getUser = myInternalAction({ * args: { email: z.string().email() }, * handler: async (ctx, args) => { * console.log(args.email); * return ctx.user; * }, * }); * * @param action The action to be modified. Usually `action` or `internalAction` * from `_generated/server`. * @param customization The customization to be applied to the action, changing ctx and args. * @returns A new action builder using zod validation to define queries. */ export function zCustomAction< CustomArgsValidator extends PropertyValidators, CustomCtx extends Record, CustomMadeArgs extends Record, Visibility extends FunctionVisibility, DataModel extends GenericDataModel, ExtraArgs extends Record = object, >( action: ActionBuilder, customization: Customization< GenericActionCtx, CustomArgsValidator, CustomCtx, CustomMadeArgs, ExtraArgs >, ) { return customFnBuilder(action, customization) as CustomBuilder< "action", CustomArgsValidator, CustomCtx, CustomMadeArgs, GenericActionCtx, Visibility, ExtraArgs >; } // #endregion // #region Convex IDs /** * Creates a validator for a Convex `Id`. * * - When **used within Zod**, it will only check that the ID is a string. * - When **converted to a Convex validator** (e.g. through {@link zodToConvex}), * it will check that it's for the right table. * * @param tableName - The table that the `Id` references. i.e. `Id` * @returns A Zod schema representing a Convex `Id` */ export const zid = < DataModel extends GenericDataModel, TableName extends TableNamesInDataModel = TableNamesInDataModel, >( tableName: TableName, ): Zid => { const result = z.custom>( (val) => typeof val === "string", ); _zidRegistry.add(result, { tableName }); return result; }; /** The type of Convex validators in Zod */ export type Zid = z.ZodCustom< GenericId, string > & zCore.$ZodRecordKey; /** * Useful to get the input context type for a custom function using Zod. */ export type ZCustomCtx = Builder extends CustomBuilder< any, any, infer CustomCtx, any, infer InputCtx, any, any > ? Overwrite : never; // #endregion // #region Zod → Convex /** * Turns a Zod or Zod Mini validator into a Convex validator. * * The Convex validator will be as close to possible to the Zod validator, * but might be broader than the Zod validator: * * ```ts * zodToConvex(z.string().email()) // → v.string() * ``` * * This function is useful when running the Zod validator _after_ running the Convex validator * (i.e. the Convex validator validates the input of the Zod validator). Hence, the Convex types * will match the _input type_ of Zod transformations: * ```ts * zodToConvex(z.object({ * name: z.string().default("Nicolas"), * })) // → v.object({ name: v.optional(v.string()) }) * * zodToConvex(z.object({ * name: z.string().transform(s => s.length) * })) // → v.object({ name: v.string() }) * ```` * * This function is useful for: * * **Validating function arguments with Zod**: through {@link zCustomQuery}, * {@link zCustomMutation} and {@link zCustomAction}, you can define the argument validation logic * using Zod validators instead of Convex validators. `zodToConvex` will generate a Convex validator * from your Zod validator. This will allow you to: * - validate at run time that Convex IDs are from the right table (using {@link zid}) * - allow some features of Convex to understand the expected shape of the arguments * (e.g. argument validation/prefilling in the function runner on the Convex dashboard) * - still run the full Zod validation when the function runs * (which is useful for more advanced Zod validators like `z.string().email()`) * * **Validating data after reading it from the database**: if you want to write your DB schema * with Zod, you can run Zod whenever you read from the database to check that the data * still matches the schema. Note that this approach won’t ensure that the data stored in the DB * matches the Zod schema; see * https://stack.convex.dev/typescript-zod-function-validation#can-i-use-zod-to-define-my-database-types-too * for more details. * * Note that some values might be valid in Zod but not in Convex, * in the same way that valid JavaScript values might not be valid * Convex values for the corresponding Convex type. * (see the limits of Convex data types on https://docs.convex.dev/database/types). * * ``` * ┌─────────────────────────────────────┬─────────────────────────────────────┐ * │ **zodToConvex** │ zodOutputToConvex │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ For when the Zod validator runs │ For when the Zod validator runs │ * │ _after_ the Convex validator │ _before_ the Convex validator │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ Convex types use the _input types_ │ Convex types use the _return types_ │ * │ of Zod transformations │ of Zod transformations │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ The Convex validator can be less │ The Convex validator can be less │ * │ strict (i.e. some inputs might be │ strict (i.e. the type in Convex can │ * │ accepted by Convex then rejected │ be less precise than the type in │ * │ by Zod) │ the Zod output) │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ When using Zod schemas │ When using Zod schemas │ * │ for function definitions: │ for function definitions: │ * │ used for _arguments_ │ used for _return values_ │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ When validating contents of the │ When validating contents of the │ * │ database with a Zod schema: │ database with a Zod schema: │ * │ used to validate data │ used to validate data │ * │ _after reading_ │ _before writing_ │ * └─────────────────────────────────────┴─────────────────────────────────────┘ * ``` * * @param zod Zod validator can be a Zod object, or a Zod type like `z.string()` * @returns Convex Validator (e.g. `v.string()` from "convex/values") * @throws If there is no equivalent Convex validator for the value (e.g. `z.date()`) */ export function zodToConvex( validator: Z, ): ConvexValidatorFromZod { const visited = new WeakSet(); function zodToConvexInner(validator: zCore.$ZodType): GenericValidator { // Circular validator definitions are not supported by Convex validators, // so we use v.any() when there is a cycle. if (visited.has(validator)) { return v.any(); } visited.add(validator); const result = validator instanceof zCore.$ZodDefault ? v.optional(zodToConvexInner(validator._zod.def.innerType)) : validator instanceof zCore.$ZodPipe ? zodToConvexInner(validator._zod.def.in) : zodToConvexCommon(validator, zodToConvexInner); // After returning, we remove the validator from the visited set because // we only want to detect circular types, not cases where part of a type // is reused (e.g. `v.object({ field1: mySchema, field2: mySchema })`). visited.delete(validator); return result; } // `as any` because ConvexValidatorFromZod is defined from the behavior of zodToConvex. // We assume the type is correct to simplify the life of the compiler. return zodToConvexInner(validator) as any; } /** * Converts a Zod or Zod Mini validator to a Convex validator that checks the value _after_ * it has been validated (and possibly transformed) by the Zod validator. * * This is similar to {@link zodToConvex}, but is meant for cases where the Convex * validator runs _after_ the Zod validator. Thus, the Convex type refers to the * _output_ type of the Zod transformations: * ```ts * zodOutputToConvex(z.object({ * name: z.string().default("Nicolas"), * })) // → v.object({ name: v.string() }) * * zodOutputToConvex(z.object({ * name: z.string().transform(s => s.length) * })) // → v.object({ name: v.number() }) * ```` * * This function can be useful for: * - **Validating function return values with Zod**: through {@link zCustomQuery}, * {@link zCustomMutation} and {@link zCustomAction}, you can define the `returns` property * of a function using Zod validators instead of Convex validators. * - **Validating data after reading it from the database**: if you want to write your DB schema * Zod validators, you can run Zod whenever you write to the database to ensure your data matches * the expected format. Note that this approach won’t ensure that the data stored in the DB * isn’t modified manually in a way that doesn’t match your Zod schema; see * https://stack.convex.dev/typescript-zod-function-validation#can-i-use-zod-to-define-my-database-types-too * for more details. * * ``` * ┌─────────────────────────────────────┬─────────────────────────────────────┐ * │ zodToConvex │ **zodOutputToConvex** │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ For when the Zod validator runs │ For when the Zod validator runs │ * │ _after_ the Convex validator │ _before_ the Convex validator │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ Convex types use the _input types_ │ Convex types use the _return types_ │ * │ of Zod transformations │ of Zod transformations │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ The Convex validator can be less │ The Convex validator can be less │ * │ strict (i.e. some inputs might be │ strict (i.e. the type in Convex can │ * │ accepted by Convex then rejected │ be less precise than the type in │ * │ by Zod) │ the Zod output) │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ When using Zod schemas │ When using Zod schemas │ * │ for function definitions: │ for function definitions: │ * │ used for _arguments_ │ used for _return values_ │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ When validating contents of the │ When validating contents of the │ * │ database with a Zod schema: │ database with a Zod schema: │ * │ used to validate data │ used to validate data │ * │ _after reading_ │ _before writing_ │ * └─────────────────────────────────────┴─────────────────────────────────────┘ * ``` * * @param z The zod validator * @returns Convex Validator (e.g. `v.string()` from "convex/values") * @throws If there is no equivalent Convex validator for the value (e.g. `z.date()`) */ export function zodOutputToConvex( validator: Z, ): ConvexValidatorFromZodOutput { const visited = new WeakSet(); function zodOutputToConvexInner(validator: zCore.$ZodType): GenericValidator { // Circular validator definitions are not supported by Convex validators, // so we use v.any() when there is a cycle. if (visited.has(validator)) { return v.any(); } visited.add(validator); const result = validator instanceof zCore.$ZodDefault ? zodOutputToConvexInner(validator._zod.def.innerType) : validator instanceof zCore.$ZodPipe ? zodOutputToConvexInner(validator._zod.def.out) : validator instanceof zCore.$ZodTransform ? v.any() : zodToConvexCommon(validator, zodOutputToConvexInner); // After returning, we remove the validator from the visited set because // we only want to detect circular types, not cases where part of a type // is reused (e.g. `v.object({ field1: mySchema, field2: mySchema })`). visited.delete(validator); return result; } // `as any` because ConvexValidatorFromZodOutput is defined from the behavior of zodOutputToConvex. // We assume the type is correct to simplify the life of the compiler. return zodOutputToConvexInner(validator) as any; } type ZodFields = Record; /** * Like {@link zodToConvex}, but it takes in a bare object, as expected by Convex * function arguments, or the argument to {@link defineTable}. * * ```ts * zodToConvexFields({ * name: z.string().default("Nicolas"), * }) // → { name: v.optional(v.string()) } * ``` * * @param fields Object with string keys and Zod validators as values * @returns Object with the same keys, but with Convex validators as values */ export function zodToConvexFields(fields: Fields) { return Object.fromEntries( Object.entries(fields).map(([k, v]) => [k, zodToConvex(v)]), ) as { [k in keyof Fields]: Fields[k] extends zCore.$ZodType ? ConvexValidatorFromZod : never; }; } /** * Like {@link zodOutputToConvex}, but it takes in a bare object, as expected by * Convex function arguments, or the argument to {@link defineTable}. * * ```ts * zodOutputToConvexFields({ * name: z.string().default("Nicolas"), * }) // → { name: v.string() } * ``` * * This is different from {@link zodToConvexFields} because it generates the * Convex validator for the output of the Zod validator, not the input; * see the documentation of {@link zodToConvex} and {@link zodOutputToConvex} * for more details. * * @param zod Object with string keys and Zod validators as values * @returns Object with the same keys, but with Convex validators as values */ export function zodOutputToConvexFields( fields: Fields, ) { return Object.fromEntries( Object.entries(fields).map(([k, v]) => [k, zodOutputToConvex(v)]), ) as { [k in keyof Fields]: ConvexValidatorFromZodOutput; }; } // #endregion // #region Convex → Zod /** * Turns a Convex validator into a Zod validator. * * This is useful when you want to use types you defined using Convex validators * with external libraries that expect to receive a Zod validator. * * ```ts * convexToZod(v.string()) // → z.string() * ``` * * This function returns Zod validators, not Zod Mini validators. * * @param convexValidator Convex validator can be any validator from "convex/values" e.g. `v.string()` * @returns Zod validator (e.g. `z.string()`) with inferred type matching the Convex validator */ export function convexToZod( convexValidator: V, ): ZodValidatorFromConvex { const isOptional = (convexValidator as any).isOptional === "optional"; let zodValidator: zCore.$ZodType; const { kind } = convexValidator; switch (kind) { case "id": convexValidator satisfies VId; zodValidator = zid(convexValidator.tableName); break; case "string": zodValidator = z.string(); break; case "float64": zodValidator = z.number(); break; case "int64": zodValidator = z.bigint(); break; case "boolean": zodValidator = z.boolean(); break; case "null": zodValidator = z.null(); break; case "any": zodValidator = z.any(); break; case "array": { convexValidator satisfies VArray; zodValidator = z.array(convexToZod(convexValidator.element)); break; } case "object": { convexValidator satisfies VObject; zodValidator = z.object(convexToZodFields(convexValidator.fields)); break; } case "union": { convexValidator satisfies VUnion; if (convexValidator.members.length === 0) { zodValidator = z.never(); break; } if (convexValidator.members.length === 1) { zodValidator = convexToZod(convexValidator.members[0]!); break; } const memberValidators = convexValidator.members.map( (member: GenericValidator) => convexToZod(member), ); zodValidator = z.union([...memberValidators]); break; } case "literal": { const literalValidator = convexValidator as VLiteral; zodValidator = z.literal(literalValidator.value); break; } case "record": { convexValidator satisfies VRecord; zodValidator = z.record( convexToZod(convexValidator.key) as zCore.$ZodRecordKey, convexToZod(convexValidator.value), ); break; } case "bytes": throw new Error("v.bytes() is not supported"); default: kind satisfies never; throw new Error(`Unknown convex validator type: ${kind}`); } return isOptional ? (z.optional(zodValidator) as ZodValidatorFromConvex) : (zodValidator as ZodValidatorFromConvex); } /** * Like {@link convexToZod}, but it takes in a bare object, as expected by Convex * function arguments, or the argument to {@link defineTable}. * * ```ts * convexToZodFields({ * name: v.string(), * }) // → { name: z.string() } * ``` * * @param convexValidators Object with string keys and Convex validators as values * @returns Object with the same keys, but with Zod validators as values */ export function convexToZodFields( convexValidators: C, ) { return Object.fromEntries( Object.entries(convexValidators).map(([k, v]) => [k, convexToZod(v)]), ) as { [k in keyof C]: ZodValidatorFromConvex }; } // #endregion // #region Utils /** * Zod helper for adding Convex system fields to a record to return. * * ```ts * withSystemFields("users", { * name: z.string(), * }) * // → { * // name: z.string(), * // _id: zid("users"), * // _creationTime: z.number(), * // } * ``` * * @param tableName - The table where records are from, i.e. Doc * @param zObject - Validators for the user-defined fields on the document. * @returns Zod shape for use with `z.object(shape)` that includes system fields. */ export function withSystemFields< Table extends string, T extends { [key: string]: zCore.$ZodType }, >(tableName: Table, zObject: T) { return { ...zObject, _id: zid(tableName), _creationTime: z.number() }; } // #endregion // #region Implementation: Convex function definition with Zod /** * A builder that customizes a Convex function, whether or not it validates * arguments. If the customization requires arguments, however, the resulting * builder will require argument validation too. */ export type CustomBuilder< FuncType extends "query" | "mutation" | "action", CustomArgsValidator extends PropertyValidators, CustomCtx extends Record, CustomMadeArgs extends Record, InputCtx, Visibility extends FunctionVisibility, ExtraArgs extends Record, > = { < ArgsValidator extends ZodFields | zCore.$ZodObject | void, ReturnsZodValidator extends zCore.$ZodType | ZodFields | void = void, ReturnValue extends ReturnValueInput = any, // Note: this differs from customFunctions.ts b/c we don't need to track // the exact args to match the standard builder types. For Zod we don't // try to ever pass a custom function as a builder to another custom // function, so we can be looser here. >( func: | ({ /** * Specify the arguments to the function as a Zod validator. */ args?: ArgsValidator; handler: ( ctx: Overwrite, ...args: ArgsForHandlerType< ArgsOutput, CustomMadeArgs > ) => ReturnValue; /** * Validates the value returned by the function. * Note: you can't pass an object directly without wrapping it * in `z.object()`. */ returns?: ReturnsZodValidator; /** * If true, the function will not be validated by Convex, * in case you're seeing performance issues with validating twice. */ skipConvexValidation?: boolean; } & { [key in keyof ExtraArgs as key extends | "args" | "handler" | "skipConvexValidation" | "returns" ? never : key]: ExtraArgs[key]; }) | { ( ctx: Overwrite, ...args: ArgsForHandlerType< ArgsOutput, CustomMadeArgs > ): ReturnValue; }, ): Registration< FuncType, Visibility, ArgsArrayToObject< CustomArgsValidator extends Record ? ArgsInput : ArgsInput extends [infer A] ? [Expand>] : [ObjectType] >, ReturnsZodValidator extends void ? ReturnValue : ReturnValueOutput >; }; function customFnBuilder( builder: (args: any) => any, customization: Customization, ) { // Most of the code in here is identical to customFnBuilder in zod3.ts. // If making changes, please keep zod3.ts in sync. // Looking forward to when input / args / ... are optional const customInput: Customization["input"] = customization.input ?? NoOp.input; const inputArgs = customization.args ?? NoOp.args; return function customBuilder(fn: any): any { const { args, handler = fn, skipConvexValidation = false, returns: maybeObject, ...extra } = fn; const returns = maybeObject && !(maybeObject instanceof zCore.$ZodType) ? z.object(maybeObject) : maybeObject; const returnValidator = returns && !skipConvexValidation ? { returns: zodOutputToConvex(returns) } : null; if (args) { let argsValidator = args; if (argsValidator instanceof zCore.$ZodType) { if (argsValidator instanceof zCore.$ZodObject) { argsValidator = argsValidator._zod.def.shape; } else { throw new Error( "Unsupported zod type as args validator: " + argsValidator.constructor.name, ); } } const convexValidator = zodToConvexFields(argsValidator); return builder({ args: skipConvexValidation ? undefined : addFieldsToValidator(convexValidator, inputArgs), ...returnValidator, handler: async (ctx: any, allArgs: any) => { const added = await customInput( ctx, pick(allArgs, Object.keys(inputArgs)) as any, extra, ); const rawArgs = pick(allArgs, Object.keys(argsValidator)); const parsed = await z.object(argsValidator).safeParseAsync(rawArgs); if (!parsed.success) { throw new ConvexError({ ZodError: JSON.parse( JSON.stringify(parsed.error.issues, null, 2), ) as Value[], }); } const args = parsed.data; const finalCtx = { ...ctx, ...added.ctx }; const finalArgs = { ...args, ...added.args }; const ret = await handler(finalCtx, finalArgs); // We don't catch the error here. It's a developer error and we // don't want to risk exposing the unexpected value to the client. const result = returns ? await returns.parseAsync(ret === undefined ? null : ret) : ret; if (added.onSuccess) { await added.onSuccess({ ctx, args, result }); } return result; }, }); } if (skipConvexValidation && Object.keys(inputArgs).length > 0) { throw new Error( "If you're using a custom function with arguments for the input " + "customization, you cannot skip convex validation.", ); } return builder({ ...returnValidator, handler: async (ctx: any, args: any) => { const added = await customInput(ctx, args, extra); const finalCtx = { ...ctx, ...added.ctx }; const finalArgs = { ...args, ...added.args }; const ret = await handler(finalCtx, finalArgs); // We don't catch the error here. It's a developer error and we // don't want to risk exposing the unexpected value to the client. const result = returns ? await returns.parseAsync(ret === undefined ? null : ret) : ret; if (added.onSuccess) { await added.onSuccess({ ctx, args, result }); } return result; }, }); }; } type ArgsForHandlerType< OneOrZeroArgs extends [] | [Record], CustomMadeArgs extends Record, > = CustomMadeArgs extends Record ? OneOrZeroArgs : OneOrZeroArgs extends [infer A] ? [Expand] : [CustomMadeArgs]; // Copied from convex/src/server/api.ts since they aren't exported type NullToUndefinedOrNull = T extends null ? T | undefined | void : T; type Returns = Promise> | NullToUndefinedOrNull; // The return value before it's been validated: returned by the handler type ReturnValueInput< ReturnsValidator extends zCore.$ZodType | ZodFields | void, > = [ReturnsValidator] extends [zCore.$ZodType] ? Returns> : [ReturnsValidator] extends [ZodFields] ? Returns>> : any; // The return value after it's been validated: returned to the client type ReturnValueOutput< ReturnsValidator extends zCore.$ZodType | ZodFields | void, > = [ReturnsValidator] extends [zCore.$ZodType] ? Returns> : [ReturnsValidator] extends [ZodFields] ? Returns>> : any; // The args before they've been validated: passed from the client type ArgsInput | void> = [ArgsValidator] extends [zCore.$ZodObject] ? [zCore.input] : ArgsValidator extends Record ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type [{}] : [ArgsValidator] extends [Record] ? [zCore.input>] : OneArgArray; // The args after they've been validated: passed to the handler type ArgsOutput< ArgsValidator extends ZodFields | zCore.$ZodObject | void, > = [ArgsValidator] extends [zCore.$ZodObject] ? [zCore.output] : [ArgsValidator] extends [ZodFields] ? [zCore.output>] : OneArgArray; type Overwrite = Omit & U; type OneArgArray = [ArgsObject]; // #endregion // #region Implementation: Zod → Convex /** * Return type of {@link zodToConvex}. */ export type ConvexValidatorFromZod< Z extends zCore.$ZodType, IsOptional extends "required" | "optional", > = // `unknown` / `any`: we can’t infer a precise return type at compile time IsUnknownOrAny extends true ? GenericValidator : // z.default() Z extends zCore.$ZodDefault // input: Treat like optional ? VOptional> : // z.pipe() Z extends zCore.$ZodPipe< infer Input extends zCore.$ZodType, infer _Output extends zCore.$ZodType > ? ConvexValidatorFromZod : // All other schemas have the same input/output types ConvexValidatorFromZodCommon; /** * Return type of {@link zodOutputToConvex}. */ export type ConvexValidatorFromZodOutput< Z extends zCore.$ZodType, IsOptional extends "required" | "optional", > = // `unknown` / `any`: we can't infer a precise return type at compile time IsUnknownOrAny extends true ? GenericValidator : // z.default() Z extends zCore.$ZodDefault // output: always there ? VRequired> : // z.pipe() - use output schema for zodOutputToConvex Z extends zCore.$ZodPipe< infer _Input extends zCore.$ZodType, infer Output extends zCore.$ZodType > ? ConvexValidatorFromZodOutput : // z.optional() - handle here to use output types consistently Z extends zCore.$ZodOptional ? VOptional> : // z.nullable() - handle here to use output types consistently Z extends zCore.$ZodNullable ? ConvexValidatorFromZodOutput extends Validator< any, "optional", any > ? VUnion< | ConvexValidatorFromZodOutput["type"] | null | undefined, [ VRequired>, VNull, ], "optional", ConvexValidatorFromZodOutput["fieldPaths"] > : VUnion< | ConvexValidatorFromZodOutput["type"] | null, [ VRequired>, VNull, ], IsOptional, ConvexValidatorFromZodOutput["fieldPaths"] > : // All other schemas have the same input/output types ConvexValidatorFromZodCommon; // Conversions used for both zodToConvex and zodOutputToConvex type ConvexValidatorFromZodCommon< Z extends zCore.$ZodType, IsOptional extends "required" | "optional", > = // Basic types Z extends Zid ? VId> : Z extends zCore.$ZodString ? VString, IsOptional> : Z extends zCore.$ZodNumber ? VFloat64, IsOptional> : Z extends zCore.$ZodNaN ? VFloat64, IsOptional> : Z extends zCore.$ZodBigInt ? VInt64, IsOptional> : Z extends zCore.$ZodBoolean ? VBoolean, IsOptional> : Z extends zCore.$ZodNull ? VNull, IsOptional> : Z extends zCore.$ZodUnknown ? VAny : Z extends zCore.$ZodAny ? VAny, "required"> : // z.array() Z extends zCore.$ZodArray< infer Inner extends zCore.$ZodType > ? ConvexValidatorFromZod< Inner, "required" > extends GenericValidator ? VArray< ConvexValidatorFromZod["type"][], ConvexValidatorFromZod, IsOptional > : never : // z.object() Z extends zCore.$ZodObject< infer Fields extends Readonly > ? VObject< zCore.infer, ConvexObjectFromZodShape, IsOptional > : // z.never() (→ z.union() with no elements) Z extends zCore.$ZodNever ? VUnion : // z.union() Z extends zCore.$ZodUnion< infer T extends readonly zCore.$ZodType[] > ? ConvexUnionValidatorFromZod : // z.tuple() Z extends zCore.$ZodTuple< infer Inner extends readonly zCore.$ZodType[], infer Rest extends null | zCore.$ZodType > ? VArray< null extends Rest ? Array< ConvexValidatorFromZod< Inner[number], "required" >["type"] > : Array< | ConvexValidatorFromZod< Inner[number], "required" >["type"] | zCore.infer >, null extends Rest ? ConvexUnionValidatorFromZod : ConvexUnionValidatorFromZod< [ ...Inner, Rest extends zCore.$ZodType // won’t be null here ? Rest : never, ] >, IsOptional > : // z.literal() Z extends zCore.$ZodLiteral< infer Literal extends zCore.util.Literal > ? ConvexLiteralFromZod : // z.enum() Z extends zCore.$ZodEnum< infer EnumContents extends zCore.util.EnumLike > ? VUnion< zCore.infer, keyof EnumContents extends string ? { [K in keyof EnumContents]: VLiteral< EnumContents[K], "required" >; }[keyof EnumContents][] : never, IsOptional > : // z.optional() Z extends zCore.$ZodOptional< infer Inner extends zCore.$ZodType > ? VOptional< ConvexValidatorFromZod< Inner, "optional" > > : // z.nonoptional() Z extends zCore.$ZodNonOptional< infer Inner extends zCore.$ZodType > ? VRequired< ConvexValidatorFromZod< Inner, "required" > > : // z.nullable() Z extends zCore.$ZodNullable< infer Inner extends zCore.$ZodType > ? ConvexValidatorFromZod< Inner, IsOptional > extends Validator< any, "optional", any > ? VUnion< | ConvexValidatorFromZod< Inner, IsOptional >["type"] | null | undefined, [ VRequired< ConvexValidatorFromZod< Inner, IsOptional > >, VNull, ], "optional", ConvexValidatorFromZod< Inner, IsOptional >["fieldPaths"] > : VUnion< | ConvexValidatorFromZod< Inner, IsOptional >["type"] | null, [ VRequired< ConvexValidatorFromZod< Inner, IsOptional > >, VNull, ], IsOptional, ConvexValidatorFromZod< Inner, IsOptional >["fieldPaths"] > : // z.brand() Z extends zCore.$ZodBranded< infer Inner extends zCore.$ZodType, infer Brand > ? Inner extends zCore.$ZodString ? VString< string & zCore.$brand, IsOptional > : Inner extends zCore.$ZodNumber ? VFloat64< number & zCore.$brand, IsOptional > : Inner extends zCore.$ZodBigInt ? VInt64< bigint & zCore.$brand, IsOptional > : Inner extends zCore.$ZodObject< infer Fields extends Readonly > ? VObject< zCore.infer & zCore.$brand, ConvexObjectFromZodShape, IsOptional > : ConvexValidatorFromZod< Inner, IsOptional > : // z.record() Z extends zCore.$ZodRecord< infer Key extends zCore.$ZodRecordKey, infer Value extends zCore.$ZodType > ? ConvexValidatorFromZodRecord< Key, Value, IsOptional > : // z.readonly() Z extends zCore.$ZodReadonly< infer Inner extends zCore.$ZodType > ? ConvexValidatorFromZod< Inner, IsOptional > : // z.lazy() Z extends zCore.$ZodLazy< infer Inner extends zCore.$ZodType > ? ConvexValidatorFromZod< Inner, IsOptional > : // z.templateLiteral() Z extends zCore.$ZodTemplateLiteral< infer Template extends string > ? VString< Template, IsOptional > : // z.catch() Z extends zCore.$ZodCatch< infer T extends zCore.$ZodType > ? ConvexValidatorFromZod< T, IsOptional > : // z.transform() Z extends zCore.$ZodTransform< any, any > ? VAny // No runtime info about types so we use v.any() : // z.custom() Z extends zCore.$ZodCustom ? VAny : // z.intersection() // We could do some more advanced logic here where we compute // the Convex validator that results from the intersection. // For now, we simply use v.any() Z extends zCore.$ZodIntersection ? VAny< any, "required" > : // unencodable types IsConvexUnencodableType extends true ? never : // Other validators: we don’t return VAny // because it might be a type that is // recognized at runtime but is not // recognized at typecheck time // (e.g. zCore.$ZodType) GenericValidator; type ConvexUnionValidatorFromZod = VUnion< ConvexValidatorFromZod["type"], T extends readonly [ infer Head extends zCore.$ZodType, ...infer Tail extends zCore.$ZodType[], ] ? [ VRequired>, ...ConvexUnionValidatorFromZodMembers, ] : T extends readonly [] ? [] : Validator[], "required", ConvexValidatorFromZod["fieldPaths"] >; type ConvexUnionValidatorFromZodMembers = T extends readonly [ infer Head extends zCore.$ZodType, ...infer Tail extends zCore.$ZodType[], ] ? [ VRequired>, ...ConvexUnionValidatorFromZodMembers, ] : T extends readonly [] ? [] : Validator[]; type ConvexObjectFromZodShape> = Fields extends infer F // dark magic to get the TypeScript compiler happy about circular types ? { [K in keyof F]: F[K] extends zCore.$ZodType ? ConvexValidatorFromZod : Validator; } : never; type ConvexObjectValidatorFromRecord< Key extends string, Value extends zCore.$ZodType, IsOptional extends "required" | "optional", IsPartial extends "partial" | "full", > = VObject< IsPartial extends "partial" ? { [K in Key]?: zCore.infer; } : MakeUndefinedPropertiesOptional<{ [K in Key]: zCore.infer; }>, IsPartial extends "partial" ? { [K in Key]: VOptional>; } : { [K in Key]: ConvexValidatorFromZod; }, IsOptional >; // MakeUndefinedPropertiesOptional<{ a: string | undefined; b: string }> = { a?: string | undefined; b: string } // ^ type MakeUndefinedPropertiesOptional = Expand< { [K in keyof Obj as undefined extends Obj[K] ? never : K]: Obj[K]; } & { [K in keyof Obj as undefined extends Obj[K] ? K : never]?: Obj[K]; } >; type ConvexValidatorFromZodRecord< Key extends zCore.$ZodRecordKey, Value extends zCore.$ZodType, IsOptional extends "required" | "optional", > = // key = v.string() / v.id() / v.union(v.id()) Key extends | zCore.$ZodString | Zid | zCore.$ZodUnion[]> ? VRecord< Record, NotUndefined>>, VRequired>, VRequired>, IsOptional > : // key = v.literal() Key extends zCore.$ZodLiteral ? ConvexObjectValidatorFromRecord< Literal, Value, IsOptional, Key extends zCore.$partial ? "partial" : "full" > : // key = v.union(v.literal()) Key extends zCore.$ZodUnion< infer Literals extends readonly zCore.$ZodLiteral[] > ? ConvexObjectValidatorFromRecord< zCore.infer extends string ? zCore.infer : never, Value, IsOptional, Key extends zCore.$partial ? "partial" : "full" > : // key = v.any() / otehr VRecord< Record>>, VString, VRequired>, IsOptional >; type IsConvexUnencodableType = Z extends | zCore.$ZodDate | zCore.$ZodSymbol | zCore.$ZodMap | zCore.$ZodSet | zCore.$ZodPromise | zCore.$ZodFile | zCore.$ZodFunction // undefined is not a valid Convex value. Consider using v.optional() or v.null() instead | zCore.$ZodUndefined | zCore.$ZodVoid ? true : false; type IsUnion = T extends unknown ? [U] extends [T] ? false : true : false; type ConvexLiteralFromZod< Literal extends zCore.util.Literal, IsOptional extends "required" | "optional", > = undefined extends Literal // undefined is not a valid Convex valvue ? never : // z.literal(null) → v.null() [Literal] extends [null] ? VNull : // z.literal([…]) (multiple values) IsUnion extends true ? VUnion< Literal, Array< // `extends unknown` forces TypeScript to map over each member of the union Literal extends unknown ? ConvexLiteralFromZod : never >, IsOptional, never > : VLiteral; type IsUnknownOrAny = // any? 0 extends 1 & T ? true : // unknown? unknown extends T ? true : false; function zodToConvexCommon( validator: Z, toConvex: (x: zCore.$ZodType) => GenericValidator, ): GenericValidator { if (validator instanceof zCore.$ZodString) { return v.string(); } if ( validator instanceof zCore.$ZodNumber || validator instanceof zCore.$ZodNaN ) { return v.number(); } if (validator instanceof zCore.$ZodBigInt) { return v.int64(); } if (validator instanceof zCore.$ZodBoolean) { return v.boolean(); } if (validator instanceof zCore.$ZodNull) { return v.null(); } if ( validator instanceof zCore.$ZodAny || validator instanceof zCore.$ZodUnknown ) { return v.any(); } if (validator instanceof zCore.$ZodArray) { const inner = toConvex(validator._zod.def.element); if (inner.isOptional === "optional") { throw new Error("Arrays of optional values are not supported"); } return v.array(inner); } if (validator instanceof zCore.$ZodObject) { return v.object( Object.fromEntries( Object.entries(validator._zod.def.shape).map(([k, v]) => [ k, toConvex(v), ]), ), ); } if (validator instanceof zCore.$ZodUnion) { return v.union(...validator._zod.def.options.map(toConvex)); } if (validator instanceof zCore.$ZodNever) { return v.union(); } if (validator instanceof zCore.$ZodTuple) { const { items, rest } = validator._zod.def; return v.array( v.union( ...[ ...items, // + rest if set ...(rest !== null ? [rest] : []), ].map(toConvex), ), ); } if (validator instanceof zCore.$ZodLiteral) { const { values } = validator._zod.def; if (values.length === 1) { return convexToZodLiteral(values[0]); } return v.union(...values.map(convexToZodLiteral)); } if (validator instanceof zCore.$ZodEnum) { return v.union( ...Object.entries(validator._zod.def.entries) .filter(([key, value]) => key === value || isNaN(Number(key))) .map(([_key, value]) => v.literal(value)), ); } if (validator instanceof zCore.$ZodOptional) { return v.optional(toConvex(validator._zod.def.innerType)); } if (validator instanceof zCore.$ZodNonOptional) { return vRequired(toConvex(validator._zod.def.innerType)); } if (validator instanceof zCore.$ZodNullable) { const inner = toConvex(validator._zod.def.innerType); // Invert z.optional().nullable() → v.optional(v.nullable()) if (inner.isOptional === "optional") { return v.optional(v.union(vRequired(inner), v.null())); } return v.union(inner, v.null()); } if (validator instanceof zCore.$ZodRecord) { const { keyType, valueType } = validator._zod.def; const isPartial = keyType._zod.values === undefined; // Convert value type, stripping optional const valueValidator = toConvex(valueType); // Convert key type const keyValidator = toConvex(keyType); // key = string literals? // If so, not supported by v.record() → use v.object() instead const stringLiterals = extractStringLiterals(keyValidator); if (stringLiterals !== null) { const fieldValue = isPartial || valueValidator.isOptional === "optional" ? v.optional(valueValidator) : vRequired(valueValidator); const fields: Record = {}; for (const literal of stringLiterals) { fields[literal] = fieldValue; } return v.object(fields); } return v.record( isValidRecordKey(keyValidator) ? keyValidator : v.string(), vRequired(valueValidator), ); } if (validator instanceof zCore.$ZodReadonly) { return toConvex(validator._zod.def.innerType); } if (validator instanceof zCore.$ZodLazy) { return toConvex(validator._zod.def.getter()); } if (validator instanceof zCore.$ZodTemplateLiteral) { return v.string(); } if (validator instanceof zCore.$ZodCustom) { // Check for zid (Convex ID) validators inside the $ZodCustom branch // since zid() produces a $ZodCustom. Keeping this check here (rather // than at the top of the function) ensures type-specific instanceof // handlers always take priority. const idTableName = _zidRegistry.get(validator); if ( idTableName !== undefined && typeof idTableName.tableName === "string" ) { return v.id(idTableName.tableName); } return v.any(); } if (validator instanceof zCore.$ZodIntersection) { return v.any(); } if (validator instanceof zCore.$ZodCatch) { return toConvex(validator._zod.def.innerType); } if ( validator instanceof zCore.$ZodDate || validator instanceof zCore.$ZodSymbol || validator instanceof zCore.$ZodMap || validator instanceof zCore.$ZodSet || validator instanceof zCore.$ZodPromise || validator instanceof zCore.$ZodFile || validator instanceof zCore.$ZodFunction || validator instanceof zCore.$ZodVoid || validator instanceof zCore.$ZodUndefined ) { throw new Error( `Validator ${validator.constructor.name} is not supported in Convex`, ); } // Unsupported type return v.any(); } function convexToZodLiteral(literal: zCore.util.Literal): GenericValidator { if (literal === undefined) { throw new Error("undefined is not a valid Convex value"); } if (literal === null) { return v.null(); } return v.literal(literal); } function extractStringLiterals(validator: GenericValidator): string[] | null { if (validator.kind === "literal") { const literalValidator = validator as VLiteral; if (typeof literalValidator.value === "string") { return [literalValidator.value]; } return null; } if (validator.kind === "union") { const unionValidator = validator as VUnion; const literals: string[] = []; for (const member of unionValidator.members) { const memberLiterals = extractStringLiterals(member); if (memberLiterals === null) { return null; // Not all members are string literals } literals.push(...memberLiterals); } return literals; } return null; // Not a literal or union of literals } function isValidRecordKey(validator: GenericValidator): boolean { if (validator.kind === "string" || validator.kind === "id") { return true; } if (validator.kind === "union") { const unionValidator = validator as VUnion; return unionValidator.members.every(isValidRecordKey); } return false; } // #endregion // #region Implementation: Convex → Zod /** * Better type conversion from a Convex validator to a Zod validator * where the output is not a generic ZodType but it's more specific. * * This allows you to use methods specific to the Zod type (e.g. `.email()` for `z.ZodString`). * * ```ts * ZodValidatorFromConvex // → z.ZodString * ``` */ export type ZodValidatorFromConvex = V extends Validator ? z.ZodOptional>> : ZodFromValidatorBase; export type ZodFromValidatorBase = V extends VId ? Zid>> : V extends VString ? BrandIfBranded : V extends VFloat64 ? BrandIfBranded : V extends VInt64 ? z.ZodBigInt : V extends VBoolean ? z.ZodBoolean : V extends VNull ? z.ZodNull : V extends VArray ? Element extends VArray // This check is used to avoid TypeScript complaining about infinite type instantiation ? z.ZodArray : z.ZodArray> : V extends VObject< any, infer Fields extends Record > ? z.ZodObject, zCore.$strict> : V extends VBytes ? never : V extends VLiteral< infer T extends zCore.util.Literal, OptionalProperty > ? z.ZodLiteral> : V extends VRecord< any, infer Key, infer Value, OptionalProperty, any > ? z.ZodRecord< ZodFromStringValidator, ZodFromValidatorBase > : // Union: must handle separately cases for 0/1/2+ elements // instead of simply writing it as // V extends VUnion // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // ? z.ZodUnion<{ [k in keyof Elements]: ZodValidatorFromConvex }> // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // because the TypeScript compiler would complain about infinite type instantiation otherwise :( V extends VUnion ? z.ZodNever : V extends VUnion< any, [infer I extends GenericValidator], OptionalProperty, any > ? ZodValidatorFromConvex : V extends VUnion< any, [ infer A extends GenericValidator, ...infer Rest extends GenericValidator[], ], OptionalProperty, any > ? z.ZodUnion< readonly [ ZodValidatorFromConvex, ...{ [K in keyof Rest]: ZodValidatorFromConvex< Rest[K] >; }, ] > : // Fallback for unions whose members are a // generic array (e.g. `VLiteral[]`) rather // than a tuple. That happens when callers // spread an array into `v.union(...)` and // TypeScript cannot recover the tuple shape; // the order is unknown, so we widen to a // generic-array union of the element // validator. The `[GenericValidator] extends [E]` // guard avoids infinite recursion when `E` is the // maximally-generic validator type — that // happens during recursive type evaluation of // `convexToZod` itself. V extends VUnion< any, (infer E extends GenericValidator)[], OptionalProperty, any > ? [GenericValidator] extends [E] ? z.ZodUnion : z.ZodUnion< readonly ZodValidatorFromConvex[] > : V extends VAny ? z.ZodAny : never; type BrandIfBranded = InnerType extends zCore.$brand ? zCore.$ZodBranded : Validator; type StringValidator = Validator; type ZodFromStringValidator = V extends VId> ? Zid : V extends VString ? BrandIfBranded : // Literals V extends VLiteral ? z.ZodLiteral : // Union (see below) V extends VUnion ? z.ZodNever : V extends VUnion ? ZodFromStringValidator : V extends VUnion< any, [ infer A extends GenericValidator, ...infer Rest extends GenericValidator[], ], any, any > ? z.ZodUnion< readonly [ ZodFromStringValidator, ...{ [K in keyof Rest]: ZodFromStringValidator; }, ] > : never; type ZodShapeFromConvexObject> = Fields extends infer F // dark magic to get the TypeScript compiler happy about circular types ? { [K in keyof F]: F[K] extends GenericValidator ? ZodValidatorFromConvex : never; } : never; // #endregion // #region Implementation: zid /** Stores the table names for each `Zid` instance that is created. */ const _zidRegistry = zCore.registry<{ tableName: string }>(); // #endregion // #region Implementation: Utilities type NotUndefined = Exclude; type TableNameFromType = T extends GenericId ? TableName : string; // #endregion