|
| 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