关于 function-creator
为 Convex 开发者提供编写后端函数的指导,涵盖查询、变更和动作。重点是参数与返回值验证,以及身份验证和权限检查。
主要功能
- 01区分查询、变更与动作
- 02验证参数和返回值
- 03检查身份与资源权限
- 04说明 Node.js 动作要求
- 05提供内部函数编写规范
适用场景
- 01新增数据库读取或写入函数
- 02添加调用外部 API 的动作
- 03为公开函数加入访问控制
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ get-convex/ agent-skills --skill function-creator安装后在 Claude Code 中输入/function-creator确认已加载。
详细介绍
Convex 函数创建器
按照所有最佳实践,生成安全、类型安全的 Convex 函数。
适用场景
- 创建新的查询函数(读取数据)
- 创建新的变更函数(写入数据)
- 创建新的操作函数(调用外部 API、执行长时间任务)
- 为 Convex 后端添加 API 端点
函数类型
查询(只读)
- 只能读取数据库
- 不能修改数据或调用外部 API
- 可缓存且具有响应性
- 在事务中运行
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getTask = query({
args: { taskId: v.id("tasks") },
returns: v.union(v.object({
_id: v.id("tasks"),
text: v.string(),
completed: v.boolean(),
}), v.null()),
handler: async (ctx, args) => {
return await ctx.db.get(args.taskId);
},
});
变更(事务性写入)
- 可以读取和写入数据库
- 不能调用外部 API
- 在 ACID 事务中运行
- 冲突时自动重试
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const createTask = mutation({
args: {
text: v.string(),
priority: v.optional(v.union(
v.literal("low"),
v.literal("medium"),
v.literal("high")
)),
},
returns: v.id("tasks"),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
return await ctx.db.insert("tasks", {
text: args.text,
priority: args.priority ?? "medium",
completed: false,
createdAt: Date.now(),
});
},
});
操作(外部调用与非事务性操作)
- 可以调用外部 API(fetch、AI 等)
- 可以通过
ctx.runMutation调用变更 - 不能直接访问数据库
- 不会自动重试
- 需要使用 Node.js API 时,请使用
"use node"指令
重要提示: 如果操作需要使用 Node.js 专属 API(crypto、第三方 SDK 等),请在文件顶部添加 "use node"。包含 "use node" 的文件只能包含操作,不能包含查询或变更。
"use node"; // Required for Node.js APIs like OpenAI SDK
import { action } from "./_generated/server";
import { api } from "./_generated/api";
import { v } from "convex/values";
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export const generateTaskSuggestion = action({
args: { prompt: v.string() },
returns: v.string(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
// Call OpenAI (requires "use node")
const completion = await openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: args.prompt }],
});
const suggestion = completion.choices[0].message.content;
// Write to database via mutation
await ctx.runMutation(api.tasks.createTask, {
text: suggestion,
});
return suggestion;
},
});
注意: 如果只需要基本的 fetch(不使用 Node.js API),可以省略 "use node"。但对于第三方 SDK、crypto 或其他 Node.js 功能,则必须使用该指令。
必需组件
1. 参数验证
始终使用验证器定义 args:
args: {
id: v.id("tasks"),
text: v.string(),
count: v.number(),
enabled: v.boolean(),
tags: v.array(v.string()),
metadata: v.optional(v.object({
key: v.string(),
})),
}
2. 返回值类型验证
始终定义 returns:
returns: v.object({
_id: v.id("tasks"),
text: v.string(),
})
// Or for arrays
returns: v.array(v.object({ /* ... */ }))
// Or for nullable
returns: v.union(v.object({ /* ... */ }), v.null())
3. 身份验证
始终在公共函数中验证身份:
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
4. 授权验证
始终验证所有权/权限:
const task = await ctx.db.get(args.taskId);
if (!task) {
throw new Error("Task not found");
}
if (task.userId !== user._id) {
throw new Error("Unauthorized");
}
完整示例
带身份验证的安全查询
export const getMyTasks = query({
args: {
status: v.optional(v.union(
v.literal("active"),
v.literal("completed")
)),
},
returns: v.array(v.object({
_id: v.id("tasks"),
text: v.string(),
completed: v.boolean(),
})),
handler: 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");
let query = ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", user._id));
const tasks = await query.collect();
if (args.status) {
return tasks.filter(t =>
args.status === "completed" ? t.completed : !t.completed
);
}
return tasks;
},
});
带验证的安全变更
export const updateTask = mutation({
args: {
taskId: v.id("tasks"),
text: v.optional(v.string()),
completed: v.optional(v.boolean()),
},
returns: v.id("tasks"),
handler: async (ctx, args) => {
// 1. Authentication
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
// 2. Get user
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");
// 3. Get resource
const task = await ctx.db.get(args.taskId);
if (!task) throw new Error("Task not found");
// 4. Authorization
if (task.userId !== user._id) {
throw new Error("Unauthorized");
}
// 5. Update
const updates: Partial<any> = {};
if (args.text !== undefined) updates.text = args.text;
if (args.completed !== undefined) updates.completed = args.completed;
await ctx.db.patch(args.taskId, updates);
return args.taskId;
},
});
调用外部 API 的操作
需要 Node.js 的操作应创建单独的文件:
// convex/taskActions.ts
"use node"; // Required for SendGrid SDK
import { action } from "./_generated/server";
import { api } from "./_generated/api";
import { v } from "convex/values";
import sendgrid from "@sendgrid/mail";
sendgrid.setApiKey(process.env.SENDGRID_API_KEY);
export const sendTaskReminder = action({
args: { taskId: v.id("tasks") },
returns: v.boolean(),
handler: async (ctx, args) => {
// 1. Auth
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
// 2. Get data via query
const task = await ctx.runQuery(api.tasks.getTask, {
taskId: args.taskId,
});
if (!task) throw new Error("Task not found");
// 3. Call external service (using Node.js SDK)
await sendgrid.send({
to: identity.email,
from: "[email protected]",
subject: "Task Reminder",
text: `Don't forget: ${task.text}`,
});
// 4. Update via mutation
await ctx.runMutation(api.tasks.markReminderSent, {
taskId: args.taskId,
});
return true;
},
});
注意: 将查询和变更放在 convex/tasks.ts 中(不含 "use node"),将需要 Node.js 的操作放在 convex/taskActions.ts 中(含 "use node")。
内部函数
用于仅供后端使用的函数(由调度程序或其他函数调用):
import { internalMutation } from "./_generated/server";
export const processExpiredTasks = internalMutation({
args: {},
handler: async (ctx) => {
// No auth needed - only callable from backend
const now = Date.now();
const expired = await ctx.db
.query("tasks")
.withIndex("by_due_date", q => q.lt("dueDate", now))
.collect();
for (const task of expired) {
await ctx.db.patch(task._id, { status: "expired" });
}
},
});
检查清单
- 使用验证器定义了
args - 使用验证器定义了
returns - 身份验证(
ctx.auth.getUserIdentity()) - 授权验证(所有权/权限)
- 已等待所有 Promise
- 使用索引查询(查询中不使用
.filter()) - 使用清晰的消息处理错误
- 已调度函数使用
internal.*,而非api.* - 使用 Node.js API 时:在文件顶部添加
"use node" - 文件包含
"use node"时:只能包含操作(不能包含查询/变更) - 使用 "use node" 时,将操作与查询/变更放在不同的文件中
评论
支持 Markdown