UNPKG

convex

Version:

Client for the Convex Cloud

477 lines (450 loc) 13.6 kB
import { Expand } from "../type_utils.js"; import { GenericId } from "./index.js"; import { OptionalProperty, VAny, VArray, VBoolean, VBytes, VFloat64, VId, VInt64, VLiteral, VNull, VObject, VOptional, VRecord, VString, VUnion, Validator, } from "./validators.js"; /** * The type that all validators must extend. * * @public */ export type GenericValidator = Validator<any, any, any>; export function isValidator(v: any): v is GenericValidator { return !!v.isConvexValidator; } /** * Coerce an object with validators as properties to a validator. * If a validator is passed, return it. * * @public */ export function asObjectValidator< V extends Validator<any, any, any> | PropertyValidators, >( obj: V, ): V extends Validator<any, any, any> ? V : V extends PropertyValidators ? Validator<ObjectType<V>> : never { if (isValidator(obj)) { return obj as any; } else { return v.object(obj as PropertyValidators) as any; } } /** * Coerce an object with validators as properties to a validator. * If a validator is passed, return it. * * @public */ export type AsObjectValidator< V extends Validator<any, any, any> | PropertyValidators, > = V extends Validator<any, any, any> ? V : V extends PropertyValidators ? Validator<ObjectType<V>> : never; /** * The validator builder. * * This builder allows you to build validators for Convex values. Validators * are used in two places: * * 1. **Schema definitions** - to define the shape of documents in your tables. * 2. **Function arguments and return values** - to validate inputs and outputs * of your Convex queries, mutations, and actions. * * Always include `args` and `returns` validators on all Convex functions. If a * function doesn't return a value, use `returns: v.null()`. * * **Convex type reference:** * * | Convex Type | JS/TS Type | Validator | * |-------------|---------------|--------------------------------| * | Id | `string` | `v.id("tableName")` | * | Null | `null` | `v.null()` | * | Float64 | `number` | `v.number()` | * | Int64 | `bigint` | `v.int64()` | * | Boolean | `boolean` | `v.boolean()` | * | String | `string` | `v.string()` | * | Bytes | `ArrayBuffer` | `v.bytes()` | * | Array | `Array` | `v.array(element)` | * | Object | `Object` | `v.object({ field: value })` | * | Record | `Record` | `v.record(keys, values)` | * * **Modifiers and meta-types:** * - `v.union(member1, member2)` - a value matching at least one validator * - `v.literal("value")` - a specific literal string, number, bigint, or boolean * - `v.optional(validator)` - makes a property optional in an object (`T | undefined`) * * **Important notes:** * - JavaScript's `undefined` is **not** a valid Convex value. Functions that * return `undefined` or have no return will return `null` to the client. * Objects with `undefined` values will strip those keys during serialization. * For arrays, use an explicit `null` instead. * - `v.bigint()` is deprecated, use `v.int64()` instead. * - `v.map()` and `v.set()` are not supported. Use `v.array()` of tuples or * `v.record()` as alternatives. * * @example * ```typescript * import { v } from "convex/values"; * * // Use in function definition: * export const createUser = mutation({ * args: { * name: v.string(), * email: v.string(), * age: v.optional(v.number()), * }, * returns: v.id("users"), * handler: async (ctx, args) => { * return await ctx.db.insert("users", args); * }, * }); * ``` * * @see https://docs.convex.dev/database/types * @see https://docs.convex.dev/functions/validation * @public */ export const v = { /** * Validates that the value is a document ID for the given table. * * IDs are strings at runtime but are typed as `Id<"tableName">` in * TypeScript for type safety. * * @example * ```typescript * args: { userId: v.id("users") } * ``` * * @param tableName The name of the table. */ id: <TableName extends string>(tableName: TableName) => { return new VId<GenericId<TableName>>({ isOptional: "required", tableName, }); }, /** * Validates that the value is `null`. * * Use `returns: v.null()` for functions that don't return a meaningful value. * JavaScript `undefined` is not a valid Convex value, it is automatically * converted to `null`. */ null: () => { return new VNull({ isOptional: "required" }); }, /** * Validates that the value is a JavaScript `number` (Convex Float64). * * Supports all IEEE-754 double-precision floating point numbers including * NaN and Infinity. * * Alias for `v.float64()`. */ number: () => { return new VFloat64({ isOptional: "required" }); }, /** * Validates that the value is a JavaScript `number` (Convex Float64). * * Supports all IEEE-754 double-precision floating point numbers. */ float64: () => { return new VFloat64({ isOptional: "required" }); }, /** * @deprecated Use `v.int64()` instead. */ bigint: () => { return new VInt64({ isOptional: "required" }); }, /** * Validates that the value is a JavaScript `bigint` (Convex Int64). * * Supports BigInts between -2^63 and 2^63-1. * * @example * ```typescript * args: { timestamp: v.int64() } * // Usage: createDoc({ timestamp: 1234567890n }) * ``` */ int64: () => { return new VInt64({ isOptional: "required" }); }, /** * Validates that the value is a `boolean`. */ boolean: () => { return new VBoolean({ isOptional: "required" }); }, /** * Validates that the value is a `string`. * * Strings are stored as UTF-8 and their storage size is calculated as their * UTF-8 encoded size. */ string: () => { return new VString({ isOptional: "required" }); }, /** * Validates that the value is an `ArrayBuffer` (Convex Bytes). * * Use for binary data. */ bytes: () => { return new VBytes({ isOptional: "required" }); }, /** * Validates that the value is exactly equal to the given literal. * * Useful for discriminated unions and enum-like patterns. * * @example * ```typescript * // Discriminated union pattern: * v.union( * v.object({ kind: v.literal("error"), message: v.string() }), * v.object({ kind: v.literal("success"), value: v.number() }), * ) * ``` * * @param literal The literal value to compare against. */ literal: <T extends string | number | bigint | boolean>(literal: T) => { return new VLiteral<T>({ isOptional: "required", value: literal }); }, /** * Validates that the value is an `Array` where every element matches the * given validator. * * Arrays can have at most 8192 elements. * * @example * ```typescript * args: { tags: v.array(v.string()) } * args: { coordinates: v.array(v.number()) } * args: { items: v.array(v.object({ name: v.string(), qty: v.number() })) } * ``` * * @param element The validator for the elements of the array. */ array: <T extends Validator<any, "required", any>>(element: T) => { return new VArray<T["type"][], T>({ isOptional: "required", element }); }, /** * Validates that the value is an `Object` with the specified properties. * * Objects can have at most 1024 entries. Field names must be non-empty and * must not start with `"$"` or `"_"` (`_` is reserved for system fields * like `_id` and `_creationTime`; `$` is reserved for Convex internal use). * * @example * ```typescript * args: { * user: v.object({ * name: v.string(), * email: v.string(), * age: v.optional(v.number()), * }) * } * ``` * * @param fields An object mapping property names to their validators. */ object: <T extends PropertyValidators>(fields: T) => { return new VObject<ObjectType<T>, T>({ isOptional: "required", fields }); }, /** * Validates that the value is a `Record` (object with dynamic keys). * * Records are objects at runtime but allow dynamic keys, unlike `v.object()` * which requires known property names. Keys must be ASCII characters only, * non-empty, and not start with `"$"` or `"_"`. * * @example * ```typescript * // Map of user IDs to scores: * args: { scores: v.record(v.id("users"), v.number()) } * * // Map of string keys to string values: * args: { metadata: v.record(v.string(), v.string()) } * ``` * * @param keys The validator for the keys of the record. * @param values The validator for the values of the record. */ record: < Key extends Validator<string, "required", any>, Value extends Validator<any, "required", any>, >( keys: Key, values: Value, ) => { return new VRecord<Record<Infer<Key>, Value["type"]>, Key, Value>({ isOptional: "required", key: keys, value: values, }); }, /** * Validates that the value matches at least one of the given validators. * * @example * ```typescript * // Allow string or number: * args: { value: v.union(v.string(), v.number()) } * * // Discriminated union (recommended pattern): * v.union( * v.object({ kind: v.literal("text"), body: v.string() }), * v.object({ kind: v.literal("image"), url: v.string() }), * ) * * // Nullable value: * returns: v.union(v.object({ ... }), v.null()) * ``` * * @param members The validators to match against. */ union: <T extends Validator<any, "required", any>[]>(...members: T) => { return new VUnion<T[number]["type"], T>({ isOptional: "required", members, }); }, /** * A validator that accepts any Convex value without validation. * * Prefer using specific validators when possible for better type safety * and runtime validation. */ any: () => { return new VAny({ isOptional: "required" }); }, /** * Makes a property optional in an object validator. * * An optional property can be omitted entirely when creating a document or * calling a function. This is different from `v.nullable()` which requires * the property to be present but allows `null`. * * @example * ```typescript * v.object({ * name: v.string(), // required * nickname: v.optional(v.string()), // can be omitted * }) * * // Valid: { name: "Alice" } * // Valid: { name: "Alice", nickname: "Ali" } * // Invalid: { name: "Alice", nickname: null } - use v.nullable() for this * ``` * * @param value The property value validator to make optional. */ optional: <T extends GenericValidator>(value: T) => { return value.asOptional() as VOptional<T>; }, /** * Allows a value to be either the given type or `null`. * * This is shorthand for `v.union(value, v.null())`. Unlike `v.optional()`, * the property must still be present, but may be `null`. * * @example * ```typescript * v.object({ * name: v.string(), * deletedAt: v.nullable(v.number()), // must be present, can be null * }) * * // Valid: { name: "Alice", deletedAt: null } * // Valid: { name: "Alice", deletedAt: 1234567890 } * // Invalid: { name: "Alice" } - deletedAt is required * ``` */ nullable: <T extends Validator<any, "required", any>>(value: T) => { return v.union(value, v.null()); }, }; /** * Validators for each property of an object. * * This is represented as an object mapping the property name to its * {@link Validator}. * * @public */ export type PropertyValidators = Record< string, Validator<any, OptionalProperty, any> >; /** * Compute the type of an object from {@link PropertyValidators}. * * @public */ export type ObjectType<Fields extends PropertyValidators> = Expand< // Map each key to the corresponding property validator's type making // the optional ones optional. { // This `Exclude<..., undefined>` does nothing unless // the tsconfig.json option `"exactOptionalPropertyTypes": true,` // is used. When it is it results in a more accurate type. // When it is not the `Exclude` removes `undefined` but it is // added again by the optional property. [Property in OptionalKeys<Fields>]?: Exclude< Infer<Fields[Property]>, undefined >; } & { [Property in RequiredKeys<Fields>]: Infer<Fields[Property]>; } >; type OptionalKeys<PropertyValidators extends Record<string, GenericValidator>> = { [Property in keyof PropertyValidators]: PropertyValidators[Property]["isOptional"] extends "optional" ? Property : never; }[keyof PropertyValidators]; type RequiredKeys<PropertyValidators extends Record<string, GenericValidator>> = Exclude<keyof PropertyValidators, OptionalKeys<PropertyValidators>>; /** * Extract a TypeScript type from a validator. * * Example usage: * ```ts * const objectSchema = v.object({ * property: v.string(), * }); * type MyObject = Infer<typeof objectSchema>; // { property: string } * ``` * @typeParam V - The type of a {@link Validator} constructed with {@link v}. * * @public */ export type Infer<T extends Validator<any, OptionalProperty, any>> = T["type"];