Skip to content

Commit 349fee0

Browse files
authored
Merge pull request #53 from n9te9/feat/federation-tag-directive
Feat/federation tag directive
2 parents 5a3f8af + f116ea1 commit 349fee0

6 files changed

Lines changed: 1209 additions & 1 deletion

File tree

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
# Design Doc : Federation Tag Directive
2+
3+
## Background
4+
5+
現在の go-graphql-federation-gateway は、`@tag` ディレクティブのパース機能は実装されているものの、このディレクティブが持つ本来の役割である「メタデータの伝搬とフィルタリング」の活用機能が実装されていません。
6+
7+
Apollo Federation v2 の仕様では、`@tag` ディレクティブは以下の目的で使用されます:
8+
- スキーマ要素(型、フィールド、引数)に任意のメタデータをタグ付けする
9+
- タグ情報をスーパーグラフに伝搬する
10+
- 契約バリアント(Contract Variants)でのフィルタリングに使用する
11+
- ドキュメント生成やツールでのメタデータとして活用する
12+
13+
例えば、以下のようなユースケースがあります:
14+
15+
```graphql
16+
type Product @key(fields: "id") {
17+
id: ID!
18+
name: String!
19+
price: Float! @tag(name: "public")
20+
internalCost: Float! @tag(name: "internal")
21+
}
22+
```
23+
24+
この場合、`@tag(name: "public")` はクライアント向けAPIに公開するフィールドをマークし、`@tag(name: "internal")` は内部ツール専用のフィールドをマークします。
25+
26+
## Summary
27+
28+
このドキュメントでは、`@tag` ディレクティブの情報をスーパーグラフに伝搬し、メタデータとして活用できるようにするための設計方針と実装アプローチを提案します。具体的には、タグ情報のスーパーグラフへのマージ、タグ情報の取得API、および将来的なフィルタリング機能の基盤を整備します。
29+
30+
## Goals
31+
32+
- `@tag` ディレクティブのタグ情報をスーパーグラフスキーマに伝搬する機能の実装
33+
- フィールド、型、引数のタグ情報を取得するAPI の実装
34+
- タグ情報をメタデータとして保持する構造の整備
35+
36+
## Non-Goals
37+
38+
- 契約バリアント(Contract Variants)の完全な実装
39+
- タグベースのスキーマフィルタリング機能(将来的には検討)
40+
- タグ情報のIntrospectionクエリへの露出(Apollo Studioとの互換性が必要な場合は将来実装)
41+
- カスタムディレクティブとしての @tag の動的な定義
42+
43+
## Algorithm
44+
45+
### 現在の実装状況
46+
47+
**パース機能(実装済み):**
48+
49+
```go
50+
// subgraph_v2.go:214-220
51+
case "tag":
52+
// Parse name argument of @tag directive
53+
for _, arg := range d.Arguments {
54+
if arg.Name.String() == "name" {
55+
tagName := strings.Trim(arg.Value.String(), "\"")
56+
f.Tags = append(f.Tags, tagName)
57+
}
58+
}
59+
60+
// subgraph_v2.go:259-261
61+
func (f *Field) GetTags() []string {
62+
return f.Tags
63+
}
64+
```
65+
66+
**未実装箇所:**
67+
- 型レベルの @tag パース
68+
- 引数レベルの @tag パース
69+
- タグ情報のスーパーグラフへのマージ
70+
- タグ情報の取得API
71+
72+
### 修正箇所 1: subgraph_v2.go での型レベル @tag パース
73+
74+
**実装方針:**
75+
76+
```go
77+
// Entity 構造にタグ情報を追加
78+
type Entity struct {
79+
Keys []EntityKey
80+
isExtension bool
81+
Fields map[string]*Field
82+
isInterfaceObject bool
83+
Tags []string // ← 追加
84+
}
85+
86+
// ObjectTypeDefinition の Directives から @tag を抽出
87+
func parseTypeTags(directives []*ast.Directive) []string {
88+
var tags []string
89+
for _, d := range directives {
90+
if d.Name == "tag" {
91+
for _, arg := range d.Arguments {
92+
if arg.Name.String() == "name" {
93+
tagName := strings.Trim(arg.Value.String(), "\"")
94+
tags = append(tags, tagName)
95+
}
96+
}
97+
}
98+
}
99+
return tags
100+
}
101+
102+
// Entity 作成時にタグを設定
103+
entity := &Entity{
104+
Keys: parseEntityKeys(objType.Directives),
105+
isExtension: false,
106+
Fields: make(map[string]*Field),
107+
isInterfaceObject: hasDirective(objType.Directives, "interfaceObject"),
108+
Tags: parseTypeTags(objType.Directives),
109+
}
110+
```
111+
112+
### 修正箇所 2: super_graph_v2.go でのタグ情報のマージ
113+
114+
**実装方針:**
115+
116+
```go
117+
// スーパーグラフにタグメタデータマップを追加
118+
type SuperGraphV2 struct {
119+
Schema *ast.Document
120+
SubGraphs []*SubGraphV2
121+
TypeTags map[string][]string // typeName -> tags
122+
FieldTags map[string]map[string][]string // typeName -> fieldName -> tags
123+
// ...
124+
}
125+
126+
// buildTagMetadata() でタグ情報を収集
127+
func (sg *SuperGraphV2) buildTagMetadata() {
128+
sg.TypeTags = make(map[string][]string)
129+
sg.FieldTags = make(map[string]map[string][]string)
130+
131+
for _, subGraph := range sg.SubGraphs {
132+
for typeName, entity := range subGraph.GetEntities() {
133+
// 型レベルのタグをマージ
134+
if len(entity.Tags) > 0 {
135+
sg.TypeTags[typeName] = mergeUniqueTags(
136+
sg.TypeTags[typeName],
137+
entity.Tags,
138+
)
139+
}
140+
141+
// フィールドレベルのタグをマージ
142+
if sg.FieldTags[typeName] == nil {
143+
sg.FieldTags[typeName] = make(map[string][]string)
144+
}
145+
for fieldName, field := range entity.Fields {
146+
if len(field.Tags) > 0 {
147+
sg.FieldTags[typeName][fieldName] = mergeUniqueTags(
148+
sg.FieldTags[typeName][fieldName],
149+
field.Tags,
150+
)
151+
}
152+
}
153+
}
154+
}
155+
}
156+
157+
// タグの重複を排除してマージ
158+
func mergeUniqueTags(existing, new []string) []string {
159+
tagSet := make(map[string]bool)
160+
for _, tag := range existing {
161+
tagSet[tag] = true
162+
}
163+
for _, tag := range new {
164+
tagSet[tag] = true
165+
}
166+
167+
result := make([]string, 0, len(tagSet))
168+
for tag := range tagSet {
169+
result = append(result, tag)
170+
}
171+
sort.Strings(result) // 一貫性のためソート
172+
return result
173+
}
174+
```
175+
176+
```mermaid
177+
flowchart TD
178+
Start([buildTagMetadata]) --> InitMaps[TypeTags, FieldTags<br>マップを初期化]
179+
InitMaps --> LoopSG{全サブグラフを走査}
180+
LoopSG -- 次の SubGraph --> LoopEntity{全エンティティを走査}
181+
LoopEntity -- 次のエンティティ --> HasTypeTags{型レベルのタグ<br>存在?}
182+
HasTypeTags -- Yes --> MergeTypeTags[TypeTags に<br>マージ]
183+
HasTypeTags -- No --> LoopFields
184+
MergeTypeTags --> LoopFields{全フィールドを走査}
185+
LoopFields -- 次のフィールド --> HasFieldTags{フィールドレベルの<br>タグ存在?}
186+
HasFieldTags -- Yes --> MergeFieldTags[FieldTags に<br>マージ]
187+
HasFieldTags -- No --> LoopFields
188+
MergeFieldTags --> LoopFields
189+
LoopFields -- 完了 --> LoopEntity
190+
LoopEntity -- 完了 --> LoopSG
191+
LoopSG -- 完了 --> End([終了])
192+
```
193+
194+
### 修正箇所 3: タグ情報取得 API
195+
196+
**実装方針:**
197+
198+
```go
199+
// SuperGraphV2 にタグ情報取得メソッドを追加
200+
201+
// GetTypeTags returns the tags for a given type
202+
func (sg *SuperGraphV2) GetTypeTags(typeName string) []string {
203+
return sg.TypeTags[typeName]
204+
}
205+
206+
// GetFieldTags returns the tags for a given field
207+
func (sg *SuperGraphV2) GetFieldTags(typeName, fieldName string) []string {
208+
if fieldMap, ok := sg.FieldTags[typeName]; ok {
209+
return fieldMap[fieldName]
210+
}
211+
return nil
212+
}
213+
214+
// HasTag checks if a type has a specific tag
215+
func (sg *SuperGraphV2) HasTypeTag(typeName, tag string) bool {
216+
tags := sg.GetTypeTags(typeName)
217+
for _, t := range tags {
218+
if t == tag {
219+
return true
220+
}
221+
}
222+
return false
223+
}
224+
225+
// HasFieldTag checks if a field has a specific tag
226+
func (sg *SuperGraphV2) HasFieldTag(typeName, fieldName, tag string) bool {
227+
tags := sg.GetFieldTags(typeName, fieldName)
228+
for _, t := range tags {
229+
if t == tag {
230+
return true
231+
}
232+
}
233+
return false
234+
}
235+
```
236+
237+
### 修正箇所 4: スーパーグラフスキーマへのタグディレクティブの追加
238+
239+
**実装方針:**
240+
241+
```go
242+
// mergeSchemaDeepPass1() でフィールドマージ時にタグディレクティブを保持
243+
func mergeFieldWithTags(
244+
existingField *ast.FieldDefinition,
245+
newField *ast.FieldDefinition,
246+
) {
247+
// 既存のタグディレクティブを収集
248+
existingTags := extractTagDirectives(existingField.Directives)
249+
newTags := extractTagDirectives(newField.Directives)
250+
251+
// マージしたタグを新しいディレクティブとして追加
252+
mergedTags := mergeUniqueTags(existingTags, newTags)
253+
254+
// 既存のタグディレクティブを削除
255+
existingField.Directives = removeTagDirectives(existingField.Directives)
256+
257+
// マージしたタグを追加
258+
for _, tag := range mergedTags {
259+
tagDirective := createTagDirective(tag)
260+
existingField.Directives = append(existingField.Directives, tagDirective)
261+
}
262+
}
263+
```
264+
265+
---
266+
267+
## Request Sequence
268+
269+
### タグ情報の伝搬と取得
270+
271+
```mermaid
272+
sequenceDiagram
273+
participant Developer
274+
participant Gateway
275+
participant SubGraphA
276+
participant SubGraphB
277+
278+
Note over SubGraphA: type Product @tag(name: "public") {<br> id: ID!<br> name: String! @tag(name: "public")<br>}
279+
Note over SubGraphB: extend type Product {<br> internalCost: Float! @tag(name: "internal")<br>}
280+
281+
Developer->>Gateway: スーパーグラフ構築
282+
Note over Gateway: buildTagMetadata():<br>各サブグラフからタグ情報を収集
283+
Gateway->>SubGraphA: スキーマ取得
284+
SubGraphA-->>Gateway: Product: ["public"]<br>name: ["public"]
285+
Gateway->>SubGraphB: スキーマ取得
286+
SubGraphB-->>Gateway: internalCost: ["internal"]
287+
Note over Gateway: タグ情報をマージ:<br>Product.TypeTags = ["public"]<br>Product.name.FieldTags = ["public"]<br>Product.internalCost.FieldTags = ["internal"]
288+
289+
Developer->>Gateway: GetFieldTags("Product", "internalCost")
290+
Gateway-->>Developer: ["internal"]
291+
```
292+
293+
### 将来的なフィルタリング(参考)
294+
295+
```mermaid
296+
sequenceDiagram
297+
participant Client
298+
participant Gateway
299+
participant FilterEngine
300+
301+
Note over Client: publicクライアント
302+
Client->>Gateway: query { product { id name internalCost } }
303+
Gateway->>FilterEngine: フィルタリング設定: exclude tag="internal"
304+
FilterEngine->>Gateway: internalCost を除外
305+
Note over Gateway: HasFieldTag("Product", "internalCost", "internal") == true<br>→ フィールドを削除
306+
Gateway-->>Client: Error: field 'internalCost' does not exist on type 'Product'
307+
```
308+
309+
---
310+
311+
## Development Command For AI Agent
312+
313+
### Process
314+
315+
**重要:** 以下のプロセスは TDD(テスト駆動開発)を厳守すること。各機能の実装前に必ずテストを書き、Red → Green → Refactor のサイクルを回すこと。
316+
317+
1. **SubGraph V2 拡張 (TDD)**
318+
1.1. **RED: テストを先に書く** - `subgraph_v2_test.go`
319+
- 型レベル @tag のパーステスト
320+
- フィールドレベル @tag のパーステスト(既存)
321+
- 複数タグの処理テスト
322+
- テストを実行して失敗することを確認
323+
1.2. **GREEN: 最小限の実装** - `subgraph_v2.go`
324+
- Entity 構造に Tags フィールドを追加
325+
- parseTypeTags() 関数を追加
326+
- Entity 作成時に型レベルのタグをパース
327+
- テストを実行して成功することを確認
328+
1.3. **REFACTOR: リファクタリング**
329+
- コードの重複を排除
330+
- 可読性を向上
331+
- テストが引き続き成功することを確認
332+
333+
2. **SuperGraph V2 拡張 (TDD)**
334+
2.1. **RED: テストを先に書く** - `super_graph_v2_test.go`
335+
- 複数サブグラフからのタグマージテスト
336+
- タグの重複排除テスト
337+
- タグ情報取得 API の動作確認テスト(GetTypeTags, GetFieldTags, HasTypeTag, HasFieldTag)
338+
- テストを実行して失敗することを確認
339+
2.2. **GREEN: 最小限の実装** - `super_graph_v2.go`
340+
- SuperGraphV2 構造に TypeTags, FieldTags マップを追加
341+
- buildTagMetadata() 関数を追加
342+
- mergeUniqueTags() ヘルパー関数を追加
343+
- NewSuperGraphV2() で buildTagMetadata() を呼び出し
344+
- タグ情報取得 API を追加(GetTypeTags, GetFieldTags, HasTypeTag, HasFieldTag)
345+
- テストを実行して成功することを確認
346+
2.3. **REFACTOR: リファクタリング**
347+
- コードの重複を排除
348+
- 可読性を向上
349+
- テストが引き続き成功することを確認
350+
351+
3. **スキーママージの改善(オプション・TDD)**
352+
3.1. **RED: テストを書く**
353+
- スーパーグラフスキーマに @tag ディレクティブがマージされることのテスト
354+
3.2. **GREEN: 実装**
355+
- mergeSchemaDeepPass1() でタグディレクティブを保持
356+
- スーパーグラフスキーマに @tag ディレクティブをマージ
357+
3.3. **REFACTOR: 改善**
358+
359+
4. **ドキュメントとサンプル**
360+
4.1. タグ機能の使用例を README に追加
361+
4.2. `_example` にタグ使用のサンプルを追加(オプション)
362+
363+
5. **結合テスト**
364+
5.1. `make test-all` で全ドメインのテストが通ることを確認
365+
5.2. タグ機能が既存の動作を壊していないことを確認
366+
367+
**TDD チェックリスト:**
368+
- [ ] 各機能について、実装前にテストを書いたか?
369+
- [ ] テストが最初は失敗することを確認したか?(RED)
370+
- [ ] テストが成功する最小限のコードを書いたか?(GREEN)
371+
- [ ] リファクタリング後もテストが成功することを確認したか?(REFACTOR)
372+
- [ ] 全てのテストが通ることを確認したか?
373+
374+
### Expected Outcomes
375+
376+
- `@tag` ディレクティブのタグ情報がスーパーグラフに伝搬される
377+
- タグ情報をメタデータとして取得できる API が提供される
378+
- 将来的なフィルタリング機能の基盤が整備される
379+
- スキーマのメタデータ管理が向上する
380+
- ドキュメント生成やツールでタグ情報を活用できる

0 commit comments

Comments
 (0)