diff --git a/plugin/federation/federation.go b/plugin/federation/federation.go index 40e812a7bb0..7d9abc9774c 100644 --- a/plugin/federation/federation.go +++ b/plugin/federation/federation.go @@ -95,11 +95,13 @@ func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source { } entities += e.Name - resolverArgs := "" - for _, field := range e.KeyFields { - resolverArgs += fmt.Sprintf("%s: %s,", field.Field.Name, field.Field.Type.String()) + if e.ResolverName != "" { + 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) } - resolvers += fmt.Sprintf("\t%s(%s): %s!\n", e.ResolverName, resolverArgs, e.Def.Name) } @@ -109,18 +111,24 @@ func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source { return nil } + // resolvers can be empty if a service defines only "empty + // extend" types. This should be rare. + if resolvers != "" { + resolvers = ` +# fake type to build resolver interfaces for users to implement +type Entity { + ` + resolvers + ` +} +` + } + 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 -type Entity { - ` + resolvers + ` -} - +` + resolvers + ` type _Service { sdl: String } @@ -162,9 +170,20 @@ type RequireField struct { TypeReference *config.TypeReference // The Go representation of that field type } +func (e *Entity) allFieldsAreExternal() bool { + for _, field := range e.Def.Fields { + if field.Directives.ForName("external") == nil { + return false + } + } + return true +} + func (f *federation) GenerateCode(data *codegen.Data) error { if len(f.Entities) > 0 { - data.Objects.ByName("Entity").Root = true + if data.Objects.ByName("Entity") != nil { + data.Objects.ByName("Entity").Root = true + } for _, e := range f.Entities { obj := data.Objects.ByName(e.Def.Name) for _, field := range obj.Fields { @@ -251,13 +270,36 @@ func (f *federation) setEntities(schema *ast.Schema) { } - f.Entities = append(f.Entities, &Entity{ + e := &Entity{ Name: schemaType.Name, KeyFields: keyFields, Def: schemaType, ResolverName: resolverName, Requires: requires, - }) + } + // If our schema has a field with a type defined in + // another service, then we need to define an "empty + // extend" of that type in this service, so this service + // knows what the type is like. But the graphql-server + // will never ask us to actually resolve this "empty + // extend", so we don't require a resolver function for + // it. (Well, it will never ask in practice; it's + // unclear whether the spec guarantees this. See + // https://github.com/apollographql/apollo-server/issues/3852 + // ). Example: + // type MyType { + // myvar: TypeDefinedInOtherService + // } + // // Federation needs this type, but + // // it doesn't need a resolver for it! + // extend TypeDefinedInOtherService @key(fields: "id") { + // id: ID @external + // } + if e.allFieldsAreExternal() { + e.ResolverName = "" + } + + f.Entities = append(f.Entities, e) } } } diff --git a/plugin/federation/federation.gotpl b/plugin/federation/federation.gotpl index 2661495a388..96c25e85723 100644 --- a/plugin/federation/federation.gotpl +++ b/plugin/federation/federation.gotpl @@ -34,29 +34,31 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } switch typeName { {{ range .Entities }} - case "{{.Def.Name}}": - {{ range $i, $keyField := .KeyFields -}} - id{{$i}}, err := ec.{{.TypeReference.UnmarshalFunc}}(ctx, rep["{{$keyField.Field.Name}}"]) - if err != nil { - return nil, errors.New(fmt.Sprintf("Field %s undefined in schema.", "{{$keyField.Field.Name}}")) - } - {{end}} - - entity, err := ec.resolvers.Entity().{{.ResolverName | go}}(ctx, - {{ range $i, $_ := .KeyFields -}} id{{$i}}, {{end}}) - if err != nil { - return nil, err - } - - {{ range .Requires }} - {{ range .Fields}} - entity.{{.NameGo}}, err = ec.{{.TypeReference.UnmarshalFunc}}(ctx, rep["{{.Name}}"]) + {{ if .ResolverName }} + case "{{.Def.Name}}": + {{ range $i, $keyField := .KeyFields -}} + id{{$i}}, err := ec.{{.TypeReference.UnmarshalFunc}}(ctx, rep["{{$keyField.Field.Name}}"]) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("Field %s undefined in schema.", "{{$keyField.Field.Name}}")) } + {{end}} + + entity, err := ec.resolvers.Entity().{{.ResolverName | go}}(ctx, + {{ range $i, $_ := .KeyFields -}} id{{$i}}, {{end}}) + if err != nil { + return nil, err + } + + {{ range .Requires }} + {{ range .Fields}} + entity.{{.NameGo}}, err = ec.{{.TypeReference.UnmarshalFunc}}(ctx, rep["{{.Name}}"]) + if err != nil { + return nil, err + } + {{ end }} {{ end }} + list = append(list, entity) {{ end }} - list = append(list, entity) {{ end }} default: return nil, errors.New("unknown type: "+typeName) diff --git a/plugin/resolvergen/testdata/followschema/out/resolver.go b/plugin/resolvergen/testdata/followschema/out/resolver.go index 61296288110..fbe00ecff89 100644 --- a/plugin/resolvergen/testdata/followschema/out/resolver.go +++ b/plugin/resolvergen/testdata/followschema/out/resolver.go @@ -1,6 +1,7 @@ +package customresolver + // This file will not be regenerated automatically. // // It serves as dependency injection for your app, add any dependencies you require here. -package customresolver type CustomResolverType struct{} diff --git a/plugin/resolvergen/testdata/singlefile/out/resolver.go b/plugin/resolvergen/testdata/singlefile/out/resolver.go index 99a46892ae4..54d778636ae 100644 --- a/plugin/resolvergen/testdata/singlefile/out/resolver.go +++ b/plugin/resolvergen/testdata/singlefile/out/resolver.go @@ -1,6 +1,7 @@ -// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES. package customresolver +// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES. + import ( "context" ) @@ -10,11 +11,15 @@ type CustomResolverType struct{} func (r *queryCustomResolverType) Resolver(ctx context.Context) (*Resolver, error) { panic("not implemented") } + func (r *resolverCustomResolverType) Name(ctx context.Context, obj *Resolver) (string, error) { panic("not implemented") } -func (r *CustomResolverType) Query() QueryResolver { return &queryCustomResolverType{r} } +// Query returns QueryResolver implementation. +func (r *CustomResolverType) Query() QueryResolver { return &queryCustomResolverType{r} } + +// Resolver returns ResolverResolver implementation. func (r *CustomResolverType) Resolver() ResolverResolver { return &resolverCustomResolverType{r} } type queryCustomResolverType struct{ *CustomResolverType }