Skip to content

Commit 5a3f8af

Browse files
authored
Merge pull request #52 from n9te9/feat/interface-object-directive
Feat/interface object directive
2 parents 16d2a1e + 0bec0ab commit 5a3f8af

7 files changed

Lines changed: 1194 additions & 57 deletions

File tree

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
# Design Doc : Federation InterfaceObject Directive
2+
3+
## Background
4+
5+
現在の go-graphql-federation-gateway は、`@interfaceObject` ディレクティブのパース機能は実装されているものの、このディレクティブが持つ本来の役割である「インターフェース型のエンティティ表現」の完全なサポートが不足しています。
6+
7+
Apollo Federation v2 の仕様では、`@interfaceObject` ディレクティブは以下の目的で使用されます:
8+
- インターフェース型自体をエンティティとして扱う
9+
- インターフェースの実装型ではなく、インターフェース型として _entities クエリで解決できるようにする
10+
- 異なるサブグラフでインターフェースのフィールドを拡張する
11+
12+
例えば、Node インターフェースを複数のサブグラフで拡張する場合:
13+
14+
```graphql
15+
# Subgraph A
16+
interface Node @interfaceObject @key(fields: "id") {
17+
id: ID!
18+
}
19+
20+
# Subgraph B
21+
interface Node @interfaceObject @key(fields: "id") {
22+
id: ID!
23+
createdAt: DateTime!
24+
}
25+
```
26+
27+
これにより、`node(id: "123")` クエリで異なるサブグラフのフィールドを統合できます。
28+
29+
## Summary
30+
31+
このドキュメントでは、`@interfaceObject` ディレクティブの完全な実装のための設計方針と実装アプローチを提案します。具体的には、インターフェース型のエンティティ解決、_entities クエリでのインターフェース型の処理、およびフィールドマージロジックの実装を行います。
32+
33+
## Goals
34+
35+
- `@interfaceObject` でマークされたインターフェース型をエンティティとして扱う機能の実装
36+
- インターフェース型の _entities クエリ解決の実装
37+
- 異なるサブグラフのインターフェースフィールドのマージ機能の実装
38+
- インターフェース型のエンティティキー解決の実装
39+
40+
## Non-Goals
41+
42+
- インターフェースの実装型の自動推論
43+
- @interfaceObject と通常のインターフェースの混在パターンの最適化
44+
- Relay スタイルの Connection インターフェースの特別処理
45+
46+
## Algorithm
47+
48+
### 現在の実装状況
49+
50+
**パース機能(実装済み):**
51+
52+
```go
53+
// subgraph_v2.go:87
54+
isInterfaceObject: hasDirective(objType.Directives, "interfaceObject"),
55+
56+
// subgraph_v2.go:249-251
57+
func (e *Entity) IsInterfaceObject() bool {
58+
return e.isInterfaceObject
59+
}
60+
```
61+
62+
**未実装箇所:**
63+
- インターフェース型定義の @interfaceObject パース
64+
- SuperGraph でのインターフェースエンティティのマージ
65+
- Planner でのインターフェースエンティティ解決
66+
- Executor での __typename: "InterfaceName" の処理
67+
68+
### 修正箇所 1: subgraph_v2.go でのインターフェース型パース
69+
70+
**実装方針:**
71+
72+
```go
73+
// InterfaceTypeDefinition と InterfaceTypeExtension も Entity として扱う
74+
for _, def := range doc.Definitions {
75+
// 既存: ObjectTypeDefinition の処理
76+
// ...
77+
78+
// 追加: InterfaceTypeDefinition の処理
79+
if intfType, ok := def.(*ast.InterfaceTypeDefinition); ok {
80+
if hasDirective(intfType.Directives, "interfaceObject") {
81+
entity := &Entity{
82+
Keys: parseEntityKeys(intfType.Directives),
83+
isExtension: false,
84+
Fields: make(map[string]*Field),
85+
isInterfaceObject: true,
86+
}
87+
88+
for _, field := range intfType.Fields {
89+
entity.Fields[field.Name.String()] = parseField(field)
90+
}
91+
92+
sg.entities[intfType.Name.String()] = entity
93+
}
94+
}
95+
96+
// 追加: InterfaceTypeExtension の処理
97+
if intfExt, ok := def.(*ast.InterfaceTypeExtension); ok {
98+
if hasDirective(intfExt.Directives, "interfaceObject") {
99+
entity := &Entity{
100+
Keys: parseEntityKeys(intfExt.Directives),
101+
isExtension: true,
102+
Fields: make(map[string]*Field),
103+
isInterfaceObject: true,
104+
}
105+
106+
for _, field := range intfExt.Fields {
107+
entity.Fields[field.Name.String()] = parseField(field)
108+
}
109+
110+
sg.entities[intfExt.Name.String()] = entity
111+
}
112+
}
113+
}
114+
```
115+
116+
### 修正箇所 2: super_graph_v2.go でのインターフェースマージ
117+
118+
**実装方針:**
119+
120+
```go
121+
// mergeSchemaDeepPass1() でインターフェース定義もマージ
122+
func (sg *SuperGraphV2) mergeSchemaDeepPass1() error {
123+
// 既存: ObjectTypeDefinition のマージ
124+
// ...
125+
126+
// 追加: InterfaceTypeDefinition のマージ
127+
for _, subGraph := range sg.SubGraphs {
128+
for _, def := range subGraph.Schema.Definitions {
129+
if intfType, ok := def.(*ast.InterfaceTypeDefinition); ok {
130+
existingIntf := findInterfaceType(sg.Schema, intfType.Name.String())
131+
if existingIntf == nil {
132+
// 新しいインターフェースを追加
133+
sg.Schema.Definitions = append(sg.Schema.Definitions, intfType)
134+
} else {
135+
// フィールドをマージ
136+
mergeInterfaceFields(existingIntf, intfType)
137+
}
138+
}
139+
}
140+
}
141+
return nil
142+
}
143+
144+
// インターフェースフィールドのマージ
145+
func mergeInterfaceFields(
146+
existing *ast.InterfaceTypeDefinition,
147+
new *ast.InterfaceTypeDefinition,
148+
) {
149+
for _, newField := range new.Fields {
150+
found := false
151+
for _, existingField := range existing.Fields {
152+
if existingField.Name.String() == newField.Name.String() {
153+
found = true
154+
break
155+
}
156+
}
157+
if !found {
158+
existing.Fields = append(existing.Fields, newField)
159+
}
160+
}
161+
}
162+
```
163+
164+
```mermaid
165+
flowchart TD
166+
Start([インターフェースマージ]) --> Loop{全サブグラフを走査}
167+
Loop -- 次の SubGraph --> CheckIntf{InterfaceTypeDefinition<br>存在?}
168+
CheckIntf -- Yes --> FindExisting{スーパーグラフに<br>同名のインターフェース<br>存在?}
169+
FindExisting -- No --> AddNew[新しいインターフェース<br>を追加]
170+
FindExisting -- Yes --> MergeFields[フィールドをマージ]
171+
AddNew --> Loop
172+
MergeFields --> Loop
173+
CheckIntf -- No --> Loop
174+
Loop -- 完了 --> End([終了])
175+
```
176+
177+
### 修正箇所 3: planner_v2.go でのインターフェースエンティティ解決
178+
179+
**実装方針:**
180+
181+
```go
182+
// selectSubGraphForEntityResolution() を拡張
183+
func (p *PlannerV2) selectSubGraphForEntityResolution(
184+
typeName string,
185+
parentSubGraph string,
186+
) *graph.SubGraphV2 {
187+
// インターフェース型の場合
188+
for _, sg := range p.SuperGraph.SubGraphs {
189+
if entity, ok := sg.GetEntity(typeName); ok {
190+
if entity.IsInterfaceObject() && entity.IsResolvable() {
191+
return sg
192+
}
193+
}
194+
}
195+
196+
// 通常のエンティティ解決
197+
return p.SuperGraph.GetEntityOwnerSubGraph(typeName)
198+
}
199+
```
200+
201+
### 修正箇所 4: executor_v2.go での __typename 処理
202+
203+
**実装方針:**
204+
205+
```go
206+
// buildRepresentation() でインターフェース型を正しく処理
207+
func buildRepresentation(
208+
entity map[string]interface{},
209+
keyFields []string,
210+
typename string, // ← 追加: 明示的に typename を渡す
211+
) map[string]interface{} {
212+
repr := map[string]interface{}{
213+
"__typename": typename, // インターフェース名を使用
214+
}
215+
216+
for _, kf := range keyFields {
217+
if val, ok := entity[kf]; ok {
218+
repr[kf] = val
219+
}
220+
}
221+
222+
return repr
223+
}
224+
225+
// extractRepresentations() で typename を保持
226+
// インターフェース型の場合、実装型ではなくインターフェース名を使用
227+
```
228+
229+
---
230+
231+
## Request Sequence
232+
233+
### インターフェースオブジェクトの解決
234+
235+
```mermaid
236+
sequenceDiagram
237+
participant Client
238+
participant Gateway
239+
participant CoreService
240+
participant MetadataService
241+
242+
Note over CoreService: interface Node @interfaceObject @key(fields: "id") {<br> id: ID!<br>}
243+
Note over MetadataService: interface Node @interfaceObject @key(fields: "id") {<br> id: ID!<br> metadata: Metadata!<br>}
244+
245+
Client->>Gateway: query { node(id: "123") { id metadata { ... } } }
246+
Note over Gateway: Planner: Node はインターフェースエンティティ<br>CoreService から id を取得
247+
Gateway->>CoreService: query { node(id: "123") { id } }
248+
CoreService-->>Gateway: { "__typename": "Node", "id": "123" }
249+
Note over Gateway: Entity Fetch: Node.metadata は<br>MetadataService が所有
250+
Gateway->>MetadataService: _entities([{ __typename: "Node", id: "123" }])
251+
MetadataService-->>Gateway: { "metadata": { ... } }
252+
Gateway->>Client: { node: { id: "123", metadata: { ... } } }
253+
```
254+
255+
### 実装型とインターフェースの混在
256+
257+
```mermaid
258+
sequenceDiagram
259+
participant Client
260+
participant Gateway
261+
participant Products
262+
participant Reviews
263+
264+
Note over Products: type Product implements Node @key(fields: "id") {<br> id: ID!<br> name: String!<br>}
265+
Note over Reviews: interface Node @interfaceObject @key(fields: "id") {<br> id: ID!<br> reviewCount: Int!<br>}
266+
267+
Client->>Gateway: query { product(id: "p1") { id name reviewCount } }
268+
Gateway->>Products: query { product(id: "p1") { id name } }
269+
Products-->>Gateway: { "__typename": "Product", "id": "p1", "name": "Widget" }
270+
Note over Gateway: reviewCount は Node インターフェース経由で取得
271+
Gateway->>Reviews: _entities([{ __typename: "Node", id: "p1" }])
272+
Reviews-->>Gateway: { "reviewCount": 42 }
273+
Gateway->>Client: { product: { id: "p1", name: "Widget", reviewCount: 42 } }
274+
```
275+
276+
---
277+
278+
## Development Command For AI Agent
279+
280+
### Process
281+
282+
**重要:** 以下のプロセスは TDD(テスト駆動開発)を厳守すること。各機能の実装前に必ずテストを書き、Red → Green → Refactor のサイクルを回すこと。
283+
284+
1. **SubGraph V2 拡張 (TDD)**
285+
1.1. **RED: テストを先に書く** - `subgraph_v2_test.go`
286+
- インターフェース型のエンティティパーステスト
287+
- @key, @requires, @provides の組み合わせテスト
288+
- テストを実行して失敗することを確認
289+
1.2. **GREEN: 最小限の実装** - `subgraph_v2.go`
290+
- InterfaceTypeDefinition の @interfaceObject パース
291+
- InterfaceTypeExtension の @interfaceObject パース
292+
- テストを実行して成功することを確認
293+
1.3. **REFACTOR: リファクタリング**
294+
- コードの重複を排除
295+
- 可読性を向上
296+
- テストが引き続き成功することを確認
297+
298+
2. **SuperGraph V2 拡張 (TDD)**
299+
2.1. **RED: テストを先に書く** - `super_graph_v2_test.go`
300+
- 複数サブグラフでのインターフェースマージテスト
301+
- フィールドの重複処理テスト
302+
- @interfaceObject と通常のインターフェースの区別テスト
303+
- テストを実行して失敗することを確認
304+
2.2. **GREEN: 最小限の実装** - `super_graph_v2.go`
305+
- mergeSchemaDeepPass1() でインターフェースマージ
306+
- mergeSchemaDeepPass2() でインターフェース拡張マージ
307+
- mergeInterfaceFields() ヘルパー関数追加
308+
- テストを実行して成功することを確認
309+
2.3. **REFACTOR: リファクタリング**
310+
- コードの重複を排除
311+
- 可読性を向上
312+
- テストが引き続き成功することを確認
313+
314+
3. **Planner V2 拡張 (TDD)**
315+
3.1. **RED: テストを先に書く** - `planner_v2_interfaceobject_test.go` (新規作成)
316+
- インターフェースオブジェクトのクエリプランニングテスト
317+
- 実装型とインターフェースの混在パターンテスト
318+
- 複数サブグラフでのインターフェースフィールド解決テスト
319+
- テストを実行して失敗することを確認
320+
3.2. **GREEN: 最小限の実装** - `planner_v2.go`
321+
- selectSubGraphForEntityResolution() でインターフェース型を考慮
322+
- インターフェースエンティティの Entity Fetch 生成
323+
- テストを実行して成功することを確認
324+
3.3. **REFACTOR: リファクタリング**
325+
- コードの重複を排除
326+
- 可読性を向上
327+
- テストが引き続き成功することを確認
328+
329+
4. **Executor V2 拡張 (TDD)**
330+
4.1. **RED: テストを先に書く** - `executor_v2_test.go`
331+
- インターフェース型の _entities クエリ実行テスト
332+
- __typename の正しい伝搬テスト
333+
- テストを実行して失敗することを確認
334+
4.2. **GREEN: 最小限の実装** - `executor_v2.go`
335+
- buildRepresentation() に typename 引数を追加
336+
- インターフェース型の representation 構築
337+
- extractRepresentations() でインターフェース型を正しく処理
338+
- テストを実行して成功することを確認
339+
4.3. **REFACTOR: リファクタリング**
340+
- コードの重複を排除
341+
- 可読性を向上
342+
- テストが引き続き成功することを確認
343+
344+
5. **結合テスト**
345+
5.1. `_example` にインターフェースオブジェクトのシナリオを追加(オプション)
346+
5.2. `make test-all` で全ドメインのテストが通ることを確認
347+
348+
**TDD チェックリスト:**
349+
- [ ] 各機能について、実装前にテストを書いたか?
350+
- [ ] テストが最初は失敗することを確認したか?(RED)
351+
- [ ] テストが成功する最小限のコードを書いたか?(GREEN)
352+
- [ ] リファクタリング後もテストが成功することを確認したか?(REFACTOR)
353+
- [ ] 全てのテストが通ることを確認したか?
354+
355+
### Expected Outcomes
356+
357+
- インターフェース型がエンティティとして正しく解決される
358+
- 異なるサブグラフのインターフェースフィールドが正しくマージされる
359+
- `@interfaceObject` ディレクティブが Apollo Federation v2 仕様に準拠して動作する
360+
- Relay スタイルの Node インターフェースパターンがサポートされる

0 commit comments

Comments
 (0)