UNPKG

convex-helpers

Version:

A collection of useful code to complement the official convex package.

206 lines (199 loc) 6.29 kB
import { defineTable, QueryBuilder, MutationBuilder, GenericDataModel, WithoutSystemFields, DocumentByName, RegisteredMutation, RegisteredQuery, FunctionVisibility, paginationOptsValidator, PaginationResult, } from "convex/server"; import { GenericId, Infer, ObjectType, Validator, v } from "convex/values"; import { Expand } from ".."; /** * Define a table with system fields _id and _creationTime. This also returns * helpers for working with the table in validators. See: * https://stack.convex.dev/argument-validation-without-repetition#table-helper-for-schema-definition--validation * * @param name The table name. This should also be used in defineSchema. * @param fields Table fields, as you'd pass to defineTable. * @returns Object of shape: { * table: from defineTable, * withSystemFields: Input fields with _id and _creationTime, * withoutSystemFields: The fields passed in, * doc: a validator for the table doc as a v.object(). This is useful when * defining arguments to actions where you're passing whole documents. * } */ export function Table< T extends Record<string, Validator<any, any, any>>, TableName extends string >(name: TableName, fields: T) { const table = defineTable(fields); const _id = v.id(name); const systemFields = { _id, _creationTime: v.number(), }; const withSystemFields = { ...fields, ...systemFields, } as Expand<T & typeof systemFields>; return { name, table, doc: v.object(withSystemFields), withoutSystemFields: fields, withSystemFields, systemFields, _id, }; } /** * * @param envVarName - The missing environment variable, e.g. OPENAI_API_KEY * @param whereToGet - Where to get it, e.g. "https://platform.openai.com/account/api-keys" * @returns A string with instructions on how to set the environment variable. */ export function missingEnvVariableUrl(envVarName: string, whereToGet: string) { const deployment = deploymentName(); if (!deployment) return `Missing ${envVarName} in environment variables.`; return ( `\n Missing ${envVarName} in environment variables.\n\n` + ` Get it from ${whereToGet} .\n Paste it on the Convex dashboard:\n` + ` https://dashboard.convex.dev/d/${deployment}/settings/environment-variables?var=${envVarName}` ); } /** * Get the deployment name from the CONVEX_CLOUD_URL environment variable. * @returns The deployment name, like "screaming-lemur-123" */ export function deploymentName() { const url = process.env.CONVEX_CLOUD_URL; if (!url) return undefined; const regex = new RegExp("https://(.+).convex.cloud"); return regex.exec(url)?.[1]; } import { partial } from "../validators"; /** * Create CRUD operations for a table. * You can expose these operations in your API. For example, in convex/users.ts: * * ```ts * // in convex/users.ts * import { crud } from "convex-helpers/server"; * import { query, mutation } from "./convex/_generated/server"; * * const Users = Table("users", { * name: v.string(), * ///... * }); * * export const { create, read, paginate, update, destroy } = * crud(Users, query, mutation); * ``` * * Then from a client, you can access `api.users.create`. * * @param table The table to create CRUD operations for. * Of type returned from Table() in "convex-helpers/server". * @param query The query to use - use internalQuery or query from * "./convex/_generated/server" or a customQuery. * @param mutation The mutation to use - use internalMutation or mutation from * "./convex/_generated/server" or a customMutation. * @returns An object with create, read, update, and delete functions. */ export function crud< Fields extends Record<string, Validator<any, any, any>>, TableName extends string, DataModel extends GenericDataModel, QueryVisibility extends FunctionVisibility, MutationVisibility extends FunctionVisibility >( table: { name: TableName; _id: Validator<GenericId<TableName>>; withoutSystemFields: Fields; }, query: QueryBuilder<DataModel, QueryVisibility>, mutation: MutationBuilder<DataModel, MutationVisibility> ) { return { create: mutation({ args: table.withoutSystemFields, handler: async (ctx, args) => { const id = await ctx.db.insert( table.name, args as unknown as WithoutSystemFields< DocumentByName<DataModel, TableName> > ); return (await ctx.db.get(id))!; }, }) as RegisteredMutation< MutationVisibility, ObjectType<Fields>, Promise<DocumentByName<DataModel, TableName>> >, read: query({ args: { id: table._id }, handler: async (ctx, args) => { return await ctx.db.get(args.id); }, }) as RegisteredQuery< QueryVisibility, { id: GenericId<TableName> }, Promise<DocumentByName<DataModel, TableName> | null> >, paginate: query({ args: { paginationOpts: paginationOptsValidator, }, handler: async (ctx, args) => { return ctx.db.query(table.name).paginate(args.paginationOpts); }, }) as RegisteredQuery< QueryVisibility, { paginationOpts: Infer<typeof paginationOptsValidator> }, Promise<PaginationResult<DocumentByName<DataModel, TableName>>> >, update: mutation({ args: { id: v.id(table.name), patch: v.object(partial(table.withoutSystemFields)), }, handler: async (ctx, args) => { await ctx.db.patch( args.id, args.patch as Partial<DocumentByName<DataModel, TableName>> ); }, }) as RegisteredMutation< MutationVisibility, { id: GenericId<TableName>; patch: Partial< WithoutSystemFields<DocumentByName<DataModel, TableName>> >; }, Promise<void> >, destroy: mutation({ args: { id: table._id }, handler: async (ctx, args) => { const old = await ctx.db.get(args.id); if (old) { await ctx.db.delete(args.id); } return old; }, }) as RegisteredMutation< MutationVisibility, { id: GenericId<TableName> }, Promise<null | DocumentByName<DataModel, TableName>> >, }; }