convex
Version:
Client for the Convex Cloud
592 lines (565 loc) • 15.7 kB
text/typescript
/**
* 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 values.v}. Make sure to export the schema as the
* default export.
*
* ```ts
* import { defineSchema, defineTable } from "convex/server";
* import { v } from "convex/values";
*
* export default defineSchema({
* messages: defineTable({
* body: v.string(),
* user: v.id("users"),
* }),
* users: defineTable({
* name: v.string(),
* }),
* });
* ```
*
* To learn more about schemas, see [Defining a Schema](https://docs.convex.dev/using/schemas).
* @module
*/
import {
AnyDataModel,
GenericDataModel,
GenericTableIndexes,
GenericTableSearchIndexes,
GenericTableVectorIndexes,
TableNamesInDataModel,
} from "../server/data_model.js";
import {
IdField,
IndexTiebreakerField,
SystemFields,
SystemIndexes,
} from "../server/system_fields.js";
import { Expand } from "../type_utils.js";
import {
GenericValidator,
ObjectType,
isValidator,
v,
} from "../values/validator.js";
import { VObject, Validator } from "../values/validators.js";
/**
* Extract all of the index field paths within a {@link Validator}.
*
* This is used within {@link defineTable}.
* @public
*/
type ExtractFieldPaths<T extends Validator<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 Validator} and
* add on the system fields.
*
* This is used within {@link defineTable}.
* @public
*/
type ExtractDocument<T extends Validator<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 full text search index.
*
* @public
*/
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 configuration for a vector index.
*
* @public
*/
export interface VectorIndexConfig<
VectorField extends string,
FilterFields extends string,
> {
/**
* The field to index for vector search.
*
* This must be a field of type `v.array(v.float64())` (or a union)
*/
vectorField: VectorField;
/**
* The length of the vectors indexed. This must be between 2 and 2048 inclusive.
*/
dimensions: number;
/**
* Additional fields to index for fast filtering when running vector searches.
*/
filterFields?: FilterFields[];
}
/**
* @internal
*/
export type VectorIndex = {
indexDescriptor: string;
vectorField: string;
dimensions: number;
filterFields: string[];
};
/**
* @internal
*/
export type Index = {
indexDescriptor: string;
fields: string[];
};
/**
* @internal
*/
export type SearchIndex = {
indexDescriptor: string;
searchField: string;
filterFields: string[];
};
/**
* The definition of a table within a schema.
*
* This should be produced by using {@link defineTable}.
* @public
*/
export class TableDefinition<
DocumentType extends Validator<any, any, any> = Validator<any, any, any>,
// eslint-disable-next-line @typescript-eslint/ban-types
Indexes extends GenericTableIndexes = {},
// eslint-disable-next-line @typescript-eslint/ban-types
SearchIndexes extends GenericTableSearchIndexes = {},
// eslint-disable-next-line @typescript-eslint/ban-types
VectorIndexes extends GenericTableVectorIndexes = {},
> {
private indexes: Index[];
private searchIndexes: SearchIndex[];
private vectorIndexes: VectorIndex[];
// The type of documents stored in this table.
validator: DocumentType;
/**
* @internal
*/
constructor(documentType: DocumentType) {
this.indexes = [];
this.searchIndexes = [];
this.vectorIndexes = [];
this.validator = 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 ExtractFieldPaths<DocumentType>,
RestFieldPaths extends ExtractFieldPaths<DocumentType>[],
>(
name: IndexName,
fields: [FirstFieldPath, ...RestFieldPaths],
): TableDefinition<
DocumentType,
// 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,
VectorIndexes
> {
this.indexes.push({ indexDescriptor: name, fields });
return this;
}
/**
* Define a search index on this table.
*
* To learn about search indexes, see [Search](https://docs.convex.dev/text-search).
*
* @param name - The name of the index.
* @param indexConfig - The search index configuration object.
* @returns A {@link TableDefinition} with this search index included.
*/
searchIndex<
IndexName extends string,
SearchField extends ExtractFieldPaths<DocumentType>,
FilterFields extends ExtractFieldPaths<DocumentType> = never,
>(
name: IndexName,
indexConfig: Expand<SearchIndexConfig<SearchField, FilterFields>>,
): TableDefinition<
DocumentType,
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;
}
>
>,
VectorIndexes
> {
this.searchIndexes.push({
indexDescriptor: name,
searchField: indexConfig.searchField,
filterFields: indexConfig.filterFields || [],
});
return this;
}
/**
* Define a vector index on this table.
*
* To learn about vector indexes, see [Vector Search](https://docs.convex.dev/vector-search).
*
* @param name - The name of the index.
* @param indexConfig - The vector index configuration object.
* @returns A {@link TableDefinition} with this vector index included.
*/
vectorIndex<
IndexName extends string,
VectorField extends ExtractFieldPaths<DocumentType>,
FilterFields extends ExtractFieldPaths<DocumentType> = never,
>(
name: IndexName,
indexConfig: Expand<VectorIndexConfig<VectorField, FilterFields>>,
): TableDefinition<
DocumentType,
Indexes,
SearchIndexes,
Expand<
VectorIndexes &
Record<
IndexName,
{
vectorField: VectorField;
dimensions: number;
filterFields: FilterFields;
}
>
>
> {
this.vectorIndexes.push({
indexDescriptor: name,
vectorField: indexConfig.vectorField,
dimensions: indexConfig.dimensions,
filterFields: indexConfig.filterFields || [],
});
return this;
}
/**
* Work around for https://github.com/microsoft/TypeScript/issues/57035
*/
protected self(): TableDefinition<
DocumentType,
Indexes,
SearchIndexes,
VectorIndexes
> {
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,
vectorIndexes: this.vectorIndexes,
documentType: this.validator.json,
};
}
}
/**
* Define a table in a schema.
*
* You can either specify the schema of your documents as an object like
* ```ts
* defineTable({
* field: v.string()
* });
* ```
*
* or as a schema type like
* ```ts
* defineTable(
* v.union(
* v.object({...}),
* v.object({...})
* )
* );
* ```
*
* @param documentSchema - The type of documents stored in this table.
* @returns A {@link TableDefinition} for the table.
*
* @public
*/
export function defineTable<
DocumentSchema extends Validator<Record<string, any>, "required", any>,
>(documentSchema: DocumentSchema): TableDefinition<DocumentSchema>;
/**
* Define a table in a schema.
*
* You can either specify the schema of your documents as an object like
* ```ts
* defineTable({
* field: v.string()
* });
* ```
*
* or as a schema type like
* ```ts
* defineTable(
* v.union(
* v.object({...}),
* v.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, GenericValidator>,
>(
documentSchema: DocumentSchema,
): TableDefinition<VObject<ObjectType<DocumentSchema>, DocumentSchema>>;
export function defineTable<
DocumentSchema extends
| Validator<Record<string, any>, "required", any>
| Record<string, GenericValidator>,
>(documentSchema: DocumentSchema): TableDefinition<any, any, any> {
if (isValidator(documentSchema)) {
return new TableDefinition(documentSchema);
} else {
return new TableDefinition(v.object(documentSchema));
}
}
/**
* A type describing the schema of a Convex project.
*
* This should be constructed using {@link defineSchema}, {@link defineTable},
* and {@link v}.
* @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,
StrictTableTypes extends boolean,
> {
public tables: Schema;
public strictTableNameTypes!: StrictTableTypes;
private readonly schemaValidation: boolean;
/**
* @internal
*/
constructor(tables: Schema, options?: DefineSchemaOptions<StrictTableTypes>) {
this.tables = tables;
this.schemaValidation =
options?.schemaValidation === undefined ? true : options.schemaValidation;
}
/**
* Export the contents of this definition.
*
* This is called internally by the Convex framework.
* @internal
*/
export(): string {
return JSON.stringify({
tables: Object.entries(this.tables).map(([tableName, definition]) => {
const { indexes, searchIndexes, vectorIndexes, documentType } =
definition.export();
return {
tableName,
indexes,
searchIndexes,
vectorIndexes,
documentType,
};
}),
schemaValidation: this.schemaValidation,
});
}
}
/**
* Options for {@link defineSchema}.
*
* @public
*/
export interface DefineSchemaOptions<StrictTableNameTypes extends boolean> {
/**
* Whether Convex should validate at runtime that all documents match
* your schema.
*
* If `schemaValidation` is `true`, Convex will:
* 1. Check that all existing documents match your schema when your schema
* is pushed.
* 2. Check that all insertions and updates match your schema during mutations.
*
* If `schemaValidation` is `false`, Convex will not validate that new or
* existing documents match your schema. You'll still get schema-specific
* TypeScript types, but there will be no validation at runtime that your
* documents match those types.
*
* By default, `schemaValidation` is `true`.
*/
schemaValidation?: boolean;
/**
* Whether the TypeScript types should allow accessing tables not in the schema.
*
* If `strictTableNameTypes` is `true`, using tables not listed in the schema
* will generate a TypeScript compilation error.
*
* If `strictTableNameTypes` is `false`, you'll be able to access tables not
* listed in the schema and their document type will be `any`.
*
* `strictTableNameTypes: false` is useful for rapid prototyping.
*
* Regardless of the value of `strictTableNameTypes`, your schema will only
* validate documents in the tables listed in the schema. You can still create
* and modify other tables on the dashboard or in JavaScript mutations.
*
* By default, `strictTableNameTypes` is `true`.
*/
strictTableNameTypes?: StrictTableNameTypes;
}
/**
* 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,
StrictTableNameTypes extends boolean = true,
>(
schema: Schema,
options?: DefineSchemaOptions<StrictTableNameTypes>,
): SchemaDefinition<Schema, StrictTableNameTypes> {
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 DocumentType,
infer Indexes,
infer SearchIndexes,
infer VectorIndexes
>
? {
// We've already added all of the system fields except for `_id`.
// Add that here.
document: Expand<IdField<TableName> & ExtractDocument<DocumentType>>;
fieldPaths:
| keyof IdField<TableName>
| ExtractFieldPaths<DocumentType>;
indexes: Expand<Indexes & SystemIndexes>;
searchIndexes: SearchIndexes;
vectorIndexes: VectorIndexes;
}
: never;
},
SchemaDef["strictTableNameTypes"]
>;
type MaybeMakeLooseDataModel<
DataModel extends GenericDataModel,
StrictTableNameTypes extends boolean,
> = StrictTableNameTypes extends true
? DataModel
: Expand<DataModel & AnyDataModel>;
const systemSchema = defineSchema({
_scheduled_functions: defineTable({
name: v.string(),
args: v.array(v.any()),
scheduledTime: v.float64(),
completedTime: v.optional(v.float64()),
state: v.union(
v.object({ kind: v.literal("pending") }),
v.object({ kind: v.literal("inProgress") }),
v.object({ kind: v.literal("success") }),
v.object({ kind: v.literal("failed"), error: v.string() }),
v.object({ kind: v.literal("canceled") }),
),
}),
_storage: defineTable({
sha256: v.string(),
size: v.float64(),
contentType: v.optional(v.string()),
}),
});
export interface SystemDataModel
extends DataModelFromSchemaDefinition<typeof systemSchema> {}
export type SystemTableNames = TableNamesInDataModel<SystemDataModel>;