UNPKG

convex-helpers

Version:

A collection of useful code to complement the official convex package.

1,039 lines (1,005 loc) 33.3 kB
import type { DataModelFromSchemaDefinition, Expand, GenericDatabaseReader, GenericDataModel, SchemaDefinition, TableNamesInDataModel, } from "convex/server"; import type { GenericId, GenericValidator, Infer, ObjectType, OptionalProperty, PropertyValidators, Validator, VAny, VArray, VBoolean, VBytes, VFloat64, VId, VInt64, VLiteral, VNull, VObject, VOptional, VRecord, VString, VUnion, } from "convex/values"; import { asObjectValidator, v } from "convex/values"; import { assert } from "./index.js"; /** * Helper for defining a union of literals more concisely. * * e.g. `literals("a", 1, false)` is equivalent to * `v.union(v.literal("a"), v.literal(1), v.literal(false))` * To use with an array: * ```ts * const myLiterals = ["a", 1, false] as const; * const literalValidator = literals(...myLiterals) * ``` * A similar result can be achieved with `v.union(...myLiterals.map(v.literal))` * however the type of each union member will be the union of literal types, * rather than each member being a specific literal type. * * @param args Values you want to use in a union of literals. * @returns A validator for the union of the literals. */ export const literals = <T extends Array<string | number | boolean | bigint>>( ...args: T ) => { return v.union(...args.map(v.literal)) as VUnion< T[number], NoInfer<{ [K in keyof T]: VLiteral<T[K]> }> >; }; /** * nullable define a validator that can be the value or null more consisely. * * @param x The validator to make nullable. As in, it can be the value or null. * @returns A new validator that can be the value or null. */ export const nullable = <V extends Validator<any, "required", any>>(x: V) => v.union(v.null(), x); /** * partial helps you define an object of optional validators more concisely. * * `partial({a: v.string(), b: v.number()})` is equivalent to * `{a: v.optional(v.string()), b: v.optional(v.number())}` * * @param obj The object of validators to make optional. e.g. {a: v.string()} * @returns A new object of validators that can be the value or undefined. */ export function partial<T extends PropertyValidators>( obj: T, ): { [K in keyof T]: VOptional<T[K]>; }; /** * partial helps you define an object of optional validators more concisely. * * `partial(v.object({a: v.string(), b: v.number()}))` is equivalent to * `v.object({a: v.optional(v.string()), b: v.optional(v.number())})` * * @param obj The object of validators to make optional. e.g. v.object({a: v.string()}) * @returns A new object of validators that can be the value or undefined. */ export function partial< T, V extends Record<string, GenericValidator>, O extends OptionalProperty, >(obj: VObject<T, V, O>): PartialVObject<T, V, O>; /** * partial helps you define a union of optional validators more concisely. * * `partial(v.union(v.object({a: v.string()}), v.object({b: v.number()})))` * is equivalent to * `v.union(v.object({a: v.optional(v.string())}), v.object({b: v.optional(v.number())}))` * * @param obj The union of validators to make optional. e.g. v.union(v.object({a: v.string()}), v.object({b: v.number()})) * @returns A new union of validators that can be the value or undefined. */ export function partial< T, V extends Validator<T, "required", any>[], O extends OptionalProperty, >(obj: VUnion<T, V, O>): PartialVUnion<T, V, O>; /** * partial helps you define a union of optional validators more concisely. * * `partial(v.union(v.object({a: v.string()}), v.object({b: v.number()})))` * is equivalent to * `v.union(v.object({a: v.optional(v.string())}), v.object({b: v.optional(v.number())}))` * * @param obj The union of validators to make optional. e.g. v.union(v.object({a: v.string()}), v.object({b: v.number()})) * @returns A new union of validators that can be the value or undefined. */ export function partial< Object extends VObject<any, any, any>, Union extends VUnion<any, any[], any>, >( obj: Object | Union, ): | PartialVUnion<Union["type"], Union["members"], Object["isOptional"]> | PartialVObject<Object["type"], Object["fields"], Object["isOptional"]>; export function partial( fieldsOrObjOrUnion: | PropertyValidators | VObject<any, any, any> | VUnion<any, any[], any>, ) { if (fieldsOrObjOrUnion.isConvexValidator) { if (fieldsOrObjOrUnion.kind === "object") { return partialVObject(fieldsOrObjOrUnion); } if (fieldsOrObjOrUnion.kind === "union") { return partialUnion(fieldsOrObjOrUnion); } throw new Error( "partial only works with union or object Validators, or a Record<string, Validator> currently", ); } return partialFields(fieldsOrObjOrUnion as PropertyValidators); } /** * partialFields helps you define an object of optional validators more concisely. * * e.g. `partialFields({a: v.string(), b: v.number()})` is equivalent to * `{a: v.optional(v.string()), b: v.optional(v.number())}` * * @param obj The object of validators to make optional. e.g. {a: v.string()} * @returns A new object of validators that can be the value or undefined. */ function partialFields<T extends PropertyValidators>( obj: T, ): { [K in keyof T]: VOptional<T[K]>; } { return Object.fromEntries( Object.entries(obj).map(([k, vv]) => [ k, vv.isOptional === "optional" ? vv : v.optional(vv), ]), ) as any; } /** * partialObject helps you define an object of optional validators more concisely. * * e.g. `partialObject({a: v.string(), b: v.number()})` is equivalent to * `{a: v.optional(v.string()), b: v.optional(v.number())}` * * @param obj The object of validators to make optional. e.g. {a: v.string()} * @returns A new object of validators that can be the value or undefined. */ function partialVObject< T, V extends Record<string, GenericValidator>, Optional extends OptionalProperty, >(obj: VObject<T, V, Optional>): PartialVObject<T, V, Optional> { const o = v.object(partialFields(obj.fields)); if (obj.isOptional === "optional") { return v.optional(o) as any; } return o as any; } type PartialVObject< T, V extends Record<string, GenericValidator>, Optional extends OptionalProperty, > = VObject< Partial<T>, { [K in keyof V]: VOptional<V[K]>; }, Optional >; type PartialUnionMembers< Members extends readonly Validator<any, "required", any>[], > = { [K in keyof Members]: Members[K] extends VObject<any, any, "required"> ? Members[K] extends VObject< infer MemberT, infer MemberV extends PropertyValidators, "required" > ? PartialVObject<MemberT, MemberV, "required"> : Members[K] : Members[K] extends VUnion<any, any, "required"> ? Members[K] extends VUnion<infer MemberT, infer MemberV, "required"> ? PartialVUnion<MemberT, MemberV, "required"> : Members[K] : Members[K]; }; function partialUnion< T, V extends Validator<T, "required", any>[], Optional extends OptionalProperty, >(union: VUnion<T, V, Optional>): PartialVUnion<T, V, Optional> { const u = v.union( ...union.members.map((m) => { assert(m.isOptional === "required", "Union members cannot be optional"); if (m.kind === "object") { return partialVObject(m) as VObject<any, any, "required">; } if (m.kind === "union") { return partialUnion(m) as VUnion<any, any[], "required">; } throw new Error(`Invalid union member type: ${m.kind}`); }), ) as any; if (union.isOptional === "optional") { return v.optional(u) as any; } return u as any; } type PartialVUnion< T, Members extends Validator<T, "required", any>[], Optional extends OptionalProperty, > = VUnion<Partial<T>, PartialUnionMembers<Members>, Optional>; // Shorthand for defining validators that look like types. /** @deprecated Use `v.string()` instead. Any string value. */ export const string = v.string(); /** @deprecated Use `v.float64()` instead. JavaScript number, represented as a float64 in the database. */ export const number = v.float64(); /** @deprecated Use `v.float64()` instead. JavaScript number, represented as a float64 in the database. */ export const float64 = v.float64(); /** @deprecated Use `v.boolean()` instead. boolean value. For typing it only as true, use `l(true)` */ export const boolean = v.boolean(); /** @deprecated Use `v.int64()` instead. bigint, though stored as an int64 in the database. */ export const biging = v.int64(); /** @deprecated Use `v.int64()` instead. bigint, though stored as an int64 in the database. */ export const int64 = v.int64(); /** @deprecated Use `v.any()` instead. Any Convex value */ export const any = v.any(); /** @deprecated Use `v.null()` instead. Null value. Underscore is so it doesn't shadow the null builtin */ export const null_ = v.null(); /** @deprecated Use `v.*()` instead. */ export const { id, object, array, bytes, literal, optional, union } = v; /** @deprecated Use `v.bytes()` instead. ArrayBuffer validator. */ export const arrayBuffer = v.bytes(); /** * Utility to get the validators for fields associated with a table. * e.g. for systemFields("users") it would return: * { _id: v.id("users"), _creationTime: v.number() } * * @param tableName The table name in the schema. * @returns Validators for the system fields: _id and _creationTime */ export const systemFields = <TableName extends string>( tableName: TableName, ) => ({ _id: v.id(tableName), _creationTime: v.number(), }); export type SystemFields<TableName extends string> = ReturnType< typeof systemFields<TableName> >; /** * Utility to add system fields to an object with fields mapping to validators. * e.g. withSystemFields("users", { name: v.string() }) would return: * { name: v.string(), _id: v.id("users"), _creationTime: v.number() } * * @param tableName Table name in the schema. * @param fields The fields of the table mapped to their validators. * @returns The fields plus system fields _id and _creationTime. */ export const withSystemFields = < TableName extends string, T extends Record<string, GenericValidator>, >( tableName: TableName, fields: T, ) => { const system = systemFields(tableName); return { ...fields, ...system, } as Expand<T & typeof system>; }; export type AddFieldsToValidator< V extends Validator<any, any, any>, Fields extends PropertyValidators, > = V extends VObject<infer T, infer F, infer O> ? VObject<Expand<T & ObjectType<Fields>>, Expand<F & Fields>, O> : Validator< Expand<V["type"] & ObjectType<Fields>>, V["isOptional"], V["fieldPaths"] & { [Property in keyof Fields & string]: | `${Property}.${Fields[Property]["fieldPaths"]}` | Property; }[keyof Fields & string] & string >; /** * Equivalent to `v.object({ ...validator, ...fields })`. */ export function addFieldsToValidator< Props extends PropertyValidators, Fields extends PropertyValidators, >( validator: Props, fields: Fields, ): VObject<ObjectType<Props & Fields>, Props & Fields, "required">; /** * Add fields to an object validator. * Equivalent to `v.object({ ...validator.fields, ...fields })`. */ export function addFieldsToValidator< V extends VObject<any, any, any>, Fields extends PropertyValidators, >( validator: V, fields: Fields, ): VObject< V["type"] & ObjectType<Fields>, V["fields"] & Fields, V["isOptional"] >; /** * Add fields to a union validator. * Equivalent to `v.union(...validator.members.map((m) => addFieldsToValidator(m, fields)))`. */ export function addFieldsToValidator< V extends VUnion<any, any[], any>, Fields extends PropertyValidators, >(validator: V, fields: Fields): AddFieldsToValidator<V, Fields>; export function addFieldsToValidator< V extends VObject<any, any, any> | VUnion<any, any[], any>, Fields extends PropertyValidators, >(validator: V, fields: Fields): AddFieldsToValidator<V, Fields>; export function addFieldsToValidator< V extends | PropertyValidators | VObject<any, any, any> | VUnion<any, any[], any>, Fields extends PropertyValidators, >(validatorOrFields: V, fields: Fields) { const validator = asObjectValidator(validatorOrFields); if (Object.keys(fields).length === 0) { return validator; } switch (validator.kind) { case "object": return v.object(intersectValidators(validator.fields, fields)); case "union": return v.union( ...validator.members.map((m) => addFieldsToValidator(m, fields)), ); default: throw new Error( "Cannot add arguments to a validator that is not an object or union.", ); } } function intersectValidators( fields: PropertyValidators, fields2: PropertyValidators, ): PropertyValidators { const specificFields = { ...fields }; for (const [k, v] of Object.entries(fields2)) { const existing = specificFields[k]; if (existing) { if (existing.kind !== v.kind) { // TODO: handle unions & literals & other sub-types (incl. optionals) throw new Error( `Cannot intersect validators with different kinds: ${existing.kind} and ${v.kind}`, ); } if (existing.isOptional !== v.isOptional) { if (existing.isOptional === "optional") { // prefer the required validator specificFields[k] = v; } } } else { specificFields[k] = v; } } return specificFields; } export const doc = < Schema extends SchemaDefinition<any, boolean>, TableName extends TableNamesInDataModel< DataModelFromSchemaDefinition<Schema> >, >( schema: Schema, tableName: TableName, ): AddFieldsToValidator< Schema["tables"][TableName]["validator"], SystemFields<TableName> > => { function addSystemFields<V extends Validator<any, any, any>>( validator: V, ): any { if (validator.kind === "object") { return v.object({ ...validator.fields, ...systemFields(tableName), }); } if (validator.kind !== "union") { throw new Error( "Only object and union validators are supported for documents", ); } return v.union(...validator.members.map(addSystemFields)); } return addSystemFields(schema.tables[tableName].validator); }; /** * Creates a validator with a type-safe `.id(table)` and a new `.doc(table)`. * Can be used instead of `v` for function arugments & return validators. * However, it cannot be used as part of defining a schema, since it would be * circular. * ```ts * import schema from "./schema"; * export const vv = typedV(schema); * * export const myQuery = query({ * args: { docId: vv.id("mytable") }, * returns: vv.doc("mytable"), * handler: (ctx, args) => ctx.db.get(args.docId), * }) * * @param schema Typically from `import schema from "./schema"`. * @returns A validator like `v` with type-safe `v.id` and a new `v.doc` */ export function typedV<Schema extends SchemaDefinition<any, boolean>>( schema: Schema, ): Omit<typeof v, "id"> & { id: < TableName extends TableNamesInDataModel< DataModelFromSchemaDefinition<Schema> >, >( tableName: TableName, ) => VId<GenericId<TableName>, "required">; doc: < TableName extends TableNamesInDataModel< DataModelFromSchemaDefinition<Schema> >, >( tableName: TableName, ) => AddFieldsToValidator< Schema["tables"][TableName]["validator"], SystemFields<TableName> >; } { return { ...v, /** * Similar to v.id but is type-safe on the table name. * @param tableName A table named in your schema. * @returns A validator for an ID to the named table. */ id: (tableName) => v.id(tableName), /** * Generates a validator for a document, including system fields. * To be used in validators when passing a full document in or out of a * function. * @param tableName A table named in your schema. * @returns A validator that matches the schema validator, adding _id and * _creationTime. If the validator was a union, it will update all documents * recursively, but will currently lose the VUnion-specific type. */ doc: (tableName) => doc(schema, tableName), }; } /** * A string validator that is a branded string type. * * Read more at https://stack.convex.dev/using-branded-types-in-validators * * @param _brand - A unique string literal to brand the string with */ export const brandedString = <T extends string>(_brand: T) => v.string() as VString<string & { _: T }>; /** Mark fields as deprecated with this permissive validator typed as null */ export const deprecated = v.optional(v.any()) as Validator<null, "optional">; /** A maximally permissive validator that type checks as a given validator. * * If you want to have types that match some validator but you have invalid data * and you want to temporarily not validate schema for this field, * you can use this function to cast the permissive validator. * * Example in a schema: * ```ts * export default defineSchema({ * myTable: defineTable({ * myString: pretend(v.array(v.string())), * }), * }); * //...in some mutation * ctx.db.insert("myTable", { myString: 123 as any }); // no runtime error * ``` * Example in function argument validation: * ```ts * const myQuery = defineQuery({ * args: { myNumber: pretend(v.number()) }, * handler: async (ctx, args) => { * // args.myNumber is typed as number, but it's not validated. * const num = typeof args.myNumber === "number" ? * args.myNumber : Number(args.myNumber); * }, * }); */ export const pretend = <T extends GenericValidator>(_typeToImmitate: T): T => v.optional(v.any()) as T; /** A validator that validates as optional but type checks as required. * * If you want to assume a field is set for type checking, but your data may not * actually have it set for all documents (e.g. when adding a new field), * you can use this function to allow the field to be unset at runtime. * This is unsafe, but can be convenient in these situations: * * 1. You are developing locally and want to add a required field and write * code assuming it is set. Once you push the code & schema, you can update * the data to match before running your code. * 2. You are going to run a migration right after pushing code, and are ok with * and you don't want to edit your code to handle the field being unset, * your app being in an inconsistent state until the migration completes. * * This differs from {@link pretend} in that it type checks the inner validator, * if the value is provided. * * Example in a schema: * ```ts * export default defineSchema({ * myTable: defineTable({ * myString: pretendRequired(v.array(v.string())), * }), * }); * //...in some mutation * ctx.db.insert("myTable", { myString: undefined }); // no runtime error * ``` * Example in function argument validation: * ```ts * const myQuery = defineQuery({ * args: { myNumber: pretendRequired(v.number()) }, * handler: async (ctx, args) => { * // args.myNumber is typed as number, but it might be undefined * const num = args.myNumber || 0; * }, * }); */ export const pretendRequired = <T extends Validator<any, "required", any>>( optionalType: T, ): T => v.optional(optionalType) as unknown as T; export class ValidationError extends Error { constructor( public expected: string, public got: string, public path?: string, ) { const message = `Validator error${path ? ` for ${path}` : ""}: Expected \`${expected}\`, got \`${got}\``; super(message); this.name = "ValidationError"; } } /** * Validate a value against a validator. * * WARNING: This does not validate that v.id is an ID for the given table. * It only validates that the ID is a string. Function `args`, `returns` and * schema definitions will validate that the ID is an ID for the given table. * * @param validator The validator to validate against. * @param value The value to validate. * @returns Whether the value is valid against the validator. */ export function validate<T extends Validator<any, any, any>>( validator: T, value: unknown, opts?: { /* If true, throw an error if the value is not valid. */ throw?: boolean; /* If provided, v.id validation will check that the id is for the table. */ db?: GenericDatabaseReader<GenericDataModel>; /* If true, allow fields that are not in an object validator. */ allowUnknownFields?: boolean; /* A prefix for the path of the value being validated, for error reporting. This is used for recursive calls, do not set it manually. */ _pathPrefix?: string; }, ): value is T["type"] { let valid = true; let expected: string = validator.kind; let got: string | undefined; if (value === undefined) { if (validator.isOptional !== "optional") { valid = false; } } else { switch (validator.kind) { case "null": { if (value !== null) { valid = false; } break; } case "float64": { if (typeof value !== "number") { expected = "number"; valid = false; } break; } case "int64": { if (typeof value !== "bigint") { expected = "bigint"; valid = false; } break; } case "boolean": { if (typeof value !== "boolean") { valid = false; } break; } case "string": { if (typeof value !== "string") { valid = false; } break; } case "bytes": { if (!(value instanceof ArrayBuffer)) { valid = false; } break; } case "any": { break; } case "literal": { if (value !== validator.value) { valid = false; expected = validator.value; if ( ["string", "number", "boolean", "bigint"].includes(typeof value) ) { got = `"${value}"`; } } break; } case "id": { if (typeof value !== "string") { valid = false; } else if (opts?.db) { expected = `Id<${validator.tableName}>`; const id = opts.db.normalizeId(validator.tableName, value); if (!id) { valid = false; } } break; } case "array": { if (!Array.isArray(value)) { valid = false; break; } for (const [index, v] of value.entries()) { const path = `${opts?._pathPrefix ?? ""}[${index}]`; valid = validate(validator.element, v, { ...opts, _pathPrefix: path, }); if (!valid) { expected = validator.element.kind; break; } } break; } case "object": { if (typeof value !== "object" || value === null) { valid = false; break; } const prototype = Object.getPrototypeOf(value); const isSimple = prototype === null || prototype === Object.prototype || // Objects generated from other contexts (e.g. across Node.js `vm` modules) will not satisfy the previous // conditions but are still simple objects. prototype?.constructor?.name === "Object"; if (!isSimple) { expected = (prototype?.constructor?.name ?? typeof prototype) || "object"; valid = false; break; } for (const [k, fieldValidator] of Object.entries(validator.fields)) { valid = validate(fieldValidator, (value as any)[k], { ...opts, _pathPrefix: appendPath(opts, k), }); if (!valid) { break; } } if (!opts?.allowUnknownFields) { for (const k of Object.keys(value)) { if (validator.fields[k] === undefined) { if (opts?.throw) { throw new ValidationError( "nothing", typeof (value as any)[k], appendPath(opts, k), ); } valid = false; break; } } } break; } case "union": { valid = false; let error: ValidationError | undefined; for (const member of validator.members) { try { if (validate(member, value, opts)) { valid = true; break; } } catch (e) { error = e as ValidationError; } } if (!valid && error) { throw error; } break; } case "record": { if (typeof value !== "object" || value === null) { valid = false; break; } for (const [k, fieldValue] of Object.entries(value)) { valid = validate(validator.key, k, { ...opts, _pathPrefix: appendPath(opts, k), }); if (!valid) { expected = validator.key.kind; break; } valid = validate(validator.value, fieldValue, { ...opts, _pathPrefix: appendPath(opts, k), }); if (!valid) { expected = validator.value.kind; break; } } break; } } } if (!valid && opts?.throw) { throw new ValidationError( expected, got ?? (value === null ? "null" : typeof value), opts?._pathPrefix, ); } return valid; } /** * Parse a value, using a Convex validator. This differs from `validate` in that * it strips unknown fields instead of throwing an error on them. * * @param validator - The Convex validator to parse the value against. * @param value - The value to parse. * @returns The parsed value, without fields not specified in the validator. */ export function parse<T extends Validator<any, any, any>>( validator: T, value: unknown, ): Infer<T> { validate(validator, value, { allowUnknownFields: true, throw: true }); return stripUnknownFields(validator, value); } function stripUnknownFields<T extends Validator<any, any, any>>( validator: T, value: Infer<T>, ): Infer<T> { if (validator.isOptional === "optional" && value === undefined) { return value; } assert(value !== undefined); switch (validator.kind) { case "object": { const result: Infer<T> = {}; for (const [k, v] of Object.entries(value)) { if (validator.fields[k] !== undefined && v !== undefined) { result[k] = stripUnknownFields(validator.fields[k], v); } } return result; } case "record": { const result: Infer<T> = {}; for (const [k, v] of Object.entries(value)) { result[k] = stripUnknownFields(validator.value, v); } return result; } case "array": { return (value as any[]).map((e) => stripUnknownFields(validator.element, e), ); } case "union": { // First try a strict match for (const member of validator.members) { if (validate(member, value, { allowUnknownFields: false })) { return stripUnknownFields(member, value); } } // Then try a permissive match for (const member of validator.members) { if (validate(member, value, { allowUnknownFields: true })) { return stripUnknownFields(member, value); } } throw new Error("No matching member in union"); } default: { return value as Infer<T>; } } } function appendPath(opts: { _pathPrefix?: string } | undefined, path: string) { return opts?._pathPrefix ? `${opts._pathPrefix}.${path}` : path; } type NotUndefined<T> = Exclude<T, undefined>; /** * A type that converts an optional validator to a required validator. * * This is the inverse of `VOptional`. It takes a validator that may be optional * and returns the equivalent required validator type. * * @example * ```ts * type OptionalString = VOptional<VString<string, "required">>; * type RequiredString = VRequired<OptionalString>; // VString<string, "required"> * ``` */ export type VRequired<T extends Validator<any, OptionalProperty, any>> = T extends VId<infer Type, OptionalProperty> ? VId<NotUndefined<Type>, "required"> : T extends VString<infer Type, OptionalProperty> ? VString<NotUndefined<Type>, "required"> : T extends VFloat64<infer Type, OptionalProperty> ? VFloat64<NotUndefined<Type>, "required"> : T extends VInt64<infer Type, OptionalProperty> ? VInt64<NotUndefined<Type>, "required"> : T extends VBoolean<infer Type, OptionalProperty> ? VBoolean<NotUndefined<Type>, "required"> : T extends VNull<infer Type, OptionalProperty> ? VNull<NotUndefined<Type>, "required"> : T extends VAny<infer Type, OptionalProperty> ? VAny<NotUndefined<Type>, "required"> : T extends VLiteral<infer Type, OptionalProperty> ? VLiteral<NotUndefined<Type>, "required"> : T extends VBytes<infer Type, OptionalProperty> ? VBytes<NotUndefined<Type>, "required"> : T extends VObject< infer Type, infer Fields, OptionalProperty, infer FieldPaths > ? VObject< NotUndefined<Type>, Fields, "required", FieldPaths > : T extends VArray< infer Type, infer Element, OptionalProperty > ? VArray<NotUndefined<Type>, Element, "required"> : T extends VRecord< infer Type, infer Key, infer Value, OptionalProperty, infer FieldPaths > ? VRecord< NotUndefined<Type>, Key, Value, "required", FieldPaths > : T extends VUnion< infer Type, infer Members, OptionalProperty, infer FieldPaths > ? VUnion< NotUndefined<Type>, Members, "required", FieldPaths > : never; /** * Converts an optional validator to a required validator. * * This is the inverse of `v.optional()`. It takes a validator that may be optional * and returns the equivalent required validator. * * ```ts * const optionalString = v.optional(v.string()); * const requiredString = vRequired(optionalString); // v.string() * * // Already required validators are returned as-is * const alreadyRequired = v.string(); * const stillRequired = vRequired(alreadyRequired); // v.string() * ``` * * @param validator The validator to make required. * @returns A required version of the validator. */ export function vRequired<T extends Validator<any, OptionalProperty, any>>( validator: T, ): VRequired<T> { const { kind, isOptional } = validator; if (isOptional === "required") { // TypeScript can't prove T is already VRequired<T>, so we go via unknown. return validator as unknown as VRequired<T>; } switch (kind) { case "id": return v.id(validator.tableName) as VRequired<T>; case "string": return v.string() as VRequired<T>; case "float64": return v.float64() as VRequired<T>; case "int64": return v.int64() as VRequired<T>; case "boolean": return v.boolean() as VRequired<T>; case "null": return v.null() as VRequired<T>; case "any": return v.any() as VRequired<T>; case "literal": return v.literal(validator.value) as VRequired<T>; case "bytes": return v.bytes() as VRequired<T>; case "object": return v.object(validator.fields) as VRequired<T>; case "array": return v.array(validator.element) as VRequired<T>; case "record": return v.record(validator.key, validator.value) as VRequired<T>; case "union": return v.union(...validator.members) as VRequired<T>; default: kind satisfies never; throw new Error("Unknown Convex validator type: " + kind); } }