mcpField
F
function-creator

function-creator

github.com/get-convex開発69550 回インストール1 日前に更新
リポジトリ

function-creator について

Convex のバックエンド関数を作成する開発者向けのスキルです。クエリ、ミューテーション、アクションの違いに加え、検証や認証・認可を扱います。

主な機能

  1. 01クエリ・ミューテーション・アクションの解説
  2. 02引数と戻り値の検証
  3. 03認証・認可チェック
  4. 04Node.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 を await している
  • インデックス付きクエリを使用している(クエリで .filter() を使用していない)
  • 説明的なメッセージを含むエラーハンドリングを行っている
  • スケジュール関数で api.* ではなく internal.* を使用している
  • Node.js API を使用する場合:ファイルの先頭に "use node" がある
  • ファイルに "use node" がある場合:アクションのみ(クエリやミューテーションなし)
  • "use node" を使用する場合、アクションをクエリやミューテーションとは別のファイルにしている

コメント