Skip to content

Commit

Permalink
Correctly generate a federated schema when no entity has a @key.
Browse files Browse the repository at this point in the history
Normally, when a service is taking part in graphql federation, it will
have at least one type defined with a `@key` field, so that other
services can link to (that is, have an edge pointing to) the type that
this service provides.  The previous federation code assumed that was
the case.

But it's not required: a service could not define `@key` on any of its
types.  It might seem that would mean the service is unreachable,
since there is no possibility of edges into the service, but there are
two edges that can exist even without a `@key`: top level Query edges
and top level Mutation edges.  That is, if a service only provides a
top-level query or top-level mutation, it might not need to define a
`@key`.

This commit updates the federation code to support that use case.
  • Loading branch information
csilvers committed Jan 10, 2020
1 parent 28c032d commit b38f728
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 29 deletions.
60 changes: 42 additions & 18 deletions plugin/federation/federation.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ func (f *federation) MutateConfig(cfg *config.Config) error {
"_Any": {
Model: config.StringList{"github.com/99designs/gqlgen/graphql.Map"},
},
"Entity": {
}
if len(entityFields) != 0 {
builtins["Entity"] = config.TypeMapEntry{
Fields: entityFields,
},
}
}
for typeName, entry := range builtins {
if cfg.Models.Exists(typeName) {
Expand All @@ -79,7 +81,14 @@ func (f *federation) MutateConfig(cfg *config.Config) error {
// the fields that had the @key directive
func (f *federation) InjectSources(cfg *config.Config) {
cfg.AdditionalSources = append(cfg.AdditionalSources, f.getSource(false))

f.setEntities(cfg)
if len(f.Entities) == 0 {
// It's unusual for a service not to have any entities, but
// possible if it only exports top-level queries and mutations.
return
}

s := "type Entity {\n"
for _, e := range f.Entities {
s += fmt.Sprintf("\t%s(%s: %s): %s!\n", e.ResolverName, e.Field.Name, e.Field.Type.String(), e.Def.Name)
Expand All @@ -88,9 +97,9 @@ func (f *federation) InjectSources(cfg *config.Config) {
cfg.AdditionalSources = append(cfg.AdditionalSources, &ast.Source{Name: "entity.graphql", Input: s, BuiltIn: true})
}

// MutateSchema creates types and query declarations
// that are required by the federation spec.
func (f *federation) MutateSchema(s *ast.Schema) error {
// addEntityToSchema adds the _Entity Union and _entities query to schema.
// This is part of MutateSchema.
func (f *federation) addEntityToSchema(s *ast.Schema) {
// --- Set _Entity Union ---
union := &ast.Definition{
Name: "_Entity",
Expand Down Expand Up @@ -124,8 +133,11 @@ func (f *federation) MutateSchema(s *ast.Schema) error {
s.Types["Query"] = s.Query
}
s.Query.Fields = append(s.Query.Fields, fieldDef)
}

// --- set _Service type ---
// addServiceToSchema adds the _Service type and _service query to schema.
// This is part of MutateSchema.
func (f *federation) addServiceToSchema(s *ast.Schema) {
typeDef := &ast.Definition{
Kind: ast.Object,
Name: "_Service",
Expand All @@ -144,6 +156,17 @@ func (f *federation) MutateSchema(s *ast.Schema) error {
Type: ast.NonNullNamedType("_Service", nil),
}
s.Query.Fields = append(s.Query.Fields, _serviceDef)
}

// MutateSchema creates types and query declarations
// that are required by the federation spec.
func (f *federation) MutateSchema(s *ast.Schema) error {
// It's unusual for a service not to have any entities, but
// possible if it only exports top-level queries and mutations.
if len(f.Entities) > 0 {
f.addEntityToSchema(s)
}
f.addServiceToSchema(s)
return nil
}

Expand Down Expand Up @@ -196,18 +219,19 @@ func (f *federation) GenerateCode(data *codegen.Data) error {
return err
}
f.SDL = sdl
data.Objects.ByName("Entity").Root = true
for _, e := range f.Entities {
obj := data.Objects.ByName(e.Def.Name)
for _, f := range obj.Fields {
if f.Name == e.Field.Name {
e.FieldTypeGo = f.TypeReference.GO.String()
}
for _, r := range e.Requires {
for _, rf := range r.Fields {
if rf.Name == f.Name {
rf.TypeReference = f.TypeReference
rf.NameGo = f.GoFieldName
if len(f.Entities) > 0 {
data.Objects.ByName("Entity").Root = true
for _, e := range f.Entities {
obj := data.Objects.ByName(e.Def.Name)
for _, f := range obj.Fields {
if f.Name == e.Field.Name {
e.FieldTypeGo = f.TypeReference.GO.String()
}
for _, r := range e.Requires {
for _, rf := range r.Fields {
if rf.Name == f.Name {
rf.TypeReference = f.TypeReference
rf.NameGo = f.GoFieldName
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions plugin/federation/federation.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (federation.
}, nil
}

{{if .Entities}}
func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) ([]_Entity, error) {
list := []_Entity{}
for _, rep := range representations {
Expand Down Expand Up @@ -46,3 +47,4 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati
}
return list, nil
}
{{end}}
40 changes: 34 additions & 6 deletions plugin/federation/federation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import (
)

func TestInjectSources(t *testing.T) {
var cfg config.Config
cfg, err := config.LoadConfig("test_data/gqlgen.yml")
require.NoError(t, err)
f := &federation{}
f.InjectSources(&cfg)
f.InjectSources(cfg)
if len(cfg.AdditionalSources) != 2 {
t.Fatalf("expected an additional source but got %v", len(cfg.AdditionalSources))
t.Fatalf("expected 2 additional sources but got %v", len(cfg.AdditionalSources))
}
}

Expand All @@ -30,10 +31,9 @@ func TestMutateSchema(t *testing.T) {
if gqlErr != nil {
t.Fatal(gqlErr)
}

err := f.MutateSchema(schema)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
}

func TestGetSDL(t *testing.T) {
Expand All @@ -53,3 +53,31 @@ func TestMutateConfig(t *testing.T) {
err = f.MutateConfig(cfg)
require.NoError(t, err)
}

func TestInjectSourcesNoKey(t *testing.T) {
cfg, err := config.LoadConfig("test_data/nokey.yml")
require.NoError(t, err)
f := &federation{}
f.InjectSources(cfg)
if len(cfg.AdditionalSources) != 1 {
t.Fatalf("expected an additional source but got %v", len(cfg.AdditionalSources))
}
}

func TestGetSDLNoKey(t *testing.T) {
cfg, err := config.LoadConfig("test_data/nokey.yml")
require.NoError(t, err)
f := &federation{}
_, err = f.getSDL(cfg)
require.NoError(t, err)
}

func TestMutateConfigNoKey(t *testing.T) {
cfg, err := config.LoadConfig("test_data/nokey.yml")
require.NoError(t, err)
require.NoError(t, cfg.Check())

f := &federation{}
err = f.MutateConfig(cfg)
require.NoError(t, err)
}
8 changes: 8 additions & 0 deletions plugin/federation/test_data/nokey.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type Hello {
name: String!
}

type Query {
hello: Hello!
}

3 changes: 3 additions & 0 deletions plugin/federation/test_data/nokey.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
schema:
- "test_data/nokey.graphql"
federated: true
11 changes: 6 additions & 5 deletions plugin/federation/test_data/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type Hello @key(fields: "name") {
name: String!
}
type Query {
hello: Hello!
}
name: String!
}

type Query {
hello: Hello!
}

0 comments on commit b38f728

Please sign in to comment.