Skip to content

Commit c812a6a

Browse files
authored
Merge pull request #55 from n9te9/improve-directive-override
Improve directive override
2 parents c9f65d4 + dfd5e67 commit c812a6a

4 files changed

Lines changed: 670 additions & 24 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Design Doc : Improve Directive Override
2+
3+
## Background
4+
5+
現在の go-graphql-federation-gateway は、`@override` ディレクティブのパース機能は実装されているものの、クエリプランニング時にフィールドの所有権オーバーライドを正しく適用する検証テストが不足しています。これにより、`@override(from: "subgraphA")` で指定されたフィールドが、実際にサブグラフBで解決されるべきところ、元のサブグラフAに送信されてしまう可能性があります。
6+
7+
Apollo Federation v2 の仕様では、`@override` ディレクティブはフィールドの所有権を別のサブグラフに移譲するために使用されます。例えば、Product.description フィールドが元々 products サービスで定義されていたが、後に catalog サービスに移行する場合、catalog サービスで `description: String! @override(from: "products")` と定義することで、Gateway は catalog サービスから description を取得するようになります。
8+
9+
## Summary
10+
11+
このドキュメントでは、`@override` ディレクティブの動作を検証し、フィールドの所有権オーバーライドが正しく機能することを確保するための設計方針と実装アプローチを提案します。具体的には、`super_graph_v2.go``buildOwnershipMap()`@override を正しく処理していることの検証、および `planner_v2.go` がオーバーライドされたフィールドを正しいサブグラフに送信することの確認を行います。
12+
13+
## Goals
14+
15+
- `@override` ディレクティブによるフィールド所有権の移譲が正しく動作することの検証
16+
- 複数サブグラフでの順次オーバーライド(A→B→C)のサポート確認
17+
- オーバーライド元サブグラフへのクエリが送信されないことの確認
18+
19+
## Non-Goals
20+
21+
- `@override` の段階的ロールアウト機能(progressive @override)の実装
22+
- `@override` のバリデーションエラー検出機能(存在しないサブグラフ名の検出など)
23+
- スキーマ変更時の自動マイグレーション機能
24+
25+
## Algorithm
26+
27+
### 現在の実装状況
28+
29+
**パース機能(実装済み):**
30+
31+
```go
32+
// subgraph_v2.go:204-210
33+
case "override":
34+
// Parse from argument of @override directive
35+
for _, arg := range d.Arguments {
36+
if arg.Name.String() == "from" {
37+
from := strings.Trim(arg.Value.String(), "\"")
38+
f.Override = &OverrideMetadata{From: from}
39+
}
40+
}
41+
```
42+
43+
**所有権マップ構築(実装済み):**
44+
45+
```go
46+
// super_graph_v2.go:367-407
47+
// buildOwnershipMap() で @override を考慮
48+
if field.Override != nil {
49+
// @override(from: "subgraphName") がある場合、
50+
// 元のサブグラフを所有権マップから除外
51+
delete(ownershipMap, field.Override.From)
52+
}
53+
```
54+
55+
### 検証が必要な動作
56+
57+
```mermaid
58+
flowchart TD
59+
Start([buildOwnershipMap]) --> Loop{全サブグラフを走査}
60+
Loop -- 次の SubGraph --> CheckFields{フィールドを走査}
61+
CheckFields -- 次のフィールド F --> HasOverride{F に @override あり?}
62+
HasOverride -- Yes --> RemoveOriginal[ownershipMap から<br>F.Override.From を削除]
63+
RemoveOriginal --> AddNew[現在のサブグラフを<br>ownershipMap に追加]
64+
AddNew --> CheckFields
65+
HasOverride -- No --> AddNormal[通常通り<br>ownershipMap に追加]
66+
AddNormal --> CheckFields
67+
CheckFields -- 完了 --> Loop
68+
Loop -- 完了 --> End([終了])
69+
```
70+
71+
### 修正箇所 1: super_graph_v2_test.go の検証テスト追加
72+
73+
**追加するテスト:**
74+
75+
```go
76+
// TestSuperGraphV2_Override_BasicOverride
77+
// - productsサービスで定義されたフィールドをcatalogサービスでオーバーライド
78+
// - GetSubGraphsForField() がcatalogのみを返すこと
79+
80+
// TestSuperGraphV2_Override_ChainedOverride
81+
// - A→B→Cの順次オーバーライド
82+
// - 最終的なオーナーがCになること
83+
84+
// TestSuperGraphV2_Override_PartialFields
85+
// - 一部のフィールドのみオーバーライド
86+
// - オーバーライドされていないフィールドは元のサブグラフが所有
87+
```
88+
89+
### 修正箇所 2: planner_v2_override_test.go の結合テスト追加
90+
91+
**追加するテスト:**
92+
93+
```go
94+
// TestPlannerV2_Override_QueryRouting
95+
// - クエリが正しいサブグラフにルーティングされること
96+
// - オーバーライド元サブグラフへのクエリが生成されないこと
97+
98+
// TestPlannerV2_Override_WithEntityFetch
99+
// - エンティティフェッチ時のオーバーライド動作
100+
// - @override されたフィールドが正しいサブグラフで解決されること
101+
```
102+
103+
---
104+
105+
## Request Sequence
106+
107+
### 基本的な @override の動作
108+
109+
```mermaid
110+
sequenceDiagram
111+
participant Client
112+
participant Gateway
113+
participant Catalog
114+
participant Products
115+
116+
Note over Products: 旧実装: description フィールドを所有
117+
Note over Catalog: 新実装: description @override(from: "products")
118+
119+
Client->>Gateway: query { product { id name description } }
120+
Note over Gateway: buildOwnershipMap():<br>description の所有権は catalog
121+
Gateway->>Catalog: query { product { id name description } }
122+
Note over Gateway: products サービスにはクエリを送信しない
123+
Catalog-->>Gateway: { id: "p1", name: "Widget", description: "..." }
124+
Gateway->>Client: { product: { id, name, description } }
125+
```
126+
127+
### 順次オーバーライド (A→B→C)
128+
129+
```mermaid
130+
sequenceDiagram
131+
participant Client
132+
participant Gateway
133+
participant ServiceC
134+
participant ServiceB
135+
participant ServiceA
136+
137+
Note over ServiceA: 最初の実装
138+
Note over ServiceB: @override(from: "serviceA")
139+
Note over ServiceC: @override(from: "serviceB")
140+
141+
Client->>Gateway: query { product { field } }
142+
Note over Gateway: buildOwnershipMap():<br>最終的な所有者は serviceC
143+
Gateway->>ServiceC: query { product { field } }
144+
ServiceC-->>Gateway: { field: "value" }
145+
Gateway->>Client: { product: { field: "value" } }
146+
```
147+
148+
---
149+
150+
## Development Command For AI Agent
151+
152+
### Process
153+
154+
**重要:** 以下のプロセスは TDD(テスト駆動開発)を厳守すること。既存の実装を検証するためのテストを先に書き、Red → Green → Refactor のサイクルを回すこと。
155+
156+
1. **SuperGraph V2 テスト追加 (TDD)**
157+
1.1. **RED: テストを先に書く** - `super_graph_v2_test.go`
158+
- 基本的な @override の所有権移譲テスト
159+
- 順次オーバーライド(A→B→C)テスト
160+
- 部分的なフィールドオーバーライドテスト
161+
- テストを実行(既存の実装で成功するはずだが、念のため確認)
162+
1.2. **GREEN: 実装の検証と修正**
163+
- 既存の実装で全テストが通ることを確認
164+
- テストが失敗した場合は実装を修正
165+
- テストカバレッジは 95% 以上を目指す
166+
1.3. **REFACTOR: リファクタリング**
167+
- 必要に応じてコードを改善
168+
- テストが引き続き成功することを確認
169+
170+
2. **Planner V2 テスト追加 (TDD)**
171+
2.1. **RED: テストを先に書く** - `planner_v2_override_test.go` (新規作成)
172+
- @override されたフィールドへのクエリが正しいサブグラフに送信されることのテスト
173+
- オーバーライド元サブグラフへのクエリが生成されないことのテスト
174+
- エンティティフェッチ時の @override 動作テスト
175+
- テストを実行(既存の実装で成功するはずだが、念のため確認)
176+
2.2. **GREEN: 実装の検証と修正**
177+
- 既存の実装で全テストが通ることを確認
178+
- テストが失敗した場合は実装を修正
179+
2.3. **REFACTOR: リファクタリング**
180+
- 必要に応じてコードを改善
181+
- テストが引き続き成功することを確認
182+
183+
3. **結合テスト**
184+
3.1. `_example` にオーバーライドシナリオを追加(オプション)
185+
3.2. `make test-all` で全ドメインのテストが通ることを確認
186+
187+
**TDD チェックリスト:**
188+
- [ ] 各機能について、実装前(または既存実装の検証前)にテストを書いたか?
189+
- [ ] テストを実行して、期待通りの結果が得られることを確認したか?
190+
- [ ] テストが失敗した場合は実装を修正したか?(GREEN)
191+
- [ ] リファクタリング後もテストが成功することを確認したか?(REFACTOR)
192+
- [ ] 全てのテストが通ることを確認したか?
193+
194+
### Expected Outcomes
195+
196+
- `@override` ディレクティブの動作が包括的にテストされる
197+
- フィールドの所有権移譲が正しく機能することが保証される
198+
- 回帰テストとして将来の変更を保護できる

federation/graph/super_graph_v2.go

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -502,47 +502,40 @@ func (sg *SuperGraphV2) buildOwnershipMap() error {
502502
fieldName := field.Name.String()
503503
key := fmt.Sprintf("%s.%s", typeName, fieldName)
504504

505-
// Check for @override directive
506-
var overrideFrom string
507-
var overrideSubGraph *SubGraphV2
508-
505+
// Collect all @override relationships for this field.
506+
// overrideChain maps each "from" subgraph name to the subgraph that
507+
// overrides it. This supports chained overrides: A→B→C where C is
508+
// the ultimate owner and both A and B must be excluded.
509+
overrideChain := make(map[string]*SubGraphV2)
509510
for _, subGraph := range sg.SubGraphs {
510511
if entity, exists := subGraph.GetEntity(typeName); exists {
511512
if entityField, ok := entity.Fields[fieldName]; ok {
512513
if override := entityField.GetOverride(); override != nil {
513-
overrideFrom = override.From
514-
overrideSubGraph = subGraph
515-
break
514+
overrideChain[override.From] = subGraph
516515
}
517516
}
518517
}
519518
}
520519

521-
// Traverse all subgraphs to find those that can resolve this field
520+
// Build the set of excluded subgraphs — those that appear as the
521+
// "from" target of any @override. In a chain A→B→C, both A and B
522+
// are excluded so that only C remains as the owner.
523+
excludedByOverride := make(map[string]bool, len(overrideChain))
524+
for fromName := range overrideChain {
525+
excludedByOverride[fromName] = true
526+
}
527+
528+
// Traverse all subgraphs to find those that can resolve this field,
529+
// skipping any that have been superseded by @override.
522530
for _, subGraph := range sg.SubGraphs {
523-
// Skip the original owner if @override is present
524-
if overrideFrom != "" && subGraph.Name == overrideFrom {
531+
if excludedByOverride[subGraph.Name] {
525532
continue
526533
}
527534

528535
if sg.canResolveField(subGraph, typeName, fieldName) {
529536
sg.Ownership[key] = append(sg.Ownership[key], subGraph)
530537
}
531538
}
532-
533-
// Ensure the override subgraph is in the ownership list
534-
if overrideSubGraph != nil {
535-
found := false
536-
for _, owner := range sg.Ownership[key] {
537-
if owner.Name == overrideSubGraph.Name {
538-
found = true
539-
break
540-
}
541-
}
542-
if !found {
543-
sg.Ownership[key] = append(sg.Ownership[key], overrideSubGraph)
544-
}
545-
}
546539
}
547540
}
548541

0 commit comments

Comments
 (0)