关于 convex-helpers-guide
convex-helpers-guide 面向使用 convex-helpers 扩展 Convex 后端的开发者,汇总常用工具和模式。内容包括类型安全的关系查询、访问控制封装、会话、迁移、触发器与聚合。
主要功能
- 01使用类型安全工具遍历表关系
- 02通过函数封装加入身份验证和访问控制
- 03用 TypeScript 逻辑筛选查询结果
- 04跟踪匿名会话
- 05使用 Zod 验证和数据迁移
- 06运行触发器并计算聚合
适用场景
- 01为后端函数添加统一授权逻辑
- 02跨数据表加载关联记录
- 03在注册前跟踪访客购物车
- 04回填字段或响应数据变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ get-convex/ convex-agent-plugins --skill convex-helpers-guide安装后在 Claude Code 中输入/convex-helpers-guide确认已加载。
详细介绍
Convex Helpers 指南
使用 convex-helpers 为 Convex 后端添加常见模式和实用工具,无需重复造轮子。
什么是 convex-helpers?
convex-helpers 是官方提供的 Convex 配套实用工具集合,为常见的后端需求提供经过实战验证的模式。
安装:
npm install convex-helpers
可用的辅助工具
1. 关系辅助工具
以清晰、类型安全的方式遍历表之间的关系。
适用场景:
- 加载跨表的关联数据
- 沿外键关系查询
- 构建嵌套数据结构
示例:
import { getOneFrom, getManyFrom } from "convex-helpers/server/relationships";
export const getTaskWithUser = query({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
if (!task) return null;
// Get related user
const user = await getOneFrom(
ctx.db,
"users",
"by_id",
task.userId,
"_id"
);
// Get related comments
const comments = await getManyFrom(
ctx.db,
"comments",
"by_task",
task._id,
"taskId"
);
return { ...task, user, comments };
},
});
主要函数:
getOneFrom- 获取单个关联文档getManyFrom- 获取多个关联文档getManyVia- 通过联结表获取多对多关系
2. 自定义函数(数据保护)⭐ 最重要
这是 Convex 对行级安全(RLS)的替代方案。 不使用数据库级策略,而是使用自定义函数包装器,为所有查询和变更自动添加身份验证和访问控制。
创建具有自定义行为的查询、变更和操作包装版本。
适用场景:
- 数据保护和访问控制(主要用途)
- 希望为所有函数添加身份验证逻辑
- 多租户应用
- 基于角色的访问控制(RBAC)
- 需要向 ctx 注入通用数据
- 构建仅供内部使用的函数
- 为所有函数添加日志记录或监控
为什么选择这种方式而不是 RLS:
- ✅ 使用 TypeScript,而不是 SQL 策略
- ✅ 完全类型安全
- ✅ 易于测试和调试
- ✅ 比数据库策略更灵活
- ✅ 适用于整个后端
示例:自动身份验证的自定义查询
// convex/lib/customFunctions.ts
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
export const authenticatedQuery = customQuery(
query,
{
args: {}, // No additional args required
input: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
const user = await ctx.db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error("User not found");
// Add user to context
return { ctx: { ...ctx, user }, args };
},
}
);
// Usage in your functions
export const getMyTasks = authenticatedQuery({
handler: async (ctx) => {
// ctx.user is automatically available!
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
示例:多租户数据保护
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
// Organization-scoped query - automatic access control
export const orgQuery = customQuery(query, {
args: { orgId: v.id("organizations") },
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
// Verify user is a member of this organization
const member = await ctx.db
.query("organizationMembers")
.withIndex("by_org_and_user", q =>
q.eq("orgId", args.orgId).eq("userId", user._id)
)
.unique();
if (!member) {
throw new Error("Not authorized for this organization");
}
// Inject org context
return {
ctx: {
...ctx,
user,
orgId: args.orgId,
role: member.role
},
args
};
},
});
// Usage - data automatically scoped to organization
export const getOrgProjects = orgQuery({
args: { orgId: v.id("organizations") },
handler: async (ctx) => {
// ctx.user and ctx.orgId automatically available and verified!
return await ctx.db
.query("projects")
.withIndex("by_org", q => q.eq("orgId", ctx.orgId))
.collect();
},
});
示例:基于角色的访问控制
import { customMutation } from "convex-helpers/server/customFunctions";
import { mutation } from "../_generated/server";
export const adminMutation = customMutation(mutation, {
args: {},
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
if (user.role !== "admin") {
throw new Error("Admin access required");
}
return { ctx: { ...ctx, user }, args };
},
});
// Usage - only admins can call this
export const deleteUser = adminMutation({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
// Only admins reach this code
await ctx.db.delete(args.userId);
},
});
3. 过滤辅助工具
对数据库查询应用复杂的 TypeScript 过滤条件。
适用场景:
- 需要按计算值筛选
- 过滤逻辑过于复杂,无法使用索引
- 处理较小的结果集
示例:
import { filter } from "convex-helpers/server/filter";
export const getActiveTasks = query({
handler: async (ctx) => {
const now = Date.now();
const threeDaysAgo = now - 3 * 24 * 60 * 60 * 1000;
return await filter(
ctx.db.query("tasks"),
(task) =>
!task.completed &&
task.createdAt > threeDaysAgo &&
task.priority === "high"
).collect();
},
});
注意: 尽可能优先使用索引!对于无法通过索引实现的复杂逻辑,请使用过滤器。
4. 会话
即使用户未登录,也能跨请求跟踪用户。
适用场景:
- 需要跟踪匿名用户
- 为访客构建购物车
- 跟踪用户注册前的行为
- 无需身份验证即可进行 A/B 测试
设置:
// convex/sessions.ts
import { SessionIdArg } from "convex-helpers/server/sessions";
import { query } from "./_generated/server";
export const trackView = query({
args: {
...SessionIdArg, // Adds sessionId: v.string()
pageUrl: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("pageViews", {
sessionId: args.sessionId,
pageUrl: args.pageUrl,
timestamp: Date.now(),
});
},
});
客户端(React):
import { useSessionId } from "convex-helpers/react/sessions";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function MyComponent() {
const sessionId = useSessionId();
// Automatically includes sessionId in all requests
useQuery(api.sessions.trackView, {
sessionId,
pageUrl: window.location.href,
});
}
5. Zod 验证
使用 Zod schema 代替 Convex 验证器。
适用场景:
- 项目中已使用 Zod
- 希望使用更复杂的验证逻辑
- 需要自定义错误消息
示例:
import { zCustomQuery } from "convex-helpers/server/zod";
import { z } from "zod";
import { query } from "./_generated/server";
const argsSchema = z.object({
email: z.string().email(),
age: z.number().min(18).max(120),
});
export const createUser = zCustomQuery(query, {
args: argsSchema,
handler: async (ctx, args) => {
// args is typed from Zod schema
return await ctx.db.insert("users", args);
},
});
6. 替代方案:行级安全辅助工具
注意: Convex 建议将自定义函数(见上文 #2)作为主要的数据保护模式。此 RLS 辅助工具是另一种方案,用于模拟传统的 RLS。
使用类似 RLS 的规则实现精细的访问控制。
适用场景:
- 偏好 PostgreSQL 中类似 RLS 的模式
- 需要在多个函数中应用相同的规则
- 希望集中管理访问控制规则
不过,自定义函数通常更好,因为它们:
- ✅ 在编译时进行类型检查(RLS 在运行时检查)
- ✅ 更明确(易于查看应用了哪些身份验证)
- ✅ 错误消息更好
- ✅ 更易于测试
示例(如果你偏好 RLS 风格):
import { RowLevelSecurity } from "convex-helpers/server/rowLevelSecurity";
const rules = new RowLevelSecurity();
rules.addRule("tasks", async (ctx, task) => {
const user = await getCurrentUser(ctx);
// Users can only see their own tasks
return task.userId === user._id;
});
export const getTasks = query({
handler: async (ctx) => {
return await rules.applyRules(
ctx,
ctx.db.query("tasks").collect()
);
},
});
推荐方案:自定义函数
export const myQuery = authedQuery({
handler: async (ctx) => {
// More explicit, type-safe, better errors
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
7. 迁移
安全地运行数据迁移。
适用场景:
- 回填新字段
- 转换现有数据
- 在不同 schema 版本之间迁移
示例:
import { makeMigration } from "convex-helpers/server/migrations";
export const addDefaultPriority = makeMigration({
table: "tasks",
migrateOne: async (ctx, doc) => {
if (doc.priority === undefined) {
await ctx.db.patch(doc._id, { priority: "medium" });
}
},
});
// Run: npx convex run migrations:addDefaultPriority
8. 触发器
在数据变更时自动执行代码。
适用场景:
- 数据变更时发送通知
- 更新关联记录
- 记录变更
- 维护计算字段
示例:
import { Triggers } from "convex-helpers/server/triggers";
const triggers = new Triggers();
triggers.register("tasks", "insert", async (ctx, task) => {
// Send notification when task is created
await ctx.db.insert("notifications", {
userId: task.userId,
type: "task_created",
taskId: task._id,
});
});
9. 聚合
高效计算聚合结果。
示例:
import { aggregation } from "convex-helpers/server/aggregation";
export const getTaskStats = query({
handler: async (ctx) => {
const stats = await aggregation(
ctx.db.query("tasks"),
{
total: "count",
completed: (task) => task.completed ? 1 : 0,
totalPriority: (task) =>
task.priority === "high" ? 3 : task.priority === "medium" ? 2 : 1,
}
);
return {
total: stats.total,
completed: stats.completed,
avgPriority: stats.totalPriority / stats.total,
};
},
});
常见模式
模式 1:带用户上下文的身份验证查询
import { customQuery } from "convex-helpers/server/customFunctions";
export const authedQuery = customQuery(query, {
args: {},
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
return { ctx: { ...ctx, user }, args };
},
});
// Now all queries automatically have user in context
export const getMyData = authedQuery({
handler: async (ctx) => {
// ctx.user is typed and available!
return await ctx.db
.query("data")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
模式 2:加载关联数据
import { getOneFrom, getManyFrom } from "convex-helpers/server/relationships";
export const getPostWithDetails = query({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {
const post = await ctx.db.get(args.postId);
if (!post) return null;
// Load author
const author = await getOneFrom(
ctx.db,
"users",
"by_id",
post.authorId,
"_id"
);
// Load comments
const comments = await getManyFrom(
ctx.db,
"comments",
"by_post",
post._id,
"postId"
);
// Load tags (many-to-many)
const tagLinks = await getManyFrom(
ctx.db,
"postTags",
"by_post",
post._id,
"postId"
);
const tags = await Promise.all(
tagLinks.map(link =>
getOneFrom(ctx.db, "tags", "by_id", link.tagId, "_id")
)
);
return { ...post, author, comments, tags };
},
});
模式 3:带错误处理的批量操作
import { asyncMap } from "convex-helpers";
export const batchUpdateTasks = mutation({
args: {
taskIds: v.array(v.id("tasks")),
status: v.string(),
},
handler: async (ctx, args) => {
const results = await asyncMap(args.taskIds, async (taskId) => {
try {
const task = await ctx.db.get(taskId);
if (task) {
await ctx.db.patch(taskId, { status: args.status });
return { success: true, taskId };
}
return { success: false, taskId, error: "Not found" };
} catch (error) {
return { success: false, taskId, error: error.message };
}
});
return results;
},
});
最佳实践
-
从 convex-helpers 开始
- 不要重复实现常见模式
- 使用经过实战检验的工具
- 如果构建了有用的功能,请回馈社区
-
使用自定义函数处理身份验证
- 创建
authedQuery、authedMutation等 - 自动注入用户上下文
- 减少样板代码
- 创建
-
优先使用关联关系,而非嵌套
- 使用关联关系辅助工具
- 保持数据规范化
- 按需加载关联数据
-
谨慎使用筛选
- 尽可能优先使用索引
- 对复杂的计算逻辑使用筛选
- 适用于结果集较小的情况
-
为匿名用户使用会话
- 在注册前进行跟踪
- 之后迁移到用户账户
- 非常适合购物车、偏好设置等场景
文档
检查清单
- 已安装 convex-helpers:
npm install convex-helpers - 使用关联关系辅助工具处理关联数据
- 已为常见的身份验证模式创建自定义函数
- 使用会话跟踪匿名用户(如有需要)
- 尽可能优先使用索引,而非筛选
- 查看 convex-helpers 文档,了解新的工具
如何选择
| 需求 | 使用 | 从以下位置导入 |
|---|---|---|
| 加载关联数据 | getOneFrom、getManyFrom | convex-helpers/server/relationships |
| 在所有函数中处理身份验证 | customQuery | convex-helpers/server/customFunctions |
| 复杂筛选 | filter | convex-helpers/server/filter |
| 匿名用户 | useSessionId | convex-helpers/react/sessions |
| Zod 验证 | zCustomQuery | convex-helpers/server/zod |
| 数据迁移 | makeMigration | convex-helpers/server/migrations |
| 触发器 | Triggers | convex-helpers/server/triggers |