convex
Version:
Client for the Convex Cloud
623 lines (581 loc) • 15.6 kB
text/typescript
import { GenericId } from "./index.js";
import { GenericValidator } from "./validator.js";
import { JSONValue, convexToJson } from "./value.js";
type TableNameFromType<T> =
T extends GenericId<infer TableName> ? TableName : string;
/**
* Avoid using `instanceof BaseValidator`; this is inheritence for code reuse
* not type heirarchy.
*/
abstract class BaseValidator<
Type,
IsOptional extends OptionalProperty = "required",
FieldPaths extends string = never,
> {
/**
* Only for TypeScript, the TS type of the JS values validated
* by this validator.
*/
readonly type!: Type;
/**
* Only for TypeScript, if this an Object validator, then
* this is the TS type of its property names.
*/
readonly fieldPaths!: FieldPaths;
/**
* Whether this is an optional Object property value validator.
*/
readonly isOptional: IsOptional;
/**
* Always `"true"`.
*/
readonly isConvexValidator: true;
constructor({ isOptional }: { isOptional: IsOptional }) {
this.isOptional = isOptional;
this.isConvexValidator = true;
}
/** @deprecated - use isOptional instead */
get optional(): boolean {
return this.isOptional === "optional" ? true : false;
}
/** @internal */
abstract get json(): ValidatorJSON;
/** @internal */
abstract asOptional(): Validator<Type | undefined, "optional", FieldPaths>;
}
/**
* The type of the `v.id(tableName)` validator.
*/
export class VId<
Type,
IsOptional extends OptionalProperty = "required",
> extends BaseValidator<Type, IsOptional> {
/**
* The name of the table that the validated IDs must belong to.
*/
readonly tableName: TableNameFromType<Type>;
/**
* The kind of validator, `"id"`.
*/
readonly kind = "id" as const;
/**
* Usually you'd use `v.id(tableName)` instead.
*/
constructor({
isOptional,
tableName,
}: {
isOptional: IsOptional;
tableName: TableNameFromType<Type>;
}) {
super({ isOptional });
this.tableName = tableName;
}
/** @internal */
get json(): ValidatorJSON {
return { type: "id", tableName: this.tableName };
}
/** @internal */
asOptional() {
return new VId<Type | undefined, "optional">({
isOptional: "optional",
tableName: this.tableName,
});
}
}
/**
* The type of the `v.float64()` validator.
*/
export class VFloat64<
Type = number,
IsOptional extends OptionalProperty = "required",
> extends BaseValidator<Type, IsOptional> {
/**
* The kind of validator, `"float64"`.
*/
readonly kind = "float64" as const;
/** @internal */
get json(): ValidatorJSON {
// Server expects the old name `number` string instead of `float64`.
return { type: "number" };
}
/** @internal */
asOptional() {
return new VFloat64<Type | undefined, "optional">({
isOptional: "optional",
});
}
}
/**
* The type of the `v.int64()` validator.
*/
export class VInt64<
Type = bigint,
IsOptional extends OptionalProperty = "required",
> extends BaseValidator<Type, IsOptional> {
/**
* The kind of validator, `"int64"`.
*/
readonly kind = "int64" as const;
/** @internal */
get json(): ValidatorJSON {
// Server expects the old name `bigint`.
return { type: "bigint" };
}
/** @internal */
asOptional() {
return new VInt64<Type | undefined, "optional">({ isOptional: "optional" });
}
}
/**
* The type of the `v.boolean()` validator.
*/
export class VBoolean<
Type = boolean,
IsOptional extends OptionalProperty = "required",
> extends BaseValidator<Type, IsOptional> {
/**
* The kind of validator, `"boolean"`.
*/
readonly kind = "boolean" as const;
/** @internal */
get json(): ValidatorJSON {
return { type: this.kind };
}
/** @internal */
asOptional() {
return new VBoolean<Type | undefined, "optional">({
isOptional: "optional",
});
}
}
/**
* The type of the `v.bytes()` validator.
*/
export class VBytes<
Type = ArrayBuffer,
IsOptional extends OptionalProperty = "required",
> extends BaseValidator<Type, IsOptional> {
/**
* The kind of validator, `"bytes"`.
*/
readonly kind = "bytes" as const;
/** @internal */
get json(): ValidatorJSON {
return { type: this.kind };
}
/** @internal */
asOptional() {
return new VBytes<Type | undefined, "optional">({ isOptional: "optional" });
}
}
/**
* The type of the `v.string()` validator.
*/
export class VString<
Type = string,
IsOptional extends OptionalProperty = "required",
> extends BaseValidator<Type, IsOptional> {
/**
* The kind of validator, `"string"`.
*/
readonly kind = "string" as const;
/** @internal */
get json(): ValidatorJSON {
return { type: this.kind };
}
/** @internal */
asOptional() {
return new VString<Type | undefined, "optional">({
isOptional: "optional",
});
}
}
/**
* The type of the `v.null()` validator.
*/
export class VNull<
Type = null,
IsOptional extends OptionalProperty = "required",
> extends BaseValidator<Type, IsOptional> {
/**
* The kind of validator, `"null"`.
*/
readonly kind = "null" as const;
/** @internal */
get json(): ValidatorJSON {
return { type: this.kind };
}
/** @internal */
asOptional() {
return new VNull<Type | undefined, "optional">({ isOptional: "optional" });
}
}
/**
* The type of the `v.any()` validator.
*/
export class VAny<
Type = any,
IsOptional extends OptionalProperty = "required",
FieldPaths extends string = string,
> extends BaseValidator<Type, IsOptional, FieldPaths> {
/**
* The kind of validator, `"any"`.
*/
readonly kind = "any" as const;
/** @internal */
get json(): ValidatorJSON {
return {
type: this.kind,
};
}
/** @internal */
asOptional() {
return new VAny<Type | undefined, "optional", FieldPaths>({
isOptional: "optional",
});
}
}
/**
* The type of the `v.object()` validator.
*/
export class VObject<
Type,
Fields extends Record<string, GenericValidator>,
IsOptional extends OptionalProperty = "required",
FieldPaths extends string = {
[Property in keyof Fields]:
| JoinFieldPaths<Property & string, Fields[Property]["fieldPaths"]>
| Property;
}[keyof Fields] &
string,
> extends BaseValidator<Type, IsOptional, FieldPaths> {
/**
* An object with the validator for each property.
*/
readonly fields: Fields;
/**
* The kind of validator, `"object"`.
*/
readonly kind = "object" as const;
/**
* Usually you'd use `v.object({ ... })` instead.
*/
constructor({
isOptional,
fields,
}: {
isOptional: IsOptional;
fields: Fields;
}) {
super({ isOptional });
this.fields = fields;
}
/** @internal */
get json(): ValidatorJSON {
return {
type: this.kind,
value: globalThis.Object.fromEntries(
globalThis.Object.entries(this.fields).map(([k, v]) => [
k,
{
fieldType: v.json,
optional: v.isOptional === "optional" ? true : false,
},
]),
),
};
}
/** @internal */
asOptional() {
return new VObject<Type | undefined, Fields, "optional", FieldPaths>({
isOptional: "optional",
fields: this.fields,
});
}
}
/**
* The type of the `v.literal()` validator.
*/
export class VLiteral<
Type,
IsOptional extends OptionalProperty = "required",
> extends BaseValidator<Type, IsOptional> {
/**
* The value that the validated values must be equal to.
*/
readonly value: Type;
/**
* The kind of validator, `"literal"`.
*/
readonly kind = "literal" as const;
/**
* Usually you'd use `v.literal(value)` instead.
*/
constructor({ isOptional, value }: { isOptional: IsOptional; value: Type }) {
super({ isOptional });
this.value = value;
}
/** @internal */
get json(): ValidatorJSON {
return {
type: this.kind,
value: convexToJson(this.value as string | boolean | number | bigint),
};
}
/** @internal */
asOptional() {
return new VLiteral<Type | undefined, "optional">({
isOptional: "optional",
value: this.value,
});
}
}
/**
* The type of the `v.array()` validator.
*/
export class VArray<
Type,
Element extends Validator<any, "required", any>,
IsOptional extends OptionalProperty = "required",
> extends BaseValidator<Type, IsOptional> {
/**
* The validator for the elements of the array.
*/
readonly element: Element;
/**
* The kind of validator, `"array"`.
*/
readonly kind = "array" as const;
/**
* Usually you'd use `v.array(element)` instead.
*/
constructor({
isOptional,
element,
}: {
isOptional: IsOptional;
element: Element;
}) {
super({ isOptional });
this.element = element;
}
/** @internal */
get json(): ValidatorJSON {
return {
type: this.kind,
value: this.element.json,
};
}
/** @internal */
asOptional() {
return new VArray<Type | undefined, Element, "optional">({
isOptional: "optional",
element: this.element,
});
}
}
/**
* Validator for an object that produces indexed types.
*
* If the value validator is not optional, it produces a `Record` type, which is an alias
* for `{[key: K]: V}`.
*
* If the value validator is optional, it produces a mapped object type,
* with optional keys: `{[key in K]?: V}`.
*
* This is used within the validator builder, {@link v}.
*/
export class VRecord<
Type,
Key extends Validator<string, "required", any>,
Value extends Validator<any, "required", any>,
IsOptional extends OptionalProperty = "required",
FieldPaths extends string = string,
> extends BaseValidator<Type, IsOptional, FieldPaths> {
readonly key: Key;
readonly value: Value;
readonly kind = "record" as const;
constructor({
isOptional,
key,
value,
}: {
isOptional: IsOptional;
key: Key;
value: Value;
}) {
super({ isOptional });
this.key = key;
this.value = value;
}
/** @internal */
get json(): ValidatorJSON {
return {
type: this.kind,
keys: this.key.json,
values: {
fieldType: this.value.json,
optional: false,
},
};
}
/** @internal */
asOptional() {
return new VRecord<Type | undefined, Key, Value, "optional", FieldPaths>({
isOptional: "optional",
key: this.key,
value: this.value,
});
}
}
/**
* The type of the `v.union()` validator.
*/
export class VUnion<
Type,
T extends Validator<any, "required", any>[],
IsOptional extends OptionalProperty = "required",
FieldPaths extends string = T[number]["fieldPaths"],
> extends BaseValidator<Type, IsOptional, FieldPaths> {
/**
* The array of validators, one of which must match the value.
*/
readonly members: T;
/**
* The kind of validator, `"union"`.
*/
readonly kind = "union" as const;
/**
* Usually you'd use `v.union(...members)` instead.
*/
constructor({ isOptional, members }: { isOptional: IsOptional; members: T }) {
super({ isOptional });
this.members = members;
}
/** @internal */
get json(): ValidatorJSON {
return {
type: this.kind,
value: this.members.map((v) => v.json),
};
}
/** @internal */
asOptional() {
return new VUnion<Type | undefined, T, "optional">({
isOptional: "optional",
members: this.members,
});
}
}
// prettier-ignore
export type VOptional<T extends Validator<any, OptionalProperty, any>> =
T extends VId<infer Type, OptionalProperty> ? VId<Type | undefined, "optional">
: T extends VString<infer Type, OptionalProperty>
? VString<Type | undefined, "optional">
: T extends VFloat64<infer Type, OptionalProperty>
? VFloat64<Type | undefined, "optional">
: T extends VInt64<infer Type, OptionalProperty>
? VInt64<Type | undefined, "optional">
: T extends VBoolean<infer Type, OptionalProperty>
? VBoolean<Type | undefined, "optional">
: T extends VNull<infer Type, OptionalProperty>
? VNull<Type | undefined, "optional">
: T extends VAny<infer Type, OptionalProperty>
? VAny<Type | undefined, "optional">
: T extends VLiteral<infer Type, OptionalProperty>
? VLiteral<Type | undefined, "optional">
: T extends VBytes<infer Type, OptionalProperty>
? VBytes<Type | undefined, "optional">
: T extends VObject< infer Type, infer Fields, OptionalProperty, infer FieldPaths>
? VObject<Type | undefined, Fields, "optional", FieldPaths>
: T extends VArray<infer Type, infer Element, OptionalProperty>
? VArray<Type | undefined, Element, "optional">
: T extends VRecord< infer Type, infer Key, infer Value, OptionalProperty, infer FieldPaths>
? VRecord<Type | undefined, Key, Value, "optional", FieldPaths>
: T extends VUnion<infer Type, infer Members, OptionalProperty, infer FieldPaths>
? VUnion<Type | undefined, Members, "optional", FieldPaths>
: never
/**
* Type representing whether a property in an object is optional or required.
*
* @public
*/
export type OptionalProperty = "optional" | "required";
/**
* A validator for a Convex value.
*
* This should be constructed using the validator builder, {@link v}.
*
* A validator encapsulates:
* - The TypeScript type of this value.
* - Whether this field should be optional if it's included in an object.
* - The TypeScript type for the set of index field paths that can be used to
* build indexes on this value.
* - A JSON representation of the validator.
*
* Specific types of validators contain additional information: for example
* an `ArrayValidator` contains an `element` property with the validator
* used to validate each element of the list. Use the shared 'kind' property
* to identity the type of validator.
*
* More validators can be added in future releases so an exhaustive
* switch statement on validator `kind` should be expected to break
* in future releases of Convex.
*
* @public
*/
export type Validator<
Type,
IsOptional extends OptionalProperty = "required",
FieldPaths extends string = never,
> =
| VId<Type, IsOptional>
| VString<Type, IsOptional>
| VFloat64<Type, IsOptional>
| VInt64<Type, IsOptional>
| VBoolean<Type, IsOptional>
| VNull<Type, IsOptional>
| VAny<Type, IsOptional>
| VLiteral<Type, IsOptional>
| VBytes<Type, IsOptional>
| VObject<
Type,
Record<string, Validator<any, OptionalProperty, any>>,
IsOptional,
FieldPaths
>
| VArray<Type, Validator<any, "required", any>, IsOptional>
| VRecord<
Type,
Validator<string, "required", any>,
Validator<any, "required", any>,
IsOptional,
FieldPaths
>
| VUnion<Type, Validator<any, "required", any>[], IsOptional, FieldPaths>;
/**
* Join together two index field paths.
*
* This is used within the validator builder, {@link v}.
* @public
*/
export type JoinFieldPaths<
Start extends string,
End extends string,
> = `${Start}.${End}`;
export type ObjectFieldType = { fieldType: ValidatorJSON; optional: boolean };
export type ValidatorJSON =
| { type: "null" }
| { type: "number" }
| { type: "bigint" }
| { type: "boolean" }
| { type: "string" }
| { type: "bytes" }
| { type: "any" }
| { type: "literal"; value: JSONValue }
| { type: "id"; tableName: string }
| { type: "array"; value: ValidatorJSON }
| { type: "record"; keys: ValidatorJSON; values: ObjectFieldType }
| { type: "object"; value: Record<string, ObjectFieldType> }
| { type: "union"; value: ValidatorJSON[] };