convex-functions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConvex functions
Convex 函数
Every exported function in uses the object form with and validators. Pick the type by what the handler touches: queries read, mutations write, actions call out.
convex/argsreturnsconvex/argsreturnsPick the function type
选择函数类型
| Type | Database | External calls | Callable by | Use for |
|---|---|---|---|---|
| Read | No | Clients, other functions | Reads. Cached and reactive. |
| Read and write | No | Clients, other functions | Writes. One transaction. |
| Only via | Yes | Clients, scheduler, other actions | |
| Same as the public form | Same | Only other Convex functions | Scheduled work, crons, privileged writes |
| Only via | Yes | HTTP requests in | Webhooks, REST endpoints |
Default to query or mutation. Reach for an action only when the handler must talk to something outside Convex.
| 类型 | 数据库 | 外部调用 | 可调用方 | 用途 |
|---|---|---|---|---|
| 读取 | 否 | 客户端、其他函数 | 用于读取。会被缓存,并支持 reactive 更新。 |
| 读取和写入 | 否 | 客户端、其他函数 | 用于写入。属于单个 transaction。 |
| 仅通过 | 是 | 客户端、scheduler、其他 action | |
| 与公开形式相同 | 与公开形式相同 | 仅限其他 Convex 函数 | 定时任务、cron、特权写入 |
| 仅通过 | 是 | | Webhook、REST endpoint |
默认选择 query 或 mutation。仅当 handler 必须与 Convex 外部的系统通信时,才使用 action。
The object form
对象形式
Declare and on every function. A function that returns nothing declares and returns . Hoist a shared document validator when several functions return the same shape.
argsreturnsreturns: v.null()nulltypescript
// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
const taskValidator = v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
userId: v.id("users"),
title: v.string(),
completed: v.boolean(),
});
export const get = query({
args: { taskId: v.id("tasks") },
returns: v.union(taskValidator, v.null()),
handler: async (ctx, args) => {
return await ctx.db.get(args.taskId);
},
});
export const remove = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.taskId);
return null;
},
});每个函数都要声明 和 。无返回值的函数应声明 并返回 。如果多个函数返回相同的数据结构,应将共享的文档校验器提升为顶层定义。
argsreturnsreturns: v.null()nulltypescript
// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
const taskValidator = v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
userId: v.id("users"),
title: v.string(),
completed: v.boolean(),
});
export const get = query({
args: { taskId: v.id("tasks") },
returns: v.union(taskValidator, v.null()),
handler: async (ctx, args) => {
return await ctx.db.get(args.taskId);
},
});
export const remove = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.taskId);
return null;
},
});Reading data
读取数据
Use for one document by id. For everything else use against an index defined in . Never call on a table query; it scans the whole table.
ctx.db.get(id)withIndexconvex/schema.ts.filter()typescript
export const listByUser = query({
args: { userId: v.id("users") },
returns: v.array(taskValidator),
handler: async (ctx, args) => {
return await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.order("desc")
.take(50);
},
});Pick the terminal method by how many documents you expect:
| Method | Returns | Use when |
|---|---|---|
| One doc or null, throws on more than one | The index guarantees at most one match |
| First doc or null | You want the newest or oldest match |
| Up to n docs | A bounded list such as a recent feed |
| Every match | The result set is small and stays small |
| A page plus cursor | The table is unbounded |
Paginated queries take (from ) as an argument.
paginationOpts: paginationOptsValidatorconvex/server按 ID 获取单个文档时使用 。其他所有情况都应针对 中定义的索引使用 。切勿在表查询上调用 ,因为它会扫描整张表。
ctx.db.get(id)convex/schema.tswithIndex.filter()typescript
export const listByUser = query({
args: { userId: v.id("users") },
returns: v.array(taskValidator),
handler: async (ctx, args) => {
return await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.order("desc")
.take(50);
},
});根据预期返回的文档数量选择终止方法:
| 方法 | 返回值 | 使用场景 |
|---|---|---|
| 一个文档或 null;匹配超过一个时抛出异常 | 索引保证最多只有一个匹配项 |
| 第一个文档或 null | 需要最新或最旧的匹配项 |
| 最多 n 个文档 | 数量有界的列表,例如最近动态 |
| 所有匹配项 | 结果集很小且会保持很小 |
| 一页数据及 cursor | 表中的数据量无上限 |
分页查询需要将 (来自 )作为参数。
paginationOpts: paginationOptsValidatorconvex/serverWriting data
写入数据
| Method | What it does |
|---|---|
| Inserts and returns the new id |
| Shallow merges fields. Throws if the doc is missing |
| Replaces the whole doc. Throws if missing |
| Deletes the doc |
Patch directly when you do not need the old value. Reading first widens the window for write conflicts. Make mutations safe to retry.
typescript
export const rename = mutation({
args: { taskId: v.id("tasks"), title: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.taskId, { title: args.title });
return null;
},
});| 方法 | 作用 |
|---|---|
| 插入文档并返回新 ID |
| 浅合并字段。文档不存在时抛出异常 |
| 替换整个文档。文档不存在时抛出异常 |
| 删除文档 |
不需要旧值时应直接执行 patch。先读取再写入会扩大写入冲突的时间窗口。应确保 mutation 可以安全重试。
typescript
export const rename = mutation({
args: { taskId: v.id("tasks"), title: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.taskId, { title: args.title });
return null;
},
});Internal functions and references
内部函数与引用
querymutationactioninternalQueryinternalMutationinternalActionReference functions through the generated objects in :
./_generated/api- points at a public function in
api.tasks.getconvex/tasks.ts - points at an internal function in the same file
internal.tasks.markPaid - Folders map to paths: gives
convex/billing/invoices.tsapi.billing.invoices.list
Always schedule . Scheduled functions and crons run without a client, so a public reference there skips the auth checks a client call would hit.
internal.*typescript
// convex/messages.ts
import { mutation, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const send = mutation({
args: { channelId: v.id("channels"), content: v.string() },
returns: v.id("messages"),
handler: async (ctx, args) => {
const messageId = await ctx.db.insert("messages", args);
await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {
channelId: args.channelId,
messageId,
});
return messageId;
},
});
export const notifySubscribers = internalMutation({
args: { channelId: v.id("channels"), messageId: v.id("messages") },
returns: v.null(),
handler: async (ctx, args) => {
const subs = await ctx.db
.query("subscriptions")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
await Promise.all(
subs.map((sub) =>
ctx.db.insert("notifications", {
userId: sub.userId,
messageId: args.messageId,
read: false,
}),
),
);
return null;
},
});querymutationactioninternalQueryinternalMutationinternalAction通过 中生成的对象引用函数:
./_generated/api- 指向
api.tasks.get中的公开函数convex/tasks.ts - 指向同一文件中的内部函数
internal.tasks.markPaid - 文件夹与路径一一对应:对应
convex/billing/invoices.tsapi.billing.invoices.list
始终调度 。定时函数和 cron 在没有客户端的情况下运行,因此在那里引用公开函数会跳过客户端调用原本会触发的鉴权检查。
internal.*typescript
// convex/messages.ts
import { mutation, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const send = mutation({
args: { channelId: v.id("channels"), content: v.string() },
returns: v.id("messages"),
handler: async (ctx, args) => {
const messageId = await ctx.db.insert("messages", args);
await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {
channelId: args.channelId,
messageId,
});
return messageId;
},
});
export const notifySubscribers = internalMutation({
args: { channelId: v.id("channels"), messageId: v.id("messages") },
returns: v.null(),
handler: async (ctx, args) => {
const subs = await ctx.db
.query("subscriptions")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
await Promise.all(
subs.map((sub) =>
ctx.db.insert("notifications", {
userId: sub.userId,
messageId: args.messageId,
read: false,
}),
),
);
return null;
},
});Actions and runtime boundaries
Action 与 runtime 边界
Actions have no . They read through and write through . Each call is its own transaction, so keep the count low and do related reads and writes inside one mutation.
ctx.dbctx.runQueryctx.runMutationfetch"use node";"use node"typescript
// convex/orders.ts (default runtime)
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v, ConvexError } from "convex/values";
import { Doc } from "./_generated/dataModel";
export const charge = action({
args: { orderId: v.id("orders") },
returns: v.null(),
handler: async (ctx, args) => {
// Same file call: annotate the result so TypeScript does not hit a circular type
const order: Doc<"orders"> | null = await ctx.runQuery(
internal.orders.getForCharge,
{ orderId: args.orderId },
);
if (!order) {
throw new ConvexError("Order not found");
}
const res = await fetch("https://api.payments.example/charge", {
method: "POST",
body: JSON.stringify({ amount: order.total }),
});
await ctx.runMutation(internal.orders.setStatus, {
orderId: args.orderId,
status: res.ok ? "paid" : "failed",
});
return null;
},
});DocId./_generated/dataModelAction 没有 。它们通过 读取数据,通过 写入数据。每次调用都是独立的 transaction,因此应尽量减少调用次数,并将相关读写放在同一个 mutation 中。
ctx.dbctx.runQueryctx.runMutationfetch"use node";"use node"typescript
// convex/orders.ts (default runtime)
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v, ConvexError } from "convex/values";
import { Doc } from "./_generated/dataModel";
export const charge = action({
args: { orderId: v.id("orders") },
returns: v.null(),
handler: async (ctx, args) => {
// Same file call: annotate the result so TypeScript does not hit a circular type
const order: Doc<"orders"> | null = await ctx.runQuery(
internal.orders.getForCharge,
{ orderId: args.orderId },
);
if (!order) {
throw new ConvexError("Order not found");
}
const res = await fetch("https://api.payments.example/charge", {
method: "POST",
body: JSON.stringify({ amount: order.total }),
});
await ctx.runMutation(internal.orders.setStatus, {
orderId: args.orderId,
status: res.ok ? "paid" : "failed",
});
return null;
},
});DocId./_generated/dataModelErrors
错误
Throw from for anything a client should read. Its reaches the client; a plain message is redacted in production. Return for expected absences such as a lookup that finds nothing. Throw for real failures: not authenticated, not authorized, invalid input.
ConvexErrorconvex/valuesdataErrornulltypescript
import { ConvexError } from "convex/values";
throw new ConvexError({ code: "NOT_FOUND", message: "Task not found" });对于任何客户端应读取的错误,都应从 抛出 。其 会传递到客户端;普通 的消息在生产环境中会被隐藏。对于预期内的缺失情况,例如查询不到结果,应返回 。对于真正的失败——未认证、未授权、输入无效——应抛出异常。
convex/valuesConvexErrordataErrornulltypescript
import { ConvexError } from "convex/values";
throw new ConvexError({ code: "NOT_FOUND", message: "Task not found" });Thin wrappers
轻量包装函数
Keep handlers short. Put auth lookups, validation, and business logic in plain async functions that take first, then call them from the wrapper. Plain helpers are testable and shared between queries and mutations without a hop.
ctxctx.runQuerytypescript
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";
export async function getCurrentUser(ctx: QueryCtx | MutationCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError("Not authenticated");
}
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier),
)
.unique();
if (!user) {
throw new ConvexError("User not found");
}
return user;
}From a query or mutation, call the helper directly. and are for actions and component boundaries.
ctx.runQueryctx.runMutation保持 handler 简短。将鉴权查询、校验和业务逻辑放入以 为首参数的普通 async 函数,然后从包装函数调用它们。普通辅助函数易于测试,并可在 query 与 mutation 之间共享,而无需通过 中转。
ctxctx.runQuerytypescript
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";
export async function getCurrentUser(ctx: QueryCtx | MutationCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError("Not authenticated");
}
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier),
)
.unique();
if (!user) {
throw new ConvexError("User not found");
}
return user;
}在 query 或 mutation 中,应直接调用辅助函数。 和 用于 action 和组件边界。
ctx.runQueryctx.runMutationCommon mistakes
常见错误
| Mistake | Why it breaks | Do instead |
|---|---|---|
No | Return shape drifts and client types lie | Declare |
| Full table scan | Add an index, use |
| Actions have no database handle | |
| Transactions must be deterministic | Move it to an action |
Scheduling | Runs public code without a client, skips auth | Schedule |
| Bundler rejects the file | Split actions into their own file |
| Breaks caching and reactivity | Pass time as an arg or store a status field |
Many | Each is a separate transaction, races appear | One mutation that does the related work |
Plain | Message is hidden in production | |
Missing | Write may not commit | Await every |
| 错误 | 原因 | 正确做法 |
|---|---|---|
未声明 | 返回结构发生漂移,导致客户端类型不准确 | 声明 |
在表查询上使用 | 扫描整张表 | 添加索引并使用 |
在 action 中使用 | Action 没有数据库句柄 | 使用 |
在 query 或 mutation 中使用 | Transaction 必须是确定性的 | 将其移至 action |
调度 | 在没有客户端的情况下运行公开代码,并跳过鉴权 | 调度 |
在包含 query 的文件中使用 | Bundler 会拒绝该文件 | 将 action 拆分到单独的文件中 |
在 query 中使用 | 破坏缓存和 reactive 特性 | 将时间作为参数传入,或存储 status 字段 |
一个 action 中调用过多 | 每次调用都是独立 transaction,因此会出现竞态 | 使用一个 mutation 完成相关工作 |
使用普通 | 消息在生产环境中会被隐藏 | 使用 |
对 | 写入可能无法提交 | 等待每个 |
Checklist
检查清单
- Object form with and
argson every exported functionreturns - and
returns: v.null()when there is nothing to returnreturn null - Reads use or
ctx.db.get(id), neverwithIndex.filter() - Unbounded tables use or
.paginate(), not.take(n).collect() - Mutations patch directly and are safe to retry
- Scheduled and cron targets are
internal.* - Actions never touch
ctx.db - only in files that export actions and need Node
"use node" - Same file and
runQueryresults have a type annotationrunMutation - Client visible errors are
ConvexError - Every promise is awaited
ctx.*
- 每个导出函数都采用对象形式,并包含 和
argsreturns - 无返回值时使用 并执行
returns: v.null()return null - 读取使用 或
ctx.db.get(id),绝不使用withIndex.filter() - 无上限的表使用 或
.paginate(),而不是.take(n).collect() - Mutation 直接执行 patch,并可安全重试
- 定时任务和 cron 的目标为
internal.* - Action 绝不访问
ctx.db - 仅在需要 Node 且只导出 action 的文件中使用
"use node" - 同一文件中的 和
runQuery结果具有类型标注runMutation - 客户端可见错误使用
ConvexError - 等待每个 Promise
ctx.*