convex-helpers
Version:
A collection of useful code to complement the official convex package.
1,942 lines • 75.9 kB
text/typescript
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<string, any>,
CustomMadeArgs extends Record<string, any>,
Visibility extends FunctionVisibility,
DataModel extends GenericDataModel,
ExtraArgs extends Record<string, any> = object,
>(
query: QueryBuilder<DataModel, Visibility>,
customization: Customization<
GenericQueryCtx<DataModel>,
CustomArgsValidator,
CustomCtx,
CustomMadeArgs,
ExtraArgs
>,
) {
return customFnBuilder(query, customization) as CustomBuilder<
"query",
CustomArgsValidator,
CustomCtx,
CustomMadeArgs,
GenericQueryCtx<DataModel>,
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<string, any>,
CustomMadeArgs extends Record<string, any>,
Visibility extends FunctionVisibility,
DataModel extends GenericDataModel,
ExtraArgs extends Record<string, any> = object,
>(
mutation: MutationBuilder<DataModel, Visibility>,
customization: Customization<
GenericMutationCtx<DataModel>,
CustomArgsValidator,
CustomCtx,
CustomMadeArgs,
ExtraArgs
>,
) {
return customFnBuilder(mutation, customization) as CustomBuilder<
"mutation",
CustomArgsValidator,
CustomCtx,
CustomMadeArgs,
GenericMutationCtx<DataModel>,
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<string, any>,
CustomMadeArgs extends Record<string, any>,
Visibility extends FunctionVisibility,
DataModel extends GenericDataModel,
ExtraArgs extends Record<string, any> = object,
>(
action: ActionBuilder<DataModel, Visibility>,
customization: Customization<
GenericActionCtx<DataModel>,
CustomArgsValidator,
CustomCtx,
CustomMadeArgs,
ExtraArgs
>,
) {
return customFnBuilder(action, customization) as CustomBuilder<
"action",
CustomArgsValidator,
CustomCtx,
CustomMadeArgs,
GenericActionCtx<DataModel>,
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<tableName>`
* @returns A Zod schema representing a Convex `Id`
*/
export const zid = <
DataModel extends GenericDataModel,
TableName extends
TableNamesInDataModel<DataModel> = TableNamesInDataModel<DataModel>,
>(
tableName: TableName,
): Zid<TableName> => {
const result = z.custom<GenericId<TableName>>(
(val) => typeof val === "string",
);
_zidRegistry.add(result, { tableName });
return result;
};
/** The type of Convex validators in Zod */
export type Zid<TableName extends string> = z.ZodCustom<GenericId<TableName>> &
zCore.$ZodRecordKey;
/**
* Useful to get the input context type for a custom function using Zod.
*/
export type ZCustomCtx<Builder> =
Builder extends CustomBuilder<
any,
any,
infer CustomCtx,
any,
infer InputCtx,
any,
any
>
? Overwrite<InputCtx, CustomCtx>
: 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<Z extends zCore.$ZodType>(
validator: Z,
): ConvexValidatorFromZod<Z, "required"> {
const visited = new WeakSet<zCore.$ZodType>();
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<Z extends zCore.$ZodType>(
validator: Z,
): ConvexValidatorFromZodOutput<Z, "required"> {
const visited = new WeakSet<zCore.$ZodType>();
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<string, zCore.$ZodType>;
/**
* 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 extends ZodFields>(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<Fields[k], "required">
: 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 extends ZodFields>(
fields: Fields,
) {
return Object.fromEntries(
Object.entries(fields).map(([k, v]) => [k, zodOutputToConvex(v)]),
) as {
[k in keyof Fields]: ConvexValidatorFromZodOutput<Fields[k], "required">;
};
}
// #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<V extends GenericValidator>(
convexValidator: V,
): ZodValidatorFromConvex<V> {
const isOptional = (convexValidator as any).isOptional === "optional";
let zodValidator: zCore.$ZodType;
const { kind } = convexValidator;
switch (kind) {
case "id":
convexValidator satisfies VId<any>;
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<any, any>;
zodValidator = z.array(convexToZod(convexValidator.element));
break;
}
case "object": {
convexValidator satisfies VObject<any, any>;
zodValidator = z.object(convexToZodFields(convexValidator.fields));
break;
}
case "union": {
convexValidator satisfies VUnion<any, any, any, any>;
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<any>;
zodValidator = z.literal(literalValidator.value);
break;
}
case "record": {
convexValidator satisfies VRecord<any, any, any, any, any>;
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<V>)
: (zodValidator as ZodValidatorFromConvex<V>);
}
/**
* 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<C extends PropertyValidators>(
convexValidators: C,
) {
return Object.fromEntries(
Object.entries(convexValidators).map(([k, v]) => [k, convexToZod(v)]),
) as { [k in keyof C]: ZodValidatorFromConvex<C[k]> };
}
// #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<tableName>
* @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<string, any>,
CustomMadeArgs extends Record<string, any>,
InputCtx,
Visibility extends FunctionVisibility,
ExtraArgs extends Record<string, any>,
> = {
<
ArgsValidator extends ZodFields | zCore.$ZodObject<any> | void,
ReturnsZodValidator extends zCore.$ZodType | ZodFields | void = void,
ReturnValue extends ReturnValueInput<ReturnsZodValidator> = 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<InputCtx, CustomCtx>,
...args: ArgsForHandlerType<
ArgsOutput<ArgsValidator>,
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<InputCtx, CustomCtx>,
...args: ArgsForHandlerType<
ArgsOutput<ArgsValidator>,
CustomMadeArgs
>
): ReturnValue;
},
): Registration<
FuncType,
Visibility,
ArgsArrayToObject<
CustomArgsValidator extends Record<string, never>
? ArgsInput<ArgsValidator>
: ArgsInput<ArgsValidator> extends [infer A]
? [Expand<A & ObjectType<CustomArgsValidator>>]
: [ObjectType<CustomArgsValidator>]
>,
ReturnsZodValidator extends void
? ReturnValue
: ReturnValueOutput<ReturnsZodValidator>
>;
};
function customFnBuilder(
builder: (args: any) => any,
customization: Customization<any, any, any, any, any>,
) {
// 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<any, any, any, any, any>["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<string, any>],
CustomMadeArgs extends Record<string, any>,
> =
CustomMadeArgs extends Record<string, never>
? OneOrZeroArgs
: OneOrZeroArgs extends [infer A]
? [Expand<A & CustomMadeArgs>]
: [CustomMadeArgs];
// Copied from convex/src/server/api.ts since they aren't exported
type NullToUndefinedOrNull<T> = T extends null ? T | undefined | void : T;
type Returns<T> = Promise<NullToUndefinedOrNull<T>> | NullToUndefinedOrNull<T>;
// 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<zCore.input<ReturnsValidator>>
: [ReturnsValidator] extends [ZodFields]
? Returns<zCore.input<zCore.$ZodObject<ReturnsValidator>>>
: 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<zCore.output<ReturnsValidator>>
: [ReturnsValidator] extends [ZodFields]
? Returns<zCore.output<zCore.$ZodObject<ReturnsValidator, zCore.$strict>>>
: any;
// The args before they've been validated: passed from the client
type ArgsInput<ArgsValidator extends ZodFields | zCore.$ZodObject<any> | void> =
[ArgsValidator] extends [zCore.$ZodObject<any>]
? [zCore.input<ArgsValidator>]
: ArgsValidator extends Record<string, never>
? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
[{}]
: [ArgsValidator] extends [Record<string, z.ZodTypeAny>]
? [zCore.input<zCore.$ZodObject<ArgsValidator, zCore.$strict>>]
: OneArgArray;
// The args after they've been validated: passed to the handler
type ArgsOutput<
ArgsValidator extends ZodFields | zCore.$ZodObject<any> | void,
> = [ArgsValidator] extends [zCore.$ZodObject<any>]
? [zCore.output<ArgsValidator>]
: [ArgsValidator] extends [ZodFields]
? [zCore.output<zCore.$ZodObject<ArgsValidator, zCore.$strict>>]
: OneArgArray;
type Overwrite<T, U> = Omit<T, keyof U> & U;
type OneArgArray<ArgsObject extends DefaultFunctionArgs = DefaultFunctionArgs> =
[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<Z> extends true
? GenericValidator
: // z.default()
Z extends zCore.$ZodDefault<infer Inner extends zCore.$ZodType> // input: Treat like optional
? VOptional<ConvexValidatorFromZod<Inner, "optional">>
: // z.pipe()
Z extends zCore.$ZodPipe<
infer Input extends zCore.$ZodType,
infer _Output extends zCore.$ZodType
>
? ConvexValidatorFromZod<Input, IsOptional>
: // All other schemas have the same input/output types
ConvexValidatorFromZodCommon<Z, IsOptional>;
/**
* 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<Z> extends true
? GenericValidator
: // z.default()
Z extends zCore.$ZodDefault<infer Inner extends zCore.$ZodType> // output: always there
? VRequired<ConvexValidatorFromZodOutput<Inner, "required">>
: // z.pipe() - use output schema for zodOutputToConvex
Z extends zCore.$ZodPipe<
infer _Input extends zCore.$ZodType,
infer Output extends zCore.$ZodType
>
? ConvexValidatorFromZodOutput<Output, IsOptional>
: // z.optional() - handle here to use output types consistently
Z extends zCore.$ZodOptional<infer Inner extends zCore.$ZodType>
? VOptional<ConvexValidatorFromZodOutput<Inner, "optional">>
: // z.nullable() - handle here to use output types consistently
Z extends zCore.$ZodNullable<infer Inner extends zCore.$ZodType>
? ConvexValidatorFromZodOutput<Inner, IsOptional> extends Validator<
any,
"optional",
any
>
? VUnion<
| ConvexValidatorFromZodOutput<Inner, IsOptional>["type"]
| null
| undefined,
[
VRequired<ConvexValidatorFromZodOutput<Inner, IsOptional>>,
VNull,
],
"optional",
ConvexValidatorFromZodOutput<Inner, IsOptional>["fieldPaths"]
>
: VUnion<
| ConvexValidatorFromZodOutput<Inner, IsOptional>["type"]
| null,
[
VRequired<ConvexValidatorFromZodOutput<Inner, IsOptional>>,
VNull,
],
IsOptional,
ConvexValidatorFromZodOutput<Inner, IsOptional>["fieldPaths"]
>
: // All other schemas have the same input/output types
ConvexValidatorFromZodCommon<Z, IsOptional>;
// Conversions used for both zodToConvex and zodOutputToConvex
type ConvexValidatorFromZodCommon<
Z extends zCore.$ZodType,
IsOptional extends "required" | "optional",
> = // Basic types
Z extends Zid<infer TableName>
? VId<GenericId<TableName>>
: Z extends zCore.$ZodString
? VString<zCore.infer<Z>, IsOptional>
: Z extends zCore.$ZodNumber
? VFloat64<zCore.infer<Z>, IsOptional>
: Z extends zCore.$ZodNaN
? VFloat64<zCore.infer<Z>, IsOptional>
: Z extends zCore.$ZodBigInt
? VInt64<zCore.infer<Z>, IsOptional>
: Z extends zCore.$ZodBoolean
? VBoolean<zCore.infer<Z>, IsOptional>
: Z extends zCore.$ZodNull
? VNull<zCore.infer<Z>, IsOptional>
: Z extends zCore.$ZodUnknown
? VAny<any, "required">
: Z extends zCore.$ZodAny
? VAny<zCore.infer<Z>, "required">
: // z.array()
Z extends zCore.$ZodArray<
infer Inner extends zCore.$ZodType
>
? ConvexValidatorFromZod<
Inner,
"required"
> extends GenericValidator
? VArray<
ConvexValidatorFromZod<Inner, "required">["type"][],
ConvexValidatorFromZod<Inner, "required">,
IsOptional
>
: never
: // z.object()
Z extends zCore.$ZodObject<
infer Fields extends Readonly<zCore.$ZodShape>
>
? VObject<
zCore.infer<Z>,
ConvexObjectFromZodShape<Fields>,
IsOptional
>
: // z.never() (→ z.union() with no elements)
Z extends zCore.$ZodNever
? VUnion<never, [], IsOptional, never>
: // z.union()
Z extends zCore.$ZodUnion<
infer T extends readonly zCore.$ZodType[]
>
? ConvexUnionValidatorFromZod<T>
: // 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<Rest>
>,
null extends Rest
? ConvexUnionValidatorFromZod<Inner>
: 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<Literal, IsOptional>
: // z.enum()
Z extends zCore.$ZodEnum<
infer EnumContents extends
zCore.util.EnumLike
>
? VUnion<
zCore.infer<Z>,
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<Brand>,
IsOptional
>
: Inner extends zCore.$ZodNumber
? VFloat64<
number & zCore.$brand<Brand>,
IsOptional
>
: Inner extends zCore.$ZodBigInt
? VInt64<
bigint &
zCore.$brand<Brand>,
IsOptional
>
: Inner extends zCore.$ZodObject<
infer Fields extends
Readonly<zCore.$ZodShape>
>
? VObject<
zCore.infer<Inner> &
zCore.$brand<Brand>,
ConvexObjectFromZodShape<Fields>,
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<any, "required"> // No runtime info about types so we use v.any()
: // z.custom()
Z extends zCore.$ZodCustom<any>
? VAny<any, "required">
: // 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<any>
? VAny<
any,
"required"
>
: // unencodable types
IsConvexUnencodableType<Z> 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<string>)
GenericValidator;
type ConvexUnionValidatorFromZod<T extends readonly zCore.$ZodType[]> = VUnion<
ConvexValidatorFromZod<T[number], "required">["type"],
T extends readonly [
infer Head extends zCore.$ZodType,
...infer Tail extends zCore.$ZodType[],
]
? [
VRequired<ConvexValidatorFromZod<Head, "required">>,
...ConvexUnionValidatorFromZodMembers<Tail>,
]
: T extends readonly []
? []
: Validator<any, "required", any>[],
"required",
ConvexValidatorFromZod<T[number], "required">["fieldPaths"]
>;
type ConvexUnionValidatorFromZodMembers<T extends readonly zCore.$ZodType[]> =
T extends readonly [
infer Head extends zCore.$ZodType,
...infer Tail extends zCore.$ZodType[],
]
? [
VRequired<ConvexValidatorFromZod<Head, "required">>,
...ConvexUnionValidatorFromZodMembers<Tail>,
]
: T extends readonly []
? []
: Validator<any, "required", any>[];
type ConvexObjectFromZodShape<Fields extends Readonly<zCore.$ZodShape>> =
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<F[K], "required">
: Validator<any, "required", any>;
}
: 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<Value>;
}
: MakeUndefinedPropertiesOptional<{
[K in Key]: zCore.infer<Value>;
}>,
IsPartial extends "partial"
? {
[K in Key]: VOptional<ConvexValidatorFromZod<Value, "required">>;
}
: {
[K in Key]: ConvexValidatorFromZod<Value, "required">;
},
IsOptional
>;
// MakeUndefinedPropertiesOptional<{ a: string | undefined; b: string }> = { a?: string | undefined; b: string }
// ^
type MakeUndefinedPropertiesOptional<Obj extends object> = 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<any>
| zCore.$ZodUnion<infer _Ids extends readonly Zid<any>[]>
? VRecord<
Record<zCore.infer<Key>, NotUndefined<zCore.infer<Value>>>,
VRequired<ConvexValidatorFromZod<Key, "required">>,
VRequired<ConvexValidatorFromZod<Value, "required">>,
IsOptional
>
: // key = v.literal()
Key extends zCore.$ZodLiteral<infer Literal extends string>
? 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<Literals[number]> extends string
? zCore.infer<Literals[number]>
: never,
Value,
IsOptional,
Key extends zCore.$partial ? "partial" : "full"
>
: // key = v.any() / otehr
VRecord<
Record<string, NotUndefined<zCore.infer<Value>>>,
VString<string, "required">,
VRequired<ConvexValidatorFromZod<Value, "required">>,
IsOptional
>;
type IsConvexUnencodableType<Z extends zCore.$ZodType> = 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, U extends T = T> = 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<null, IsOptional>
: // z.literal([…]) (multiple values)
IsUnion<Literal> extends true
? VUnion<
Literal,
Array<
// `extends unknown` forces TypeScript to map over each member of the union
Literal extends unknown
? ConvexLiteralFromZod<Literal, "required">
: never
>,
IsOptional,
never
>
: VLiteral<Literal, IsOptional>;
type IsUnknownOrAny<T> =
// any?
0 extends 1 & T
? true
: // unknown?
unknown extends T
? true
: false;
function zodToConvexCommon<Z extends zCore.$ZodType>(
validator: Z,
toConvex: (x: zCore.$ZodType) => GenericValidator,
): GenericValidator {
// Check for zid (Convex ID) validators
const idTableName = _zidRegistry.get(validator);
if (idTableName !== undefined) {
return v.id(idTableName.tableName);
}
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<string, GenericValidator> = {};
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 ||
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<any>;
if (typeof literalValidator.value === "string") {
return [literalValidator.value];
}
return null;
}
if (validator.kind === "union") {
const unionValidator = validator as VUnion<any, any, any, any>;
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<any, any, any, any>;
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<typeof v.string()> // → z.ZodString
* ```
*/
export type ZodValidatorFromConvex<V extends GenericValidator> =
V extends Validator<any, "optional", any>
? z.ZodOptional<ZodFromValidatorBase<VRequired<V>>>
: ZodFromValidatorBase<V>;
export type ZodFromValidatorBase<V extends GenericValidator> =
V extends VId<infer Type>
? Zid<TableNameFromType<NotUndefined<Type>>>
: V extends VString<infer T>
? BrandIfBranded<T, z.ZodString>
: V extends VFloat64<infer T>
? BrandIfBranded<T, z.ZodNumber>
: V extends VInt64<any>
? z.ZodBigInt
: V extends VBoolean<any>
? z.ZodBoolean
: V extends VNull<any>
? z.ZodNull
: V extends VArray<any, infer Element>
? Element extends VArray<any, any> // This check is used to avoid TypeScript complaining about infinite type instantiation
? z.ZodArray<zCore.SomeType>
: z.ZodArray<ZodFromValidatorBase<Element>>
: V extends VObject<
any,
infer Fields extends Record<string, GenericValidator>
>
? z.ZodObject<ZodShapeFromConvexObject<Fields>, zCore.$strict>
: V extends VBytes<any, any>
? never
: V extends VLiteral<
infer T extends zCore.util.Literal,
OptionalProperty
>
? z.ZodLiteral<NotUndefined<T>>
: V extends VRecord<
any,
infer Key,
infer Value,
OptionalProperty,
any
>
? z.ZodRecord<
ZodFromStringValidator<Key>,
ZodFromValidatorBase<Value>
>
: // Union: must handle separately cases for 0/1/2+ elements
// instead of simply writing it as
// V extends VUnion<any, infer Elements extends GenericValidator[], any, any>
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ? z.ZodUnion<{ [k in keyof Elements]: ZodValidatorFromConvex<Elements[k]> }>
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// because the TypeScript compiler would complain about infinite type instantiation otherwise :(
V extends VUnion<any, [], OptionalProperty, any>
? z.ZodNever
: V extends VUnion<
any,
[infer I extends GenericValidator],
OptionalProperty,
any
>
? ZodValidatorFromConvex<I>
: V extends VUnion<
any,
[
infer A extends GenericValidator,
...infer Rest extends GenericValidator[],
],
OptionalProperty,
any
>
? z.ZodUnion<
readonly [
ZodValidatorFromConvex<A>,
...{
[K in keyof Rest]: ZodValidatorFromConvex<
Rest[K]
>;
},
]
>
: V extends VAny<any, OptionalProperty, any>
? z.ZodAny
: never;
type BrandIfBranded<InnerType, Validator extends zCore.SomeType> =
InnerType extends zCore.$brand<infer Brand>
? zCore.$ZodBranded<Validator, Brand>
: Validator;
type StringValidator = Validator<string, "required", any>;
type ZodFromStringValidator<V extends StringValidator> =
V extends VId<GenericId<infer TableName extends string>>
? Zid<TableName>
: V extends VString<infer T, any>
? BrandIfBranded<T, z.ZodString>
: // Literals
V extends VLiteral<infer Literal extends string>
? z.ZodLiteral<Literal>
: // Union (see below)
V extends VUnion<any, [], any, any>
? z.ZodNever
: V extends VUnion<any, [infer I extends GenericValidator], any, any>
? ZodFromStringValidator<I>
: V extends VUnion<
any,
[
infer A extends GenericValidator,
...infer Rest extends GenericValidator[],
],
any,
any
>
? z.ZodUnion<
readonly [
ZodFromStringValidator<A>,
...{
[K in keyof Rest]: ZodFromStringValidator<Rest[K]>;
},
]
>
: never;
type ZodShapeFromConvexObject<Fields extends Record<string, GenericValidator>> =
Fields extends infer F // dark magic to get the TypeScript compiler happy about circular types
? {
[K in keyof F]: F[K] extends GenericValidator
? ZodValidatorFromConvex<F[K]>
: 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<T> = Exclude<T, undefined>;
type TableNameFromType<T> =
T extends GenericId<infer TableName> ? TableName : string;
// #endregion