mcpField
C
convex-schema-validator

convex-schema-validator

github.com/waynesuttonデータ・データベース4053.0k 回インストール4 週間前に更新
リポジトリ

convex-schema-validator について

Convex 開発者向けのスキーマ設計リファレンスです。バリデーター、ドキュメント間の関係、インデックス規則を扱い、既存データの検証とクエリパスへの対応を支援します。

主な機能

  1. 01フィールドバリデーターと型対応
  2. 02optional と null の違い
  3. 03ユニオン型のモデリング
  4. 04クエリフィールドに沿ったインデックス
  5. 05リレーションと埋め込みの選択
  6. 06よくあるスキーマエラーの確認

活用シーン

  1. 01テーブルの新規作成やフィールド追加
  2. 02withIndex クエリ用インデックスの追加
  3. 03オブジェクトの埋め込みと v.id の選択
  4. 04npx convex dev の検証エラー調査

インストール

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

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

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

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

詳細

Convex スキーマバリデーター

すべてのドキュメントに型を付け、すべてのクエリパスにインデックスを付与し、データベースにすでに保存されているデータに対する検証に合格する convex/schema.ts を生成します。ルールは1つです。関数内のすべての withIndex に対して、ここに対応する .index() が必要です。フィールド名に基づいて名前を付け、クエリのフィールド順に並べます。

使いどころ

  • 新しいテーブルを作成するとき、または既存のテーブルにフィールドを追加するとき
  • クエリで .filter() を使っており、代わりにインデックスが必要なとき
  • オブジェクトを埋め込むか、v.id でリンクするかを決めるとき
  • 複数の形状を持つドキュメントをモデリングするとき
  • npx convex dev がスキーマ検証エラーで失敗するとき

スキーマのひな型

// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
    avatarUrl: v.optional(v.string()),
  }).index("by_email", ["email"]),

  tasks: defineTable({
    userId: v.id("users"),
    title: v.string(),
    completed: v.boolean(),
    priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
  })
    .index("by_userId", ["userId"])
    .index("by_userId_and_completed", ["userId", "completed"]),
});

defineTable には、フィールドバリデーターのオブジェクト、または v.object バリデーターの単一の v.union(判別可能なユニオンを参照)のどちらかを渡します。すべてのテーブルには _id と _creationTime が自動的に付与されます。これらは宣言しないでください。

バリデーター

バリデーターTypeScript の型備考
v.string()stringUTF-8、1 MB 未満
v.number()numberFloat64。タイムスタンプやカウントに使用します
v.boolean()boolean
v.null()nullundefined は Convex の値ではありません。代わりに null を返します
v.int64()bigint非推奨の v.bigint() ではありません
v.bytes()ArrayBuffer1 MB 未満
v.id("table")Id<"table">型付き参照です。Convex は参照先が存在することを強制しません
v.array(t)T[]最大 8192 個
v.object({...}){...}最大 1024 個。キーの先頭に $ または _ は使用できません
v.record(k, t)Record<K, T>動的な ASCII キーです。v.map や v.set はありません
v.union(a, b)A | B列挙型には v.literal のメンバーを使用します
v.literal("x")"x"
v.optional(t)T | undefinedフィールドを省略できます
v.any()any最終手段です。型安全性と検証が失われます

オプショナルと null 許容

v.optional は、ドキュメントからキーを省略できることを意味します。v.union(t, v.null()) は、キーが常に存在し、null を保持できることを意味します。検証時には異なる扱いになります。

items: defineTable({
  description: v.optional(v.string()),           // may be absent
  deletedAt: v.union(v.number(), v.null()),       // always present, may be null
  notes: v.optional(v.union(v.string(), v.null())), // either
}),

テーブルにデータが入った後で追加されたフィールドには v.optional を使用します。「明示的にクリアされた」ことに意味がある場合は、v.union(..., v.null()) を使用します。

判別可能なユニオン

ドキュメントが複数の形状を取るテーブルでは、v.object バリデーターの v.union を defineTable に渡します。各メンバーには同じリテラルキーを持たせることで、TypeScript がそのキーに基づいて型を絞り込みます。

events: defineTable(
  v.union(
    v.object({
      kind: v.literal("signup"),
      userId: v.id("users"),
      email: v.string(),
    }),
    v.object({
      kind: v.literal("purchase"),
      userId: v.id("users"),
      orderId: v.id("orders"),
      amount: v.number(),
    }),
  ),
).index("by_kind", ["kind"]),

ユニオンテーブルのインデックスでは、判別子(kind)を先頭に置き、クエリが特定の形状に絞り込めるようにします。v.optional フィールドを大量に含む1つの大きなオブジェクトより、この方法を優先してください。

インデックス

名前とフィールド順

フィールド名を順番どおりに使ってインデックスに名前を付けます。例:by_field1_and_field2。クエリも同じ順序に従う必要があります。フィールドの接頭辞部分に等価条件を指定し、その次のフィールドには範囲条件を最大1つ指定できます。

messages: defineTable({
  channelId: v.id("channels"),
  authorId: v.id("users"),
  sentAt: v.number(),
})
  .index("by_channelId", ["channelId"])
  .index("by_channelId_and_authorId", ["channelId", "authorId"])
  .index("by_channelId_and_sentAt", ["channelId", "sentAt"]),
// Valid: equality on channelId, range on sentAt
await ctx.db
  .query("messages")
  .withIndex("by_channelId_and_sentAt", (q) =>
    q.eq("channelId", args.channelId).gt("sentAt", args.since),
  )
  .order("desc")
  .take(50);

このインデックスでは channelId を飛ばして sentAt だけで絞り込むことはできません。そのようなクエリがある場合は、by_sentAt を追加します。すべてのインデックスには _creationTime が自動的に追加されるため、接頭辞部分が等しい結果は作成時刻順に並びます。

予約済みの名前:by_id と by_creation_time。上限:テーブルごとにインデックス 32 個、インデックスごとにフィールド 16 個。

追加するタイミング
  • 関数が withIndex、.eq、または範囲比較に渡すすべてのフィールド
  • 子テーブルのすべての外部キー(userId、channelId、orgId)
  • ページネーション付きリストの並べ替えフィールド。スコープフィールドを先頭に付けます
  • ドキュメントを取得した後にしか読み取らないフィールドには追加しません
  • .collect() してからメモリ内でフィルターしても問題ない小さなテーブルには追加しません

クエリで .filter() を使っている場合は、インデックスを追加して withIndex に切り替える必要があるというサインです。

リレーションシップ

子ドキュメントに v.id("table") を使ってドキュメントをリンクします。親の中に、増え続けるオブジェクトの配列をネストしないでください。

// Good: one to many via a foreign key
posts: defineTable({
  authorId: v.id("users"),
  title: v.string(),
}).index("by_authorId", ["authorId"]),

comments: defineTable({
  postId: v.id("posts"),
  authorId: v.id("users"),
  body: v.string(),
}).index("by_postId", ["postId"]),

// Many to many via a join table
postTags: defineTable({
  postId: v.id("posts"),
  tagId: v.id("tags"),
})
  .index("by_postId", ["postId"])
  .index("by_tagId", ["tagId"]),

データ量に上限があり、常に親と一緒に読み込まれ、同時に更新される場合に限り、v.object または小さな v.array を使って埋め込みます。ユーザーの settings オブジェクトは埋め込みに適しています。一方、ユーザーの posts 配列は適していません。8192 個の上限に達し、投稿を編集するたびにユーザードキュメント全体が書き換えられるためです。

システムフィールド

_id: Id<"table"> と _creationTime: number(エポックからの経過時間、ミリ秒)は、すべてのドキュメントに存在します。関数がドキュメント全体を返す場合は、戻り値のバリデーターにこれらを含めてください。

returns: v.array(
  v.object({
    _id: v.id("tasks"),
    _creationTime: v.number(),
    userId: v.id("users"),
    title: v.string(),
    completed: v.boolean(),
  }),
),

挿入時刻とは異なる値が必要な場合を除き、独自の createdAt を追加しないでください。

検索インデックスとベクトルインデックス

通常のインデックスと同様に、テーブルで宣言します。filterFields はトップレベルのフィールドである必要があります。

articles: defineTable({
  title: v.string(),
  body: v.string(),
  category: v.string(),
  embedding: v.array(v.number()),
})
  .searchIndex("search_body", {
    searchField: "body",
    filterFields: ["category"],
  })
  .vectorIndex("by_embedding", {
    vectorField: "embedding",
    dimensions: 1536,
    filterFields: ["category"],
  }),

クエリでは withSearchIndex を使って検索インデックスをクエリします。ベクトル検索は、ctx.vectorSearch を使ったアクションでのみ実行されます。

よくある間違い

間違い問題が起きる理由代わりに行うこと
対応する .index() がない状態での withIndex("by_userId")デプロイに失敗しますまずスキーマでインデックスを宣言します
["userId", "status"] に対するインデックス名 by_user対象が分かりにくく、誤用しやすくなりますby_userId_and_status
userId なしで by_userId_and_status の status をクエリするインデックスのプレフィックスルールに反しますby_status を追加するか、userId を含めます
行があるテーブルに newField: v.string() を追加する既存のドキュメントでバリデーションに失敗しますv.optional を使い、バックフィルしてから必須にします
users に posts: v.array(v.object(...)) を追加する8192 件の上限があり、編集のたびに書き込みの競合が起きますby_authorId を持つ独立した posts テーブルを作成します
v.bigint()非推奨ですv.int64()
defineTable で _id または _creationTime を宣言する拒否されますこれらは自動で設定されます
手早く済ませるために v.any() を使うバリデーションも型もありませんデータの形を定義するか、実際のケースを v.union で列挙します
新しいフィールドに v.union(v.string(), v.null()) を使う古いドキュメントにはキー自体がありませんv.optional(v.string()) を使います
Date を保存するConvex の値ではありませんミリ秒単位のタイムスタンプとして v.number() を使います

チェックリスト

  • スキーマが convex/schema.ts にあり、defineSchema(...) をデフォルトエクスポートしている
  • すべてのテーブルに明示的なフィールドバリデーターがあり、正当な理由がない限り v.any() を使っていない
  • convex/ 内のすべての withIndex 呼び出しに、フィールド名を含む対応する .index() がある
  • 外部キーが v.id("table") であり、子テーブルにインデックスがある
  • 親ドキュメントに、上限のないオブジェクト配列を埋め込んでいない
  • データのあるテーブルに追加するフィールドが v.optional になっている
  • 列挙型と多相的なデータ形状で、v.literal または v.object のメンバーを v.union で使っている
  • ドキュメント全体を返す場合、戻り値のバリデーターに _id と _creationTime が含まれている
  • npx convex dev がスキーマバリデーションエラーなしでプッシュできる

ドキュメント

コメント