convex-helpers-guide
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConvex Helpers Guide
Convex Helpers 使用指南
Use convex-helpers to add common patterns and utilities to your Convex backend without reinventing the wheel.
使用convex-helpers为你的Convex后端添加常见开发模式与工具,无需重复造轮子。
What is convex-helpers?
什么是convex-helpers?
convex-helpersInstallation:
bash
npm install convex-helpersconvex-helpers安装:
bash
npm install convex-helpersAvailable Helpers
可用工具
1. Relationship Helpers
1. 关系处理工具
Traverse relationships between tables in a readable, type-safe way.
Use when:
- Loading related data across tables
- Following foreign key relationships
- Building nested data structures
Example:
typescript
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 };
},
});Key Functions:
- - Get single related document
getOneFrom - - Get multiple related documents
getManyFrom - - Get many-to-many relationships through junction table
getManyVia
以可读、类型安全的方式遍历数据表之间的关联关系。
适用场景:
- 跨表加载关联数据
- 外键关系查询
- 构建嵌套数据结构
示例:
typescript
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;
// 获取关联用户
const user = await getOneFrom(
ctx.db,
"users",
"by_id",
task.userId,
"_id"
);
// 获取关联评论
const comments = await getManyFrom(
ctx.db,
"comments",
"by_task",
task._id,
"taskId"
);
return { ...task, user, comments };
},
});核心函数:
- - 获取单个关联文档
getOneFrom - - 获取多个关联文档
getManyFrom - - 通过中间表获取多对多关联关系
getManyVia
2. Custom Functions (Data Protection) - MOST IMPORTANT
2. 自定义函数(数据保护)—— 最重要
This is Convex's alternative to Row Level Security (RLS). Instead of database-level policies, use custom function wrappers to automatically add auth and access control to all queries and mutations.
Create wrapped versions of query/mutation/action with custom behavior.
Use when:
- Data protection and access control (PRIMARY USE CASE)
- Want to add auth logic to all functions
- Multi-tenant applications
- Role-based access control (RBAC)
- Need to inject common data into ctx
- Building internal-only functions
- Adding logging/monitoring to all functions
Why this instead of RLS:
- TypeScript, not SQL policies
- Full type safety
- Easy to test and debug
- More flexible than database policies
- Works across your entire backend
Example: Custom Query with Auto-Auth
typescript
// 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();
},
});Example: Multi-Tenant Data Protection
typescript
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();
},
});Example: Role-Based Access Control
typescript
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);
},
});这是Convex提供的行级安全(RLS)替代方案。 无需数据库级别的策略,使用自定义函数包装器即可为所有查询和变更操作自动添加认证与访问控制逻辑。
创建带有自定义行为的查询/变更/操作包装版本。
适用场景:
- 数据保护与访问控制(主要使用场景)
- 为所有函数添加认证逻辑
- 多租户应用
- 基于角色的访问控制(RBAC)
- 需要向ctx注入通用数据
- 构建内部专用函数
- 为所有函数添加日志/监控
为什么选择该方案而非RLS:
- 基于TypeScript而非SQL策略
- 完全的类型安全
- 易于测试与调试
- 比数据库策略更灵活
- 适用于整个后端
示例:带自动认证的自定义查询
typescript
// convex/lib/customFunctions.ts
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
export const authenticatedQuery = customQuery(
query,
{
args: {}, // 无需额外参数
input: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("未认证");
}
const user = await ctx.db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error("用户未找到");
// 将用户信息添加到上下文
return { ctx: { ...ctx, user }, args };
},
}
);
// 在函数中使用
export const getMyTasks = authenticatedQuery({
handler: async (ctx) => {
// ctx.user 已自动可用!
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});示例:多租户数据保护
typescript
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
// 组织范围的查询——自动访问控制
export const orgQuery = customQuery(query, {
args: { orgId: v.id("organizations") },
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
// 验证用户是否为该组织成员
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("无该组织访问权限");
}
// 注入组织上下文
return {
ctx: {
...ctx,
user,
orgId: args.orgId,
role: member.role
},
args
};
},
});
// 使用方式——数据自动限定在组织范围内
export const getOrgProjects = orgQuery({
args: { orgId: v.id("organizations") },
handler: async (ctx) => {
// ctx.user 和 ctx.orgId 已自动可用并验证通过!
return await ctx.db
.query("projects")
.withIndex("by_org", q => q.eq("orgId", ctx.orgId))
.collect();
},
});示例:基于角色的访问控制
typescript
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("需要管理员权限");
}
return { ctx: { ...ctx, user }, args };
},
});
// 使用方式——仅管理员可调用
export const deleteUser = adminMutation({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
// 只有管理员能执行到此处代码
await ctx.db.delete(args.userId);
},
});3. Filter Helper
3. 过滤工具
Apply complex TypeScript filters to database queries.
Use when:
- Need to filter by computed values
- Filtering logic is too complex for indexes
- Working with small result sets
Example:
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();
},
});Note: Still prefer indexes when possible! Use filter for complex logic that can't be indexed.
将复杂的TypeScript过滤逻辑应用到数据库查询中。
适用场景:
- 需要根据计算值过滤数据
- 过滤逻辑过于复杂无法使用索引
- 处理小结果集
示例:
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();
},
});注意: 仍优先使用索引!仅当无法通过索引实现复杂逻辑时再使用filter。
4. Sessions
4. 会话管理
Track users across requests even when not logged in.
Use when:
- Need to track anonymous users
- Building shopping cart for guests
- Tracking user behavior before signup
- A/B testing without auth
Setup:
typescript
// 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(),
});
},
});Client (React):
typescript
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,
});
}即使在用户未登录的情况下,也能跨请求跟踪用户。
适用场景:
- 需要跟踪匿名用户
- 为访客构建购物车
- 跟踪用户注册前的行为
- 无需认证的A/B测试
设置:
typescript
// convex/sessions.ts
import { SessionIdArg } from "convex-helpers/server/sessions";
import { query } from "./_generated/server";
export const trackView = query({
args: {
...SessionIdArg, // 添加 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):
typescript
import { useSessionId } from "convex-helpers/react/sessions";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function MyComponent() {
const sessionId = useSessionId();
// 自动在所有请求中包含sessionId
useQuery(api.sessions.trackView, {
sessionId,
pageUrl: window.location.href,
});
}5. Zod Validation
5. Zod 验证
Use Zod schemas instead of Convex validators.
Use when:
- Already using Zod in your project
- Want more complex validation logic
- Need custom error messages
Example:
typescript
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);
},
});使用Zod模式替代Convex内置验证器。
适用场景:
- 项目中已使用Zod
- 需要更复杂的验证逻辑
- 需要自定义错误提示
示例:
typescript
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 类型由Zod模式自动推导
return await ctx.db.insert("users", args);
},
});6. Alternative: Row-Level Security Helper
6. 替代方案:行级安全工具
Note: Convex recommends using custom functions (see #2 above) as the primary data protection pattern. This RLS helper is an alternative approach that mimics traditional RLS.
However, custom functions are usually better because:
- Type-safe at compile time (RLS is runtime)
- More explicit (easy to see what auth is applied)
- Better error messages
- Easier to test
注意: Convex推荐使用自定义函数(见上文第2部分)作为主要的数据保护模式。该RLS工具是模仿传统RLS的替代方案。
但自定义函数通常更优,因为:
- 编译时类型安全(RLS为运行时)
- 更明确(易于查看应用的认证逻辑)
- 错误提示更友好
- 更易于测试
7. Migrations
7. 数据迁移
Run data migrations safely.
Use when:
- Backfilling new fields
- Transforming existing data
- Moving between schema versions
Example:
typescript
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安全地执行数据迁移。
适用场景:
- 回填新字段
- 转换现有数据
- 不同版本 schema 之间的迁移
示例:
typescript
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" });
}
},
});
// 运行:npx convex run migrations:addDefaultPriority8. Triggers
8. 触发器
Execute code automatically when data changes.
Use when:
- Sending notifications on data changes
- Updating related records
- Logging changes
- Maintaining computed fields
Example:
typescript
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,
});
});当数据发生变化时自动执行代码。
适用场景:
- 数据变化时发送通知
- 更新关联记录
- 记录变更日志
- 维护计算字段
示例:
typescript
import { Triggers } from "convex-helpers/server/triggers";
const triggers = new Triggers();
triggers.register("tasks", "insert", async (ctx, task) => {
// 任务创建时发送通知
await ctx.db.insert("notifications", {
userId: task.userId,
type: "task_created",
taskId: task._id,
});
});Common Patterns
常见模式
Pattern 1: Authenticated Queries with User Context
模式1:带用户上下文的认证查询
typescript
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();
},
});typescript
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 };
},
});
// 现在所有查询自动包含用户上下文
export const getMyData = authedQuery({
handler: async (ctx) => {
// ctx.user 已自动推导类型并可用!
return await ctx.db
.query("data")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});Pattern 2: Loading Related Data
模式2:加载关联数据
typescript
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;
const author = await getOneFrom(ctx.db, "users", "by_id", post.authorId, "_id");
const comments = await getManyFrom(ctx.db, "comments", "by_post", post._id, "postId");
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 };
},
});typescript
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;
const author = await getOneFrom(ctx.db, "users", "by_id", post.authorId, "_id");
const comments = await getManyFrom(ctx.db, "comments", "by_post", post._id, "postId");
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 };
},
});Pattern 3: Batch Operations with Error Handling
模式3:带错误处理的批量操作
typescript
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;
},
});typescript
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: "未找到" };
} catch (error) {
return { success: false, taskId, error: error.message };
}
});
return results;
},
});When to Use What
工具选择指南
| Need | Use | Import From |
|---|---|---|
| Load related data | | |
| Auth in all functions | | |
| Complex filters | | |
| Anonymous users | | |
| Zod validation | | |
| Data migrations | | |
| Triggers | | |
| 需求 | 使用工具 | 导入路径 |
|---|---|---|
| 加载关联数据 | | |
| 为所有函数添加认证 | | |
| 复杂过滤逻辑 | | |
| 匿名用户跟踪 | | |
| Zod 验证 | | |
| 数据迁移 | | |
| 触发器 | | |
Checklist
检查清单
- Installed convex-helpers:
npm install convex-helpers - Using relationship helpers for related data
- Created custom functions for common auth patterns
- Using sessions for anonymous tracking (if needed)
- Prefer indexes over filter when possible
- Check convex-helpers docs for new utilities
- 已安装convex-helpers:
npm install convex-helpers - 使用关系处理工具加载关联数据
- 为通用认证模式创建自定义函数
- (如有需要)使用会话管理跟踪匿名用户
- 优先使用索引而非filter
- 查看convex-helpers文档获取新工具