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);
  },
});