convex
Version:
Client for the Convex Cloud
509 lines (486 loc) • 13.4 kB
text/typescript
/* 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;