mcpField
C
convex-functions

convex-functions

github.com/waynesutton開発4053.2k 回インストール4 週間前に更新
リポジトリ

convex-functions について

Convex のクエリ、ミューテーション、アクション、HTTP アクションの実装ガイドです。データアクセス、検証、スケジューリング、エラー処理を扱い、バックエンド関数の作成やレビューに役立ちます。

主な機能

  1. 01クエリ、ミューテーション、アクションの役割を区別
  2. 02引数と戻り値のバリデーターを宣言
  3. 03インデックス読み取りとページネーションを説明
  4. 04内部関数と安全なスケジューリングを解説
  5. 05アクションのランタイムとエラー処理を案内

活用シーン

  1. 01Convex のデータ読み取りと書き込みを実装
  2. 02外部リクエストをアクションで処理
  3. 03内部関数を使ったスケジュール処理を設定
  4. 04検証、ページネーション、エラー処理の問題を調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/waynesutton/builder-skills --skill convex-functions

インストール後、Claude Code で/convex-functionsを入力して読み込まれたか確認します。

同梱ファイルagents/openai.yamlassets/small-logo.svg

詳細

Convex 関数

convex/ 内でエクスポートする関数はすべて、args と returns のバリデーターを含むオブジェクト形式を使います。ハンドラーが行う操作に応じて型を選びます。クエリは読み取り、ミューテーションは書き込み、アクションは外部への呼び出しを行います。

関数の種類を選ぶ

種類データベース外部呼び出し呼び出し可能な対象用途
query読み取りなしクライアント、他の関数読み取り。キャッシュされ、リアクティブです。
mutation読み取りと書き込みなしクライアント、他の関数書き込み。1つのトランザクションで実行します。
actionrunQuery と runMutation 経由のみありクライアント、スケジューラー、他のアクションfetch、サードパーティ SDK、Node API
internalQuery、internalMutation、internalAction公開形式と同じ同じ他の Convex 関数のみスケジュールされた処理、cron、権限が必要な書き込み
httpActionrunQuery と runMutation 経由のみありconvex/http.ts への HTTP リクエストWebhook、REST エンドポイント

基本的には query または mutation を使います。ハンドラーが Convex の外部と通信する必要がある場合にのみ、action を使います。

オブジェクト形式

すべての関数で args と returns を宣言します。何も返さない関数では returns: v.null() を宣言し、null を返します。複数の関数が同じ形式を返す場合は、共通のドキュメントバリデーターを関数の外で定義します。

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

データの読み取り

IDで1つのドキュメントを取得するには、ctx.db.get(id) を使います。それ以外の場合は、convex/schema.ts で定義したインデックスに対して withIndex を使います。テーブルクエリに .filter() を使わないでください。テーブル全体をスキャンします。

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()1つのドキュメントまたは null。複数ある場合はエラーになりますインデックスによって一致する項目が最大1つと保証される場合
.first()最初のドキュメントまたは null最新または最古の一致項目を取得する場合
.take(n)最大 n 件のドキュメント最近のフィードなど、件数に上限のあるリスト
.collect()一致するすべての項目結果セットが小さく、今後も小さいままの場合
.paginate(opts)ページとカーソルテーブルの件数に上限がない場合

ページネーションを使うクエリでは、引数として paginationOpts: paginationOptsValidator(convex/server から)を受け取ります。

データの書き込み

メソッド動作
ctx.db.insert("tasks", doc)挿入し、新しい ID を返します
ctx.db.patch(id, fields)フィールドを浅くマージします。ドキュメントがない場合はエラーになります
ctx.db.replace(id, doc)ドキュメント全体を置き換えます。ドキュメントがない場合はエラーになります
ctx.db.delete(id)ドキュメントを削除します

以前の値が不要なら、直接 patch してください。先に読み取ると、書き込み競合が起きる可能性のある時間が長くなります。ミューテーションは安全に再試行できるようにしてください。

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

内部関数と参照

query、mutation、action は公開関数です。デプロイ URL を知っている人なら誰でも呼び出せます。他の Convex コードからのみ実行するコード(スケジュールされたジョブ、cron、Webhook ハンドラー、権限が必要な書き込み)には、internalQuery、internalMutation、internalAction を使います。

./_generated/api 内の生成されたオブジェクトを通じて、関数を参照します。

  • api.tasks.get は convex/tasks.ts 内の公開関数を参照します
  • internal.tasks.markPaid は同じファイル内の内部関数を参照します
  • フォルダーはパスに対応します。convex/billing/invoices.ts は api.billing.invoices.list になります

必ず internal.* をスケジュールしてください。スケジュールされた関数と cron はクライアントなしで実行されるため、公開参照を使うと、クライアントからの呼び出しなら実行される認証チェックが省略されます。

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

アクションとランタイムの境界

アクションには ctx.db がありません。ctx.runQuery で読み取り、ctx.runMutation で書き込みます。呼び出しごとに別のトランザクションになるため、呼び出し回数を抑え、関連する読み取りと書き込みは1つのミューテーション内で行ってください。

fetch はデフォルトのランタイムで動作します。アクションで Node 組み込み機能または Node 専用 SDK が必要な場合に限り、ファイルの1行目に "use node"; を追加します。"use node" ファイルからエクスポートできるのはアクションのみです。クエリとミューテーションは別のファイルに置きます。

// 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 から取得します。注釈が必要なのは、呼び出す関数が同じファイルにある場合のみです。

エラー

クライアントが参照すべき内容には、convex/values の ConvexError をスローしてください。その data はクライアントに届きますが、通常の Error のメッセージは本番環境では伏せられます。見つからないレコードの検索など、想定される不在には null を返してください。実際の失敗、つまり未認証、権限不足、無効な入力の場合はスローしてください。

import { ConvexError } from "convex/values";

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

薄いラッパー

ハンドラーは短く保ってください。認証情報の取得、検証、ビジネスロジックは、最初の引数に ctx を取る通常の非同期関数にまとめ、ラッパーから呼び出してください。通常のヘルパーはテストしやすく、ctx.runQuery を経由せずにクエリとミューテーションの両方で共有できます。

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

クエリまたはミューテーションからは、ヘルパーを直接呼び出してください。ctx.runQuery と ctx.runMutation は、アクションやコンポーネントの境界で使用します。

よくある間違い

間違い問題が起きる理由代わりに行うこと
returns バリデーターを指定しない返却値の形が変化し、クライアントの型が実態と異なるreturns を宣言し、何も返さない場合は v.null() を使う
テーブルクエリで .filter() を使うテーブル全体をスキャンするインデックスを追加し、withIndex を使う
アクション内で ctx.db を使うアクションにはデータベースハンドルがないctx.runQuery と ctx.runMutation を使う
クエリまたはミューテーション内で fetch を使うトランザクションの決定性が損なわれるアクションに移す
api.* をスケジュールするクライアントなしで公開コードを実行し、認証をスキップするinternal.* をスケジュールする
クエリを含むファイルで "use node" を使うバンドラーがファイルを拒否するアクションを別のファイルに分ける
クエリで Date.now() を使うキャッシュとリアクティビティが損なわれる時刻を引数として渡すか、ステータスフィールドに保存する
1つのアクションから runQuery を何度も呼び出す呼び出しごとに別のトランザクションとなり、競合が発生する関連する処理を1つのミューテーションにまとめる
ユーザー向けメッセージに通常の Error を使う本番環境ではメッセージが隠されるConvexError を使う
ctx.db またはスケジューラー呼び出しに await を付けない書き込みがコミットされない場合があるすべての ctx 呼び出しを await する

チェックリスト

  • エクスポートするすべての関数で、args と returns を持つオブジェクト形式を使う
  • 返すものがない場合は、returns: v.null() と return null を使う
  • 読み取りには ctx.db.get(id) または withIndex を使い、.filter() は使わない
  • 件数に上限のないテーブルでは .collect() ではなく、.paginate() または .take(n) を使う
  • ミューテーションでは直接パッチし、安全に再試行できるようにする
  • スケジュールされた処理と cron の呼び出し先には internal.* を指定する
  • アクションでは ctx.db に触れない
  • "use node" は、アクションをエクスポートし、Node が必要なファイルでのみ使う
  • 同じファイル内の runQuery と runMutation の結果には型注釈を付ける
  • クライアントに表示するエラーには ConvexError を使う
  • すべての ctx.* の Promise を await する

ドキュメント

コメント