UNPKG

convex

Version:

Client for the Convex Cloud

509 lines (486 loc) 13.4 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 { GenericDocument, GenericTableIndexes } 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. * - 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, FieldPaths extends string> { readonly type!: TypeScriptType; 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>, never> { return new SchemaType(new Set([tableName])); }, null(): SchemaType<null, never> { return new SchemaType(); }, number(): SchemaType<number, never> { return new SchemaType(); }, bigint(): SchemaType<bigint, never> { return new SchemaType(); }, boolean(): SchemaType<boolean, never> { return new SchemaType(); }, string(): SchemaType<string, never> { return new SchemaType(); }, bytes(): SchemaType<ArrayBuffer, never> { return new SchemaType(); }, literal<T extends string | number | bigint | boolean>( literal: T ): SchemaType<T, never> { return new SchemaType(); }, array<T>(values: SchemaType<T, any>): SchemaType<T[], never> { return new SchemaType(values.referencedTableNames); }, set<T>(values: SchemaType<T, any>): SchemaType<Set<T>, never> { return new SchemaType(values.referencedTableNames); }, map<K, V>( keys: SchemaType<K, any>, values: SchemaType<V, any> ): SchemaType<Map<K, V>, never> { return new SchemaType( new Set([...keys.referencedTableNames, ...values.referencedTableNames]) ); }, object<T extends Record<string, SchemaType<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, any>, SchemaType<any, any>, ...SchemaType<any, any>[] ] >(...schemaTypes: T): SchemaType<T[number]["type"], 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, string> { return new SchemaType(); }, }; /** * 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>> > = SchemaType< // Compute the TypeScript type for this object. Just map the keys to the values // inside the SchemaTypes { [Property in keyof SchemaValueType]: SchemaValueType[Property] extends SchemaType< infer InnerType, any > ? InnerType : never; }, // 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]: SchemaValueType[Property] extends SchemaType< any, infer FieldPaths > ? JoinFieldPaths<Property & string, FieldPaths> | Property : never; }[keyof SchemaValueType] & string >; /** * 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>> = T extends SchemaType< any, infer FieldPaths > ? // Add in the system fields available in index definitions. // This should be everything except for `_id` because thats added to indexes // automatically. FieldPaths | keyof SystemFields : never; /** * 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>> = T extends SchemaType< infer Value, any > ? Value extends GenericDocument ? // Add the system fields to `Value` (except `_id` because it depends on //the table name) and trick TypeScript into expanding them. Expand<SystemFields & Value> : never : never; /** * 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 TableIndexes extends GenericTableIndexes = {} > { // A map of index name to index fields. private indexes: { indexDescriptor: string; fields: string[] }[]; // The type of documents stored in this table. private documentType: SchemaType<any, any>; /** * @internal */ constructor(documentType: SchemaType<any, any>) { this.indexes = []; 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 `TableIndexes` include the new index and use `Expand` to make the // types look pretty in editors. Expand< TableIndexes & Record< IndexName, [FirstFieldPath, ...RestFieldPaths, IndexTiebreakerField] > > > { this.indexes.push({ indexDescriptor: name, fields }); return this; } /** * Export the contents of this definition. * * This is called internally by the Convex framework. * @internal */ export() { return { indexes: this.indexes, 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>, 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>> >( documentSchema: DocumentSchema ): TableDefinition< ExtractDocument<ObjectSchemaType<DocumentSchema>>, ExtractFieldPaths<ObjectSchemaType<DocumentSchema>> >; export function defineTable< DocumentSchema extends | SchemaType<Record<string, any>, any> | Record<string, SchemaType<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> { private tables: GenericSchema; /** * @internal */ constructor(tables: Schema) { this.tables = tables; } /** * 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, 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, }; }), }); } } /** * 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. * @returns The schema. * * @public */ export function defineSchema<Schema extends GenericSchema>( schema: Schema ): SchemaDefinition<Schema> { return new SchemaDefinition(schema); } /** * Internal type used in Convex code generation! * * Convert a {@link SchemaDefinition} into a {@link server.GenericDataModel}. * * @public */ export type DataModelFromSchemaDefinition< SchemaDef extends SchemaDefinition<any> > = SchemaDef extends SchemaDefinition<infer Schema> ? { [TableName in keyof Schema & string]: Schema[TableName] extends TableDefinition< infer Document, infer FieldPaths, infer TableIndexes > ? { // 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<TableIndexes & SystemIndexes>; } : never; } : never; /** * Internal type used in Convex code generation! * * Convert a {@link SchemaDefinition} into an object type that maps table names * to their document types. * * This is similar to {@link server.GenericDataModel} but it doesn't contain * index information. This is nice for making some types appear simpler in * VSCode. * * @public */ export type DocumentMapFromSchemaDefinition< SchemaDef extends SchemaDefinition<any> > = SchemaDef extends SchemaDefinition<infer Schema> ? { [TableName in keyof Schema & string]: Schema[TableName] extends TableDefinition< infer Document, any, any > ? Expand<IdField<TableName> & Document> : never; } : never;