mcpField
S
schema-builder

schema-builder

github.com/get-convexデータ・データベース69545 回インストール1 日前に更新
リポジトリ

schema-builder について

schema-builderは、データベース構造を作成・変更するConvex開発者向けのスキーマ設計スキルです。リレーション、インデックス設計、厳密なバリデーターに重点を置き、ネストしたデータをリレーショナル構造へ移行する方法も示します。

主な機能

  1. 01Convexスキーマの作成と拡張
  2. 02一対多・多対多リレーションの設計
  3. 03単一フィールド・複合インデックスの設計
  4. 04厳密な型バリデーターの適用
  5. 05ネストデータのリレーショナル構造への変換

活用シーン

  1. 01convex/schema.tsの新規作成
  2. 02既存テーブルへのインデックス追加
  3. 03ユーザーとプロジェクトのデータ関係設計
  4. 04ネストしたレコードの関連テーブルへの分割

インストール

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

$npx skills add https://github.com/get-convex/agent-skills --skill schema-builder

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

詳細

Convex スキーマビルダー

リレーションシップ、インデックス、バリデーターに関するベストプラクティスに従って、適切に構造化された Convex スキーマを作成します。

使用する場面

  • 新しい convex/schema.ts ファイルを作成するとき
  • 既存のスキーマにテーブルを追加するとき
  • データモデルのリレーションシップを設計するとき
  • インデックスを追加または最適化するとき
  • ネストされたデータをリレーショナル構造に変換するとき

スキーマ設計の原則

  1. ドキュメントとリレーショナルの併用:深いネストではなく、ID 参照を使ったフラットなドキュメントを使用します
  2. 外部キーにインデックスを設定:検索に使うフィールド(userId、teamId など)には必ずインデックスを設定します
  3. 配列を制限:配列は小規模で上限が定められたコレクション(8192 件未満)にのみ使用します
  4. 型安全性:v.* 型を使った厳密なバリデーターを使用します

スキーマのテンプレート

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

export default defineSchema({
  tableName: defineTable({
    // Required fields
    field: v.string(),

    // Optional fields
    optional: v.optional(v.number()),

    // Relations (use IDs)
    userId: v.id("users"),

    // Enums with union + literal
    status: v.union(
      v.literal("active"),
      v.literal("pending"),
      v.literal("archived")
    ),

    // Timestamps
    createdAt: v.number(),
    updatedAt: v.optional(v.number()),
  })
    // Index for queries by this field
    .index("by_user", ["userId"])
    // Compound index for common query patterns
    .index("by_user_and_status", ["userId", "status"])
    // Index for time-based queries
    .index("by_created", ["createdAt"]),
});

よく使われるパターン

1 対多のリレーションシップ
export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
  }).index("by_email", ["email"]),

  posts: defineTable({
    userId: v.id("users"),
    title: v.string(),
    content: v.string(),
  }).index("by_user", ["userId"]),
});
中間テーブルを使った多対多のリレーションシップ
export default defineSchema({
  users: defineTable({
    name: v.string(),
  }),

  projects: defineTable({
    name: v.string(),
  }),

  projectMembers: defineTable({
    userId: v.id("users"),
    projectId: v.id("projects"),
    role: v.union(v.literal("owner"), v.literal("member")),
  })
    .index("by_user", ["userId"])
    .index("by_project", ["projectId"])
    .index("by_project_and_user", ["projectId", "userId"]),
});
階層データ
export default defineSchema({
  comments: defineTable({
    postId: v.id("posts"),
    parentId: v.optional(v.id("comments")), // null for top-level
    userId: v.id("users"),
    text: v.string(),
  })
    .index("by_post", ["postId"])
    .index("by_parent", ["parentId"]),
});
小規模で上限が定められた配列(使用可能)
export default defineSchema({
  users: defineTable({
    name: v.string(),
    // Small, bounded collections are fine
    roles: v.array(v.union(
      v.literal("admin"),
      v.literal("editor"),
      v.literal("viewer")
    )),
    tags: v.array(v.string()), // e.g., max 10 tags
  }),
});

バリデーターのリファレンス

// Primitives
v.string()
v.number()
v.boolean()
v.null()
v.id("tableName")

// Optional
v.optional(v.string())

// Union types (enums)
v.union(v.literal("a"), v.literal("b"))

// Objects
v.object({
  key: v.string(),
  nested: v.number(),
})

// Arrays
v.array(v.string())

// Records (arbitrary keys)
v.record(v.string(), v.boolean())

// Any (avoid if possible)
v.any()

インデックス戦略

  1. 単一フィールドのインデックス:単純な検索に使用します

    • by_user: ["userId"]
    • by_email: ["email"]
  2. 複合インデックス:絞り込みクエリーに使用します

    • by_user_and_status: ["userId", "status"]
    • by_team_and_created: ["teamId", "createdAt"]
  3. 重複を削除:by_a_and_b は通常、by_a をカバーします

チェックリスト

  • すべての外部キーにインデックスが設定されている
  • 一般的なクエリーパターンに複合インデックスが設定されている
  • 配列が小規模で上限が定められている(またはリレーションシップに変換されている)
  • すべてのフィールドに適切なバリデーターが設定されている
  • 列挙型に v.union(v.literal(...)) パターンを使用している
  • タイムスタンプに v.number()(エポックからのミリ秒)を使用している

ネスト構造からリレーショナル構造への移行

ネストされた構造から変換する場合:

変換前:

users: defineTable({
  posts: v.array(v.object({
    title: v.string(),
    comments: v.array(v.object({ text: v.string() })),
  })),
})

変換後:

users: defineTable({
  name: v.string(),
}),

posts: defineTable({
  userId: v.id("users"),
  title: v.string(),
}).index("by_user", ["userId"]),

comments: defineTable({
  postId: v.id("posts"),
  text: v.string(),
}).index("by_post", ["postId"]),

コメント