function-creator

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Convex Function Creator

Convex 函数创建工具

Generate secure, type-safe Convex functions following all best practices.
遵循最佳实践,生成安全、类型安全的Convex函数。

When to Use

适用场景

  • Creating new query functions (read data)
  • Creating new mutation functions (write data)
  • Creating new action functions (external APIs, long-running)
  • Adding API endpoints to your Convex backend
  • 创建新的查询函数(读取数据)
  • 创建新的变更函数(写入数据)
  • 创建新的动作函数(调用外部API、长时间运行任务)
  • 为Convex后端添加API端点

Function Types

函数类型

Queries (Read-Only)

查询函数(只读)

  • Can only read from database
  • Cannot modify data or call external APIs
  • Cached and reactive
  • Run in transactions
typescript
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
  • 支持缓存与响应式
  • 在事务中执行
typescript
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);
  },
});

Mutations (Transactional Writes)

变更函数(事务性写入)

  • Can read and write to database
  • Cannot call external APIs
  • Run in ACID transactions
  • Automatic retries on conflicts
typescript
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
  • 在ACID事务中执行
  • 冲突时自动重试
typescript
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(),
    });
  },
});

Actions (External + Non-Transactional)

动作函数(外部调用+非事务性)

  • Can call external APIs (fetch, AI, etc.)
  • Can call mutations via
    ctx.runMutation
  • Cannot directly access database
  • No automatic retries
  • Use
    "use node"
    directive when needing Node.js APIs
Important: If your action needs Node.js-specific APIs (crypto, third-party SDKs, etc.), add
"use node"
at the top of the file. Files with
"use node"
can ONLY contain actions, not queries or mutations.
typescript
"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;
  },
});
Note: If you only need basic fetch (no Node.js APIs), you can omit
"use node"
. But for third-party SDKs, crypto, or other Node.js features, you must use it.
  • 可调用外部API(fetch、AI服务等)
  • 可通过
    ctx.runMutation
    调用变更函数
  • 无法直接访问数据库
  • 无自动重试机制
  • 需要Node.js API时,添加
    "use node"
    指令
重要说明: 如果你的动作函数需要Node.js专属API(加密、第三方SDK等),请在文件顶部添加
"use node"
。带有
"use node"
的文件只能包含动作函数,不能包含查询或变更函数。
typescript
"use node"; // 调用OpenAI SDK等Node.js API时必填

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");

    // 调用OpenAI(需要"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;

    // 通过变更函数写入数据库
    await ctx.runMutation(api.tasks.createTask, {
      text: suggestion,
    });

    return suggestion;
  },
});
注意: 如果仅需要基础fetch(无需Node.js API),可以省略
"use node"
。但对于第三方SDK、加密或其他Node.js特性,必须添加该指令。

Required Components

必备组件

1. Argument Validation

1. 参数验证

Always define
args
with validators:
typescript
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(),
  })),
}
务必使用验证器定义
args
typescript
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. Return Type Validation

2. 返回类型验证

Always define
returns
:
typescript
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())
务必定义
returns
typescript
returns: v.object({
  _id: v.id("tasks"),
  text: v.string(),
})

// 数组类型示例
returns: v.array(v.object({ /* ... */ }))

// 可空类型示例
returns: v.union(v.object({ /* ... */ }), v.null())

3. Authentication Check

3. 身份认证检查

Always verify auth in public functions:
typescript
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
  throw new Error("Not authenticated");
}
务必在公开函数中验证身份:
typescript
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
  throw new Error("未通过身份认证");
}

4. Authorization Check

4. 权限校验

Always verify ownership/permissions:
typescript
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");
}
务必验证所有权/权限:
typescript
const task = await ctx.db.get(args.taskId);
if (!task) {
  throw new Error("任务不存在");
}

if (task.userId !== user._id) {
  throw new Error("无操作权限");
}

Complete Examples

完整示例

Secure Query with Auth

带身份认证的安全查询函数

typescript
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;
  },
});
typescript
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("未通过身份认证");

    const user = await ctx.db
      .query("users")
      .withIndex("by_token", q =>
        q.eq("tokenIdentifier", identity.tokenIdentifier)
      )
      .unique();

    if (!user) throw new Error("用户不存在");

    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;
  },
});

Secure Mutation with Validation

带验证的安全变更函数

typescript
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<typeof task> = {};
    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;
  },
});
typescript
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. 身份认证
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("未通过身份认证");

    // 2. 获取用户信息
    const user = await ctx.db
      .query("users")
      .withIndex("by_token", q =>
        q.eq("tokenIdentifier", identity.tokenIdentifier)
      )
      .unique();

    if (!user) throw new Error("用户不存在");

    // 3. 获取目标资源
    const task = await ctx.db.get(args.taskId);
    if (!task) throw new Error("任务不存在");

    // 4. 权限校验
    if (task.userId !== user._id) {
      throw new Error("无操作权限");
    }

    // 5. 执行更新
    const updates: Partial<typeof task> = {};
    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;
  },
});

Action Calling External API

调用外部API的动作函数

Create separate file for actions that need Node.js:
typescript
// 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: "noreply@example.com",
      subject: "Task Reminder",
      text: `Don't forget: ${task.text}`,
    });

    // 4. Update via mutation
    await ctx.runMutation(api.tasks.markReminderSent, {
      taskId: args.taskId,
    });

    return true;
  },
});
Note: Keep queries and mutations in
convex/tasks.ts
(without "use node"), and actions that need Node.js in
convex/taskActions.ts
(with "use node").
需要Node.js的动作函数请单独创建文件:
typescript
// convex/taskActions.ts
"use node"; // 使用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. 身份认证
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("未通过身份认证");

    // 2. 通过查询函数获取数据
    const task = await ctx.runQuery(api.tasks.getTask, {
      taskId: args.taskId,
    });

    if (!task) throw new Error("任务不存在");

    // 3. 调用外部服务(使用Node.js SDK)
    await sendgrid.send({
      to: identity.email,
      from: "noreply@example.com",
      subject: "任务提醒",
      text: `别忘记:${task.text}`,
    });

    // 4. 通过变更函数更新数据
    await ctx.runMutation(api.tasks.markReminderSent, {
      taskId: args.taskId,
    });

    return true;
  },
});
注意: 将查询和变更函数放在
convex/tasks.ts
(无需
"use node"
),需要Node.js的动作函数放在
convex/taskActions.ts
(带
"use node"
)。

Internal Functions

内部函数

For backend-only functions (called by scheduler, other functions):
typescript
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" });
    }
  },
});
仅后端调用的函数(由调度器或其他函数调用):
typescript
import { internalMutation } from "./_generated/server";

export const processExpiredTasks = internalMutation({
  args: {},
  handler: async (ctx) => {
    // 无需身份认证 - 仅能从后端调用
    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" });
    }
  },
});

Checklist

检查清单

  • args
    defined with validators
  • returns
    defined with validator
  • Authentication check (
    ctx.auth.getUserIdentity()
    )
  • Authorization check (ownership/permissions)
  • All promises awaited
  • Indexed queries (no
    .filter()
    on queries)
  • Error handling with descriptive messages
  • Scheduled functions use
    internal.*
    not
    api.*
  • If using Node.js APIs:
    "use node"
    at top of file
  • If file has
    "use node"
    : Only actions (no queries/mutations)
  • Actions in separate file from queries/mutations when using "use node"
  • 使用验证器定义了
    args
  • 使用验证器定义了
    returns
  • 添加了身份认证检查(
    ctx.auth.getUserIdentity()
  • 添加了权限校验(所有权/权限验证)
  • 所有Promise都已await
  • 查询使用了索引(未在查询中使用
    .filter()
  • 错误处理包含描述性信息
  • 调度函数使用
    internal.*
    而非
    api.*
  • 使用Node.js API时:文件顶部添加了
    "use node"
  • 文件带有
    "use node"
    时:仅包含动作函数(无查询/变更函数)
  • 需要Node.js的动作函数与查询/变更函数分文件存放