function-creator について
Convex のバックエンド関数を作成する開発者向けのスキルです。クエリ、ミューテーション、アクションの違いに加え、検証や認証・認可を扱います。
主な機能
- 01クエリ・ミューテーション・アクションの解説
- 02引数と戻り値の検証
- 03認証・認可チェック
- 04Node.js アクションの要件
- 05内部関数の記述パターン
活用シーン
- 01データベースの読み取り・書き込み関数の追加
- 02外部 API を呼び出すアクションの作成
- 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"を使用する場合、アクションをクエリやミューテーションとは別のファイルにしている
コメント
Markdown 対応