Decision Log: gRPC Service → GraphQL 設計セッション
- 日時: 2026-07-11
- 進め方: グリリング形式(設計ツリーの根本から一問ずつ、選択肢と推奨案を提示して意思決定)。並行してコードベース調査と外部ライブラリ調査を実施
- 本ドキュメントの目的: 各決定の「なぜ」と、検討して捨てた代替案を後から追跡できるようにする。設計の結論だけを知りたい場合は design.md を参照
0. 出発点(初期要求)
この proto-graphql を拡張し、gRPC(Connect RPC) の Service の RPC 定義から完全な graphql schema を生成するような仕組みを考えたい。 rpc がそのまま query あるいは mutation となる。また、この Service から生成した GraphQL Schema は GraphQL Federation の subschema として運用されるイメージ。 federation 利用があるので、BatchGet RPC から dataloader 生成や、そこに
@keyを利用した entity 拡張の仕組みなどが必要になるはず。
1. 事前調査で判明した前提
質問設計の土台になった主要な事実(詳細は research.md):
- Service/Method レベルの proto オプション、Query/Mutation 生成、federation、dataloader はすべて存在しない(完全グリーンフィールド)。サービスを触る既存コードは
exceptRequestOrResponseのみ - protoc-gen-pothos は protobuf-es v2 をフルサポート済み。出力は「1 proto = 1 ファイル」でユーザーの
builderに side-effect 登録 - Connect-ES v2 は protobuf-es v2 必須。
protoc-gen-esv2 単体で message +GenServiceを生成しcreateClient(Service, transport)に渡せる - Pothos federation plugin の
resolveReferenceは representation 毎に呼ばれる(バッチ版なし)→ dataloader が公式推奨。asEntityは既存 TypeRef に後付け可能 - 旧 e2e は golden test framework に置換済み。query 実行スナップショット経路は配線済みだが未使用(休眠)
2. 意思決定の記録
以下、時系列。各項目は「提示した選択肢 → 決定(→ 推奨と違う場合はその旨)→ 理由・含意」の形式。
Q1. 実行モデル — 生成コードはどこまで実装を含むか
- 選択肢: (a) フル実装を生成(推奨) / (b) スケルトン生成(resolver はユーザー実装) / (c) フル実装 + オーバーライド機構
- 決定: (a) フル実装を生成
- 理由: 「完全な schema を生成」という要件に合致。Connect の
Transport抽象により、別プロセスへの呼び出し(BFF 構成)と同一プロセス内実行(createRouterTransport)を同じ生成コードでカバーできることが判明したため、実行形態の柔軟性は Transport 注入で担保できる - 含意: ユーザーが書くのは builder / context / server 組み立てのみ。オーバーライドは Pothos の resolver 差し替え等で自然に可能なため専用機構は不要
Q2. 対応ランタイム
- 選択肢: (a) protobuf-es v2 専用(推奨) / (b) ts-proto も同時対応 / (c) 将来も protobuf-es のみと割り切る
- 決定: (a) protobuf-es v2 専用(ts-proto は将来の拡張余地として設計上塗り固めない)
- 理由: Connect-ES v2 が protobuf-es v2 必須。ts-proto 対応は nice-grpc 等の別エコシステムのクライアント生成が丸ごと必要になり、PoC の初期コストが倍増する
- 含意:
protobuf_lib != protobuf-esで(graphql.service)を検出したらエラーにする
Q3〜Q6. アーキテクチャの探索(3 度のピボットの経緯)★重要
この区間は最終決定に至るまでに 3 案を行き来した。検討済みの代替案と、それぞれを見送った理由がここに残っている。
Q3. 提供形態(1 回目)
- 選択肢: (a) protoc-gen-pothos に統合(推奨) / (b) 新プラグインを作る / (c) 統合だが別ファイル出力
- ユーザー選択: (b) 新プラグイン(推奨に反する選択)
- この時点で提示した事実: entity 化は
asEntityの後付けで message printer 変更なしに可能 / printer 層は protoc-gen-pothos 内部にあり共有にはリファクタリングが先行する
Q4. 新プラグインのスコープ → 根本的な問題提起が発生
- 選択肢: (a) コンパニオン型(型は protoc-gen-pothos に任せ RPC 由来だけ生成、推奨) / (b) スーパーセット型(単体で全部生成)
- ユーザー回答は選択肢外: 「resolver をユーザーが書かないなら Pothos(code-first DSL)は冗長では」という問題提起。代替として:
protoc-gen-graphql(素の.graphqlSDL 生成)+ server preset の出力に合わせた resolver 実装生成- gqlkit を前提とした
protoc-gen-gqlkitで最小限の生成物にする
- この指摘は正しい: Pothos の価値は「手書きコードの型安全性」であり、全生成なら中間 DSL を挟む必然性は薄い
事実確認: server preset と SDL 直生成の実態
- GraphQL Code Generator の Server Preset(
@eddeee888/gcg-typescript-resolver-files)は「1 フィールド = 1 resolver ファイルの人間が編集するスタブを生成し、既存実装は上書きしない」思想 → 機械が実装を全生成する用途とは思想が衝突 - ただし型レイヤ(
typescript-resolversのResolvers型)は再利用可能で、「preset 互換」=この型規約に合わせた完全実装済み resolvers map の生成、という解釈なら成立 - SDL 路線の最小構成を定式化:
schema.graphql(federation directive 込み)+ 完全実装済み typed resolvers map を生成し、ユーザーはbuildSubgraphSchema({ typeDefs, resolvers: {...generated, ...overrides} })するだけ。Pothos も preset も不要、yoga/apollo 非依存
Q5. 主軸の選択(2 回目)
- 選択肢: (a) SDL + resolvers map(推奨) / (b) gqlkit 前提(protoc-gen-gqlkit) / (c) SDL をコアに両展開
- 決定: (a) SDL + resolvers map — federation は SDL の本家表現、framework 非依存、gqlkit アダプタは後から追加可能な設計とする
- gqlkit を即採用しなかった理由(当時): federation 完全未対応で前提作業が発生、pre-1.0 の外部プロジェクトへの結合
Q6. 値フロー → gqlkit 回帰(3 回目)
- SDL 路線の核心の質問「resolver の parent 値は protobuf-es の MessageShape をそのまま流すか、全変換するか」を提示したところ、ユーザーが前言撤回: 「それを考えるとやはり protoc-gen-gqlkit を実装したうえでそれをベースにするのがいい。gqlkit は TypeScript object の値変換の機構を持つため」
- → gqlkit をクローンして精査(詳細は research.md §3)。判明した実態:
- gqlkit は静的解析器で「TS モデル型 = resolver parent 型」。構造的検出のため生成 TS はそのまま通る(protobuf-es/Prisma 生成型を模した golden test も存在)
- ただし自動の値変換は限定的(enum 値マップ、カスタムスカラー実装注入、ignoreFields、
$typeName除去)。任意のフィールド変換はdefineFieldで表現する = Pothos 版の field resolver 生成と本質的に同等の生成が必要 - 具体的ギャップ:
bigint未対応で hard fail(int64 系)/ oneof の{case, value}はそのままでは union にならない / proto enum 規約(UNSPECIFIED 除去)なし / federation 完全未対応(directive 機構はあるため@keydirective 生成 +buildSubgraphSchemaglue で非改修でも組める見込み)/ dataloader 規約なし
Q6(再). 最終方針の確定
- 選択肢: (a) gqlkit ベースで確定(当時の推奨) / (b) やはり SDL 直生成 / (c) 両段構え(コア共通化)
- ユーザー最終判断(選択肢外・確定): 「やっぱり protoc-gen-pothos の拡張から始めるのがいい。そっちで PoC 的に進めつつ、うまくいきそうなら protoc-gen-gqlkit 実装時に resolver 対応や federation 対応を考える。Pothos への federation subgraph 対応も『resolver 対応』→『federation 対応』とステップを踏む」
- この判断の合理性:
- 値変換問題は既存 protoc-gen-pothos の機構(
objectRef<MessageShape>+ フィールド毎 resolver)が既に解決済み asEntityの後付けにより federation 対応も message printer 変更なしで載る- PoC の知見(オプション設計、マッピング規則、dataloader)は gqlkit 版にほぼそのまま還元できる
- 値変換問題は既存 protoc-gen-pothos の機構(
- 確定事項: protoc-gen-pothos 拡張 / PoC / Step 1 = resolver 対応 → Step 2 = federation 対応 / protoc-gen-gqlkit は将来課題
Q7. Query / Mutation の判定ルール
- 選択肢: (a) 明示オプション必須(推奨) / (b) 規約デフォルト + オーバーライド / (c) 命名規約ベース
- 決定: (b)
idempotency_level = NO_SIDE_EFFECTS→ Query、それ以外 → Mutation、(graphql.rpc).operationで上書き(推奨に反する選択) - 理由: idempotency_level は proto 標準の副作用シグナル(Connect も HTTP GET 対応に使用)。アノテーション量を抑える
- 「全 RPC 自動公開事故」の懸念は Q8 の opt-in で担保する前提
Q8. 生成対象のオプトイン単位
- 選択肢: (a) service オプション(推奨) / (b) プラグインパラメータ / (c) 両方
- 決定: (a)
(graphql.service)を付けたサービスのみ生成 - 理由: proto が唯一の真実。同一ファイル内で公開/非公開サービスを共存できる。プラグインパラメータ追加不要。個別 RPC の除外は
(graphql.rpc).ignore
Q9. フィールド名の導出
- 選択肢: (a) camelCase そのまま(推奨) / (b) AIP 風 prefix 除去(GetUser → user) / (c) name 指定必須
- 決定: (a)
GetUser→getUser、(graphql.rpc).nameで上書き - 理由: 予測可能で誤爆なし(
GetOrCreateSession等)。「デフォルトは機械的変換、美学はオプションで」という既存(graphql.field).nameと同じ思想
Q10. 引数の展開方式
- 選択肢: (a) 常に flatten(推奨) / (b) 常に single input / (c) Query は flatten / Mutation は input
- 決定: ユーザー独自案 = (c) + suffix 変換オプション
- Query: request フィールドを引数に flatten
- Mutation: 単一
input引数 - さらに
ignore_requests/ignore_responsesの発展形として、RPC のXxxRequest→XxxInput(Input object)、XxxResponse→XxxPayload(object type)へ suffix 変換して生成するオプションを新設
- 理由: GraphQL コミュニティの慣習(参照系はフラット、更新系は Relay 流 input/payload)に一致し、既存オプションとの思想的連続性がある。Relay の mutation payload 慣習が proto から直接導出される
Q11. Query のラッパー response の unwrap
- 選択肢: (a) 変換のみ + 明示 unwrap(推奨) / (b) 単一フィールド自動 unwrap / (c) 常に変換のみ
- 決定: (a) デフォルトは構造保持(
GetUserResponse→GetUserPayload)、unwrap したい RPC のみ(graphql.rpc).expose_fieldで指定 - 理由: 自動 unwrap は「response にフィールドが追加された瞬間、GraphQL スキーマが
User→GetUserPayloadへ暗黙に破壊的変更される」進化ハザードがあるため不採用。Connect/buf 界隈はラッパー response が主流なので unwrap の需要自体はある → 明示指定で両立
Q12. エラー変換ポリシー
- 選択肢: (a) code 付き GraphQLError(推奨) / (b) NOT_FOUND は null に変換 / (c) 素通し(masking 任せ)
- 決定: (a)
ConnectError→GraphQLError(message = rawMessage, extensions.code = "NOT_FOUND" 等)。details はデフォルト非掲載(情報漏洩防止、オプトインで拡張)。変換関数は runtime helper で差し替え可能 - 補足: federation の
_entitiesは「見つからない entity は null」がプロトコル仕様なので、Step 2 の dataloader 経路では NotFound → null が構造的に強制される(ポリシー選択ではない) - あわせて承認されたデフォルト: streaming RPC は生成対象外(警告、明示 operation 指定時はエラー)、
google.protobuf.Emptyは request → 引数ゼロ / response →Boolean(常に true)
Q13. Connect client の注入方式
- 選択肢: (a) context 規約 + runtime helper(推奨) / (b) 生成 init 関数(モジュールグローバル) / (c) Pothos plugin 化
- 決定: (a) GraphQL context に所定のキー(transport + 任意の callOptions hook)を置く規約とし、runtime パッケージの
getClient(ctx, Service)が per-service に memoize して client を返す - 理由: per-request ヘッダ伝搬・テストでの transport 差し替え(
createRouterTransport)・サービス毎の transport 振り分けをすべて自然にカバー。context 型が規約を満たさなければ生成コードが型エラーになりコンパイル時に検出できる - 含意: Pothos 系初のランタイムパッケージ新設が確定(エラー変換 helper もここに載る)
Q14. @key の宣言方法
- 選択肢: (a)
(graphql.object_type).federation.key(推奨) / (b)object_type.keyにフラット追加 / (c) field オプションで指定 - 決定: (a) ネストした
GraphqlFederationOptionsに repeated fieldset 文字列 - 理由: 将来の federation 系オプション(shareable / external / inaccessible 等)の集約先を確保し、既存オプションの名前空間を汚さない。proto の後方互換制約上、フラットに追加すると後からネストへ移せない。field オプション案は複数キーセット・複合キーが表現できない
- 付随決定: fieldset は proto フィールド名で記述し plugin が GraphQL 名へ変換(
(graphql.field).nameによるリネームと自動整合)
Q15. entity 解決(resolveReference)と RPC の紐付け
- 選択肢: (a) method オプションで宣言(推奨) / (b) message 側から RPC を文字列参照 / (c) 命名規約(BatchGet*)自動検出
- 決定: (a) BatchGet 系 RPC に method オプションを付与。request のキーリスト / response の entity リストのフィールド対応は「唯一の repeated フィールド」なら自動推論、複数あれば明示必須
- 理由: 生成物(dataloader + asEntity)が service 側ファイルに集約され、「このサービスがこの entity を解決する」という所有権宣言として読める。(b) は message の proto が service を知る形で関心の向きが逆。(c) は opt-in 思想と矛盾
- 付随決定: entity resolver に指定された RPC はデフォルトで Query 非公開(公開したければ
operation: QUERYを併記)。loader は内部機構のため
Q16〜Q17. dataloader の実装と生成場所
- Q16 選択肢: (a) runtime パッケージで自前(推奨) / (b)
@pothos/plugin-dataloader/ (c) ハイブリッド - Q16 決定: 自前。ただしユーザーから「runtime で頑張るかコード生成するかは要検討。protoc-gen-dataloader を別で作りそれに依存させるのもあり」との方向づけ
@pothos/plugin-dataloaderを使わない理由: builder への plugin 追加が必須になり(federation plugin だけに抑えたい)、生成コードが plugin 固有 API に結合し、キー照合ロジックを自分の手に持てない- Q17 選択肢: (a) protoc-gen-dataloader 新設(推奨) / (b) protoc-gen-pothos に内蔵 / (c) runtime の汎用関数のみ
- Q17 決定: (a) protoc-gen-dataloader 新設
- 理由: dataloader のコードは Connect-ES + protobuf-es のみに依存し GraphQL 非依存にできる → 将来の protoc-gen-gqlkit や非 GraphQL 用途で再利用可能。cross-plugin import の懸念(Q4 で議論)は、loader ファイルが同一 proto 由来で suffix 違いになるだけなので軽微。薄い共通処理(context 規約・per-context キャッシュ・キー照合)は runtime パッケージへ
- → 独立ドキュメント ../protoc-gen-dataloader/design.md に分離(2026-07-11 のフィードバックによる。federation 対応と独立して開発を進められるため)
Q18. BatchGet レスポンスの照合方式
- 選択肢: (a) キーマッチ(推奨) / (b) 順序前提(AIP-231 準拠) / (c) デフォルトキーマッチ + 選択制
- 決定: (a) 返ってきた entity の @key フィールド値で request keys と突き合わせて並べ直す
- 理由: AIP-231 は「リクエスト順・atomic」だが、現実の BatchGet 実装は「見つかったものだけ返す」「順序保証なし」も多い。順序前提は非準拠サーバで entity の静かな取り違え(silent data corruption) を起こす。キーマッチは順序・欠損・重複すべてに頑健で、欠損は null(
_entities仕様に整合)。複合キーは fieldset 順で serialize して照合
Q19. 外部 entity 拡張の proto 表現
- 選択肢: (a) スタブ message + method オプション(推奨) / (b) method オプションのみ(型情報を文字列で) / (c) 本物の message を import して宣言
- 決定: (a) キーフィールドだけ持つスタブ message(
federation.extends宣言)+ RPC 側の method オプションで「どの型のどのフィールドになるか・request のどのフィールドに親のキーを詰めるか」を宣言 - 理由: キーの型が proto の型システムで表現される(typo・型不一致を codegen 時に検証可能)。「このサブグラフが知っている User の形」が proto 上で可視化される。将来の
@requires(非キー external フィールド)にも拡張可能。(b) は型情報が文字列に漏れる。(c) は import した message の全フィールドの扱いが複雑化し他チーム proto への依存も発生
Q20. MethodOptions の extension 名
- 選択肢: (a)
(graphql.rpc)(推奨) / (b)(graphql.method)/ (c)(graphql.operation) - 決定: (a) proto の
rpcキーワードと一致して短く直感的。「rpc に対する GraphQL マッピング設定」と読め、operation 以外の関心(entity resolver、extend、batch)も自然に同居できる。(c) は entity loader 専用 RPC が operation ではないため名前と内容がずれる
Q21. テスト戦略
- 選択肢: (a) golden 拡張 + 実行テスト(推奨) / (b) golden のみ / (c) golden + 別立て E2E
- 決定: (a) service 付き proto を testapis に追加して golden case 化(コード snapshot + 型チェック + SDL snapshot)し、さらに
createRouterTransportのフェイク Connect サーバでquery.graphql実行 → 結果 snapshot(休眠中の実行経路を初活用)。Step 2 では_entities直接実行 +@apollo/compositionの compose 検証 - 理由: resolver 実装が本機能の核であり、バグの大半(引数詰め替え・エラー変換・dataloader 照合)は実行時に宿る。(c) は旧 e2e を廃して golden に寄せた直近のリポジトリ方針と逆行
Q22. proto 拡張オプションの追加プロセス
- 選択肢: (a) 本家に experimental で追加(推奨) / (b) 最初から正式仕様 / (c) JS リポジトリでローカル試作
- 決定: (a) proto-graphql 本家に experimental 明記で追加(field number 2056 慣例)、Go binding 同時再生成、submodule 更新 →
pnpm gen:extensions - 理由: import パスが最初から正規(
graphql/schema.proto)なので将来の移行痛なし。PoC の学びを仕様に反映する余地を宣言できる。(c) は利用者の proto の import パス変更という破壊的移行が確定してしまう
Q23. ローカルリレーションのステップ配置
- 選択肢: (a) Step 2 に含める(推奨) / (b) Step 3 以降 / (c) Step 1 に前倒し
- 決定: (a) extend 機構(RPC → 既存型のフィールド化)を外部 entity とローカル型の両対応で設計・実装
- 理由: 実装の 8 割が共通(externalRef か既存 objectRef かの違いのみ)で、機構の一般性を両ケースで検証できる。dataloader 基盤(Step 2 成果物)を N+1 対策にそのまま使える。(c) はバッチング基盤なしで N+1 のまま出すか dataloader を Step 1 に引き込むかの二択になり PoC が肥大化
最終確認
- 上記すべてを統合した設計サマリを提示し、共有理解への到達を確認、ドキュメント化へ進むことに合意
3. セッション後の追加フィードバック(2026-07-11)
- kiro spec フォーマットは使わない(
.kiro/specs/形式で作成した初版をdocs/design/へ移設・再構成) - 議論・意思決定の記録ドキュメントを追加(本ドキュメント)
- protoc-gen-dataloader の設計を独立ドキュメントに分離 — federation 対応と独立して開発可能なため
- この分離に伴い、loader 宣言オプションを
(graphql.rpc).federation.entity_resolverから(graphql.rpc).batchとして federation の外に出す改訂案を提示している(federation の entity_resolver は batch 宣言を参照する薄いフラグになる)。詳細と確認事項は ../protoc-gen-dataloader/design.md を参照
- この分離に伴い、loader 宣言オプションを
4. 確定事項サマリ
| 論点 | 決定 |
|---|---|
| 実装形態 | protoc-gen-pothos の拡張(PoC)。新プラグイン・SDL 直生成・gqlkit 案は見送り(§2 Q3〜Q6) |
| resolver | フル実装を生成。Transport 注入で別プロセス / in-process 両対応 |
| ランタイム | protobuf-es v2 専用(Connect-ES v2 前提) |
| リリース | Step 1: RPC → Query/Mutation → Step 2: Federation |
| opt-in | (graphql.service)。個別除外は (graphql.rpc).ignore |
| operation 判定 | idempotency_level 規約 + operation 上書き |
| 命名 | camelCase(rpc 名)+ name 上書き |
| 引数 | Query flatten / Mutation single input |
| Request/Response | suffix 変換オプション(→ XxxInput / → XxxPayload) |
| unwrap | expose_field による明示のみ(自動 unwrap 不採用) |
| エラー | code 付き GraphQLError、details 非掲載、helper 差し替え可 |
| client 注入 | context 規約 + runtime パッケージ getClient(ctx, Service) |
| streaming / Empty | 対象外(警告)/ 引数ゼロ・Boolean |
| @key | (graphql.object_type).federation.key(proto フィールド名 fieldset) |
| entity 解決 | method オプション + 自動推論、loader はデフォルト Query 非公開 |
| dataloader | 自前実装、protoc-gen-dataloader として独立(別ドキュメント) |
| 照合 | キーマッチ(順序非依存、欠損 null) |
| entity 拡張 | スタブ message + (graphql.rpc).federation.extend。ローカル型リレーションも同機構で Step 2 |
| オプション追加 | proto-graphql 本家に experimental、Go binding 同時再生成 |
| テスト | golden 拡張 + createRouterTransport 実行テスト、Step 2 で _entities + composition 検証 |
5. 未決事項(実装時に確定)
design.md §7 および protoc-gen-dataloader/design.md の残項目を参照。主なもの:
- suffix 変換オプションの正式名と
ignore_requests/ignore_responses併用時の優先順位 - flatten 時の oneof 引数の XOR バリデーション要否
- パッケージ正式名(runtime、protoc-gen-dataloader の npm 名)
6. 詳細設計フェーズの決定(2026-07-11 続き)
実装を opus / sonnet の subagent に委譲する前提で詳細計画を立てるフェーズに移行。以下を決定:
Q24. 実装計画のスコープ
- 選択肢: (a) Step 1 込みの全体計画(推奨) / (b) federation/dataloader のみ
- 決定: (a) 本家オプション追加 → Step 1(resolver + runtime パッケージ)→ dataloader → federation の全フェーズを一つの依存グラフとして計画する。federation/dataloader は Step 1 の基盤(オプション getter、context 規約、printer 基盤)に依存するため
Q25. (graphql.rpc).batch 独立化(前回の要確認事項)
- 決定: 承認。loader 宣言は
(graphql.rpc).batchとして federation の外に置き、federation.entity_resolverは batch 宣言を参照する薄いフラグとする
Q26. group loader(1 キー → N entities)
- 選択肢: (a) 今回のスコープに含める(推奨) / (b) 後回し(entity loader のみ)
- 決定: (a)
batch.group = trueでDataLoader<K, V[]>を生成。extend RPC(ローカルリレーション含む)のバッチングに必須で、キーマッチ機構の自然な拡張(groupBy)のため実装増分が小さい
Q27. 複合キーのスコープ
- 選択肢: (a) 単一フィールドキー限定で開始(推奨) / (b) 最初から複合キー対応
- 決定: (a) 複合キー指定は codegen エラーとする。ただしオプション形式(repeated fieldset)と照合機構は複合キーへ拡張可能な形を維持する(design-ready)
Q28. BatchGet RPC が key_field 以外のフィールドを持つ場合の扱い
- 背景: DataLoader のバッチは「キー以外の入力が均一」が前提(1 バッチ = 1 RPC)。よって非キーフィールドは「リクエストの一部」ではなく「ローダーの分割単位」。実例は (a) AIP の
parent(実質複合キーの変種)、(b)view/read_mask(取得形状)、(c) locale 等(per-request 定数)、(d) フィルタ - 選択肢: (a) 非キーフィールドがあれば codegen エラー / (b) 常に unset 固定 / (c) loader パラメータ化(推奨) / (d) context hook 注入
- 決定: (c) loader パラメータ化。accessor の第 2 引数に
Omit<MessageInitShape<Request>, keyField>型の params を取り、params 値の組ごとに DataLoader を分離(二段キャッシュ: ctx → paramsKey → DataLoader)。required フィールド(behavior comment 判定)があれば params を型レベルで必須化 - 帰結: federation 経由(entity_resolver / extend)は params を渡せないため、required な非キーフィールドを持つ RPC は codegen エラー(F10)、optional は unset。将来の extend フィールド GraphQL 引数化では args → params が受け皿になる。固定値注入(
request_defaults)は将来検討 - 反映先: dataloader design §3(V9)/ §4.5、federation-design §3(F10)、実装計画 D1/D2/D3/D5/T3.1
実装フェーズで確定した設計修正(2026-07-12)
<Rpc>LoaderParamsの Omit 廃止(D5 の golden 型チェックで発覚): protobuf-es v2 のMessageInitShapeは union 型でOmitが分配されず型不整合になるため、params 型は完全な request init shapeとする。key_field が params に混入してもcallが常に上書きするため正しさは保たれる(dataloader design §4.5 に反映)DataLoader型の import:dataloaderは CJS export assignment のため protoplugin のcreateImportSymbol(named import 専用)が使えず、import type DataLoader from "dataloader";を生成ファイル毎に直接出力する- golden ルートは
tests/golden-dataloader/(pothos の resolveConfig がtests/golden/直下の未知ディレクトリで throw するため分離)
実装委譲の前提
- 実装タスクは「依頼票」形式(目的 / 前提 / 参照 / 成果物 / 受け入れ基準 / 実装ヒント / 推奨モデル)に分解し、opus または sonnet の subagent に依頼する
- 詳細は implementation-plan.md と protoc-gen-dataloader/implementation-plan.md
Q29. proto オプションの per-track landing(2026-07-12)
- 背景:
add-service-rpc-optionsブランチ(proto-graphql 本家 PR)は当初 service/operation/federation/batch の全 experimental オプションを一括追加していた。しかし本リポジトリ側でコンシューマ実装が存在するのは protoc-gen-dataloader が消費する(graphql.rpc).batchのみで、service→operation 変換(Step 1)・federation(Step 2)はまだ実装 PR が着地していない - 選択肢: (a) 全オプションを一括で upstream に着地させる / (b) 各トラックのコンシューマ実装 PR と同時に、そのトラックのオプションだけを着地させる per-track landing(推奨)
- 決定: (b) per-track landing。upstream PR を
(graphql.rpc).batch(GraphqlRpcBatchOptions+GraphqlRpcOptions.batch = 5+rpc = 2056extension)のみに絞り、service/operation/federation 関連のメッセージ・フィールドは対応する PR(Step 1 / Step 2)まで追加しない - 理由:
- 未消費・未確定形状のオプションを proto に着地させない: service→operation・federation はまだ実装で仕様が固まっておらず、実装中に API shape が変わりうる。消費者のいない experimental option を先に確定させても、実装時の手戻りで結局 upstream を再改訂することになりやすく、無消費の期間だけ「触ってはいけない/意味のない」proto 表面が増える
- proto への追加は後方互換であり、先送りは無料(free): 新しい message/field はいつでも追加でき、既存ユーザーへの影響もない。「今のうちに全部足しておく」ことに実利がなく、コンシューマと同時に着地させたほうが 1 PR あたりのレビュー単位も小さくなる
- トラック間の依存を proto レベルで作らない(dataloader トラックの upstream PR が、federation トラックの設計未確定を理由にブロックされずに済む)
- 帰結:
GraphqlRpcOptionsはbatchフィールド(5)のみを持つ。1-4(ignore/operation/name/expose_field)・10(federation)はコンシューマ実装 PR で埋める前提の慣行上の予約であり、proto のreservedキーワードでは予約していないGraphqlObjectTypeOptions.federation(field 5)、GraphqlSchemaOptionsの 5-6(requests_as_inputs/responses_as_payloads)も同様に未定義GraphqlFederationOptionsが upstream に存在しないため、batch の entity_key@keyフォールバック(protoc-gen-dataloader design §3 V5)が実装できない。entity_key は entity モード・group モードともに当面必須とし、フォールバックは federation 対応(Step 2)の PR でGraphqlFederationOptionsと同時に追加する。federation-design.md の F4(entity_resolver ↔ batch の整合チェック)も同様に Step 2 まで休眠する
- 反映先: protoc-gen-dataloader/design.md §2/§3(V5)、design.md §2、federation-design.md §1
Q30. loader パラメータの受け渡しタイミング(accessor time → load time、2026-07-13)
- 背景: ユーザー要望により、Q28 で決めた「params を accessor の第 2 引数で渡す」設計を見直した。accessor time 方式は
batchGetUsersLoader(ctx, params)のように呼び出す毎に新しい参照を作れてしまい、同一 ctx でも params の異なる呼び出しを取り違えやすい・呼び出し側が「1 回だけ ctx で解決してから複数キーを load する」自然なパターンと相性が悪い、という指摘があった - 選択肢:
- (a) 現状維持(accessor time:
(ctx, params?) => DataLoader) - (b) wrapper オブジェクト化(採用): accessor は
(ctx) => RpcLoaderのみを返し、RpcLoader.load(key, ...params)/loadMany(keys, ...params)/loader(...params)で params を load time に渡す。wrapper 内部で ctx → paramsKey → DataLoader の二段キャッシュを維持 - (c) 複合キー単一 DataLoader 化: key と params をまとめた複合キー(
{ key, paramsKey })を 1 つの DataLoader に渡し、batch 関数側で paramsKey ごとにグルーピングして複数 RPC を発行する
- (a) 現状維持(accessor time:
- 決定: (b) wrapper オブジェクト化
- (c) を却下した理由:
maxBatchSizeの意味論が濁る:maxBatchSizeは「1 RPC あたりの最大キー数」のはずだが、複合キーを 1 つの DataLoader に通すと「1 バッチ(= 1 tick 分の複合キー集合)」と「1 RPC(= paramsKey でグルーピングした後の実際の呼び出し単位)」がずれる。maxBatchSizeを複合キー単位で切ると同一 paramsKey 内の RPC 分割数が意図と食い違い、paramsKey 単位で切ろうとすると DataLoader 標準のmaxBatchSizeオプションでは表現できず自前のバッチ分割ロジックが要る- グループ単位のエラー伝搬が複雑になる: DataLoader は「1 バッチ = 1 batch 関数呼び出し = 1 reject 単位」が前提。複合キー単一 DataLoader では 1 tick に複数 paramsKey が混在しうるため、batch 関数内で paramsKey ごとに RPC を分けて呼び、それぞれの成否を元の複合キー配列の対応する位置に再マッピングする必要がある。これは今 wrapper 側でやっている「paramsKey ごとに素の DataLoader を分ける」ことを batch 関数の中に押し込めるだけで、実装が複雑になる上に「RPC 呼び出しが 1 回」という Q12 のエラー変換ポリシー(呼び出し側が reject をそのまま扱える)の前提が崩れる
- (b) は Q28 の「paramsKey ごとに DataLoader を分ける」二段キャッシュ構造をそのまま保てるため、
maxBatchSizeは DataLoader 標準機能のまま・エラー伝搬も DataLoader 単位のままで済む
- 副次効果: 生成ファイルの const アノテーションが
DataLoader<K, V>からRpcLoader<K, V, PArgs>に変わったことで、生成ファイルがdataloaderパッケージの型を直接 import しなくなった(実装フェーズで確定した設計修正の「DataLoader型の import」を参照)。RpcLoader.loader(...)が.prime()/.clear()用に生のDataLoaderを返すが、その型は@proto-graphql/connect-runtime経由でのみ参照される - 反映先: protoc-gen-dataloader/design.md §1.1/§4.2/§4.3/§4.5、
@proto-graphql/connect-runtimeのRpcLoader型・createRpcLoader、生成物の const アノテーション、golden テスト・実行テスト
7. 将来課題への申し送り(protoc-gen-gqlkit)
本 PoC の知見を protoc-gen-gqlkit に還元する際、research.md §3 の gqlkit 精査結果が前提になる。特に:
- proto オプション設計(
(graphql.service)/(graphql.rpc)/federation/batch)は生成ターゲットに依存しないためそのまま再利用できる - protoc-gen-dataloader の生成物は GraphQL 非依存のためそのまま依存できる
- gqlkit 側の要対応: bigint(branded scalar 生成で回避可)、oneof の型再整形、federation(directive 生成 +
buildSubgraphSchemaglue で非改修でも可、ネイティブ対応は gqlkit のロードマップ判断)