Setup

Setup Ents in a new Convex project

Got an existing project? Follow this guide instead.

Install the NPM library

npm install convex-ents convex-helpers

This also installs convex-helpers, used in step 3.

Create a schema

Create your schema file, for example:

convex/schema.ts
import { v } from "convex/values";
import { defineEnt, defineEntSchema, getEntDefinitions } from "convex-ents";
 
const schema = defineEntSchema({
  messages: defineEnt({
    text: v.string(),
  })
    .edge("user")
    .edges("tags"),
 
  users: defineEnt({
    name: v.string(),
  }).edges("messages", { ref: true }),
 
  tags: defineEnt({
    name: v.string(),
  }).edges("messages"),
});
 
export default schema;
 
export const entDefinitions = getEntDefinitions(schema);

For more details on declaring the schema see Ent Schema.

Setup custom functions

Add a functions.ts file with the following contents:

Click to show
convex/functions.ts
import { entsTableFactory } from "convex-ents";
import {
  customCtx,
  customMutation,
  customQuery,
} from "convex-helpers/server/customFunctions";
import {
  internalMutation as baseInternalMutation,
  internalQuery as baseInternalQuery,
  mutation as baseMutation,
  query as baseQuery,
} from "./_generated/server";
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,
    };
  }),
);

For more details see Configuring Functions.

Add helper types

Add a types.ts file with the following contents:

Click to show
convex/types.ts
import { GenericEnt, GenericEntWriter } from "convex-ents";
import { CustomCtx } from "convex-helpers/server/customFunctions";
import { TableNames } from "./_generated/dataModel";
import { mutation, query } from "./functions";
import { entDefinitions } from "./schema";
 
export type QueryCtx = CustomCtx<typeof query>;
export type MutationCtx = CustomCtx<typeof mutation>;
 
export type Ent<TableName extends TableNames> = GenericEnt<
  typeof entDefinitions,
  TableName
>;
export type EntWriter<TableName extends TableNames> = GenericEntWriter<
  typeof entDefinitions,
  TableName
>;

Use custom functions to read and write ents

In any of your Convex backend code, use the custom functions you set up in the previous step instead of the ones that come from _generated, which will provide ctx.table instead of ctx.db:

convex/messages.ts
import { v } from "convex/values";
import { mutation, query } from "./functions";
 
export const list = query(async (ctx) => {
  return await ctx.table("messages").map(async (message) => ({
    text: message.text,
    author: (await message.edge("user")).name,
  }));
});
 
export const send = mutation({
  args: { text: v.string(), userId: v.id("users") },
  handler: async (ctx, { text, userId }) => {
    await ctx.table("messages").insert({ text, userId });
  },
});
 
export const tag = mutation({
  args: { messageId: v.id("messages"), tagId: v.id("tags") },
  handler: async (ctx, { messageId, tagId }) => {
    await ctx
      .table("messages")
      .getX(messageId)
      .patch({ tags: { add: [tagId] } });
  },
});

For more details see Reading Ents and Writing Ents.