Skip to content

Commit b53be5e

Browse files
committed
feat(graphql): implement federation support
1 parent e40f7b7 commit b53be5e

10 files changed

Lines changed: 1202 additions & 8 deletions

File tree

configs/default.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,16 @@ graphql:
8080
enable_http_handler: true # Expose /graphql/docs endpoint
8181
auto_export: false # Auto-write docs to file after generation
8282
export_path: "" # Optional output file path for schema docs
83+
federation:
84+
enabled: true # Enable GraphQL federation support (_service/_entities)
85+
service_name: "helios" # Service name exposed to supergraph composition
86+
service_version: "1.0.0" # Service version metadata
87+
include_service_sdl: true # Expose schema SDL via _service.sdl
88+
strict_entities: false # If true, unknown/invalid entities fail the entire request
89+
entity_types: # Allowed federated entity types
90+
- "User"
91+
- "KVPair"
92+
- "Job"
93+
- "ShardNode"
94+
- "RaftPeer"
8395

docs/GRAPHQL_IMPLEMENTATION.md

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,116 @@ When `enable_http_handler` is true, gateway exposes:
14581458
4. **Enable auto-export** in environments where docs need to be persisted.
14591459
5. **Use type-specific queries** (`schemaTypeDocumentation`) for targeted tooling.
14601460

1461+
## GraphQL Federation Support
1462+
1463+
Helios now supports Apollo-style federation primitives for service composition in a supergraph.
1464+
1465+
### Implementation
1466+
1467+
Federation support is implemented and integrated in:
1468+
1469+
- `internal/graphql/federation.go` (federation config + manager)
1470+
- `internal/graphql/resolver.go` (`_service`, `_entities`, and federation config resolvers)
1471+
- `internal/graphql/handler.go` (federation query routing)
1472+
- `internal/graphql/schema.go` (federation SDL: directives, `_Service`, `_Entity`, entity keys)
1473+
1474+
### Federation Capabilities
1475+
1476+
- `_service { sdl }` for schema composition
1477+
- `_entities(representations: [_Any!]!)` entity resolution
1478+
- Config query: `federationConfig`
1479+
- Entity support for: `User`, `KVPair`, `Job`, `ShardNode`, `RaftPeer`
1480+
- Strict/non-strict entity resolution modes
1481+
1482+
### Configuration
1483+
1484+
Add to `configs/default.yaml` under `graphql`:
1485+
1486+
```yaml
1487+
graphql:
1488+
federation:
1489+
enabled: true
1490+
service_name: "helios"
1491+
service_version: "1.0.0"
1492+
include_service_sdl: true
1493+
strict_entities: false
1494+
entity_types:
1495+
- "User"
1496+
- "KVPair"
1497+
- "Job"
1498+
- "ShardNode"
1499+
- "RaftPeer"
1500+
```
1501+
1502+
### Programmatic Setup
1503+
1504+
```go
1505+
handler := graphql.NewHandler(resolver, authService,
1506+
graphql.WithFederationConfig(&graphql.FederationConfig{
1507+
Enabled: true,
1508+
ServiceName: "helios",
1509+
ServiceVersion: "1.0.0",
1510+
IncludeServiceSDL: true,
1511+
StrictEntities: false,
1512+
EntityTypes: []string{"User", "KVPair", "Job", "ShardNode", "RaftPeer"},
1513+
}),
1514+
)
1515+
```
1516+
1517+
### Federation Queries
1518+
1519+
Service SDL:
1520+
1521+
```graphql
1522+
query {
1523+
_service {
1524+
sdl
1525+
}
1526+
}
1527+
```
1528+
1529+
Entity resolution:
1530+
1531+
```graphql
1532+
query ResolveEntities($representations: [_Any!]!) {
1533+
_entities(representations: $representations)
1534+
}
1535+
```
1536+
1537+
Variables:
1538+
1539+
```json
1540+
{
1541+
"representations": [
1542+
{ "__typename": "User", "id": "<user-id>" },
1543+
{ "__typename": "KVPair", "key": "data:example" }
1544+
]
1545+
}
1546+
```
1547+
1548+
Runtime federation config:
1549+
1550+
```graphql
1551+
query {
1552+
federationConfig {
1553+
enabled
1554+
serviceName
1555+
serviceVersion
1556+
includeServiceSDL
1557+
strictEntities
1558+
entityTypeCount
1559+
entityTypes
1560+
}
1561+
}
1562+
```
1563+
1564+
### Best Practices
1565+
1566+
1. Keep `include_service_sdl` enabled for composition environments.
1567+
2. Use `strict_entities: true` in controlled environments to surface bad representations early.
1568+
3. Restrict `entity_types` to only entities you actually expose.
1569+
4. Monitor `_entities` traffic patterns and cache hot entity lookups upstream when possible.
1570+
14611571
## Troubleshooting
14621572

14631573
### Common Issues
@@ -1490,7 +1600,7 @@ Planned features:
14901600
- [x] Rate limiting per resolver (implemented in `internal/graphql/rate_limiter.go`)
14911601
- [x] Persisted queries (implemented in `internal/graphql/persisted_queries.go`)
14921602
- [x] Automatic schema documentation (implemented in `internal/graphql/schema_documentation.go`)
1493-
- [ ] GraphQL Federation support
1603+
- [x] GraphQL Federation support (implemented in `internal/graphql/federation.go`)
14941604
- [ ] Custom directives
14951605
- [ ] File upload support
14961606

internal/config/config.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"os"
7+
"strings"
78
"sync"
89
"time"
910

@@ -114,6 +115,7 @@ type GraphQLConfig struct {
114115
MaxComplexity int `yaml:"max_complexity" json:"max_complexity"`
115116
AllowedOrigins []string `yaml:"allowed_origins" json:"allowed_origins"`
116117
SchemaDocs GraphQLSchemaDocsConfig `yaml:"schema_docs" json:"schema_docs"`
118+
Federation GraphQLFederationConfig `yaml:"federation" json:"federation"`
117119
}
118120

119121
// GraphQLSchemaDocsConfig contains automatic schema documentation settings.
@@ -127,6 +129,16 @@ type GraphQLSchemaDocsConfig struct {
127129
ExportPath string `yaml:"export_path" json:"export_path"`
128130
}
129131

132+
// GraphQLFederationConfig contains GraphQL federation settings.
133+
type GraphQLFederationConfig struct {
134+
Enabled bool `yaml:"enabled" json:"enabled"`
135+
ServiceName string `yaml:"service_name" json:"service_name"`
136+
ServiceVersion string `yaml:"service_version" json:"service_version"`
137+
IncludeServiceSDL bool `yaml:"include_service_sdl" json:"include_service_sdl"`
138+
StrictEntities bool `yaml:"strict_entities" json:"strict_entities"`
139+
EntityTypes []string `yaml:"entity_types" json:"entity_types"`
140+
}
141+
130142
// NewManager creates a new configuration manager
131143
func NewManager(configPath string) (*Manager, error) {
132144
cfg, err := loadConfigFromFile(configPath)
@@ -764,6 +776,18 @@ func (c *Config) Validate() error {
764776
}
765777
}
766778

779+
if strings.TrimSpace(c.GraphQL.Federation.ServiceName) == "" {
780+
return fmt.Errorf("graphql.federation.service_name cannot be empty")
781+
}
782+
if strings.TrimSpace(c.GraphQL.Federation.ServiceVersion) == "" {
783+
return fmt.Errorf("graphql.federation.service_version cannot be empty")
784+
}
785+
for i, entityType := range c.GraphQL.Federation.EntityTypes {
786+
if strings.TrimSpace(entityType) == "" {
787+
return fmt.Errorf("graphql.federation.entity_types[%d] cannot be empty", i)
788+
}
789+
}
790+
767791
return nil
768792
}
769793

@@ -840,6 +864,17 @@ func (c *Config) ApplyDefaults() {
840864
if c.GraphQL.SchemaDocs.DefaultFormat == "" {
841865
c.GraphQL.SchemaDocs.DefaultFormat = "markdown"
842866
}
867+
868+
// GraphQL federation defaults
869+
if c.GraphQL.Federation.ServiceName == "" {
870+
c.GraphQL.Federation.ServiceName = "helios"
871+
}
872+
if c.GraphQL.Federation.ServiceVersion == "" {
873+
c.GraphQL.Federation.ServiceVersion = "1.0.0"
874+
}
875+
if len(c.GraphQL.Federation.EntityTypes) == 0 {
876+
c.GraphQL.Federation.EntityTypes = []string{"User", "KVPair", "Job", "ShardNode", "RaftPeer"}
877+
}
843878
}
844879

845880
// Export exports the configuration as YAML

internal/config/config_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strings"
78
"testing"
89
"time"
910
)
@@ -1058,3 +1059,49 @@ func TestConfigDiffFieldChanges(t *testing.T) {
10581059
t.Errorf("Expected 0 immutable changes, got %d", len(immutableChanges))
10591060
}
10601061
}
1062+
1063+
func TestGraphQLFederationDefaults(t *testing.T) {
1064+
cfg := &Config{}
1065+
cfg.ApplyDefaults()
1066+
1067+
if cfg.GraphQL.Federation.ServiceName != "helios" {
1068+
t.Errorf("expected default federation service name helios, got %q", cfg.GraphQL.Federation.ServiceName)
1069+
}
1070+
if cfg.GraphQL.Federation.ServiceVersion != "1.0.0" {
1071+
t.Errorf("expected default federation service version 1.0.0, got %q", cfg.GraphQL.Federation.ServiceVersion)
1072+
}
1073+
if len(cfg.GraphQL.Federation.EntityTypes) == 0 {
1074+
t.Fatal("expected default federation entity types")
1075+
}
1076+
}
1077+
1078+
func TestGraphQLFederationValidation(t *testing.T) {
1079+
tmpDir := t.TempDir()
1080+
configPath := filepath.Join(tmpDir, "config.yaml")
1081+
1082+
invalidConfig := `
1083+
immutable:
1084+
node_id: "test-node"
1085+
observability:
1086+
log_level: "INFO"
1087+
graphql:
1088+
federation:
1089+
service_name: "helios"
1090+
service_version: "1.0.0"
1091+
entity_types:
1092+
- "User"
1093+
- ""
1094+
`
1095+
1096+
if err := os.WriteFile(configPath, []byte(invalidConfig), 0644); err != nil {
1097+
t.Fatalf("Failed to write config: %v", err)
1098+
}
1099+
1100+
_, err := NewManager(configPath)
1101+
if err == nil {
1102+
t.Fatal("expected validation error for empty federation entity type")
1103+
}
1104+
if !strings.Contains(err.Error(), "graphql.federation.entity_types") {
1105+
t.Fatalf("expected federation entity types validation error, got: %v", err)
1106+
}
1107+
}

0 commit comments

Comments
 (0)