Configuring Functions

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:

convex/functions.ts
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:

  1. The table API comes with an additional level of security
  2. The table API 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:

convex/functions.ts
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:

convex/myFunctions.ts
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:

convex/myFunctions.ts
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:

convex/functions.ts
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:

convex/myFunctions.ts
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);  }, });