Writing Ents

Writing Ents to the Database

Just like for reading ents from the database, for writing Convex Ents provide a ctx.table method which replaces the built-in ctx.db object in Convex mutations (opens in a new tab).

Security

The same added level of security applies to the writing ents as it does to reading them.

Inserting a new ent

You can insert new ents into the database with the insert method chained to the result of calling ctx.table:

const taskId = await ctx.table("tasks").insert({ text: "Win at life" });

You can retrieve the just created ent with the get method:

const task = await ctx.table("tasks").insert({ text: "Win at life" }).get();
This is equivalent to the built-in:
const taskId = await ctx.db.insert("tasks", { text: "Win at life" });
const task = (await ctx.db.get(taskId))!;

Inserting many new ents

const taskIds = await ctx
  .table("tasks")
  .insertMany({ text: "Buy socks" }, { text: "Buy socks" });

Updating existing ents

To update an existing ent, call the patch or replace method on a lazy Promise of an ent, or on an already retrieved ent:

await ctx.table("tasks").getX(taskId).patch({ text: "Changed text" });
await ctx.table("tasks").getX(taskId).replace({ text: "Changed text" });
const task = await ctx.table("tasks").getX(taskId);
await task.patch({ text: "Changed text" });
await task.replace({ text: "Changed text" });

See the docs for the built-in patch and replace methods (opens in a new tab) for the difference between them.

Deleting ents

To delete an ent, call the delete method on a lazy Promise of an ent, or on an already retrieved ent:

await ctx.table("tasks").getX(taskId).delete();
const task = await ctx.table("tasks").getX(taskId);
await task.delete();

Cascading deletes

See the Cascading Deletes page for how to configure how deleting an ent affects its edges and other ents connected to it.

Creating edges

Edges can be created together with ents using the insert and insertMany methods, or they can be created for two existing ents using the replace and patch methods.

Creating 1:1 and 1:many edges

A 1:1 or 1:many edge can be created by specifying the ID of the other ent on the ent which stores the edge, either when inserting:

// First we need a user, which can have an optional profile edge
const userId = await ctx.table("users").insert({ name: "Alice" });
// Now we can create a profile with the 1:1 edge to the user
const profileId = await ctx
  .table("profiles")
  .insert({ bio: "In Wonderland", userId });

or when updating:

const profileId = await ctx.table("profiles").getX(profileId).patch({ userId });
This is equivalent to the built-in:
const posts = await ctx.db.patch(profileId, { userId });

with the addition of checking that profileId belongs to "profiles".

Creating many:many edges

Many:many edges can be created by listing the IDs of the other ents when inserting ents on either side of the edge:

// First we need a tag, which can have many:many edge to messages
const tagId = await ctx.table("tags").insert({ name: "Blue" });
// Now we can create a message with a many:many edge to the tag
const messageId = await ctx
  .table("messages")
  .insert({ text: "Hello world", tags: [tagId] });

But we could have equally created a message first, and then created a tag with a list of message IDs.

The replace method also expects a list of IDs:

await ctx
  .table("messages")
  .getX(messageId)
  .replace({ text: "Changed message", tags: [tagID, otherTagID] });

Because replace essentially behaves like a delete + insert but preserves the _id and _creationTime fields of the replaced ent, any edges which are not listed will be deleted.

The patch method on the other hand expects a description of the changes that should be made, a list of IDs to add and remove edges for:

const message = await ctx.table("messages").getX(messageId);
await message.patch({ tags: { add: [tagID] } });
await message.patch({
  tags: { add: [tagID, otherTagID], remove: [tagToDeleteID] },
});

Any edges in the add list that didn't exist are created, and any edges in the remove list that did exist are deleted. Edges to ents with ID not listed in either list are not affected by patch.

Updating ents connected by edges

The patch, replace and delete methods can be chained after edge calls to update the ent on the other end of an edge:

await ctx
  .table("users")
  .getX(userId)
  .edgeX("profile")
  .patch({ bio: "I'm the first user" });
This is equivalent to the built-in:
const profile = await ctx.db
  .query("profiles")
  .withIndex("userId", (q) => q.eq("userId", userId))
  .unique();
if (profile === null) {
  throw new Error(
    `Edge "profile" does not exist for document wit ID "${userId}"`,
  );
}
await ctx.db.patch(profile._id, { bio: "I'm the first user" });
Limitation: edge called on a loaded ent

The following code does not typecheck currently (opens in a new tab):

const user = await ctx.table("users").getX(userId);
await user.edgeX("profile").patch({ bio: "I'm the first user" });

You can either disable typechecking via // @ts-expect-error or preferably start with ctx.table:

const user = ... // loaded or passed via `map`
await ctx
  .table("users")
  .getX(user._id).edgeX("profile").patch({ bio: "I'm the first user" });