Configuring Functions
Convex Ents are designed to integrate into your Convex backend by replacing the built-in ctx.db API. This is easy to do by setting your own custom versions of the query and mutation constructor functions, using the convex-helpers package.
You can read a detailed post about customizing function constructors on Stack (opens in a new tab).
Recommended setup: Replace function constructors and hide ctx.db
We recommend having a dedicated file to configure your custom function constructors. Add a functions.ts file with the following contents:
import { customCtx, customMutation, customQuery, } from "convex-helpers/server/customFunctions"; import { query as baseQuery, mutation as baseMutation, internalQuery as baseInternalQuery, internalMutation as baseInternalMutation, } from "./_generated/server"; import { entsTableFactory } from "convex-ents"; import { entDefinitions } from "./schema"; export const query = customQuery( baseQuery, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: undefined, }; }) ); export const internalQuery = customQuery( baseInternalQuery, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: undefined, }; }) ); export const mutation = customMutation( baseMutation, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: undefined, }; }) ); export const internalMutation = customMutation( baseInternalMutation, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: undefined, }; }) );With this code you can now import query, internalQuery, mutation and internalMutation from ./functions in the convex folder, instead of importing from "./_generated/server".
Using the same names as the generated function constructors highlights that only the custom constructors should be used throughout your project. We recommend this approach because:
- The
tableAPI comes with an additional level of security - The
tableAPI preserves invariants, such as:- fields having unique values
- 1:1 edges being unique on each end of the edge
- deleting ents deletes corresponding edges
Incremental adoption: Restrict ctx.db
If you already have code using the built-in ctx.db API, and you want to adopt ctx.table for new code, you can keep ctx.db available, and restrict it to a set of tables.
In this example "messages" and "users" are accessible via ctx.db:
import { customCtx, customMutation, customQuery, } from "convex-helpers/server/customFunctions"; import { query as baseQuery, mutation as baseMutation, internalQuery as baseInternalQuery, internalMutation as baseInternalMutation, } from "./_generated/server"; import { entsTableFactory } from "convex-ents"; import { entDefinitions } from "./schema"; export const query = customQuery( baseQuery, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: ctx.db as unknown as GenericDatabaseReader< Pick<DataModel, "messages" | "users"> >, }; }) ); export const internalQuery = customQuery( baseInternalQuery, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: ctx.db as unknown as GenericDatabaseReader< Pick<DataModel, "messages" | "users"> >, }; }) ); export const mutation = customMutation( baseMutation, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: ctx.db as GenericDatabaseWriter< Pick<DataModel, "messages" | "users"> >, }; }) ); export const internalMutation = customMutation( baseInternalMutation, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: ctx.db as GenericDatabaseWriter< Pick<DataModel, "messages" | "users"> >, }; }) );Remember that this restriction of access via ctx.db is purely a TypeScript enforcement, not a runtime one.
Exposing built-in ctx.db under different name
The ctx.table API can do everything the built-in ctx.db API can do with one expection: It has to know which table you want to read or write into. So for example the following function is not implementable with ctx.table as is:
import { internalMutation } from "./_generated/server"; export const deleteAnyDocument = internalMutation({ args: { id: v.string() }, handler: async (ctx, args) => { await ctx.db.delete(args.id as any); }, });The closest you can get to this functionality with ctx.table is the following:
import { internalMutation } from "./functions"; export const deleteAnyDocument = internalMutation({ args: { table: v.string(), id: v.string() }, handler: async (ctx, args) => { await ctx .table(args.table) .getX(args.id as any) .delete(); }, });If you still want to be able to access the built-in ctx.db in your functions, you can expose it on the customized ctx, perhaps with a descriptive name:
import { customCtx, customMutation, customQuery, } from "convex-helpers/server/customFunctions"; import { query as baseQuery, mutation as baseMutation, internalQuery as baseInternalQuery, internalMutation as baseInternalMutation, } from "./_generated/server"; import { entsTableFactory } from "convex-ents"; import { entDefinitions } from "./schema"; export const query = customQuery( baseQuery, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: undefined, unsafeDB_DO_NOT_USE_OR_YOULL_BE_FIRED: db, }; }) ); export const internalQuery = customQuery( baseInternalQuery, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: undefined, unsafeDB_DO_NOT_USE_OR_YOULL_BE_FIRED: db, }; }) ); export const mutation = customMutation( baseMutation, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: undefined, unsafeDB_DO_NOT_USE_OR_YOULL_BE_FIRED: db, }; }) ); export const internalMutation = customMutation( baseInternalMutation, customCtx(async (ctx) => { return { table: entsTableFactory(ctx, entDefinitions), db: undefined, unsafeDB_DO_NOT_USE_OR_YOULL_BE_FIRED: db, }; }) );You can now access the built-in API like this:
import { internalMutation } from "./functions"; export const deleteAnyDocument = internalMutation({ args: { id: v.string() }, handler: async (ctx, args) => { // This is a detailed explanation of why we're using // the built-in database API even though we are // aware it could break invariants of our ents, // lead to outages for our users, and ultimately // the demise of our enterprise: await ctx.unsafeDB_DO_NOT_USE_OR_YOULL_BE_FIRED.delete(args.id as any); }, });