关于 convex-functions
为 Convex 开发者梳理查询、变更、操作及 HTTP 操作的实现规则,说明数据访问、验证、调度和错误处理,适合编写或审查后端函数。
主要功能
- 01区分查询、变更与操作用途
- 02为函数声明参数和返回验证器
- 03说明索引读取与分页方法
- 04介绍内部函数及安全调度
- 05指导操作运行时和错误处理
适用场景
- 01实现 Convex 数据读取与写入
- 02将外部请求放入操作处理
- 03设置仅供内部调用的定时任务
- 04排查验证器、分页或错误处理问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ waynesutton/ builder-skills --skill convex-functions安装后在 Claude Code 中输入/convex-functions确认已加载。
附带文件agents/openai.yamlassets/small-logo.svg
详细介绍
Convex 函数
convex/ 中每个导出的函数都使用带有 args 和 returns 验证器的对象形式。根据处理函数涉及的操作选择类型:查询负责读取,mutation 负责写入,action 负责调用外部服务。
选择函数类型
| 类型 | 数据库 | 外部调用 | 可由谁调用 | 用途 |
|---|---|---|---|---|
query | 读取 | 否 | 客户端、其他函数 | 读取。支持缓存和响应式更新。 |
mutation | 读取和写入 | 否 | 客户端、其他函数 | 写入。一个事务。 |
action | 仅通过 runQuery 和 runMutation | 是 | 客户端、调度器、其他 action | fetch、第三方 SDK、Node API |
internalQuery、internalMutation、internalAction | 与公开形式相同 | 相同 | 仅其他 Convex 函数 | 定时任务、crons、特权写入 |
httpAction | 仅通过 runQuery 和 runMutation | 是 | convex/http.ts 中的 HTTP 请求 | Webhook、REST 端点 |
默认选择 query 或 mutation。只有当处理函数必须与 Convex 之外的对象通信时,才使用 action。
对象形式
为每个函数声明 args 和 returns。不返回任何内容的函数应声明 returns: v.null() 并返回 null。如果多个函数返回相同结构,可将共用的文档验证器提取到函数外部。
// 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;
},
});
读取数据
使用 ctx.db.get(id) 按 ID 读取单个文档。其他情况都应使用 withIndex,并配合在 convex/schema.ts 中定义的索引。绝不要对表查询调用 .filter();这会扫描整张表。
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);
},
});
根据预期文档数量选择终结方法:
| 方法 | 返回值 | 使用场景 |
|---|---|---|
.unique() | 一个文档或 null;超过一个时抛出异常 | 索引保证最多只有一个匹配项 |
.first() | 第一个文档或 null | 需要最新或最早的匹配项 |
.take(n) | 最多 n 个文档 | 有上限的列表,例如近期动态 |
.collect() | 所有匹配项 | 结果集较小且会一直保持较小 |
.paginate(opts) | 一页数据及游标 | 表的大小没有上限 |
分页查询需要将 paginationOpts: paginationOptsValidator(来自 convex/server)作为参数。
写入数据
| 方法 | 功能 |
|---|---|
ctx.db.insert("tasks", doc) | 插入并返回新 ID |
ctx.db.patch(id, fields) | 浅合并字段。文档不存在时抛出异常 |
ctx.db.replace(id, doc) | 替换整个文档。文档不存在时抛出异常 |
ctx.db.delete(id) | 删除文档 |
如果不需要旧值,请直接执行 patch。先读取会扩大写入冲突的时间窗口。确保 mutation 可以安全重试。
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;
},
});
内部函数和引用
query、mutation 和 action 都是公开的。任何拥有部署 URL 的人都可以调用它们。对于只应由其他 Convex 代码运行的代码,请使用 internalQuery、internalMutation 和 internalAction:定时任务、crons、Webhook 处理程序、特权写入。
通过 ./_generated/api 中生成的对象引用函数:
api.tasks.get指向convex/tasks.ts中的公开函数internal.tasks.markPaid指向同一文件中的内部函数- 文件夹会映射为路径:
convex/billing/invoices.ts对应api.billing.invoices.list
始终使用 internal.* 进行调度。定时函数和 crons 在没有客户端的情况下运行,因此在那里使用公开引用会跳过客户端调用时本应执行的身份验证检查。
// 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 和运行时边界
Actions 没有 ctx.db。它们通过 ctx.runQuery 读取数据,并通过 ctx.runMutation 写入数据。每次调用都是独立事务,因此应尽量减少调用次数,并在一次 mutation 中完成相关读取和写入。
默认运行时支持 fetch。只有当某个 action 需要 Node 内置模块或仅适用于 Node 的 SDK 时,才将 "use node"; 作为文件的第一行。"use node" 文件只能导出 actions;queries 和 mutations 应放在单独的文件中。
// 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;
},
});
Doc 和 Id 来自 ./_generated/dataModel。只有被调用的函数位于同一文件中时,才需要添加类型注解。
错误
对于客户端需要读取的任何内容,都应从 convex/values 抛出 ConvexError。其 data 会传递给客户端;在生产环境中,普通 Error 的消息会被隐藏。对于预期内的缺失情况(例如查找无结果),返回 null。对于真正的失败情况,则抛出错误:未通过身份验证、未获授权或输入无效。
import { ConvexError } from "convex/values";
throw new ConvexError({ code: "NOT_FOUND", message: "Task not found" });
保持包装函数精简
处理函数应保持简短。将身份验证查询、验证和业务逻辑放入普通异步函数中,以 ctx 作为第一个参数,然后在包装函数中调用它们。普通辅助函数便于测试,也能在查询和变更之间共享,而无需经过 ctx.runQuery。
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;
}
在查询或变更中,直接调用辅助函数。ctx.runQuery 和 ctx.runMutation 用于操作和组件边界。
常见错误
| 错误 | 问题所在 | 应采取的做法 |
|---|---|---|
没有 returns 验证器 | 返回值结构会漂移,客户端类型也会失实 | 声明 returns,无返回值时使用 v.null() |
对表查询使用 .filter() | 会扫描整张表 | 添加索引,使用 withIndex |
在操作中使用 ctx.db | 操作没有数据库句柄 | 使用 ctx.runQuery 和 ctx.runMutation |
在查询或变更中使用 fetch | 事务必须具有确定性 | 将其移至操作中 |
调度 api.* | 无需客户端即可运行公开代码,并跳过身份验证 | 调度 internal.* |
在包含查询的文件中使用 "use node" | 打包器会拒绝该文件 | 将操作拆分到单独的文件中 |
在查询中使用 Date.now() | 会破坏缓存和响应性 | 将时间作为参数传入,或存储状态字段 |
在一次操作中调用多次 runQuery | 每次调用都是单独的事务,会出现竞争 | 使用一次变更完成相关工作 |
用普通 Error 返回面向用户的消息 | 生产环境中会隐藏该消息 | 使用 ConvexError |
未对 ctx.db 或调度器调用使用 await | 写入可能无法提交 | 对每个 ctx 调用都使用 await |
检查清单
- 每个导出的函数都使用包含
args和returns的对象形式 - 无返回值时使用
returns: v.null()和return null - 读取数据时使用
ctx.db.get(id)或withIndex,绝不使用.filter() - 对无界表使用
.paginate()或.take(n),而不是.collect() - 变更直接执行 patch 操作,并且可安全重试
- 调度目标和 cron 目标均为
internal.* - 操作绝不访问
ctx.db - 仅在导出操作且需要 Node 的文件中使用
"use node" - 同一文件中的
runQuery和runMutation结果都有类型注解 - 客户端可见的错误使用
ConvexError - 对每个
ctx.*Promise 都使用await