convex-functions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Convex functions

Convex 函数

Every exported function in
convex/
uses the object form with
args
and
returns
validators. Pick the type by what the handler touches: queries read, mutations write, actions call out.
convex/
中的每个导出函数都采用对象形式,并声明
args
和
returns
校验器。根据 handler 访问的内容选择类型:query 负责读取,mutation 负责写入,action 负责对外调用。

Pick the function type

选择函数类型

TypeDatabaseExternal callsCallable byUse for
query
ReadNoClients, other functionsReads. Cached and reactive.
mutation
Read and writeNoClients, other functionsWrites. One transaction.
action
Only via
runQuery
and
runMutation
YesClients, scheduler, other actions
fetch
, third party SDKs, Node APIs
internalQuery
,
internalMutation
,
internalAction
Same as the public formSameOnly other Convex functionsScheduled work, crons, privileged writes
httpAction
Only via
runQuery
and
runMutation
YesHTTP requests in
convex/http.ts
Webhooks, REST endpoints
Default to query or mutation. Reach for an action only when the handler must talk to something outside Convex.
类型数据库外部调用可调用方用途
query
读取否客户端、其他函数用于读取。会被缓存,并支持 reactive 更新。
mutation
读取和写入否客户端、其他函数用于写入。属于单个 transaction。
action
仅通过
runQuery
和
runMutation
是客户端、scheduler、其他 action
fetch
、第三方 SDK、Node API
internalQuery
、
internalMutation
、
internalAction
与公开形式相同与公开形式相同仅限其他 Convex 函数定时任务、cron、特权写入
httpAction
仅通过
runQuery
和
runMutation
是
convex/http.ts
中的 HTTP 请求
Webhook、REST endpoint
默认选择 query 或 mutation。仅当 handler 必须与 Convex 外部的系统通信时,才使用 action。

The object form

对象形式

Declare
args
and
returns
on every function. A function that returns nothing declares
returns: v.null()
and returns
null
. Hoist a shared document validator when several functions return the same shape.
typescript
// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";

const taskValidator = v.object({
  _id: v.id("tasks"),
  _creationTime: v.number(),
  userId: v.id("users"),
  title: v.string(),
  completed: v.boolean(),
});

export const get = query({
  args: { taskId: v.id("tasks") },
  returns: v.union(taskValidator, v.null()),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.taskId);
  },
});

export const remove = mutation({
  args: { taskId: v.id("tasks") },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.delete(args.taskId);
    return null;
  },
});
每个函数都要声明
args
和
returns
。无返回值的函数应声明
returns: v.null()
并返回
null
。如果多个函数返回相同的数据结构,应将共享的文档校验器提升为顶层定义。
typescript
// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";

const taskValidator = v.object({
  _id: v.id("tasks"),
  _creationTime: v.number(),
  userId: v.id("users"),
  title: v.string(),
  completed: v.boolean(),
});

export const get = query({
  args: { taskId: v.id("tasks") },
  returns: v.union(taskValidator, v.null()),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.taskId);
  },
});

export const remove = mutation({
  args: { taskId: v.id("tasks") },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.delete(args.taskId);
    return null;
  },
});

Reading data

读取数据

Use
ctx.db.get(id)
for one document by id. For everything else use
withIndex
against an index defined in
convex/schema.ts
. Never call
.filter()
on a table query; it scans the whole table.
typescript
export const listByUser = query({
  args: { userId: v.id("users") },
  returns: v.array(taskValidator),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("tasks")
      .withIndex("by_user", (q) => q.eq("userId", args.userId))
      .order("desc")
      .take(50);
  },
});
Pick the terminal method by how many documents you expect:
MethodReturnsUse when
.unique()
One doc or null, throws on more than oneThe index guarantees at most one match
.first()
First doc or nullYou want the newest or oldest match
.take(n)
Up to n docsA bounded list such as a recent feed
.collect()
Every matchThe result set is small and stays small
.paginate(opts)
A page plus cursorThe table is unbounded
Paginated queries take
paginationOpts: paginationOptsValidator
(from
convex/server
) as an argument.
按 ID 获取单个文档时使用
ctx.db.get(id)
。其他所有情况都应针对
convex/schema.ts
中定义的索引使用
withIndex
。切勿在表查询上调用
.filter()
,因为它会扫描整张表。
typescript
export const listByUser = query({
  args: { userId: v.id("users") },
  returns: v.array(taskValidator),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("tasks")
      .withIndex("by_user", (q) => q.eq("userId", args.userId))
      .order("desc")
      .take(50);
  },
});
根据预期返回的文档数量选择终止方法:
方法返回值使用场景
.unique()
一个文档或 null;匹配超过一个时抛出异常索引保证最多只有一个匹配项
.first()
第一个文档或 null需要最新或最旧的匹配项
.take(n)
最多 n 个文档数量有界的列表,例如最近动态
.collect()
所有匹配项结果集很小且会保持很小
.paginate(opts)
一页数据及 cursor表中的数据量无上限
分页查询需要将
paginationOpts: paginationOptsValidator
(来自
convex/server
)作为参数。

Writing data

写入数据

MethodWhat it does
ctx.db.insert("tasks", doc)
Inserts and returns the new id
ctx.db.patch(id, fields)
Shallow merges fields. Throws if the doc is missing
ctx.db.replace(id, doc)
Replaces the whole doc. Throws if missing
ctx.db.delete(id)
Deletes the doc
Patch directly when you do not need the old value. Reading first widens the window for write conflicts. Make mutations safe to retry.
typescript
export const rename = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.taskId, { title: args.title });
    return null;
  },
});
方法作用
ctx.db.insert("tasks", doc)
插入文档并返回新 ID
ctx.db.patch(id, fields)
浅合并字段。文档不存在时抛出异常
ctx.db.replace(id, doc)
替换整个文档。文档不存在时抛出异常
ctx.db.delete(id)
删除文档
不需要旧值时应直接执行 patch。先读取再写入会扩大写入冲突的时间窗口。应确保 mutation 可以安全重试。
typescript
export const rename = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.taskId, { title: args.title });
    return null;
  },
});

Internal functions and references

内部函数与引用

query
,
mutation
, and
action
are public. Anyone with the deployment URL can call them. Use
internalQuery
,
internalMutation
, and
internalAction
for code that should only run from other Convex code: scheduled jobs, crons, webhook handlers, privileged writes.
Reference functions through the generated objects in
./_generated/api
:
  • api.tasks.get
    points at a public function in
    convex/tasks.ts
  • internal.tasks.markPaid
    points at an internal function in the same file
  • Folders map to paths:
    convex/billing/invoices.ts
    gives
    api.billing.invoices.list
Always schedule
internal.*
. Scheduled functions and crons run without a client, so a public reference there skips the auth checks a client call would hit.
typescript
// convex/messages.ts
import { mutation, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const send = mutation({
  args: { channelId: v.id("channels"), content: v.string() },
  returns: v.id("messages"),
  handler: async (ctx, args) => {
    const messageId = await ctx.db.insert("messages", args);
    await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {
      channelId: args.channelId,
      messageId,
    });
    return messageId;
  },
});

export const notifySubscribers = internalMutation({
  args: { channelId: v.id("channels"), messageId: v.id("messages") },
  returns: v.null(),
  handler: async (ctx, args) => {
    const subs = await ctx.db
      .query("subscriptions")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .collect();
    await Promise.all(
      subs.map((sub) =>
        ctx.db.insert("notifications", {
          userId: sub.userId,
          messageId: args.messageId,
          read: false,
        }),
      ),
    );
    return null;
  },
});
query
、
mutation
和
action
是公开的。任何拥有 deployment URL 的人都可以调用它们。对于只应由其他 Convex 代码执行的代码,应使用
internalQuery
、
internalMutation
和
internalAction
,例如定时作业、cron、webhook handler 和特权写入。
通过
./_generated/api
中生成的对象引用函数:
  • api.tasks.get
    指向
    convex/tasks.ts
    中的公开函数
  • internal.tasks.markPaid
    指向同一文件中的内部函数
  • 文件夹与路径一一对应:
    convex/billing/invoices.ts
    对应
    api.billing.invoices.list
始终调度
internal.*
。定时函数和 cron 在没有客户端的情况下运行,因此在那里引用公开函数会跳过客户端调用原本会触发的鉴权检查。
typescript
// convex/messages.ts
import { mutation, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const send = mutation({
  args: { channelId: v.id("channels"), content: v.string() },
  returns: v.id("messages"),
  handler: async (ctx, args) => {
    const messageId = await ctx.db.insert("messages", args);
    await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {
      channelId: args.channelId,
      messageId,
    });
    return messageId;
  },
});

export const notifySubscribers = internalMutation({
  args: { channelId: v.id("channels"), messageId: v.id("messages") },
  returns: v.null(),
  handler: async (ctx, args) => {
    const subs = await ctx.db
      .query("subscriptions")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .collect();
    await Promise.all(
      subs.map((sub) =>
        ctx.db.insert("notifications", {
          userId: sub.userId,
          messageId: args.messageId,
          read: false,
        }),
      ),
    );
    return null;
  },
});

Actions and runtime boundaries

Action 与 runtime 边界

Actions have no
ctx.db
. They read through
ctx.runQuery
and write through
ctx.runMutation
. Each call is its own transaction, so keep the count low and do related reads and writes inside one mutation.
fetch
works in the default runtime. Add
"use node";
as the first line of a file only when an action needs Node built ins or a Node only SDK. A
"use node"
file can export actions only; queries and mutations go in a separate file.
typescript
// convex/orders.ts (default runtime)
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v, ConvexError } from "convex/values";
import { Doc } from "./_generated/dataModel";

export const charge = action({
  args: { orderId: v.id("orders") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Same file call: annotate the result so TypeScript does not hit a circular type
    const order: Doc<"orders"> | null = await ctx.runQuery(
      internal.orders.getForCharge,
      { orderId: args.orderId },
    );
    if (!order) {
      throw new ConvexError("Order not found");
    }
    const res = await fetch("https://api.payments.example/charge", {
      method: "POST",
      body: JSON.stringify({ amount: order.total }),
    });
    await ctx.runMutation(internal.orders.setStatus, {
      orderId: args.orderId,
      status: res.ok ? "paid" : "failed",
    });
    return null;
  },
});
Doc
and
Id
come from
./_generated/dataModel
. The annotation is only needed when the called function lives in the same file.
Action 没有
ctx.db
。它们通过
ctx.runQuery
读取数据,通过
ctx.runMutation
写入数据。每次调用都是独立的 transaction,因此应尽量减少调用次数,并将相关读写放在同一个 mutation 中。
fetch
可在默认 runtime 中工作。仅当 action 需要 Node 内置模块或仅支持 Node 的 SDK 时,才应在文件第一行添加
"use node";
。包含
"use node"
的文件只能导出 action;query 和 mutation 应放在单独的文件中。
typescript
// convex/orders.ts (default runtime)
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v, ConvexError } from "convex/values";
import { Doc } from "./_generated/dataModel";

export const charge = action({
  args: { orderId: v.id("orders") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Same file call: annotate the result so TypeScript does not hit a circular type
    const order: Doc<"orders"> | null = await ctx.runQuery(
      internal.orders.getForCharge,
      { orderId: args.orderId },
    );
    if (!order) {
      throw new ConvexError("Order not found");
    }
    const res = await fetch("https://api.payments.example/charge", {
      method: "POST",
      body: JSON.stringify({ amount: order.total }),
    });
    await ctx.runMutation(internal.orders.setStatus, {
      orderId: args.orderId,
      status: res.ok ? "paid" : "failed",
    });
    return null;
  },
});
Doc
和
Id
来自
./_generated/dataModel
。仅当被调用函数位于同一文件中时,才需要添加类型标注。

Errors

错误

Throw
ConvexError
from
convex/values
for anything a client should read. Its
data
reaches the client; a plain
Error
message is redacted in production. Return
null
for expected absences such as a lookup that finds nothing. Throw for real failures: not authenticated, not authorized, invalid input.
typescript
import { ConvexError } from "convex/values";

throw new ConvexError({ code: "NOT_FOUND", message: "Task not found" });
对于任何客户端应读取的错误,都应从
convex/values
抛出
ConvexError
。其
data
会传递到客户端;普通
Error
的消息在生产环境中会被隐藏。对于预期内的缺失情况,例如查询不到结果,应返回
null
。对于真正的失败——未认证、未授权、输入无效——应抛出异常。
typescript
import { ConvexError } from "convex/values";

throw new ConvexError({ code: "NOT_FOUND", message: "Task not found" });

Thin wrappers

轻量包装函数

Keep handlers short. Put auth lookups, validation, and business logic in plain async functions that take
ctx
first, then call them from the wrapper. Plain helpers are testable and shared between queries and mutations without a
ctx.runQuery
hop.
typescript
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";

export async function getCurrentUser(ctx: QueryCtx | MutationCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    throw new ConvexError("Not authenticated");
  }
  const user = await ctx.db
    .query("users")
    .withIndex("by_token", (q) =>
      q.eq("tokenIdentifier", identity.tokenIdentifier),
    )
    .unique();
  if (!user) {
    throw new ConvexError("User not found");
  }
  return user;
}
From a query or mutation, call the helper directly.
ctx.runQuery
and
ctx.runMutation
are for actions and component boundaries.
保持 handler 简短。将鉴权查询、校验和业务逻辑放入以
ctx
为首参数的普通 async 函数,然后从包装函数调用它们。普通辅助函数易于测试,并可在 query 与 mutation 之间共享,而无需通过
ctx.runQuery
中转。
typescript
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";

export async function getCurrentUser(ctx: QueryCtx | MutationCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    throw new ConvexError("Not authenticated");
  }
  const user = await ctx.db
    .query("users")
    .withIndex("by_token", (q) =>
      q.eq("tokenIdentifier", identity.tokenIdentifier),
    )
    .unique();
  if (!user) {
    throw new ConvexError("User not found");
  }
  return user;
}
在 query 或 mutation 中,应直接调用辅助函数。
ctx.runQuery
和
ctx.runMutation
用于 action 和组件边界。

Common mistakes

常见错误

MistakeWhy it breaksDo instead
No
returns
validator
Return shape drifts and client types lieDeclare
returns
, use
v.null()
for nothing
.filter()
on a table query
Full table scanAdd an index, use
withIndex
ctx.db
inside an action
Actions have no database handle
ctx.runQuery
and
ctx.runMutation
fetch
inside a query or mutation
Transactions must be deterministicMove it to an action
Scheduling
api.*
Runs public code without a client, skips authSchedule
internal.*
"use node"
in a file with queries
Bundler rejects the fileSplit actions into their own file
Date.now()
in a query
Breaks caching and reactivityPass time as an arg or store a status field
Many
runQuery
calls from one action
Each is a separate transaction, races appearOne mutation that does the related work
Plain
Error
for user messages
Message is hidden in production
ConvexError
Missing
await
on
ctx.db
or scheduler
Write may not commitAwait every
ctx
call
错误原因正确做法
未声明
returns
校验器
返回结构发生漂移,导致客户端类型不准确声明
returns
,无返回值时使用
v.null()
在表查询上使用
.filter()
扫描整张表添加索引并使用
withIndex
在 action 中使用
ctx.db
Action 没有数据库句柄使用
ctx.runQuery
和
ctx.runMutation
在 query 或 mutation 中使用
fetch
Transaction 必须是确定性的将其移至 action
调度
api.*
在没有客户端的情况下运行公开代码,并跳过鉴权调度
internal.*
在包含 query 的文件中使用
"use node"
Bundler 会拒绝该文件将 action 拆分到单独的文件中
在 query 中使用
Date.now()
破坏缓存和 reactive 特性将时间作为参数传入,或存储 status 字段
一个 action 中调用过多
runQuery
每次调用都是独立 transaction,因此会出现竞态使用一个 mutation 完成相关工作
使用普通
Error
传递用户消息
消息在生产环境中会被隐藏使用
ConvexError
对
ctx.db
或 scheduler 缺少
await
写入可能无法提交等待每个
ctx
调用

Checklist

检查清单

  • Object form with
    args
    and
    returns
    on every exported function
  • returns: v.null()
    and
    return null
    when there is nothing to return
  • Reads use
    ctx.db.get(id)
    or
    withIndex
    , never
    .filter()
  • Unbounded tables use
    .paginate()
    or
    .take(n)
    , not
    .collect()
  • Mutations patch directly and are safe to retry
  • Scheduled and cron targets are
    internal.*
  • Actions never touch
    ctx.db
  • "use node"
    only in files that export actions and need Node
  • Same file
    runQuery
    and
    runMutation
    results have a type annotation
  • Client visible errors are
    ConvexError
  • Every
    ctx.*
    promise is awaited
  • 每个导出函数都采用对象形式,并包含
    args
    和
    returns
  • 无返回值时使用
    returns: v.null()
    并执行
    return null
  • 读取使用
    ctx.db.get(id)
    或
    withIndex
    ,绝不使用
    .filter()
  • 无上限的表使用
    .paginate()
    或
    .take(n)
    ,而不是
    .collect()
  • Mutation 直接执行 patch,并可安全重试
  • 定时任务和 cron 的目标为
    internal.*
  • Action 绝不访问
    ctx.db
  • 仅在需要 Node 且只导出 action 的文件中使用
    "use node"
  • 同一文件中的
    runQuery
    和
    runMutation
    结果具有类型标注
  • 客户端可见错误使用
    ConvexError
  • 等待每个
    ctx.*
    Promise

Docs

文档