Skip to content

Commit

Permalink
federation: generate _entities / _service root queries regardless of …
Browse files Browse the repository at this point in the history
…entity count

Generate a valid federation schema extention if no object uses @key
directive.

On this case the extension will be defined as follows:
```graphql
union _Entity

type _Service {
  sdl: String
}

extend type Query {
  _entities(representations: [_Any!]!): [_Entity]!
  _service: _Service!
}
```

Signed-off-by: Alex Snast <[email protected]>
  • Loading branch information
alexsn committed May 14, 2020
1 parent 40570d1 commit 097b1d4
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 78 deletions.
4 changes: 1 addition & 3 deletions example/federation/accounts/graph/generated/federation.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions example/federation/accounts/graph/generated/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions example/federation/products/graph/generated/federation.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions example/federation/products/graph/generated/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions example/federation/reviews/graph/generated/federation.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions example/federation/reviews/graph/generated/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 40 additions & 49 deletions plugin/federation/federation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"sort"
"strings"
"text/template"

"github.com/vektah/gqlparser/v2/ast"

Expand All @@ -24,12 +25,12 @@ func New() plugin.Plugin {
}

// Name returns the plugin name
func (f *federation) Name() string {
func (federation) Name() string {
return "federation"
}

// MutateConfig mutates the configuration
func (f *federation) MutateConfig(cfg *config.Config) error {
func (federation) MutateConfig(cfg *config.Config) error {
builtins := config.TypeMap{
"_Service": {
Model: config.StringList{
Expand All @@ -52,20 +53,20 @@ func (f *federation) MutateConfig(cfg *config.Config) error {
}
for typeName, entry := range builtins {
if cfg.Models.Exists(typeName) {
return fmt.Errorf("%v already exists which must be reserved when Federation is enabled", typeName)
return fmt.Errorf("%s already exists which must be reserved when Federation is enabled", typeName)
}
cfg.Models[typeName] = entry
}
cfg.Directives["external"] = config.DirectiveConfig{SkipRuntime: true}
cfg.Directives["requires"] = config.DirectiveConfig{SkipRuntime: true}
cfg.Directives["provides"] = config.DirectiveConfig{SkipRuntime: true}
cfg.Directives["key"] = config.DirectiveConfig{SkipRuntime: true}
cfg.Directives["extends"] = config.DirectiveConfig{SkipRuntime: true}

directives := [...]string{"external", "requires", "provides", "key", "extends"}
for _, directive := range directives {
cfg.Directives[directive] = config.DirectiveConfig{SkipRuntime: true}
}

return nil
}

func (f *federation) InjectSourceEarly() *ast.Source {
func (federation) InjectSourceEarly() *ast.Source {
return &ast.Source{
Name: "federation/directives.graphql",
Input: `
Expand All @@ -82,44 +83,19 @@ directive @extends on OBJECT
}
}

// InjectSources creates a GraphQL Entity type with all
// the fields that had the @key directive
func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source {
f.setEntities(schema)

entities := ""
resolvers := ""
for i, e := range f.Entities {
if i != 0 {
entities += " | "
}
entities += e.Name
var entityTemplate = template.Must(
template.New("entity").Parse(`
{{/* a union of all types that use the @key directive */ -}}
union _Entity{{ range $i, $e := .Entities }} {{ if eq $i 0 }}={{ else }}|{{ end }} {{ $e.Name }}{{ end }}
resolverArgs := ""
for _, field := range e.KeyFields {
resolverArgs += fmt.Sprintf("%s: %s,", field.Field.Name, field.Field.Type.String())
}
resolvers += fmt.Sprintf("\t%s(%s): %s!\n", e.ResolverName, resolverArgs, e.Def.Name)

}

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

return &ast.Source{
Name: "federation/entity.graphql",
BuiltIn: true,
Input: `
# a union of all types that use the @key directive
union _Entity = ` + entities + `
# fake type to build resolver interfaces for users to implement
{{ if .Entities -}}
{{/* fake type to build resolver interfaces for users to implement */ -}}
type Entity {
` + resolvers + `
{{- range $e := .Entities }}
{{ $e.ResolverName }}({{ range $i, $f := $e.KeyFields }}{{ if ne $i 0 }}, {{ end }}{{ printf "%s: %s" $f.Field.Name $f.Field.Type.String }}{{ end }}): {{ $e.Def.Name }}!
{{- end }}
}
{{- end }}
type _Service {
sdl: String
Expand All @@ -129,7 +105,23 @@ extend type Query {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
}
`,
`),
)

// InjectSources creates a GraphQL Entity type with all
// the fields that had the @key directive
func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source {
f.setEntities(schema)

var input strings.Builder
if err := entityTemplate.Execute(&input, f); err != nil {
panic(err)
}

return &ast.Source{
Name: "federation/entity.graphql",
BuiltIn: true,
Input: input.String(),
}
}

Expand Down Expand Up @@ -196,7 +188,7 @@ func (f *federation) GenerateCode(data *codegen.Data) error {
})
}

func (f *federation) getKeyField(keyFields []*KeyField, fieldName string) *KeyField {
func (federation) getKeyField(keyFields []*KeyField, fieldName string) *KeyField {
for _, field := range keyFields {
if field.Field.Name == fieldName {
return field
Expand All @@ -218,14 +210,14 @@ func (f *federation) setEntities(schema *ast.Schema) {
panic("Nested fields are not currently supported in @key declaration.")
}

requires := []*Requires{}
requires := make([]*Requires, 0, len(schemaType.Fields))
for _, f := range schemaType.Fields {
dir := f.Directives.ForName("requires")
if dir == nil {
continue
}
fields := strings.Split(dir.Arguments[0].Value.Raw, " ")
requireFields := []*RequireField{}
requireFields := make([]*RequireField, 0, len(fields))
for _, f := range fields {
requireFields = append(requireFields, &RequireField{
Name: f,
Expand All @@ -248,7 +240,6 @@ func (f *federation) setEntities(schema *ast.Schema) {
resolverName += "And"
}
resolverName += templates.ToGo(f)

}

f.Entities = append(f.Entities, &Entity{
Expand Down
8 changes: 3 additions & 5 deletions plugin/federation/federation.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.
}

var sdl []string

for _, src := range sources {
if src.BuiltIn {
continue
Expand All @@ -24,9 +23,8 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.
}, nil
}

{{if .Entities}}
func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) ([]fedruntime.Entity, error) {
list := []fedruntime.Entity{}
func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) (list []fedruntime.Entity, _ error) {
{{- if .Entities }}
for _, rep := range representations {
typeName, ok := rep["__typename"].(string)
if !ok {
Expand Down Expand Up @@ -62,6 +60,6 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati
return nil, errors.New("unknown type: "+typeName)
}
}
{{- end }}
return list, nil
}
{{end}}
8 changes: 6 additions & 2 deletions plugin/federation/federation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ func TestWithEntities(t *testing.T) {
require.Equal(t, "findExternalExtensionByUpc", cfg.Schema.Types["Entity"].Fields[0].Name)
require.Equal(t, "findHelloByName", cfg.Schema.Types["Entity"].Fields[1].Name)
require.Equal(t, "findWorldByFooAndBar", cfg.Schema.Types["Entity"].Fields[2].Name)
require.Equal(t, "sdl", cfg.Schema.Types["_Service"].Fields[0].Name)

require.NoError(t, f.MutateConfig(cfg))
}

func TestNoEntities(t *testing.T) {
f, cfg := load(t, "test_data/nokey.yml")

err := f.MutateConfig(cfg)
require.NoError(t, err)
require.NotContains(t, cfg.Schema.Types, "Entity")
require.Empty(t, cfg.Schema.Types["_Entity"].Types)
require.Equal(t, "sdl", cfg.Schema.Types["_Service"].Fields[0].Name)

require.NoError(t, f.MutateConfig(cfg))
}

func load(t *testing.T, name string) (*federation, *config.Config) {
Expand Down

0 comments on commit 097b1d4

Please sign in to comment.