### Install convex-helpers
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/migrations.mdx
Install the latest version of `convex-helpers` using npm.
```sh
npm i convex-helpers@latest
```
--------------------------------
### Install convex-ents and convex-helpers
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup.mdx
Install the necessary NPM packages for convex-ents and convex-helpers. This command also installs `convex-helpers`, which is used in subsequent steps.
```sh
npm install convex-ents convex-helpers
```
--------------------------------
### Setup Custom Query and Mutation Contexts
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema/rules.mdx
This code replaces the default functions.ts file to integrate custom rule logic into Convex queries and mutations. It defines custom contexts for both public and internal functions, ensuring consistent setup and type inference.
```typescript
import {
customCtx,
customMutation,
customQuery,
} from "convex-helpers/server/customFunctions";
import { entsTableFactory } from "convex-ents";
import {
MutationCtx,
QueryCtx,
internalMutation as baseInternalMutation,
internalQuery as baseInternalQuery,
mutation as baseMutation,
query as baseQuery,
} from "./_generated/server";
import { getEntDefinitionsWithRules, getViewerId } from "./rules";
import { entDefinitions } from "./schema";
export const query = customQuery(
baseQuery,
customCtx(async (baseCtx) => {
return await queryCtx(baseCtx);
}),
);
export const internalQuery = customQuery(
baseInternalQuery,
customCtx(async (baseCtx) => {
return await queryCtx(baseCtx);
}),
);
export const mutation = customMutation(
baseMutation,
customCtx(async (baseCtx) => {
return await mutationCtx(baseCtx);
}),
);
export const internalMutation = customMutation(
baseInternalMutation,
customCtx(async (baseCtx) => {
return await mutationCtx(baseCtx);
}),
);
async function queryCtx(baseCtx: QueryCtx) {
const ctx = {
db: baseCtx.db as unknown as undefined,
skipRules: { table: entsTableFactory(baseCtx, entDefinitions) },
};
const entDefinitionsWithRules = getEntDefinitionsWithRules(ctx as any);
const viewerId = await getViewerId({ ...baseCtx, ...ctx });
(ctx as any).viewerId = viewerId;
const table = entsTableFactory(baseCtx, entDefinitionsWithRules);
(ctx as any).table = table;
// Example: add `viewer` and `viewerX` helpers to `ctx`:
const viewer = async () =>
viewerId !== null ? await table("users").get(viewerId) : null;
(ctx as any).viewer = viewer;
const viewerX = async () => {
const ent = await viewer();
if (ent === null) {
throw new Error("Expected authenticated viewer");
}
return ent;
};
(ctx as any).viewerX = viewerX;
return { ...ctx, table, viewer, viewerX, viewerId };
}
async function mutationCtx(baseCtx: MutationCtx) {
const ctx = {
db: baseCtx.db as unknown as undefined,
skipRules: { table: entsTableFactory(baseCtx, entDefinitions) },
};
const entDefinitionsWithRules = getEntDefinitionsWithRules(ctx as any);
const viewerId = await getViewerId({ ...baseCtx, ...ctx });
(ctx as any).viewerId = viewerId;
const table = entsTableFactory(baseCtx, entDefinitionsWithRules);
(ctx as any).table = table;
// Example: add `viewer` and `viewerX` helpers to `ctx`:
const viewer = async () =>
viewerId !== null ? await table("users").get(viewerId) : null;
(ctx as any).viewer = viewer;
const viewerX = async () => {
const ent = await viewer();
if (ent === null) {
throw new Error("Expected authenticated viewer");
}
return ent;
};
(ctx as any).viewerX = viewerX;
return { ...ctx, table, viewer, viewerX, viewerId };
}
```
--------------------------------
### Example Test File for Sending Messages
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/testing.mdx
An example test file demonstrating how to use `convexTest` and `runCtx` to set up test data, perform mutations, and query messages. It asserts the expected message content and author.
```typescript
import { expect, test } from "vitest";
import { api } from "./_generated/api";
import { convexTest, runCtx } from "./setup.testing";
import schema from "./schema";
test("sending messages", async () => {
const t = convexTest(schema);
const [sarahId, tomId] = await t.run(async (baseCtx) => {
const ctx = await runCtx(baseCtx);
return await ctx
.table("users")
.insertMany([{ name: "Sarah" }, { name: "Tom" }]);
});
await t.mutation(api.messages.send, { text: "Hi!", userId: sarahId });
await t.mutation(api.messages.send, { text: "Hey!", userId: tomId });
const messages = await t.query(api.messages.list);
expect(messages).toMatchObject([
{ body: "Hi!", author: "Sarah" },
{ body: "Hey!", author: "Tom" },
]);
});
```
--------------------------------
### App Component Setup
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/_app.mdx
This is the root component for a Next.js application. It imports global styles and renders the current page component with its props.
```javascript
import "./global.css";
export default function App({ Component, pageProps }) {
return ;
}
```
--------------------------------
### Setup Custom Convex Functions with Ents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup.mdx
Configure custom query and mutation functions to integrate `convex-ents` with your Convex backend. This setup provides a type-safe `ctx.table` for interacting with your Ent schema.
```typescript
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,
};
}),
);
```
--------------------------------
### Equivalent built-in Convex insert and get
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/write.mdx
This demonstrates the equivalent operation using Convex's built-in `ctx.db` methods for comparison.
```typescript
const taskId = await ctx.db.insert("tasks", { text: "Win at life" });
const task = (await ctx.db.get("tasks", taskId))!;
```
--------------------------------
### Annotated Code for Context Setup
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema/rules.mdx
This annotated version explains the step-by-step process of building the context object for Convex functions. It details how to initialize the context with rule-skipping tables, bind rule implementations, retrieve viewer information, and add helper functions for accessing viewer data.
```typescript
// The `ctx` object is mutated as we build it out.
// It starts off with `ctx.skipRules.table`, a version `ctx.table`
// that doesn't use the rules we defined in `rules.ts`:
const ctx = {
db: undefined,
skipRules: { table: entsTableFactory(baseCtx, entDefinitions) },
};
// We bind our rule implementations to this `ctx` object:
const entDefinitionsWithRules = getEntDefinitionsWithRules(ctx as any);
// We retrieve the viewer ID, without using rules (as our rules
// depend on having the viewer loaded), and add it to `ctx`:
const viewerId = await getViewerId({ ...baseCtx, ...ctx });
(ctx as any).viewerId = viewerId;
// We get a `ctx.table` using rules and add it to `ctx`:
const table = entsTableFactory(baseCtx, entDefinitionsWithRules);
(ctx as any).table = table;
// As an example we define helpers that allow retrieving
// the viewer as an ent. These have to be functions, to allow
// our rule implementations to use them as well.
// Anything that we want our rule implementations to have
// access to has to be added to the `ctx`.
const viewer = async () =>
viewerId !== null ? await table("users").get(viewerId) : null;
(ctx as any).viewer = viewer;
const viewerX = async () => {
const ent = await viewer();
if (ent === null) {
throw new Error("Expected authenticated viewer");
}
return ent;
};
(ctx as any).viewerX = viewerX;
// Finally we again list everything we want our
// functions to have access to. We have to do this
```
--------------------------------
### Equivalent to Map with Edges using Built-in Functions
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
This example demonstrates how to achieve the same result as mapping ents with edges using only built-in Convex and JavaScript functionality, querying directly from tables and indexes.
```ts
const usersWithMessagesAndProfile = await Promise.all(
(await ctx.db.query("users").collect()).map(async (user) => {
const [posts, profile] = Promise.all([
ctx.db
.query("messages")
.withIndex("userId", (q) => q.eq("userId", user._id))
.collect(),
(async () => {
const profile = await ctx.db
.query("profiles")
.withIndex("userId", (q) => q.eq("userId", user._id))
.unique();
if (profile === null) {
throw new Error(
`Edge "profile" does not exist for document with ID "${user._id}"`,
);
}
return profile;
})(),
]);
return { name: user.name, posts, profile };
}),
);
```
--------------------------------
### Setup Custom Functions for Ents and Legacy Tables
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup/existing.mdx
Configure custom query and mutation functions to provide type-safe access to Ents and legacy tables. Update the `LegacyTables` type to include your existing tables that will be accessed via `ctx.db`.
```ts
import { entsTableFactory } from "convex-ents";
import {
customAction,
customCtx,
customMutation,
customQuery,
} from "convex-helpers/server/customFunctions";
import { GenericDatabaseReader, GenericDatabaseWriter } from "convex/server";
import { DataModel } from "./_generated/dataModel";
import {
internalMutation as baseInternalMutation,
internalQuery as baseInternalQuery,
mutation as baseMutation,
query as baseQuery,
} from "./_generated/server";
import { entDefinitions } from "./schema";
type LegacyTables = "messages" | "users";
export const query = customQuery(
baseQuery,
customCtx(async (ctx) => {
return {
table: entsTableFactory(ctx, entDefinitions),
db: ctx.db as unknown as GenericDatabaseReader<
Pick
>,
};
}),
);
export const internalQuery = customQuery(
baseInternalQuery,
customCtx(async (ctx) => {
return {
table: entsTableFactory(ctx, entDefinitions),
db: ctx.db as unknown as GenericDatabaseReader<
Pick
>,
};
}),
);
export const mutation = customMutation(
baseMutation,
customCtx(async (ctx) => {
return {
table: entsTableFactory(ctx, entDefinitions),
db: ctx.db as GenericDatabaseWriter>,
};
}),
);
export const internalMutation = customMutation(
baseInternalMutation,
customCtx(async (ctx) => {
return {
table: entsTableFactory(ctx, entDefinitions),
db: ctx.db as GenericDatabaseWriter>,
};
}),
);
```
--------------------------------
### Map Ents with Multiple Edges
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Fetch related data from multiple edges simultaneously using `Promise.all` within a `map` transformation. This example retrieves both messages and profile information for each user.
```ts
const usersWithProfileAndMessages = await ctx.table("users").map(async (user) => {
const [messages, profile] = await Promise.all([
profile: user.edgeX("profile"),
user.edge("messages").take(5),
])
return {
name: user.name,
profile,
messages,
};
});
```
--------------------------------
### Map Ents to Include Edges
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Use the `map` method to transform each ent and add related ents via `edge` or `edgeX`. This example fetches the latest 5 messages for each user.
```ts
const usersWithMessages = await ctx.table("users").map(async (user) => {
return {
name: user.name,
messages: await user.edge("messages").take(5),
};
});
```
--------------------------------
### Use Custom Functions to Read and Write Ents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup.mdx
Utilize the custom query and mutation functions to interact with your Ents. This example shows how to list messages with their authors, send new messages, and tag messages with specific tags.
```typescript
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] } });
},
});
```
--------------------------------
### Update Related Ent via Edge (Typechecking Limitation)
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/write.mdx
This example shows a pattern that currently does not typecheck due to a limitation with chaining edge calls on a pre-loaded ent. Use `ctx.table` to start or disable typechecking with `// @ts-expect-error`.
```typescript
const user = await ctx.table("users").getX(userId);
await user.edgeX("profile").patch({ bio: "I'm the first user" });
```
```typescript
const user = ... // loaded or passed via `map`
await ctx
.table("users")
.getX(user._id).edgeX("profile").patch({ bio: "I'm the first user" });
```
--------------------------------
### Return Specific Fields from Documents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
When returning documents to clients, it's best practice to explicitly pick fields to avoid sending unintended data and maintain backwards compatibility. This example selects specific fields from the 'messages' table.
```ts
import { query } from "./functions";
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.table("messages").map((message) => ({
_id: message._id,
_creationTime: message._creationTime,
text: message.text,
userId: message.userId,
}));
},
});
```
--------------------------------
### Replace Function Constructors with Convex Ents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup/config.mdx
Use this setup to fully replace default Convex function constructors with custom ones that integrate Convex Ents. This approach hides the default `ctx.db` and provides access to Ents via `ctx.table`. It's recommended for new projects or when adopting Ents comprehensively.
```typescript
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,
};
}),
);
```
--------------------------------
### Remove ctx.db from custom functions setup
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup/existing.mdx
Once `ctx.db` is no longer used, remove it from your custom functions setup. This applies to both query and mutation functions.
```diff
// For query and internalQuery:
+ db: undefined,
- db: ctx.db as unknown as GenericDatabaseReader<
- Pick
- >,
// For mutation and internalMutation:
+ db: undefined,
- db: ctx.db as GenericDatabaseWriter<
- Pick
- >,
```
--------------------------------
### Setup convex-test for Ents Schema
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/testing.mdx
This file configures `convexTest` to work with Ents schemas and provides a `runCtx` function for using Ents within `t.run`. It resolves a TypeScript subtyping issue.
```typescript
import { convexTest as baseConvexTest } from "convex-test";
import { SchemaDefinition, StorageActionWriter } from "convex/server";
import { EntDefinition } from "convex-ents";
import { MutationCtx } from "./_generated/server";
import { entDefinitions } from "./schema";
// Work around a TypeScript subtyping issue with Ents schemas
type GenericEntSchema = Record;
export function convexTest(
schema: SchemaDefinition,
) {
return baseConvexTest(schema);
}
// Use inside t.run() to use Ents
export async function runCtx(
ctx: MutationCtx & { storage: StorageActionWriter },
) {
return {
...ctx,
table: entsTableFactory(ctx, entDefinitions),
db: undefined,
};
}
```
--------------------------------
### Querying Related Documents with Ents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/index.mdx
Example of querying related documents using the .edge() method in Convex Ents. This snippet demonstrates fetching team invites and their associated roles.
```typescript
import { query, v } from "convex/values";
export const listTeamInvites = query({
args: { teamId: v.id("teams") },
async handler(ctx, { teamId }) {
return await ctx
.table("teams")
.getX(teamId)
.edge("invites")
.map(async (invite) => ({
_id: invite._id,
email: invite.email,
role: (await invite.edge("role")).name,
})); // `{ _id: Id<"invites">, email: string, role: string }[]`
},
});
```
--------------------------------
### Insert a new profile with a 1:1 edge to a user
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/write.mdx
When inserting a new ent that has a 1:1 or 1:many edge, specify the ID of the related ent. This example shows creating a profile with a `userId`.
```typescript
const userId = await ctx.table("users").insert({ name: "Alice" });
const profileId = await ctx
.table("profiles")
.insert({ bio: "In Wonderland", userId });
```
--------------------------------
### Retrieve Raw Document using `doc()` on an Ent
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Call the `doc()` method on a retrieved ent to get the same object, but typed as a plain Convex document. This is an alternative to using `docs()` on the initial query.
```ts
import { query } from "./functions";
export const list = query({
args: {},
handler: async (ctx) => {
const usersWithMessagesAndProfile = await ctx
.table("users")
.map(async (user) => ({
...user,
profile: await user.edgeX("profile").doc(),
}));
},
});
```
--------------------------------
### Map Ents with Nested Edges
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Traverse multiple edges in a single query using nested `map` and `edge` calls. This example fetches users, their messages, and the tags associated with each message.
```ts
const usersWithMessagesAndTags = await ctx.table("users").map(async (user) => ({
...user,
messages: await user.edge("messages").map(async (message) => ({
text: message.text,
tags: await message.edge("tags"),
})),
}));
```
--------------------------------
### Explicitly Configure Scheduled Deletion Mutation
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema/deletes.mdx
Alternatively, expose the scheduled deletion mutation explicitly by providing its reference to the `scheduledDeleteFactory` and passing it during `entsTableFactory` setup.
```typescript
import { scheduledDeleteFactory } from "convex-ents";
import { entDefinitions } from "./schema";
export const myNameForScheduledDelete = scheduledDeleteFactory(entDefinitions, {
scheduledDelete: internal.someFileName.myNameForScheduledDelete,
});
```
```typescript
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";
import { internal } from "./_generated/api";
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, {
scheduledDelete: internal.someFileName.myNameForScheduledDelete,
}),
db: undefined,
};
}),
);
export const internalMutation = customMutation(
baseInternalMutation,
customCtx(async (ctx) => {
return {
table: entsTableFactory(ctx, entDefinitions, {
scheduledDelete: internal.someFileName.myNameForScheduledDelete,
}),
db: undefined,
};
}),
);
```
--------------------------------
### Retrieve Scheduled Function Status
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema/schedule.mdx
Retrieve the status of scheduled functions connected to your ents. This example maps over 'answers' and fetches the state kind of the associated scheduled function.
```typescript
return ctx.table("answers").map(async ({ question, actionId }) => ({
question,
status:
(await ctx.table("_scheduled_functions").get(actionId)?.state.kind) ??
"stale",
}));
```
--------------------------------
### Update a profile to add a 1:1 edge to a user
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/write.mdx
Use `patch()` to add or update a 1:1 or 1:many edge on an existing ent. This example adds the `userId` to an existing profile.
```typescript
const profileId = await ctx.table("profiles").getX(profileId).patch({ userId });
```
--------------------------------
### Insert a new message with a many:many edge to tags
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/write.mdx
For many:many edges, specify the IDs of related ents as a list when inserting. This example creates a message and links it to a tag using `tags: [tagId]`.
```typescript
const tagId = await ctx.table("tags").insert({ name: "Blue" });
const messageId = await ctx
.table("messages")
.insert({ text: "Hello world", tags: [tagId] });
```
--------------------------------
### Define Convex Ent Schema
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup.mdx
Create your schema file using `defineEntSchema` to declare your data models and their relationships. This example defines 'messages', 'users', and 'tags' Ents with specified fields and edges.
```typescript
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);
```
--------------------------------
### Mutating Data with Ents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/index.mdx
Example of accepting a team invite using a Convex Ent mutation. This snippet shows inserting a new member, deleting the invite, and returning the team slug.
```typescript
import { mutation, v } from "convex/values";
export const acceptInvite = mutation({
args: { inviteId: v.id("invites") },
async handler(ctx, { inviteId }) {
const invite = await ctx.table("invites").getX(inviteId);
await ctx.table("members").insert({
teamId: invite.teamId,
userId: ctx.viewerId,
roleId: invite.roleId,
});
await invite.delete();
return (await invite.edge("team")).slug;
},
});
```
--------------------------------
### Define Ent Rules with addEntRules
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema/rules.mdx
Use `addEntRules` to augment your ent definitions with custom authorization rules. This example shows how to define a read rule for the 'secrets' table, ensuring only the viewer can access their own secrets. It also includes a helper function to retrieve the viewer's ID.
```typescript
import { addEntRules } from "convex-ents";
import { entDefinitions } from "./schema";
import { QueryCtx } from "./types";
export function getEntDefinitionsWithRules(
ctx: QueryCtx,
): typeof entDefinitions {
return addEntRules(entDefinitions, {
// "secrets" is one of our tables
secrets: {
read: async (secret) => {
// Example: Only the viewer can see their secret
return ctx.viewerId === secret.userId;
},
},
});
}
// Example: Retrieve viewer ID using `ctx.auth`:
export async function getViewerId(
ctx: Omit,
): Promise | null> {
const user = await ctx.auth.getUserIdentity();
if (user === null) {
return null;
}
const viewer = await ctx.skipRules
.table("users")
.get("tokenIdentifier", user.tokenIdentifier);
return viewer?._id;
}
```
--------------------------------
### Find First Ent
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Get the first ent from a table, optionally after applying ordering or filtering. Useful when chaining operations.
```ts
const latestUser = await ctx.table("users").order("desc").first();
```
--------------------------------
### Order Ents by Creation Time
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Sort ents by their creation time in ascending or descending order using the `order` method. The default is ascending.
```ts
const posts = await ctx.table("posts").order("desc");
```
```ts
const posts = await ctx.db.query("posts").order("desc").collect();
```
--------------------------------
### Accessing System Tables
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Explains how to access and query Convex's built-in system tables.
```APIDOC
## Accessing System Tables
### Description
Provides access to Convex's internal system tables for metadata and management purposes.
### Method
`ctx.table.system("system_table_name")`
### Endpoint
N/A (Client-side operation)
### Parameters
#### Query Parameters
- **system_table_name** (string) - Required - The name of the system table (e.g., `_storage`, `_scheduled_functions`).
### Request Example
```ts
const filesMetadata = await ctx.table.system("_storage");
const scheduledFunctions = await ctx.table.system("_scheduled_functions");
```
### Response
#### Success Response (200)
- **Array of system ents** - Documents from the specified system table.
#### Response Example
```json
[
{ "_id": "...", ... } // Example for _storage or _scheduled_functions
]
```
```
--------------------------------
### Restrict ctx.db access to tables
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup/existing.mdx
Amend your `functions.ts` file to prevent `ctx.db` access to certain tables. This example removes access to the 'messages' table.
```diff
- type LegacyTables = "messages" | "users";
+ type LegacyTables = "users";
```
--------------------------------
### Retrieve Raw Documents using `docs()`
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Use the `docs()` method on a table query to retrieve raw Convex documents instead of ents with methods. This is useful when client-side methods are not needed.
```ts
import { query } from "./functions";
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.table("messages").docs();
},
});
```
--------------------------------
### Get File Metadata via Edge
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema/files.mdx
Traverse the file edge to retrieve the metadata of the connected file. This is useful for accessing file details directly from the Ent.
```typescript
const photoMetadata = await user.edge("photo");
```
--------------------------------
### Equivalent Update Using Direct Query
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/write.mdx
This code demonstrates the equivalent operation to the edge update but uses direct database queries. It first queries for the related profile and then patches it.
```typescript
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" });
```
--------------------------------
### Insert and retrieve a new ent with Convex Ents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/write.mdx
Chain the `.get()` method after `insert()` to retrieve the newly created ent immediately. This is a convenience method specific to Convex Ents.
```typescript
const task = await ctx.table("tasks").insert({ text: "Win at life" }).get();
```
--------------------------------
### Traversing Edges
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Illustrates how to navigate relationships between Convex Ents using the `edge` and `edgeX` methods.
```APIDOC
## Traversing Edges
### Description
Navigates relationships (edges) defined in the Convex schema between different Ents.
### Methods
#### `edge(edge_name)`
- **Description**: Traverses a specified edge. Returns the related ent, or `null` if the edge is optional and does not exist.
- **Parameters**: `edge_name` (string) - The name of the edge to traverse.
- **Example**: `const user = await ctx.table("profiles").getX(profileId).edge("user");`
#### `edgeX(edge_name)`
- **Description**: Traverses a specified edge. Throws an error if the edge does not exist.
- **Parameters**: `edge_name` (string) - The name of the edge to traverse.
- **Example**: `const profile = await ctx.table("users").getX(userId).edgeX("profile");`
### Response
#### Success Response (200)
- **Related Ent or null** - The ent found at the other end of the edge, or `null` for optional edges.
#### Response Example
```json
// Example for edge("user") on a profile
{ "_id": "...", "name": "...", ... } // User ent
// Example for edge("profile") on a user (if profile doesn't exist)
null
```
```
--------------------------------
### Ordering Ents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Details how to sort Convex Ents using the `order` method, with options for default sorting and sorting by specific indexes.
```APIDOC
## Ordering Ents
### Description
Sorts the results from a table query in ascending or descending order, optionally by a specific index.
### Method
`ctx.table("table_name").order(direction, index_name?)
`
### Endpoint
N/A (Client-side operation)
### Parameters
#### Query Parameters
- **direction** (string) - Required - The sort direction, either 'asc' (ascending) or 'desc' (descending).
- **index_name** (string) - Optional - The name of the index to sort by. Defaults to `_creationTime`.
### Request Example
```ts
// Default order (by _creationTime descending)
const postsDesc = await ctx.table("posts").order("desc");
// Order by numLikes descending
const postsByLikes = await ctx.table("posts").order("desc", "numLikes");
```
### Response
#### Success Response (200)
- **Array of ordered ents** - The documents sorted according to the specified criteria.
#### Response Example
```json
[
{ "_id": "...", ... },
{ "_id": "...", ... }
]
```
```
--------------------------------
### Defining Ent Schema
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/index.mdx
Example of defining a Convex Ent schema using defineEntSchema. This snippet illustrates defining tables, fields, and edges for teams, members, invites, roles, and users.
```typescript
import { defineEntSchema, defineEnt, v } from "convex/ent";
const schema = defineEntSchema({
teams: defineEnt({
name: v.string(),
})
.field("slug", v.string(), { unique: true })
.edges("members", { ref: true })
.edges("invites", { ref: true }),
members: defineEnt({}).edge("team").edge("user").edge("role"),
invites: defineEnt({})
.field("email", v.string(), { unique: true })
.edge("team")
.edge("role"),
roles: defineEnt({
isDefault: v.boolean(),
}).field("name", v.union(v.literal("Admin"), v.literal("Member")),
{ unique: true })
,
users: defineEnt({}).edges("members", { ref: true }),
});
```
--------------------------------
### Retrieve Scheduled Function Status via Edge
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema/schedule.mdx
Access the scheduled function status through the defined edge. This provides a more concise way to get the status compared to direct ID lookup.
```typescript
return ctx.table("answers").map(async (answer) => ({
question: answer.question,
status: (await answer.edge("action")?.state.kind) ?? "stale",
}));
```
--------------------------------
### Dry run a migration via CLI
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/migrations.mdx
Execute a migration in dry run mode using the Convex CLI, specifying the migration function to run.
```sh
npx convex run migrations:myMigration '{"dryRun": true, "fn": "migrations:simpleMigration"}' # --prod
```
--------------------------------
### Find First Ent or Throw
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Retrieve the first ent, throwing an error if no ents are found. Use `firstX` for cases where at least one result is expected.
```ts
const latestUser = await ctx.table("users").order("desc").firstX();
```
--------------------------------
### Customizing Convex context with db access
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup/config.mdx
This code defines custom query and mutation functions that expose the original `ctx.db` API under the name `unsafeDB_DO_NOT_USE_OR_YOULL_BE_FIRED`. This is generally discouraged due to potential invariant violations.
```typescript
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,
};
}),
);
```
--------------------------------
### Order Ents by Index
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Sort ents based on a specific index by providing the index name to the `order` method. This is a shortcut for sorting by a given index.
```ts
const posts = await ctx.table("posts").order("desc", "numLikes");
```
```ts
const posts = await ctx
.table("posts")
.withIndex("numLikes")
.order("desc")
.collect();
```
--------------------------------
### Take First N Ents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Retrieve a specific number of the first ents from a table using the `take` method.
```ts
const users = await ctx.table("users").take(5);
```
--------------------------------
### Get first many:many edge ID with chaining
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Chain `ids()` with `first()` to efficiently retrieve the ID of the first entity connected via a many:many edge. This is useful for quick lookups.
```typescript
const firstTagId = await ctx
.table("messages")
.getX(messageId)
.edge("tags")
.ids()
.first();
```
--------------------------------
### Skip Rules for Database Operations
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema/rules.mdx
Demonstrates how to bypass defined rules when interacting with the database using `ctx.skipRules.table`. It's recommended to return IDs instead of full ents to maintain invariants.
```typescript
// Avoid!!!
return await ctx.skipRules.table("foos").get(someId);
// Return an ID instead:
return (await ctx.skipRules.table("foos").get(someId))._id;
```
--------------------------------
### Get many:many edge IDs
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Use the `ids` method to retrieve only the IDs of entities connected via a many:many edge. This is efficient when you only need identifiers for client-side operations like joins.
```typescript
const tagIds = await ctx.table("messages").getX(messageId).edge("tags").ids();
```
--------------------------------
### Searching Ents via Text Search
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Demonstrates how to perform text searches on Convex Ents using the `search` method, which can be combined with other query conditions.
```APIDOC
## Searching Ents via Text Search
### Description
Performs a text search on a table, allowing for complex queries combining text search with other filters.
### Method
`ctx.table("table_name").search(index_name, (q) => q.search(text, ...))`
### Endpoint
N/A (Client-side operation)
### Parameters
#### Query Parameters
- **index_name** (string) - Required - The name of the search index to use.
- **text** (string) - Required - The text to search for.
- **filter_conditions** (function) - Optional - Additional filtering conditions.
### Request Example
```ts
const awesomeVideoPosts = await ctx.table("posts").search("text", (q) => q.search("text", "awesome").eq("type", "video"));
```
### Response
#### Success Response (200)
- **Array of matching ents** - The documents that match the search criteria.
#### Response Example
```json
[
{ "_id": "...", "text": "...", "type": "video", ... }
]
```
```
--------------------------------
### Define Optional 1:1 Edge (Shortcut)
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema.mdx
Use the `optional: true` option with the `edge` method for 1:1 relationships where the foreign key field can be null. The `field` name must be specified.
```typescript
defineEntSchema({
users: defineEnt({
name: v.string(),
}).edge("profile", { ref: true }),
profiles: defineEnt({
bio: v.string(),
}).edge("user", { field: "userId", optional: true }),
});
```
--------------------------------
### Transform many:many edge IDs with map
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Use the `map` method after `ids()` to transform each retrieved ID into a custom result. This example pairs each tag ID with a boolean indicating if another message shares that tag.
```typescript
const sharedIds = await ctx
.table("messages")
.getX(messageId)
.edge("tags")
.ids()
.map(async (id) => [
id,
await ctx.table("messages").getX(anotherMessageId).edge("tags").has(id),
]);
```
--------------------------------
### List All Ents in a Table
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Retrieve all ents from a table by simply awaiting the `ctx.table('tableName')` call. This is equivalent to `ctx.db.query('tableName').collect()`.
```typescript
const tasks = await ctx.table("users");
```
```typescript
const tasks = await ctx.db.query("users").collect();
```
--------------------------------
### Traverse 1:1 Edge or Throw
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Load a related ent through a 1:1 edge, throwing an error if the edge does not exist. Use `edgeX` when the related ent is guaranteed to exist.
```ts
const profile = await ctx.table("users").getX(userId).edgeX("profile");
```
--------------------------------
### Equivalent built-in query for 1:many edges
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
This demonstrates the equivalent Convex built-in query for loading multiple related entities from a 1:many edge. It uses `query` and `withIndex` for efficient data retrieval.
```typescript
const messages = await ctx.db
.query("messages")
.withIndex("userId", (q) => q.eq("userId", userId))
.collect();
```
--------------------------------
### Equivalent built-in Convex patch for edges
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/write.mdx
This demonstrates the equivalent operation using Convex's built-in `ctx.db.patch()` method for updating edges, highlighting the added security check in Convex Ents.
```typescript
const posts = await ctx.db.patch(profileId, { userId });
```
--------------------------------
### Patch many:many edges on an existing message (add)
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/write.mdx
Use `patch()` with an `add` property to add new IDs to a many:many edge list without affecting existing ones. This example adds a single tag ID.
```typescript
const message = await ctx.table("messages").getX(messageId);
await message.patch({ tags: { add: [tagID] } });
```
--------------------------------
### Equivalent built-in query for first many:many edge ID
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
This demonstrates the equivalent built-in Convex query for getting the first many:many edge ID. It queries the join table and returns the ID of the first matching edge.
```typescript
const edge = await ctx.db
.query("messages_to_tags")
.withIndex("messagesId", (q) => q.eq("messagesId", messageId))
.first();
if (edge === null) {
return null;
}
const firstTagId = edge.tagsId;
```
--------------------------------
### Get last many:many edge ID with ordering
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Use `order("desc").ids().first()` to retrieve the ID of the most recently created entity connected via a many:many edge. This allows fetching based on creation time.
```typescript
const lastTagId = await ctx
.table("messages")
.getX(messageId)
.edge("tags")
.order("desc")
.ids()
.first();
```
--------------------------------
### Paginate Ents
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Load a page of ents using the `paginate` method. It accepts the same arguments as the built-in pagination method.
```ts
const result = await ctx.table("posts").paginate(paginationOpts);
```
--------------------------------
### Read Multiple Ents by IDs
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Fetch a list of ents by their IDs using `ctx.table('tableName').getMany([id1, id2])`. This is equivalent to mapping `ctx.db.get` over the IDs and collecting the promises.
```typescript
const tasks = await ctx.table("tasks").getMany([taskId1, taskId2]);
```
```typescript
const tasks = await Promise.all(
[taskId1, taskId2].map((id) => ctx.db.get("tasks", id)),
);
```
--------------------------------
### Accessing ctx.db via unsafe name
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/setup/config.mdx
This function demonstrates how to access the built-in `ctx.db` API using the explicitly named `unsafeDB_DO_NOT_USE_OR_YOULL_BE_FIRED` property on the context. Use with extreme caution.
```typescript
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);
},
});
```
--------------------------------
### Define Optional 1:1 Edge (Fully Specified)
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/schema.mdx
This fully specified syntax for optional 1:1 edges explicitly defines the target table and the foreign key field. The `field` name must be specified when `optional` is true.
```typescript
defineEntSchema({
users: defineEnt({
name: v.string(),
}).edge("profile", { to: "profiles", ref: "userId" }),
profiles: defineEnt({
bio: v.string(),
}).edge("user", { to: "users", field: "userId", optional: true }),
});
```
--------------------------------
### Traverse edges after loading an entity
Source: https://github.com/get-convex/convex-ents/blob/main/docs/pages/read.mdx
Load an entity first using methods like `firstX()` and then traverse its edges using `edgeX()`. This is useful for accessing related data when you already have the primary entity.
```typescript
const user = await ctx.table("users").firstX();
const profile = await user.edgeX("profile");
```