convex-helpers-guide について
convex-helpers-guideは、convex-helpersでConvexバックエンドを拡張する開発者向けの実用ガイドです。型付きのリレーション参照、アクセス制御、セッション、移行、トリガー、集計を扱います。
主な機能
- 01型付きヘルパーでテーブル間の関係を参照
- 02関数ラッパーで認証とアクセス制御を追加
- 03TypeScriptのロジックでクエリ結果を絞り込み
- 04匿名セッションの追跡
- 05Zodによる検証とデータ移行
- 06トリガーの実行と集計
活用シーン
- 01バックエンド関数に共通の認可を適用
- 02テーブルをまたいで関連レコードを取得
- 03登録前のゲストカートを追跡
- 04フィールドの補完やデータ変更への対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ get-convex/ convex-agent-plugins --skill convex-helpers-guideインストール後、Claude Code で/convex-helpers-guideを入力して読み込まれたか確認します。
詳細
Convex Helpers ガイド
convex-helpers を使用すると、よくあるパターンやユーティリティを再発明することなく、Convex バックエンドに追加できます。
convex-helpers とは?
convex-helpers は Convex を補完するユーティリティの公式コレクションです。一般的なバックエンドのニーズに対応する、十分に検証されたパターンを提供します。
インストール:
npm install convex-helpers
利用可能なヘルパー
1. リレーションシップヘルパー
テーブル間のリレーションシップを、読みやすく型安全な方法でたどります。
使用する場面:
- テーブル間で関連データを読み込む場合
- 外部キーのリレーションシップをたどる場合
- ネストされたデータ構造を構築する場合
例:
import { getOneFrom, getManyFrom } from "convex-helpers/server/relationships";
export const getTaskWithUser = query({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
if (!task) return null;
// Get related user
const user = await getOneFrom(
ctx.db,
"users",
"by_id",
task.userId,
"_id"
);
// Get related comments
const comments = await getManyFrom(
ctx.db,
"comments",
"by_task",
task._id,
"taskId"
);
return { ...task, user, comments };
},
});
主な関数:
getOneFrom- 関連するドキュメントを1件取得しますgetManyFrom- 関連するドキュメントを複数取得しますgetManyVia- 中間テーブルを介して多対多のリレーションシップを取得します
2. カスタム関数(データ保護)⭐ 最重要
これは Convex における Row Level Security(RLS)の代替手段です。 データベースレベルのポリシーではなく、カスタム関数ラッパーを使用して、すべてのクエリとミューテーションに認証とアクセス制御を自動的に追加します。
カスタムの動作を備えた、ラップ済みのクエリ/ミューテーション/アクションを作成します。
使用する場面:
- データ保護とアクセス制御(主な用途)
- すべての関数に認証ロジックを追加したい場合
- マルチテナントアプリケーション
- ロールベースのアクセス制御(RBAC)
ctxに共通データを注入する必要がある場合- 内部専用の関数を構築する場合
- すべての関数にロギング/モニタリングを追加する場合
RLS ではなくこちらを使う理由:
- ✅ SQL ポリシーではなく TypeScript を使用
- ✅ 完全な型安全性
- ✅ テストとデバッグが簡単
- ✅ データベースポリシーより柔軟
- ✅ バックエンド全体で動作
例:認証を自動適用するカスタムクエリ
// convex/lib/customFunctions.ts
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
export const authenticatedQuery = customQuery(
query,
{
args: {}, // No additional args required
input: 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");
// Add user to context
return { ctx: { ...ctx, user }, args };
},
}
);
// Usage in your functions
export const getMyTasks = authenticatedQuery({
handler: async (ctx) => {
// ctx.user is automatically available!
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
例:マルチテナントのデータ保護
import { customQuery } from "convex-helpers/server/customFunctions";
import { query } from "../_generated/server";
// Organization-scoped query - automatic access control
export const orgQuery = customQuery(query, {
args: { orgId: v.id("organizations") },
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
// Verify user is a member of this organization
const member = await ctx.db
.query("organizationMembers")
.withIndex("by_org_and_user", q =>
q.eq("orgId", args.orgId).eq("userId", user._id)
)
.unique();
if (!member) {
throw new Error("Not authorized for this organization");
}
// Inject org context
return {
ctx: {
...ctx,
user,
orgId: args.orgId,
role: member.role
},
args
};
},
});
// Usage - data automatically scoped to organization
export const getOrgProjects = orgQuery({
args: { orgId: v.id("organizations") },
handler: async (ctx) => {
// ctx.user and ctx.orgId automatically available and verified!
return await ctx.db
.query("projects")
.withIndex("by_org", q => q.eq("orgId", ctx.orgId))
.collect();
},
});
例:ロールベースのアクセス制御
import { customMutation } from "convex-helpers/server/customFunctions";
import { mutation } from "../_generated/server";
export const adminMutation = customMutation(mutation, {
args: {},
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
if (user.role !== "admin") {
throw new Error("Admin access required");
}
return { ctx: { ...ctx, user }, args };
},
});
// Usage - only admins can call this
export const deleteUser = adminMutation({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
// Only admins reach this code
await ctx.db.delete(args.userId);
},
});
3. フィルターヘルパー
複雑な TypeScript フィルターをデータベースクエリに適用します。
使用する場面:
- 計算値によるフィルタリングが必要な場合
- フィルタリングロジックがインデックスでは複雑すぎる場合
- 小規模な結果セットを扱う場合
例:
import { filter } from "convex-helpers/server/filter";
export const getActiveTasks = query({
handler: async (ctx) => {
const now = Date.now();
const threeDaysAgo = now - 3 * 24 * 60 * 60 * 1000;
return await filter(
ctx.db.query("tasks"),
(task) =>
!task.completed &&
task.createdAt > threeDaysAgo &&
task.priority === "high"
).collect();
},
});
注: 可能な場合は、引き続きインデックスを優先してください! インデックス化できない複雑なロジックにはフィルターを使用します。
4. セッション
ログインしていない場合でも、リクエストをまたいでユーザーを追跡します。
使用する場面:
- 匿名ユーザーを追跡する必要がある場合
- ゲスト向けのショッピングカートを構築する場合
- サインアップ前のユーザー行動を追跡する場合
- 認証なしで A/B テストを行う場合
セットアップ:
// convex/sessions.ts
import { SessionIdArg } from "convex-helpers/server/sessions";
import { query } from "./_generated/server";
export const trackView = query({
args: {
...SessionIdArg, // Adds sessionId: v.string()
pageUrl: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("pageViews", {
sessionId: args.sessionId,
pageUrl: args.pageUrl,
timestamp: Date.now(),
});
},
});
クライアント(React):
import { useSessionId } from "convex-helpers/react/sessions";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function MyComponent() {
const sessionId = useSessionId();
// Automatically includes sessionId in all requests
useQuery(api.sessions.trackView, {
sessionId,
pageUrl: window.location.href,
});
}
5. Zod バリデーション
Convex バリデーターの代わりに Zod スキーマを使用します。
使用する場面:
- プロジェクトですでに Zod を使用している場合
- より複雑なバリデーションロジックが必要な場合
- カスタムエラーメッセージが必要な場合
例:
import { zCustomQuery } from "convex-helpers/server/zod";
import { z } from "zod";
import { query } from "./_generated/server";
const argsSchema = z.object({
email: z.string().email(),
age: z.number().min(18).max(120),
});
export const createUser = zCustomQuery(query, {
args: argsSchema,
handler: async (ctx, args) => {
// args is typed from Zod schema
return await ctx.db.insert("users", args);
},
});
6. 代替手段:行レベルセキュリティヘルパー
注: Convex では、主なデータ保護パターンとしてカスタム関数(上記の #2 を参照)を使用することを推奨しています。この RLS ヘルパーは、従来の RLS を模倣する代替アプローチです。
RLS 形式のルールで、きめ細かなアクセス制御を実装します。
使用する場面:
- PostgreSQL の RLS 形式のパターンを好む場合
- 多数の関数に同じルールを適用する必要がある場合
- アクセス制御ルールを一元管理したい場合
ただし、通常はカスタム関数のほうが適しています。理由は次のとおりです:
- ✅ コンパイル時に型安全(RLS は実行時)
- ✅ より明示的(適用される認証を確認しやすい)
- ✅ エラーメッセージがより適切
- ✅ テストがより簡単
例(RLS 形式を使いたい場合):
import { RowLevelSecurity } from "convex-helpers/server/rowLevelSecurity";
const rules = new RowLevelSecurity();
rules.addRule("tasks", async (ctx, task) => {
const user = await getCurrentUser(ctx);
// Users can only see their own tasks
return task.userId === user._id;
});
export const getTasks = query({
handler: async (ctx) => {
return await rules.applyRules(
ctx,
ctx.db.query("tasks").collect()
);
},
});
代わりに推奨:カスタム関数
export const myQuery = authedQuery({
handler: async (ctx) => {
// More explicit, type-safe, better errors
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
7. マイグレーション
データのマイグレーションを安全に実行します。
使用する場面:
- 新しいフィールドに既存データを補完する場合
- 既存データを変換する場合
- スキーマのバージョン間を移行する場合
例:
import { makeMigration } from "convex-helpers/server/migrations";
export const addDefaultPriority = makeMigration({
table: "tasks",
migrateOne: async (ctx, doc) => {
if (doc.priority === undefined) {
await ctx.db.patch(doc._id, { priority: "medium" });
}
},
});
// Run: npx convex run migrations:addDefaultPriority
8. トリガー
データが変更されたときにコードを自動実行します。
使用する場面:
- データ変更時に通知を送信する場合
- 関連レコードを更新する場合
- 変更を記録する場合
- 計算フィールドを維持する場合
例:
import { Triggers } from "convex-helpers/server/triggers";
const triggers = new Triggers();
triggers.register("tasks", "insert", async (ctx, task) => {
// Send notification when task is created
await ctx.db.insert("notifications", {
userId: task.userId,
type: "task_created",
taskId: task._id,
});
});
9. 集計
集計値を効率的に計算します。
例:
import { aggregation } from "convex-helpers/server/aggregation";
export const getTaskStats = query({
handler: async (ctx) => {
const stats = await aggregation(
ctx.db.query("tasks"),
{
total: "count",
completed: (task) => task.completed ? 1 : 0,
totalPriority: (task) =>
task.priority === "high" ? 3 : task.priority === "medium" ? 2 : 1,
}
);
return {
total: stats.total,
completed: stats.completed,
avgPriority: stats.totalPriority / stats.total,
};
},
});
一般的なパターン
パターン 1:ユーザーコンテキストを使った認証済みクエリ
import { customQuery } from "convex-helpers/server/customFunctions";
export const authedQuery = customQuery(query, {
args: {},
input: async (ctx, args) => {
const user = await getCurrentUser(ctx);
return { ctx: { ...ctx, user }, args };
},
});
// Now all queries automatically have user in context
export const getMyData = authedQuery({
handler: async (ctx) => {
// ctx.user is typed and available!
return await ctx.db
.query("data")
.withIndex("by_user", q => q.eq("userId", ctx.user._id))
.collect();
},
});
パターン 2:関連データの読み込み
import { getOneFrom, getManyFrom } from "convex-helpers/server/relationships";
export const getPostWithDetails = query({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {
const post = await ctx.db.get(args.postId);
if (!post) return null;
// Load author
const author = await getOneFrom(
ctx.db,
"users",
"by_id",
post.authorId,
"_id"
);
// Load comments
const comments = await getManyFrom(
ctx.db,
"comments",
"by_post",
post._id,
"postId"
);
// Load tags (many-to-many)
const tagLinks = await getManyFrom(
ctx.db,
"postTags",
"by_post",
post._id,
"postId"
);
const tags = await Promise.all(
tagLinks.map(link =>
getOneFrom(ctx.db, "tags", "by_id", link.tagId, "_id")
)
);
return { ...post, author, comments, tags };
},
});
パターン 3:エラー処理を伴う一括操作
import { asyncMap } from "convex-helpers";
export const batchUpdateTasks = mutation({
args: {
taskIds: v.array(v.id("tasks")),
status: v.string(),
},
handler: async (ctx, args) => {
const results = await asyncMap(args.taskIds, async (taskId) => {
try {
const task = await ctx.db.get(taskId);
if (task) {
await ctx.db.patch(taskId, { status: args.status });
return { success: true, taskId };
}
return { success: false, taskId, error: "Not found" };
} catch (error) {
return { success: false, taskId, error: error.message };
}
});
return results;
},
});
ベストプラクティス
-
convex-helpers から始める
- よく使われるパターンを一から作り直さないでください
- 実績のあるユーティリティを使用してください
- 役立つものを作成した場合は、ぜひ貢献してください
-
認証にはカスタム関数を使用する
authedQuery、authedMutationなどを作成します- ユーザーコンテキストを自動的に挿入します
- 定型コードを削減できます
-
ネストよりリレーションシップを優先する
- リレーションシップヘルパーを使用します
- データを正規化した状態に保ちます
- 関連データは必要に応じて読み込みます
-
フィルターの使用は必要最小限にする
- 可能な場合はインデックスを優先します
- 複雑な計算ロジックにはフィルターを使用します
- 結果セットが小さい場合に適しています
-
匿名ユーザーにはセッションを使用する
- サインアップ前から追跡します
- 後からユーザーアカウントに移行します
- カートや設定などに最適です
ドキュメント
チェックリスト
- convex-helpers をインストールしました:
npm install convex-helpers - 関連データにリレーションシップヘルパーを使用しています
- よく使う認証パターン用のカスタム関数を作成しました
- 匿名ユーザーの追跡にセッションを使用しています(必要な場合)
- 可能な場合はフィルターよりインデックスを優先しています
- convex-helpers のドキュメントで新しいユーティリティを確認してください
用途に応じた選択
| 必要なもの | 使用するもの | インポート元 |
|---|---|---|
| 関連データの読み込み | getOneFrom, getManyFrom | convex-helpers/server/relationships |
| すべての関数での認証 | customQuery | convex-helpers/server/customFunctions |
| 複雑なフィルター | filter | convex-helpers/server/filter |
| 匿名ユーザー | useSessionId | convex-helpers/react/sessions |
| Zod によるバリデーション | zCustomQuery | convex-helpers/server/zod |
| データ移行 | makeMigration | convex-helpers/server/migrations |
| トリガー | Triggers | convex-helpers/server/triggers |