mcpField
F
function-creator

function-creator

github.com/get-convex开发69550 次安装1 天前更新
代码仓库

关于 function-creator

为 Convex 开发者提供编写后端函数的指导,涵盖查询、变更和动作。重点是参数与返回值验证,以及身份验证和权限检查。

主要功能

  1. 01区分查询、变更与动作
  2. 02验证参数和返回值
  3. 03检查身份与资源权限
  4. 04说明 Node.js 动作要求
  5. 05提供内部函数编写规范

适用场景

  1. 01新增数据库读取或写入函数
  2. 02添加调用外部 API 的动作
  3. 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