### 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"); ```