Implementation Plan: protoc-gen-dataloader
設計: design.md / 全体計画: ../grpc-service-to-graphql/implementation-plan.md
実装は opus / sonnet の subagent に依頼する前提のタスク分解。各タスクは独立した依頼票として使える粒度にしてある。
外部依存(このトラックの前提)
| 前提 | 供給元 |
|---|---|
(graphql.rpc).batch オプションの TS binding(gen:extensions 済み) | 全体計画 Phase 0(本家オプション追加) |
runtime パッケージの骨格(context 規約・getClient・callRpc) | 全体計画 Phase 1 / T1.1 |
| golden ハーネス | 既存(protoc-gen-pothos 内)。D4 で汎用化 |
Phase 0 と T1.1 が完了していれば、このトラック全体は grpc-service-to-graphql の Step 1 実装(printer 等)や federation と並行して進められる。
タスク依存グラフ
(Phase 0, T1.1 完了)
├── D1: runtime createRpcLoader ──────────┐
├── D2: codegen-core BatchSpec 抽象 ──────┤
└── D4: golden ハーネス汎用化 ─────┐ │
│ ▼
│ D3: プラグイン本体(printer)
│ │
└──────┴─▶ D5: fixtures + golden + 実行テスト
│
▼
D6: docs + 命名確定D1 / D2 / D4 は相互に独立(並行可)。
D1: runtime createRpcLoader の実装
- 推奨モデル: sonnet(仕様確定済みのロジック実装)
- 依存: T1.1(runtime パッケージ骨格)
- 参照: design.md §4.3(実装仕様 1〜5)、§4.4(キー型)、§4.5(loader パラメータ)、§8-1(テスト観点)
- 変更対象: runtime パッケージ(
packages/@proto-graphql/connect-runtime/(仮))src/rpcLoader.ts+ テスト - 成果物:
createRpcLoader(entity / group のオーバーロード、二段 per-context キャッシュ(ctx → paramsKey → DataLoader)、paramsKey 算出(toBinary(create(requestSchema, params ?? {})))、キーマッチ照合、maxBatchSize 透過)- 単体テスト: 順序シャッフル / 欠損(null・
[])/ 重複キー / 余剰 entity / エラー全キー伝搬 / maxBatchSize 分割 / ctx 毎のキャッシュ分離 / 同一 ctx 内バッチ合流 / bigint キー / params(同一 params 合流・異なる params 分離・undefined≡{}・bigint 含む params)
- 受け入れ基準:
pnpm build成功、追加テストがパス、dataloaderを dependencies に追加(catalog 管理に従う) - 実装ヒント: DataLoader の batch 関数は Error インスタンスを返り値配列に混ぜられるが、本実装は「バッチ全体 throw = 全キー reject」で十分(per-key Error は使わない)。
extractEntitiesの型はcallの戻り型と generics で結合すること
D2: codegen-core への BatchSpec 抽象の追加
- 推奨モデル: opus(既存の型抽象・診断メッセージ設計との整合判断が必要)
- 依存: Phase 0(オプション binding)
- 参照: design.md §2(オプション)、§3(V1〜V9 と推論・エラー UX)、§4.4(キー型)、§4.5(loader パラメータ)、§6(検証の分担)
- 変更対象:
packages/@proto-graphql/codegen-core/src/types/(新規BatchSpec.ts等)+types/util.ts(getRpcOptionsは Phase 0 で追加済み想定) - 成果物:
DescMethod→BatchSpec | BatchSpecErrorを返す resolver(key_field / entity_field / entity_key の解決、推論、V1〜V9 検証、@key フォールバック(単一フィールドのみ)、キー TS 型の導出、非キーフィールドの列挙と required 判定(isRequiredFieldの input セマンティクス再利用)= params モデル)- エラーは「候補フィールドの列挙 + 明示オプションの記入例」を含む診断文字列を持つこと(snapshot テスト)
- 単体テスト(推論成功 / 各エラーケース)
- 受け入れ基準:
pnpm build && pnpm test成功。protoc-gen-pothos 側から import 可能な公開 API(codegen-coreの index 経由) - 実装ヒント: 既存
exceptRequestOrResponse(types/util.ts)がサービス走査の参考。EMPTY_*_OPTIONSの frozen シングルトンパターンに従う
D3: プラグイン本体(パッケージ雛形 + printer)
- 推奨モデル: sonnet(protoc-gen-pothos の既存構造の移植)
- 依存: D2(BatchSpec)。D1 は型参照のみ(並行末尾で合流可)
- 参照: design.md §4.1〜4.2(ファイルレイアウト・生成コード)、§5(パラメータ)
- 変更対象:
packages/protoc-gen-dataloader/(新規パッケージ) - 成果物:
createEcmaScriptPluginベースのプラグイン(protoc-gen-pothos のplugin.ts/printer.ts構造を踏襲、@proto-graphql/protoc-plugin-helpersにパラメータパーサを追加:filename_suffix/runtime_module等)- printer: batch 宣言のある RPC 毎に
createRpcLoader呼び出しを生成(codeタグ付きテンプレート +createImportSymbol)。非キーフィールドがある場合は<RpcName>LoaderParams型(Omit<MessageInitShape<...>, keyField>)を export し、required 有無で params 引数の必須/任意を切り替える(design.md §4.5)。BatchSpecError はCodeGeneratorResponse.errorとして報告 - ESM+CJS dual export、bin 定義、既存パッケージと同じ build 設定
- 受け入れ基準:
pnpm build成功。手元確認: testapis の batch 付き proto(D5 で追加。先行する場合は仮 fixture)に対して意図した TS を出力 - 実装ヒント: 出力ファイル名は
filenameFromProtoFile(codegen-coreprinter/util.ts)の suffix 差し替えで実現。import_prefix の解決はprotoImportPath相当を再利用
D4: golden ハーネスの汎用化
- 推奨モデル: opus(既存テスト資産のリファクタリング判断)
- 依存: なし(独立。ただし D5 より先)
- 参照: research.md §1.5(現行 golden の構造)、
packages/protoc-gen-pothos/src/__tests__/golden/_helpers/ - 成果物: golden ハーネス(
codeGenerationRunner/fileWriter/snapshotValidator/typeScriptChecker/testCaseDiscovery)を「プラグイン差し替え可能」にする。方式は次のいずれかを調査の上選択し、決定理由を PR 説明に残す:devPackages/golden-test-harnessへの抽出(将来の protoc-gen-gqlkit にも有効)- protoc-gen-pothos 内のまま plugin 注入引数を追加し、protoc-gen-dataloader からは dev 依存で利用
- 受け入れ基準: 既存 golden テストがスナップショット変更ゼロでパスし続けること(純リファクタ)。
pnpm test成功 - 注意: 挙動変更・スナップショット更新を伴う変更は行わない。ハーネスの公開 API は最小限に
D5: fixtures + golden ケース + 実行テスト
- 推奨モデル: sonnet
- 依存: D1 + D3 + D4
- 参照: design.md §8(テスト 2〜3)、testing-strategy
- 成果物:
devPackages/testapis-protoに dataloader 用 proto 追加(例:testapis/dataloader/: entity 推論成功 / 明示指定 / group / max_batch_size / 非キーフィールドあり(optional のみ・required あり) / エラー系数種)+gen:testapis系設定更新 + FileDescriptorSet 再生成- golden ケース(生成コード snapshot + 型チェック + 診断エラー snapshot)
- 実行テスト:
createRouterTransportフェイクサーバで (a) バッチ合流(RPC 呼び出し回数)、(b) シャッフル/欠損/重複照合、(c) エラー伝搬、(d) group の groupBy を検証
- 受け入れ基準:
pnpm test成功(新規ケース含む)。実行テストがフェイクサーバの呼び出し回数をアサートしていること - 実装ヒント: 実行テストの配置は golden ケース内
query-result経路ではなく、protoc-gen-dataloader パッケージの vitest として生成物を import する形でよい(GraphQL を介さないため)
D6: ドキュメント + 命名確定
- 推奨モデル: sonnet
- 依存: D3 / D5
- 成果物:
docs/protoc-gen-dataloader/(利用者向け: getting-started、buf 設定、オプションリファレンス、生成コードリファレンス)docs/proto-annotations/reference.mdに(graphql.rpc).batch追記- npm パッケージ名の空き確認(
@proto-graphql/protoc-gen-dataloader+ binprotoc-gen-dataloaderを第一候補として検証)と package.json への反映。buf plugin(BSR)公開の要否メモ
- 受け入れ基準: docs のリンク切れなし、
pnpm exec biome check(変更ファイル限定) 成功
進め方の共通規約(全タスク)
- タスク毎に feature ブランチ + worktree で分離し、Conventional Commits(
feat(protoc-gen-dataloader): ...等)で小さくコミット - 受け入れ基準のコマンドは subagent 自身が実行して結果を報告する(
pnpm build/pnpm test/pnpm exec biome check(変更ファイル限定)) - golden snapshot の更新(
vitest -u)は「意図した出力変更」の場合のみ。意図しない差分が出たら停止して報告 - 各タスク完了後、メインセッションで code-review(正しさ + 既存規約との整合)を実施してからマージ