Convex函数开发Skill convex-functions

这个技能详细介绍了如何在Convex平台上编写查询、变异、动作和HTTP动作函数,实现数据库操作和外部API集成,涵盖参数验证、错误处理、内部函数和运行时优化,适合后端开发人员。关键词:Convex, 查询函数, 变异函数, 动作函数, HTTP动作, 后端开发, 云原生, Serverless, 数据库操作

后端开发 0 次安装 0 次浏览 更新于 3/17/2026

名称: convex-functions 显示名称: Convex Functions 描述: 编写查询、变异、动作和HTTP动作,包含适当的参数验证、错误处理、内部函数和运行时考虑 版本: 1.0.0 作者: Convex 标签: [convex, functions, queries, mutations, actions, http]

Convex Functions

掌握Convex函数,包括查询、变异、动作和HTTP端点,包含适当的验证、错误处理和运行时考虑。

代码质量

本技能中的所有示例符合@convex-dev/eslint-plugin规则:

  • 使用handler属性的对象语法
  • 所有函数都有参数验证器
  • 数据库操作中明确指定表名

有关linting设置,请参阅convex-best-practices中的代码质量部分。

文档来源

在实施前,请勿假设;获取最新文档:

说明

函数类型概述

类型 数据库访问 外部API 缓存 使用场景
查询 只读 是,响应式 获取数据
变异 读/写 修改数据
动作 通过runQuery/runMutation 外部集成
HTTP动作 通过runQuery/runMutation Webhooks, APIs

查询

查询是响应式、缓存且只读的:

import { query } from "./_generated/server";
import { v } from "convex/values";

export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.union(
    v.object({
      _id: v.id("users"),
      _creationTime: v.number(),
      name: v.string(),
      email: v.string(),
    }),
    v.null(),
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get("users", args.userId);
  },
});

// 使用索引查询
export const listUserTasks = query({
  args: { userId: v.id("users") },
  returns: v.array(
    v.object({
      _id: v.id("tasks"),
      _creationTime: v.number(),
      title: v.string(),
      completed: v.boolean(),
    }),
  ),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("tasks")
      .withIndex("by_user", (q) => q.eq("userId", args.userId))
      .order("desc")
      .collect();
  },
});

变异

变异修改数据库且是事务性的:

import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";

export const createTask = mutation({
  args: {
    title: v.string(),
    userId: v.id("users"),
  },
  returns: v.id("tasks"),
  handler: async (ctx, args) => {
    // 验证用户存在
    const user = await ctx.db.get("users", args.userId);
    if (!user) {
      throw new ConvexError("用户未找到");
    }

    return await ctx.db.insert("tasks", {
      title: args.title,
      userId: args.userId,
      completed: false,
      createdAt: Date.now(),
    });
  },
});

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

动作

动作可以调用外部API但无直接数据库访问:

"use node";

import { action } from "./_generated/server";
import { v } from "convex/values";
import { api, internal } from "./_generated/api";

export const sendEmail = action({
  args: {
    to: v.string(),
    subject: v.string(),
    body: v.string(),
  },
  returns: v.object({ success: v.boolean() }),
  handler: async (ctx, args) => {
    // 调用外部API
    const response = await fetch("https://api.email.com/send", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(args),
    });

    return { success: response.ok };
  },
});

// 动作调用查询和变异
export const processOrder = action({
  args: { orderId: v.id("orders") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // 通过查询读取数据
    const order = await ctx.runQuery(api.orders.get, { orderId: args.orderId });

    if (!order) {
      throw new Error("订单未找到");
    }

    // 调用外部支付API
    const paymentResult = await processPayment(order);

    // 通过变异更新数据库
    await ctx.runMutation(internal.orders.updateStatus, {
      orderId: args.orderId,
      status: paymentResult.success ? "已支付" : "失败",
    });

    return null;
  },
});

HTTP动作

HTTP动作处理Webhooks和外部请求:

// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { api, internal } from "./_generated/api";

const http = httpRouter();

// Webhook端点
http.route({
  path: "/webhooks/stripe",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    const signature = request.headers.get("stripe-signature");
    const body = await request.text();

    // 验证Webhook签名
    if (!verifyStripeSignature(body, signature)) {
      return new Response("无效签名", { status: 401 });
    }

    const event = JSON.parse(body);

    // 处理Webhook
    await ctx.runMutation(internal.payments.handleWebhook, {
      eventType: event.type,
      data: event.data,
    });

    return new Response("OK", { status: 200 });
  }),
});

// API端点
http.route({
  path: "/api/users/:userId",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const url = new URL(request.url);
    const userId = url.pathname.split("/").pop();

    const user = await ctx.runQuery(api.users.get, {
      userId: userId as Id<"users">,
    });

    if (!user) {
      return new Response("未找到", { status: 404 });
    }

    return Response.json(user);
  }),
});

export default http;

内部函数

使用内部函数进行敏感操作:

import {
  internalMutation,
  internalQuery,
  internalAction,
} from "./_generated/server";
import { v } from "convex/values";

// 仅可从其他Convex函数调用
export const _updateUserCredits = internalMutation({
  args: {
    userId: v.id("users"),
    amount: v.number(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await ctx.db.get("users", args.userId);
    if (!user) return null;

    await ctx.db.patch("users", args.userId, {
      credits: (user.credits || 0) + args.amount,
    });
    return null;
  },
});

// 从动作调用内部函数
export const purchaseCredits = action({
  args: { userId: v.id("users"), amount: v.number() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // 外部处理支付
    await processPayment(args.amount);

    // 通过内部变异更新积分
    await ctx.runMutation(internal.users._updateUserCredits, {
      userId: args.userId,
      amount: args.amount,
    });

    return null;
  },
});

调度函数

调度函数稍后运行:

import { mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const scheduleReminder = mutation({
  args: {
    userId: v.id("users"),
    message: v.string(),
    delayMs: v.number(),
  },
  returns: v.id("_scheduled_functions"),
  handler: async (ctx, args) => {
    return await ctx.scheduler.runAfter(
      args.delayMs,
      internal.notifications.sendReminder,
      { userId: args.userId, message: args.message },
    );
  },
});

export const sendReminder = internalMutation({
  args: {
    userId: v.id("users"),
    message: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.insert("notifications", {
      userId: args.userId,
      message: args.message,
      sentAt: Date.now(),
    });
    return null;
  },
});

示例

完整函数文件

// convex/messages.ts
import { query, mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";
import { internal } from "./_generated/api";

const messageValidator = v.object({
  _id: v.id("messages"),
  _creationTime: v.number(),
  channelId: v.id("channels"),
  authorId: v.id("users"),
  content: v.string(),
  editedAt: v.optional(v.number()),
});

// 公共查询
export const list = query({
  args: {
    channelId: v.id("channels"),
    limit: v.optional(v.number()),
  },
  returns: v.array(messageValidator),
  handler: async (ctx, args) => {
    const limit = args.limit ?? 50;
    return await ctx.db
      .query("messages")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .order("desc")
      .take(limit);
  },
});

// 公共变异
export const send = mutation({
  args: {
    channelId: v.id("channels"),
    authorId: v.id("users"),
    content: v.string(),
  },
  returns: v.id("messages"),
  handler: async (ctx, args) => {
    if (args.content.trim().length === 0) {
      throw new ConvexError("消息不能为空");
    }

    const messageId = await ctx.db.insert("messages", {
      channelId: args.channelId,
      authorId: args.authorId,
      content: args.content.trim(),
    });

    // 调度通知
    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 subscribers = await ctx.db
      .query("subscriptions")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .collect();

    for (const sub of subscribers) {
      await ctx.db.insert("notifications", {
        userId: sub.userId,
        messageId: args.messageId,
        read: false,
      });
    }
    return null;
  },
});

最佳实践

  • 除非明确指示,否则永远不要运行npx convex deploy
  • 除非明确指示,否则永远不要运行任何git命令
  • 始终定义参数和返回验证器
  • 使用查询进行读操作(它们是缓存和响应式的)
  • 使用变异进行写操作(它们是事务性的)
  • 仅在调用外部API时使用动作
  • 使用内部函数进行敏感操作
  • 在使用Node.js API的动作文件顶部添加"use node";
  • 使用ConvexError处理面向用户的错误消息

常见陷阱

  1. 在数据库操作中使用动作 - 改用查询/变异
  2. 从查询/变异调用外部API - 使用动作
  3. 忘记添加"use node" - 动作中使用Node.js API时需要
  4. 缺少返回验证器 - 始终指定返回
  5. 未使用内部函数进行敏感逻辑 - 使用internalMutation保护

参考资料