import type { FieldTypeFromFieldPath, TableNamesInDataModel, GenericDataModel, GenericDatabaseReader, DocumentByName, SystemTableNames, SystemDataModel, NamedIndex, NamedTableInfo, IndexNames, FieldPaths, } from "convex/server"; import type { GenericId } from "convex/values"; import { asyncMap, nullThrows } from "../index.js"; /** * Gets a document by its ID. Throws if not found. * * @param ctx The database reader to use to get the document. * @param table The table name. * @param id The id of the document to get. * @returns The document with the given ID. */ export async function getOrThrow< DataModel extends GenericDataModel, Table extends TableNamesInDataModel, >( ctx: { db: GenericDatabaseReader }, table: Table, id: GenericId>, ): Promise>; /** * Gets a document by its ID. Throws if not found. * @param ctx The database reader to use to get the document. * @param id The id of the document to get. * @returns The document with the given ID. */ export async function getOrThrow< DataModel extends GenericDataModel, Table extends TableNamesInDataModel, >( ctx: { db: GenericDatabaseReader }, id: GenericId, ): Promise>; export async function getOrThrow( ctx: any, arg1: any, arg2?: any, ): Promise { const [table, id]: [string | null, GenericId] = arg2 !== undefined ? [arg1, arg2] : [null, arg1]; const doc = table ? await ctx.db.get(table, id) : // eslint-disable-next-line @convex-dev/explicit-table-ids -- table not available here await ctx.db.get(id); if (!doc) { throw new Error(`Could not find id ${id}`); } return doc; } /** * getAll returns a list of Documents (or null) for the `Id`s passed in. * * Nulls are returned for documents not found. * @param db A DatabaseReader, usually passed from a mutation or query ctx. * @param table The table name. * @param ids An list (or other iterable) of Ids pointing to a table. * @returns The Documents referenced by the Ids, in order. `null` if not found. */ export async function getAll< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel, >( db: GenericDatabaseReader, table: TableName, ids: | Iterable>> | Promise>>>, ): Promise<(DocumentByName | null)[]>; /** * getAll returns a list of Documents (or null) for the `Id`s passed in. * * Nulls are returned for documents not found. * @param db A DatabaseReader, usually passed from a mutation or query ctx. * @param ids An list (or other iterable) of Ids pointing to a table. * @returns The Documents referenced by the Ids, in order. `null` if not found. */ export async function getAll< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel, >( db: GenericDatabaseReader, ids: Iterable> | Promise>>, ): Promise<(DocumentByName | null)[]>; export async function getAll(db: any, arg1: any, arg2?: any): Promise { const [table, ids]: [string | null, any] = arg2 !== undefined ? [arg1, arg2] : [null, arg1]; return table ? asyncMap(ids, (id) => db.get(table, id)) : asyncMap(ids, (id) => db.get(id)); } /** * getAllOrThrow returns a list of Documents for the `Id`s passed in. * * It throws if any documents are not found (null). * @param db A DatabaseReader, usually passed from a mutation or query ctx. * @param table The table name. * @param ids An list (or other iterable) of Ids pointing to a table. * @returns The Documents referenced by the Ids, in order. */ export async function getAllOrThrow< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel, >( db: GenericDatabaseReader, table: TableName, ids: | Iterable>> | Promise>>>, ): Promise[]>; /** * getAllOrThrow returns a list of Documents for the `Id`s passed in. * * It throws if any documents are not found (null). * @param db A DatabaseReader, usually passed from a mutation or query ctx. * @param ids An list (or other iterable) of Ids pointing to a table. * @returns The Documents referenced by the Ids, in order. */ export async function getAllOrThrow< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel, >( db: GenericDatabaseReader, ids: Iterable> | Promise>>, ): Promise[]>; export async function getAllOrThrow( db: any, arg1: any, arg2?: any, ): Promise { const [table, ids]: [string | null, any] = arg2 !== undefined ? [arg1, arg2] : [null, arg1]; if (table) { return await asyncMap(ids, (id: any) => getOrThrow({ db }, table as any, id), ); } else { return await asyncMap(ids, (id: any) => getOrThrow({ db }, id)); } } type UserIndexes< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel, > = Exclude< IndexNames>, "by_creation_time" > & string; type TablesWithLookups = { [T in TableNamesInDataModel]: UserIndexes< DataModel, T > extends never ? never : T; }[TableNamesInDataModel]; type FirstIndexField< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel, IndexName extends IndexNames>, > = NamedIndex, IndexName>[0]; type TypeOfFirstIndexField< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel, IndexName extends IndexNames>, > = IndexName extends IndexNames> ? FieldTypeFromFieldPath< DocumentByName, NamedIndex, IndexName>[0] > : never; // `FieldPath`s that have an index starting with them // e.g. `.index("...", [FieldPath, ...])` on the table. type LookupFieldPaths< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel, > = { [IndexName in UserIndexes]: FirstIndexField< DataModel, TableName, IndexName >; }[UserIndexes]; // If the index is named after the first field, then the field name is optional. // To be used as a spread argument to optionally require the field name. // It also allows a field to have an index `by_${field}`, though this means // it doesn't allow fields that start with a `by_` prefix. type FieldIfDoesntMatchIndex< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel, IndexName extends UserIndexes, > = FirstIndexField extends IndexName ? // Enforce the variable itself doesn't start with "by_" IndexName extends `by_${infer _}` ? never : [FirstIndexField?] : `by_${FirstIndexField}` extends IndexName ? [FirstIndexField?] : [FirstIndexField]; function firstIndexField< DataModel extends GenericDataModel, TableName extends TablesWithLookups, IndexName extends UserIndexes, >( index: IndexName, field?: FirstIndexField, ): FirstIndexField { if (field) return field; if (index.startsWith("by_")) return index.slice(3); return index; } /** * Get a document matching the given value for a specified field. * * `null` if not found. * Useful for fetching a document with a one-to-one relationship via backref. * Requires the table to have an index on the field named the same as the field. * e.g. `defineTable({ fieldA: v.string() }).index("fieldA", ["fieldA"])` * * Getting 'string' is not assignable to parameter of type 'never'? * Make sure your index is named after your field. * * @param db DatabaseReader, passed in from the function ctx * @param table The table to fetch the target document from. * @param index The index on that table to look up the specified value by. * @param value The value to look up the document by, often an ID. * @param field The field on that table that should match the specified value. * Optional if the index is named after the field. * @returns The document matching the value, or null if none found. */ export async function getOneFrom< DataModel extends GenericDataModel, TableName extends TablesWithLookups, IndexName extends UserIndexes, >( db: GenericDatabaseReader, table: TableName, index: IndexName, value: TypeOfFirstIndexField, ...fieldArg: FieldIfDoesntMatchIndex ): Promise | null> { const field = firstIndexField(index, fieldArg[0]); return db .query(table) .withIndex(index, (q) => q.eq(field, value)) .unique(); } /** * Get a document matching the given value for a specified field. * * Throws if not found. * Useful for fetching a document with a one-to-one relationship via backref. * Requires the table to have an index on the field named the same as the field. * e.g. `defineTable({ fieldA: v.string() }).index("fieldA", ["fieldA"])` * * Getting 'string' is not assignable to parameter of type 'never'? * Make sure your index is named after your field. * * @param db DatabaseReader, passed in from the function ctx * @param table The table to fetch the target document from. * @param index The index on that table to look up the specified value by. * @param value The value to look up the document by, often an ID. * @param field The field on that table that should match the specified value. * Optional if the index is named after the field. * @returns The document matching the value. Throws if not found. */ export async function getOneFromOrThrow< DataModel extends GenericDataModel, TableName extends TablesWithLookups, IndexName extends UserIndexes, >( db: GenericDatabaseReader, table: TableName, index: IndexName, value: TypeOfFirstIndexField, ...fieldArg: FieldIfDoesntMatchIndex ): Promise> { const field = firstIndexField(index, fieldArg[0]); const ret = await db .query(table) .withIndex(index, (q) => q.eq(field, value)) .unique(); return nullThrows( ret, `Can't find a document in ${table} with field ${field} equal to ${value}`, ); } /** * Get a list of documents matching the given value for a specified field. * * Useful for fetching many documents related to a given value via backrefs. * Requires the table to have an index on the field named the same as the field. * e.g. `defineTable({ fieldA: v.string() }).index("fieldA", ["fieldA"])` * * Getting 'string' is not assignable to parameter of type 'never'? * Make sure your index is named after your field. * * @param db DatabaseReader, passed in from the function ctx * @param table The table to fetch the target document from. * @param index The index on that table to look up the specified value by. * @param value The value to look up the document by, often an ID. * @param field The field on that table that should match the specified value. * Optional if the index is named after the field. * @returns The documents matching the value, if any. */ export async function getManyFrom< DataModel extends GenericDataModel, TableName extends TablesWithLookups, IndexName extends UserIndexes, >( db: GenericDatabaseReader, table: TableName, index: IndexName, value: TypeOfFirstIndexField, ...fieldArg: FieldIfDoesntMatchIndex ): Promise[]> { const field = firstIndexField(index, fieldArg[0]); return db .query(table) .withIndex(index, (q) => q.eq(field, value)) .collect(); } // File paths to fields that are IDs, excluding "_id". type IdFilePaths< DataModel extends GenericDataModel, InTableName extends TableNamesInDataModel, TableName extends TableNamesInDataModel | SystemTableNames, > = { [FieldName in FieldPaths< NamedTableInfo >]: FieldTypeFromFieldPath< DocumentByName, FieldName > extends GenericId ? FieldName extends "_id" ? never : FieldName : never; }[FieldPaths>]; // Whether a table has an ID field that isn't its sole lookup field. // These can operate as join tables, going from one table to another. // One field has an indexed field for lookup, and another has the ID to get. type LookupAndIdFilePaths< DataModel extends GenericDataModel, TableName extends TablesWithLookups, > = { [FieldPath in IdFilePaths< DataModel, TableName, TableNamesInDataModel | SystemTableNames >]: LookupFieldPaths extends FieldPath ? never : true; }[IdFilePaths< DataModel, TableName, TableNamesInDataModel | SystemTableNames >]; // The table names that match LookupAndIdFields. // These are the possible "join" or "edge" or "relationship" tables. type JoinTables = { [TableName in TablesWithLookups]: LookupAndIdFilePaths< DataModel, TableName > extends never ? never : TableName; }[TablesWithLookups]; type DocumentByNameOrSystem< DataModel extends GenericDataModel, TableName extends TableNamesInDataModel | SystemTableNames, > = TableName extends TableNamesInDataModel ? DocumentByName : TableName extends SystemTableNames ? SystemDataModel[TableName]["document"] : never; // many-to-many via lookup table /** * Get related documents by using a join table. * * Any missing documents referenced by the join table will be null. * It will find all join table entries matching a value, then look up all the * documents pointed to by the join table entries. Useful for many-to-many * relationships. * * Requires your join table to have an index on the fromField named the same as * the fromField, and another field that is an Id type. * e.g. `defineTable({ a: v.string(), b: v.id("users") }).index("a", ["a"])` * * Getting 'string' is not assignable to parameter of type 'never'? * Make sure your index is named after your field. * * @param db DatabaseReader, passed in from the function ctx * @param table The table to fetch the target document from. * @param toField The ID field on the table pointing at target documents. * @param index The index on the join table to look up the specified value by. * @param value The value to look up the documents in join table by. * @param field The field on the join table to match the specified value. * Optional if the index is named after the field. * @returns The documents targeted by matching documents in the table, if any. */ export async function getManyVia< DataModel extends GenericDataModel, JoinTableName extends JoinTables, ToField extends IdFilePaths< DataModel, JoinTableName, TableNamesInDataModel | SystemTableNames >, IndexName extends UserIndexes, TargetTableName extends FieldTypeFromFieldPath< DocumentByName, ToField > extends GenericId ? TargetTableName : never, >( db: GenericDatabaseReader, table: JoinTableName, toField: ToField, index: IndexName, value: TypeOfFirstIndexField, ...fieldArg: FieldIfDoesntMatchIndex ): Promise<(DocumentByNameOrSystem | null)[]> { return await asyncMap( await getManyFrom(db, table, index, value, ...fieldArg), async (link: DocumentByName) => { const id = link[toField] as GenericId; try { // eslint-disable-next-line @convex-dev/explicit-table-ids -- table not available here return (await db.get(id)) as any; } catch { // eslint-disable-next-line @convex-dev/explicit-table-ids -- table not available here return (await db.system.get(id as GenericId)) as any; } }, ); } /** * Get related documents by using a join table. * * Throws an error if any documents referenced by the join table are missing. * It will find all join table entries matching a value, then look up all the * documents pointed to by the join table entries. Useful for many-to-many * relationships. * * Requires your join table to have an index on the fromField named the same as * the fromField, and another field that is an Id type. * e.g. `defineTable({ a: v.string(), b: v.id("users") }).index("a", ["a"])` * * Getting 'string' is not assignable to parameter of type 'never'? * Make sure your index is named after your field. * * @param db DatabaseReader, passed in from the function ctx * @param table The table to fetch the target document from. * @param toField The ID field on the table pointing at target documents. * @param index The index on the join table to look up the specified value by. * @param value The value to look up the documents in join table by. * @param field The field on the join table to match the specified value. * Optional if the index is named after the field. * @returns The documents targeted by matching documents in the table, if any. */ export async function getManyViaOrThrow< DataModel extends GenericDataModel, JoinTableName extends JoinTables, ToField extends IdFilePaths< DataModel, JoinTableName, TableNamesInDataModel | SystemTableNames >, IndexName extends UserIndexes, TargetTableName extends FieldTypeFromFieldPath< DocumentByName, ToField > extends GenericId ? TargetTableName : never, >( db: GenericDatabaseReader, table: JoinTableName, toField: ToField, index: IndexName, value: TypeOfFirstIndexField, ...fieldArg: FieldIfDoesntMatchIndex ): Promise[]> { return await asyncMap( await getManyFrom(db, table, index, value, ...fieldArg), async (link: DocumentByName) => { const id = link[toField] as GenericId; try { return nullThrows( // eslint-disable-next-line @convex-dev/explicit-table-ids -- table not available here (await db.get(id)) as any, `Can't find document ${id} referenced in ${table}'s field ${toField} for ${ fieldArg[0] ?? index } equal to ${value}`, ); } catch { return nullThrows( // eslint-disable-next-line @convex-dev/explicit-table-ids -- table not available here (await db.system.get(id as GenericId)) as any, `Can't find document ${id} referenced in ${table}'s field ${toField} for ${ fieldArg[0] ?? index } equal to ${value}`, ); } }, ); } /** * This prevents TypeScript from inferring that the generic `TableName` type is * a union type when `table` and `id` disagree. */ type NonUnion = T extends never // `never` is the bottom type for TypeScript unions ? never : T;