mcpField
C
convex-component-authoring

convex-component-authoring

github.com/waynesutton開発4051.1k 回インストール4 週間前に更新
リポジトリ

convex-component-authoring について

convex-component-authoring は、独自のスキーマ、関数、型付きクライアントラッパーを備えた Convex コンポーネントを作るスキルです。コンポーネントの境界と、親アプリからの登録・呼び出し方を説明します。

主な機能

  1. 01独自スキーマと関数
  2. 02型付きクライアントラッパー
  3. 03親アプリへの登録
  4. 04境界と ID の扱い方
  5. 05公開手順の案内

活用シーン

  1. 01再利用可能なバックエンド機能をパッケージ化
  2. 02Convex ディレクトリ向けコンポーネントの作成
  3. 03複数アプリでのインテグレーション共有

インストール

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

$npx skills add https://github.com/waynesutton/builder-skills --skill convex-component-authoring

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
assets/
small-logo.svg
references/
publishing.md

詳細

Convex コンポーネントの作成

独自の convex.config.ts、スキーマ、関数、親アプリから呼び出す型付きクライアントラッパーを備えた、自己完結型のコンポーネントを作成します。ルールはひとつです。コンポーネントは親アプリのテーブル、ctx.auth、process.env を読み取れません。必要な情報はすべて、アプリから引数として渡します。

どんな場合に使うか

  • カウンター、レート制限、監査ログ、通知などの機能をパッケージに切り出す
  • Convex コンポーネントディレクトリ向けのものを構築する
  • 親アプリの components.<name> にコンポーネントの関数が表示されない
  • サードパーティとの連携で、独自のテーブルとバックグラウンドジョブを管理する必要がある
  • 同じバックエンドモジュールを複数のアプリで使う

コンポーネントかヘルパー関数か

必要なもの使うもの
アプリ独自のテーブルに対する共有ロジックconvex/lib/ の通常の TypeScript ヘルパー
アプリから触れない独自のスキーマを持つテーブルコンポーネント
独自のトランザクションで実行する分離された関数コンポーネント
ひとつのインストール手順で複数のアプリから再利用コンポーネント
React フックまたはクライアントユーティリティのみnpm ライブラリ。コンポーネントは不要

API 境界の背後に永続状態が不要な機能なら、ヘルパー関数のほうが手間が少なく、型付けも簡単です。

フォルダー構成

my-component/
  package.json
  src/
    component/
      convex.config.ts     # defineComponent("counter")
      schema.ts            # tables only this component can see
      public.ts            # queries, mutations, actions
      _generated/          # created by npx convex codegen
    client/
      index.ts             # wrapper class the app imports
  example/
    convex/
      convex.config.ts     # app.use(counter) for local testing

ローカル専用のコンポーネントでは、component/ の内容を convex/components/<name>/ に配置し、client/ と package.json は省略します。

コンポーネントを定義する

// src/component/convex.config.ts
import { defineComponent } from "convex/server";

const component = defineComponent("counter");
export default component;
// src/component/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  counters: defineTable({
    name: v.string(),
    value: v.number(),
  }).index("by_name", ["name"]),
});

関数では、query と mutation をアプリ側ではなく、コンポーネント独自の _generated/server からインポートします。すべての公開関数に args と returns のバリデーターが必要です。指定しないと、親アプリでは any として扱われます。

// src/component/public.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server.js";

export const add = mutation({
  args: { name: v.string(), amount: v.number() },
  returns: v.number(),
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("counters")
      .withIndex("by_name", (q) => q.eq("name", args.name))
      .unique();
    if (!existing) {
      await ctx.db.insert("counters", { name: args.name, value: args.amount });
      return args.amount;
    }
    const value = existing.value + args.amount;
    await ctx.db.patch(existing._id, { value });
    return value;
  },
});

export const get = query({
  args: { name: v.string() },
  returns: v.number(),
  handler: async (ctx, args) => {
    const counter = await ctx.db
      .query("counters")
      .withIndex("by_name", (q) => q.eq("name", args.name))
      .unique();
    return counter?.value ?? 0;
  },
});

親アプリに登録する

// convex/convex.config.ts (parent app)
import { defineApp } from "convex/server";
import counter from "@acme/counter/convex.config.js";
// local component: import counter from "./components/counter/convex.config.js";

const app = defineApp();
app.use(counter);
// A second instance under a different name:
// app.use(counter, { name: "pageViews" });
export default app;

npx convex dev を実行します。アプリの convex/_generated/api に components.counter が生成され、コンポーネント独自の _generated/ フォルダーも生成されます。参照パスはファイルのパスを反映します。たとえば、public.ts にある関数は components.counter.public.add です。

コンポーネントの関数は、親アプリでは内部参照として扱われます。ctx.runQuery と ctx.runMutation で呼び出してください。クライアントから直接呼び出すことはできません。

クライアントラッパークラス

ラッパーはアプリの環境で実行されるため、ctx.auth と process.env を読み取り、必要な情報をコンポーネントに渡せます。引数には、最初にコンポーネント参照、次にオプションを指定します。

// src/client/index.ts
import type {
  GenericDataModel,
  GenericMutationCtx,
  GenericQueryCtx,
} from "convex/server";
import type { ComponentApi } from "../component/_generated/component.js";

// Pick only the capabilities each method needs so any ctx with runQuery works
type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;

export class Counter {
  constructor(
    public component: ComponentApi,
    private options: { prefix?: string } = {},
  ) {}

  private key(name: string): string {
    return this.options.prefix ? `${this.options.prefix}:${name}` : name;
  }

  async add(ctx: MutationCtx, name: string, amount = 1): Promise<number> {
    return await ctx.runMutation(this.component.public.add, {
      name: this.key(name),
      amount,
    });
  }

  async get(ctx: QueryCtx, name: string): Promise<number> {
    return await ctx.runQuery(this.component.public.get, { name: this.key(name) });
  }
}
// convex/counter.ts (parent app)
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { Counter } from "@acme/counter";

const counter = new Counter(components.counter, { prefix: "app" });

// The app owns auth. The component only sees the string it is handed.
export const incrementMine = mutation({
  args: {},
  returns: v.number(),
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");
    return await counter.add(ctx, identity.subject);
  },
});

export const getMine = query({
  args: {},
  returns: v.number(),
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");
    return await counter.get(ctx, identity.subject);
  },
});

React から api.counter.incrementMine 経由で呼び出すのは、これらのアプリ側の関数です。認証とレート制限はアプリ側で決定し、コンポーネントは汎用のままにします。

境界に関するルール

  • 親アプリのテーブルにはアクセスできません。 コンポーネント内の v.id("users") は、アプリのテーブルではなく、コンポーネントの users テーブルを参照します。親アプリの ID は v.string() で受け取ります。
  • ID は文字列になります。 コンポーネント内のすべての Id<"counters"> は、ComponentApi では string です。意図的に文字列として返してください。
  • ctx.auth は使用できません。 ユーザーはアプリ側で特定し、userId を渡します。
  • アプリの process.env は使用できません。 defineComponent("name", { env: { API_KEY: v.string() } }) で型付き環境変数を宣言し、アプリから app.use(c, { env: { API_KEY: ... } }) で渡すか、関数の引数として値を渡します。
  • 境界をまたいだ .paginate() は機能しません。 コンポーネント内で convex-helpers の paginator を使用してください。
  • コンポーネントの http.ts にある HTTP ルートは、アプリ側で httpPrefix を使ってマウントします。コンポーネントの HTTP アクションでは ctx.auth を利用できません。
  • アプリへのコールバックは関数ハンドルとして渡します。アプリ側で createFunctionHandle(internal.x.y) を使い、v.string() として保存し、コンポーネント内で FunctionHandle<"mutation"> にキャストします。

npm への公開(exports マップ、ビルド順序、README、バージョン管理)については、references/publishing.md を参照してください。ローカルフォルダーではなくパッケージからコンポーネントをインストールする場合に開いてください。

よくある間違い

間違い問題が発生する理由代わりに行うこと
components.<name> に関数がないapp.use(...) が追加されていないか、その後に npx convex dev を実行していませんconvex.config.ts に登録し、開発環境を実行して、参照パスがファイル名と一致していることを確認します
アプリの _generated/server から mutation をインポートする関数がコンポーネントではなくアプリに登録されますコンポーネント独自の ./_generated/server.js からインポートします
React から api.counter.add を呼び出すコンポーネント関数は親からは内部参照ですctx.runMutation(components.counter.public.add) を呼び出すアプリのミューテーションを作成します
コンポーネントの引数に v.id("users") を使うテーブル番号はコンポーネントごとに異なるため、検証に失敗します境界では v.string() を使います
コンポーネント内で ctx.auth.getUserIdentity() を呼び出す常にユーザーが存在しない状態を返しますアプリで認証し、userId を渡します
コンポーネント内で process.env.MY_KEY を使う実行時に未定義になりますdefineComponent で環境変数を宣言するか、引数として渡します
returns のないパブリック関数親では型が any になり、型安全性が失われますすべてのパブリック関数にバリデーターを追加します
モジュールスコープでコンポーネントの環境変数を読み取るデプロイ分析中は未定義になりますハンドラー内で env.X を読み取ります

チェックリスト

  • ヘルパー関数ではなく、コンポーネントが必要だと判断した
  • convex.config.ts で defineComponent("<name>") を呼び出し、エクスポートしている
  • スキーマはコンポーネント内にあり、親テーブルに対する v.id() をどこにも使っていない
  • 関数はコンポーネント独自の _generated/server からインポートしている
  • すべてのパブリック関数に args と returns のバリデーターがある
  • 親が app.use(...) で登録され、npx convex dev が問題なく実行される
  • クライアントラッパーは最初に ComponentApi、次にオプションを受け取り、Pick の ctx 型を使っている
  • アプリ側のラッパー関数が認証を処理し、IDを文字列として渡している
  • コンポーネント内で ctx.auth、process.env、.paginate() を使っていない
  • コンポーネントをインストールするサンプルアプリでテストした

ドキュメント

コメント