UNPKG

convex

Version:

Client for the Convex Cloud

639 lines (610 loc) 16.9 kB
/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Utilities for defining the schema of your Convex project. * * ## Usage * * Schemas should be placed in a `schema.ts` file in your `convex/` directory. * * Schema definitions should be built using {@link defineSchema}, * {@link defineTable}, and {@link s}. Make sure to export the schema as the * default export. * * ```ts * import { defineSchema, defineTable, s } from "convex/schema"; * * export default defineSchema({ * messages: defineTable({ * body: s.string(), * user: s.id("users"), * }), * users: defineTable({ * name: s.string(), * }), * }); * ``` * * To learn more about schemas, see [Defining a Schema](https://docs.convex.dev/using/schemas). * @module */ import { GenericId } from "../values/index.js"; import { AnyDataModel, DocumentByInfo, GenericDataModel, GenericDocument, GenericTableIndexes, GenericTableInfo, GenericTableSearchIndexes, Indexes, SearchIndexes, } from "../server/data_model.js"; import { IdField, IndexTiebreakerField, SystemFields, SystemIndexes, } from "../server/system_fields.js"; import { Expand } from "../type_utils.js"; /** * A Convex type defined in a schema. * * These should be constructed using {@link s}. * * This class encapsulates: * - The TypeScript type of this Convex type. * - 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 type. * - The table names referenced in `s.id` usages in this type. * @public */ export class SchemaType< TypeScriptType, IsOptional extends boolean = false, FieldPaths extends string = never > { readonly type!: TypeScriptType; readonly isOptional!: IsOptional; readonly fieldPaths!: FieldPaths; // Property for a bit of nominal type safety. private _isSchemaType: undefined; readonly referencedTableNames: Set<string>; constructor(referencedTableNames: Set<string> = new Set()) { this.referencedTableNames = referencedTableNames; } } /** * The schema builder. * * This builder allows you to define the types of documents stored in Convex. * @public */ export const s = { id<TableName extends string>( tableName: TableName ): SchemaType<GenericId<TableName>> { return new SchemaType(new Set([tableName])); }, null(): SchemaType<null> { return new SchemaType(); }, number(): SchemaType<number> { return new SchemaType(); }, bigint(): SchemaType<bigint> { return new SchemaType(); }, boolean(): SchemaType<boolean> { return new SchemaType(); }, string(): SchemaType<string> { return new SchemaType(); }, bytes(): SchemaType<ArrayBuffer> { return new SchemaType(); }, literal<T extends string | number | bigint | boolean>( literal: T ): SchemaType<T> { return new SchemaType(); }, array<T>(values: SchemaType<T, false, any>): SchemaType<T[]> { return new SchemaType(values.referencedTableNames); }, set<T>(values: SchemaType<T, false, any>): SchemaType<Set<T>> { return new SchemaType(values.referencedTableNames); }, map<K, V>( keys: SchemaType<K, false, any>, values: SchemaType<V, false, any> ): SchemaType<Map<K, V>> { return new SchemaType( new Set([...keys.referencedTableNames, ...values.referencedTableNames]) ); }, object<T extends Record<string, SchemaType<any, any, any>>>( schema: T ): ObjectSchemaType<T> { const referencedTableNames = new Set<string>(); for (const schemaType of Object.values(schema)) { for (const tableName of schemaType.referencedTableNames) { referencedTableNames.add(tableName); } } return new SchemaType(referencedTableNames); }, union< T extends [ SchemaType<any, false, any>, SchemaType<any, false, any>, ...SchemaType<any, false, any>[] ] >( ...schemaTypes: T ): SchemaType<T[number]["type"], false, T[number]["fieldPaths"]> { const referencedTableNames = new Set<string>(); for (const schemaType of schemaTypes) { for (const tableName of schemaType.referencedTableNames) { referencedTableNames.add(tableName); } } return new SchemaType(referencedTableNames); }, any(): SchemaType<any, false, string> { return new SchemaType(); }, optional<T extends SchemaType<any, false, any>>( inner: T ): SchemaType<T["type"], true, T["fieldPaths"]> { return new SchemaType(inner.referencedTableNames) as SchemaType< T["type"], true, T["fieldPaths"] >; }, }; /** * Calculate the {@link SchemaType} for an object. * * This is used within the SchemaBuilder {@link s}. * @public */ type ObjectSchemaType< SchemaValueType extends Record<string, SchemaType<any, any, any>> > = SchemaType< // Compute the TypeScript type for this object. // Map each key to the corresponding type making the optional ones optional. Expand< { [Property in OptionalKeys<SchemaValueType>]?: SchemaValueType[Property]["type"]; } & { [Property in RequiredKeys<SchemaValueType>]: SchemaValueType[Property]["type"]; } >, false, // Compute the field paths for this object. For every property in the object // Add on a field path for that property and extend all the field paths in the // value. { [Property in keyof SchemaValueType]: | JoinFieldPaths< Property & string, SchemaValueType[Property]["fieldPaths"] > | Property; }[keyof SchemaValueType] & string >; type OptionalKeys< SchemaValueType extends Record<string, SchemaType<any, any, any>> > = { [Property in keyof SchemaValueType]: SchemaValueType[Property]["isOptional"] extends true ? Property : never; }[keyof SchemaValueType]; type RequiredKeys< SchemaValueType extends Record<string, SchemaType<any, any, any>> > = Exclude<keyof SchemaValueType, OptionalKeys<SchemaValueType>>; /** * Join together two index field paths. * * This is used within the SchemaBuilder {@link s}. * @public */ type JoinFieldPaths< Start extends string, End extends string > = `${Start}.${End}`; /** * Extract all of the index field paths within a {@link SchemaType}. * * This is used within {@link defineTable}. * @public */ type ExtractFieldPaths<T extends SchemaType<any, any, any>> = // Add in the system fields available in index definitions. // This should be everything except for `_id` because thats added to indexes // automatically. T["fieldPaths"] | keyof SystemFields; /** * Extract the {@link GenericDocument} within a {@link SchemaType} and * add on the system fields. * * This is used within {@link defineTable}. * @public */ type ExtractDocument<T extends SchemaType<any, any, any>> = // Add the system fields to `Value` (except `_id` because it depends on //the table name) and trick TypeScript into expanding them. Expand<SystemFields & T["type"]>; /** * The configuration for a search index. * * @internal */ export interface SearchIndexConfig< SearchField extends string, FilterFields extends string > { /** * The field to index for full text search. * * This must be a field of type `string`. */ searchField: SearchField; /** * Additional fields to index for fast filtering when running search queries. */ filterFields?: FilterFields[]; } /** * The definition of a table within a schema. * * This should be produced by using {@link defineTable}. * @public */ export class TableDefinition< Document extends GenericDocument = GenericDocument, FieldPaths extends string = string, // eslint-disable-next-line @typescript-eslint/ban-types Indexes extends GenericTableIndexes = {}, // eslint-disable-next-line @typescript-eslint/ban-types SearchIndexes extends GenericTableSearchIndexes = {} > { private indexes: { indexDescriptor: string; fields: string[] }[]; private searchIndexes: { indexDescriptor: string; searchField: string; filterFields: string[]; }[]; // The type of documents stored in this table. private documentType: SchemaType<any, any, any>; /** * @internal */ constructor(documentType: SchemaType<any, any, any>) { this.indexes = []; this.searchIndexes = []; this.documentType = documentType; } /** * Define an index on this table. * * To learn about indexes, see [Defining Indexes](https://docs.convex.dev/using/indexes). * * @param name - The name of the index. * @param fields - The fields to index, in order. Must specify at least one * field. * @returns A {@link TableDefinition} with this index included. */ index< IndexName extends string, FirstFieldPath extends FieldPaths, RestFieldPaths extends FieldPaths[] >( name: IndexName, fields: [FirstFieldPath, ...RestFieldPaths] ): TableDefinition< Document, FieldPaths, // Update `Indexes` to include the new index and use `Expand` to make the // types look pretty in editors. Expand< Indexes & Record< IndexName, [FirstFieldPath, ...RestFieldPaths, IndexTiebreakerField] > >, SearchIndexes > { this.indexes.push({ indexDescriptor: name, fields }); return this; } /** * Define a search index on this table. * * @param name - The name of the index. * @param indexConfig - The search index configuration object. * @returns A {@link TableDefinition} with this search index included. * @internal */ searchIndex< IndexName extends string, SearchField extends FieldPaths, FilterFields extends FieldPaths = never >( name: IndexName, indexConfig: Expand<SearchIndexConfig<SearchField, FilterFields>> ): TableDefinition< Document, FieldPaths, Indexes, // Update `SearchIndexes` to include the new index and use `Expand` to make // the types look pretty in editors. Expand< SearchIndexes & Record< IndexName, { searchField: SearchField; filterFields: FilterFields; } > > > { this.searchIndexes.push({ indexDescriptor: name, searchField: indexConfig.searchField, filterFields: indexConfig.filterFields || [], }); return this; } /** * Export the contents of this definition. * * This is called internally by the Convex framework. * @internal */ export() { return { indexes: this.indexes, searchIndexes: this.searchIndexes, referencedTableNames: this.documentType.referencedTableNames, }; } } /** * Define a table in a schema. * * You can either specify the schema of your documents as an object like * ```ts * defineTable({ * field: s.string() * }); * ``` * * or as a schema type like * ```ts * defineTable( * s.union( * s.object({...}), * s.object({...}) * ) * ); * ``` * * @param documentSchema - The type of documents stored in this table. * @returns A {@link TableDefinition} for the table. * * @public */ export function defineTable< DocumentSchema extends SchemaType<Record<string, any>, false, any> >( documentSchema: DocumentSchema ): TableDefinition< ExtractDocument<DocumentSchema>, ExtractFieldPaths<DocumentSchema> >; /** * Define a table in a schema. * * You can either specify the schema of your documents as an object like * ```ts * defineTable({ * field: s.string() * }); * ``` * * or as a schema type like * ```ts * defineTable( * s.union( * s.object({...}), * s.object({...}) * ) * ); * ``` * * @param documentSchema - The type of documents stored in this table. * @returns A {@link TableDefinition} for the table. * * @public */ export function defineTable< DocumentSchema extends Record<string, SchemaType<any, any, any>> >( documentSchema: DocumentSchema ): TableDefinition< ExtractDocument<ObjectSchemaType<DocumentSchema>>, ExtractFieldPaths<ObjectSchemaType<DocumentSchema>> >; export function defineTable< DocumentSchema extends | SchemaType<Record<string, any>, false, any> | Record<string, SchemaType<any, any, any>> >(documentSchema: DocumentSchema): TableDefinition<any, any> { if (documentSchema instanceof SchemaType) { return new TableDefinition(documentSchema); } else { return new TableDefinition(s.object(documentSchema)); } } /** * A type describing the schema of a Convex project. * * This should be constructed using {@link defineSchema}, {@link defineTable}, * and {@link s}. * @public */ export type GenericSchema = Record<string, TableDefinition>; /** * * The definition of a Convex project schema. * * This should be produced by using {@link defineSchema}. * @public */ export class SchemaDefinition< Schema extends GenericSchema, IsStrict extends boolean > { public tables: Schema; public isStrict: IsStrict; /** * @internal */ constructor(tables: Schema, options?: DefineSchemaOptions<IsStrict>) { this.tables = tables; this.isStrict = !!options?.strict as IsStrict; } /** * Export the contents of this definition. * * This is called internally by the Convex framework. * @internal */ export(): string { const tableNames = new Set(Object.keys(this.tables)); return JSON.stringify({ tables: Object.entries(this.tables).map(([tableName, definition]) => { const { indexes, searchIndexes, referencedTableNames } = definition.export(); // Make sure all the referenced table names are actually defined. for (const referencedTableName of referencedTableNames) { if (!tableNames.has(referencedTableName)) { throw new Error( `SchemaValidationError: Table ${tableName} has a \`s.id\` ` + `expression that references table ${referencedTableName} which isn't defined in the schema.` ); } } return { tableName, indexes, searchIndexes, }; }), }); } } /** * Options for {@link defineSchema}. * * @public */ export interface DefineSchemaOptions<IsStrict extends boolean> { /** * Whether to strictly enforce this schema. * * If this schema is not strictly enforced, its type will permit: * 1. Using additional tables not explicitly listed in the schema. * 2. Using additional properties not explicitly listed in the tables. * * Loose schemas are useful for rapid prototyping. * * By default the schema is considered strict. */ strict?: IsStrict; } /** * Define the schema of this Convex project. * * This should be exported from a `schema.ts` file in your `convex/` directory * like: * * ```ts * export default defineSchema({ * ... * }); * ``` * * @param schema - A map from table name to {@link TableDefinition} for all of * the tables in this project. * @param options - Optional configuration. See {@link DefineSchemaOptions} for * a full description. * @returns The schema. * * @public */ export function defineSchema< Schema extends GenericSchema, IsStrict extends boolean = true >( schema: Schema, options?: DefineSchemaOptions<IsStrict> ): SchemaDefinition<Schema, IsStrict> { return new SchemaDefinition(schema, options); } /** * Internal type used in Convex code generation! * * Convert a {@link SchemaDefinition} into a {@link server.GenericDataModel}. * * @public */ export type DataModelFromSchemaDefinition< SchemaDef extends SchemaDefinition<any, boolean> > = MaybeMakeLooseDataModel< { [TableName in keyof SchemaDef["tables"] & string]: SchemaDef["tables"][TableName] extends TableDefinition< infer Document, infer FieldPaths, infer Indexes, infer SearchIndexes > ? MaybeMakeLooseTableInfo< { // We've already added all of the system fields except for `_id`. // Add that here. document: Expand<IdField<TableName> & Document>; fieldPaths: keyof IdField<TableName> | FieldPaths; indexes: Expand<Indexes & SystemIndexes>; searchIndexes: SearchIndexes; }, SchemaDef["isStrict"] > : never; }, SchemaDef["isStrict"] >; type MaybeMakeLooseDataModel< DataModel extends GenericDataModel, IsStrict extends boolean > = IsStrict extends true ? DataModel : Expand<DataModel & AnyDataModel>; type MaybeMakeLooseTableInfo< TableInfo extends GenericTableInfo, IsStrict extends boolean > = IsStrict extends true ? TableInfo : { document: Expand< DocumentByInfo<TableInfo> & { [propertyName: string]: any } >; fieldPaths: string; indexes: Indexes<TableInfo>; searchIndexes: SearchIndexes<TableInfo>; };