From 50292e99d5cd0021f9fbec6118e406e4da86505b Mon Sep 17 00:00:00 2001 From: Miguel Castillo Date: Sat, 13 Nov 2021 10:03:28 -0500 Subject: [PATCH] Resolve multiple federated entities in a single entityResolve call (#1709) * Resolve multiple federated entities in a single entityResolve call Entity resolver functions can only process one entity at a time. But often we want to resolve all the entities at once so that we can optimize things like database calls. And to do that you need to add you'd need to add batching with abstractions like dataloadgen or batchloader. The drawback here is that the resolver code (the domain logic) gets more complex to implement, test, and debug. An alternative is to have entity resolvers that can process all the representations in a single call so that domain logic can have access to all the representations up front, which is what Im adding in this PR. There are a few moving pieces here: 1. We need to define the directive `directive @entityResolver(multi: Boolean) on OBJECT`. 2. Then federated entities need to be annotated to enable the functionality. E.g. `type MultiHello @key(fields: "name") @entityResolver(multi: true)` 3. When that's configured, the federation plugin will create an entity resolver that will take a list of representations. Please note that this is very specific to federation and entity resolvers. This does not add support for resolving fields in an entity. Some of the implementation details worth noting. In order to efficiently process batches of entities, I group them by type so that we can process groups of entities at the same time. The resolution of groups of entities run concurrently in Go routines. If there is _only_ one type, then that's just processed without concurrency. Entities that don't have multiget enabled will still continue to resolve concurrently with Go routines, and entities that have multiget enabled just get the entire list of representations. The list of representations that are passed to entity resolvers are strongly types, and the type is generated for you. There are lots of new tests to ensure that there are no regressions and that the new functionality still functions as expected. To test: 1. Go to `plugin/federation` 2. Generate files with `go run github.com/99designs/gqlgen --config testdata/entityresolver/gqlgen.yml` 3. And run `go test ./...`. Verify they all pass. You can look at the federated code in `plugin/federation/testdata/entityresolver/gederated/federation.go` * Added `InputType` in entity to centralize logic for generating types for multiget resolvers. * reformat and regenerate Signed-off-by: Steve Coffman Co-authored-by: Steve Coffman --- .../accounts/graph/generated/federation.go | 111 ++- .../products/graph/generated/federation.go | 111 ++- .../reviews/graph/generated/federation.go | 111 ++- example/tools.go | 1 + plugin/federation/federation.go | 68 +- plugin/federation/federation.gotpl | 162 ++++- .../federation_entityresolver_test.go | 129 +++- plugin/federation/readme.md | 22 + .../entityresolver/entity.resolvers.go | 16 + .../testdata/entityresolver/generated/exec.go | 682 +++++++++++++++--- .../entityresolver/generated/federation.go | 165 ++++- .../entityresolver/generated/models.go | 20 + .../testdata/entityresolver/schema.graphql | 11 +- .../entityresolver/schema.resolvers.go | 20 - 14 files changed, 1373 insertions(+), 256 deletions(-) diff --git a/example/federation/accounts/graph/generated/federation.go b/example/federation/accounts/graph/generated/federation.go index 1d7ee7ca52..11a6382fcb 100644 --- a/example/federation/accounts/graph/generated/federation.go +++ b/example/federation/accounts/graph/generated/federation.go @@ -33,7 +33,39 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) []fedruntime.Entity { list := make([]fedruntime.Entity, len(representations)) - resolveEntity := func(ctx context.Context, i int, rep map[string]interface{}) (err error) { + + repsMap := map[string]struct { + i []int + r []map[string]interface{} + }{} + + // We group entities by typename so that we can parallelize their resolution. + // This is particularly helpful when there are entity groups in multi mode. + buildRepresentationGroups := func(reps []map[string]interface{}) { + for i, rep := range reps { + typeName, ok := rep["__typename"].(string) + if !ok { + // If there is no __typename, we just skip the representation; + // we just won't be resolving these unknown types. + ec.Error(ctx, errors.New("__typename must be an existing string")) + continue + } + + _r := repsMap[typeName] + _r.i = append(_r.i, i) + _r.r = append(_r.r, rep) + repsMap[typeName] = _r + } + } + + isMulti := func(typeName string) bool { + switch typeName { + default: + return false + } + } + + resolveEntity := func(ctx context.Context, typeName string, rep map[string]interface{}, idx []int, i int) (err error) { // we need to do our own panic handling, because we may be called in a // goroutine, where the usual panic handling can't catch us defer func() { @@ -42,10 +74,6 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } }() - typeName, ok := rep["__typename"].(string) - if !ok { - return errors.New("__typename must be an existing string") - } switch typeName { case "EmailHost": @@ -60,7 +88,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil case "User": @@ -75,7 +103,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil default: @@ -83,30 +111,71 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } } - // if there are multiple entities to resolve, parallelize (similar to - // graphql.FieldSet.Dispatch) - switch len(representations) { + resolveManyEntities := func(ctx context.Context, typeName string, reps []map[string]interface{}, idx []int) (err error) { + // we need to do our own panic handling, because we may be called in a + // goroutine, where the usual panic handling can't catch us + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + } + }() + + switch typeName { + + default: + return errors.New("unknown type: " + typeName) + } + + return nil + } + + resolveEntityGroup := func(typeName string, reps []map[string]interface{}, idx []int) { + if isMulti(typeName) { + err := resolveManyEntities(ctx, typeName, reps, idx) + if err != nil { + ec.Error(ctx, err) + } + } else { + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + var e sync.WaitGroup + e.Add(len(reps)) + for i, rep := range reps { + i, rep := i, rep + go func(i int, rep map[string]interface{}) { + err := resolveEntity(ctx, typeName, rep, idx, i) + if err != nil { + ec.Error(ctx, err) + } + e.Done() + }(i, rep) + } + e.Wait() + } + } + + buildRepresentationGroups(representations) + + switch len(repsMap) { case 0: return list case 1: - err := resolveEntity(ctx, 0, representations[0]) - if err != nil { - ec.Error(ctx, err) + for typeName, reps := range repsMap { + resolveEntityGroup(typeName, reps.r, reps.i) } return list default: var g sync.WaitGroup - g.Add(len(representations)) - for i, rep := range representations { - go func(i int, rep map[string]interface{}) { - err := resolveEntity(ctx, i, rep) - if err != nil { - ec.Error(ctx, err) - } + g.Add(len(repsMap)) + for typeName, reps := range repsMap { + go func(typeName string, reps []map[string]interface{}, idx []int) { + resolveEntityGroup(typeName, reps, idx) g.Done() - }(i, rep) + }(typeName, reps.r, reps.i) } g.Wait() return list } + + return list } diff --git a/example/federation/products/graph/generated/federation.go b/example/federation/products/graph/generated/federation.go index 410d467a82..0e416ab1a8 100644 --- a/example/federation/products/graph/generated/federation.go +++ b/example/federation/products/graph/generated/federation.go @@ -33,7 +33,39 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) []fedruntime.Entity { list := make([]fedruntime.Entity, len(representations)) - resolveEntity := func(ctx context.Context, i int, rep map[string]interface{}) (err error) { + + repsMap := map[string]struct { + i []int + r []map[string]interface{} + }{} + + // We group entities by typename so that we can parallelize their resolution. + // This is particularly helpful when there are entity groups in multi mode. + buildRepresentationGroups := func(reps []map[string]interface{}) { + for i, rep := range reps { + typeName, ok := rep["__typename"].(string) + if !ok { + // If there is no __typename, we just skip the representation; + // we just won't be resolving these unknown types. + ec.Error(ctx, errors.New("__typename must be an existing string")) + continue + } + + _r := repsMap[typeName] + _r.i = append(_r.i, i) + _r.r = append(_r.r, rep) + repsMap[typeName] = _r + } + } + + isMulti := func(typeName string) bool { + switch typeName { + default: + return false + } + } + + resolveEntity := func(ctx context.Context, typeName string, rep map[string]interface{}, idx []int, i int) (err error) { // we need to do our own panic handling, because we may be called in a // goroutine, where the usual panic handling can't catch us defer func() { @@ -42,10 +74,6 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } }() - typeName, ok := rep["__typename"].(string) - if !ok { - return errors.New("__typename must be an existing string") - } switch typeName { case "Manufacturer": @@ -60,7 +88,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil case "Product": @@ -79,7 +107,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil default: @@ -87,30 +115,71 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } } - // if there are multiple entities to resolve, parallelize (similar to - // graphql.FieldSet.Dispatch) - switch len(representations) { + resolveManyEntities := func(ctx context.Context, typeName string, reps []map[string]interface{}, idx []int) (err error) { + // we need to do our own panic handling, because we may be called in a + // goroutine, where the usual panic handling can't catch us + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + } + }() + + switch typeName { + + default: + return errors.New("unknown type: " + typeName) + } + + return nil + } + + resolveEntityGroup := func(typeName string, reps []map[string]interface{}, idx []int) { + if isMulti(typeName) { + err := resolveManyEntities(ctx, typeName, reps, idx) + if err != nil { + ec.Error(ctx, err) + } + } else { + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + var e sync.WaitGroup + e.Add(len(reps)) + for i, rep := range reps { + i, rep := i, rep + go func(i int, rep map[string]interface{}) { + err := resolveEntity(ctx, typeName, rep, idx, i) + if err != nil { + ec.Error(ctx, err) + } + e.Done() + }(i, rep) + } + e.Wait() + } + } + + buildRepresentationGroups(representations) + + switch len(repsMap) { case 0: return list case 1: - err := resolveEntity(ctx, 0, representations[0]) - if err != nil { - ec.Error(ctx, err) + for typeName, reps := range repsMap { + resolveEntityGroup(typeName, reps.r, reps.i) } return list default: var g sync.WaitGroup - g.Add(len(representations)) - for i, rep := range representations { - go func(i int, rep map[string]interface{}) { - err := resolveEntity(ctx, i, rep) - if err != nil { - ec.Error(ctx, err) - } + g.Add(len(repsMap)) + for typeName, reps := range repsMap { + go func(typeName string, reps []map[string]interface{}, idx []int) { + resolveEntityGroup(typeName, reps, idx) g.Done() - }(i, rep) + }(typeName, reps.r, reps.i) } g.Wait() return list } + + return list } diff --git a/example/federation/reviews/graph/generated/federation.go b/example/federation/reviews/graph/generated/federation.go index 55391a46ea..32389bc56a 100644 --- a/example/federation/reviews/graph/generated/federation.go +++ b/example/federation/reviews/graph/generated/federation.go @@ -33,7 +33,39 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) []fedruntime.Entity { list := make([]fedruntime.Entity, len(representations)) - resolveEntity := func(ctx context.Context, i int, rep map[string]interface{}) (err error) { + + repsMap := map[string]struct { + i []int + r []map[string]interface{} + }{} + + // We group entities by typename so that we can parallelize their resolution. + // This is particularly helpful when there are entity groups in multi mode. + buildRepresentationGroups := func(reps []map[string]interface{}) { + for i, rep := range reps { + typeName, ok := rep["__typename"].(string) + if !ok { + // If there is no __typename, we just skip the representation; + // we just won't be resolving these unknown types. + ec.Error(ctx, errors.New("__typename must be an existing string")) + continue + } + + _r := repsMap[typeName] + _r.i = append(_r.i, i) + _r.r = append(_r.r, rep) + repsMap[typeName] = _r + } + } + + isMulti := func(typeName string) bool { + switch typeName { + default: + return false + } + } + + resolveEntity := func(ctx context.Context, typeName string, rep map[string]interface{}, idx []int, i int) (err error) { // we need to do our own panic handling, because we may be called in a // goroutine, where the usual panic handling can't catch us defer func() { @@ -42,10 +74,6 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } }() - typeName, ok := rep["__typename"].(string) - if !ok { - return errors.New("__typename must be an existing string") - } switch typeName { case "Product": @@ -64,7 +92,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil case "User": @@ -89,7 +117,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil default: @@ -97,30 +125,71 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } } - // if there are multiple entities to resolve, parallelize (similar to - // graphql.FieldSet.Dispatch) - switch len(representations) { + resolveManyEntities := func(ctx context.Context, typeName string, reps []map[string]interface{}, idx []int) (err error) { + // we need to do our own panic handling, because we may be called in a + // goroutine, where the usual panic handling can't catch us + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + } + }() + + switch typeName { + + default: + return errors.New("unknown type: " + typeName) + } + + return nil + } + + resolveEntityGroup := func(typeName string, reps []map[string]interface{}, idx []int) { + if isMulti(typeName) { + err := resolveManyEntities(ctx, typeName, reps, idx) + if err != nil { + ec.Error(ctx, err) + } + } else { + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + var e sync.WaitGroup + e.Add(len(reps)) + for i, rep := range reps { + i, rep := i, rep + go func(i int, rep map[string]interface{}) { + err := resolveEntity(ctx, typeName, rep, idx, i) + if err != nil { + ec.Error(ctx, err) + } + e.Done() + }(i, rep) + } + e.Wait() + } + } + + buildRepresentationGroups(representations) + + switch len(repsMap) { case 0: return list case 1: - err := resolveEntity(ctx, 0, representations[0]) - if err != nil { - ec.Error(ctx, err) + for typeName, reps := range repsMap { + resolveEntityGroup(typeName, reps.r, reps.i) } return list default: var g sync.WaitGroup - g.Add(len(representations)) - for i, rep := range representations { - go func(i int, rep map[string]interface{}) { - err := resolveEntity(ctx, i, rep) - if err != nil { - ec.Error(ctx, err) - } + g.Add(len(repsMap)) + for typeName, reps := range repsMap { + go func(typeName string, reps []map[string]interface{}, idx []int) { + resolveEntityGroup(typeName, reps, idx) g.Done() - }(i, rep) + }(typeName, reps.r, reps.i) } g.Wait() return list } + + return list } diff --git a/example/tools.go b/example/tools.go index d182ed9edc..4c124bccfc 100644 --- a/example/tools.go +++ b/example/tools.go @@ -1,3 +1,4 @@ +//go:build tools // +build tools package main diff --git a/plugin/federation/federation.go b/plugin/federation/federation.go index dd1b8ca38e..c024e0945d 100644 --- a/plugin/federation/federation.go +++ b/plugin/federation/federation.go @@ -3,6 +3,7 @@ package federation import ( "fmt" "sort" + "strings" "github.com/vektah/gqlparser/v2/ast" @@ -89,6 +90,7 @@ func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source { entities := "" resolvers := "" + entityResolverInputDefinitions := "" for i, e := range f.Entities { if i != 0 { entities += " | " @@ -96,11 +98,20 @@ func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source { entities += e.Name if e.ResolverName != "" { - resolverArgs := "" - for _, keyField := range e.KeyFields { - resolverArgs += fmt.Sprintf("%s: %s,", keyField.Field.ToGoPrivate(), keyField.Definition.Type.String()) + if e.Multi { + entityResolverInputDefinitions += "input " + e.InputType + " {\n" + for _, keyField := range e.KeyFields { + entityResolverInputDefinitions += fmt.Sprintf("\t%s: %s\n", keyField.Field.ToGo(), keyField.Definition.Type.String()) + } + entityResolverInputDefinitions += "}\n" + resolvers += fmt.Sprintf("\t%s(reps: [%s!]!): [%s]\n", e.ResolverName, e.InputType, e.Name) + } else { + resolverArgs := "" + for _, keyField := range e.KeyFields { + resolverArgs += fmt.Sprintf("%s: %s,", keyField.Field.ToGoPrivate(), keyField.Definition.Type.String()) + } + resolvers += fmt.Sprintf("\t%s(%s): %s!\n", e.ResolverName, resolverArgs, e.Name) } - resolvers += fmt.Sprintf("\t%s(%s): %s!\n", e.ResolverName, resolverArgs, e.Def.Name) } } @@ -113,7 +124,7 @@ func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source { // resolvers can be empty if a service defines only "empty // extend" types. This should be rare. if resolvers != "" { - resolvers = ` + resolvers = entityResolverInputDefinitions + ` # fake type to build resolver interfaces for users to implement type Entity { ` + resolvers + ` @@ -143,11 +154,16 @@ extend type Query { // Entity represents a federated type // that was declared in the GQL schema. type Entity struct { + // TODO(miguel): encapsulate resolver information in its own + // struct for future work to support multiple federated keys. + Name string // The same name as the type declaration KeyFields []*KeyField // The fields declared in @key. ResolverName string // The resolver name, such as FindUserByID + InputType string // The Go generated input type for multi entity resolvers Def *ast.Definition Requires []*Requires + Multi bool } type KeyField struct { @@ -257,7 +273,6 @@ func (f *federation) setEntities(schema *ast.Schema) { } keyFields := make([]*KeyField, len(keyFieldSet)) - resolverName := fmt.Sprintf("find%sBy", schemaType.Name) for i, field := range keyFieldSet { def := field.FieldDefinition(schemaType, schema) @@ -266,19 +281,25 @@ func (f *federation) setEntities(schema *ast.Schema) { } keyFields[i] = &KeyField{Definition: def, Field: field} - if i > 0 { - resolverName += "And" - } - resolverName += field.ToGo() } e := &Entity{ - Name: schemaType.Name, - KeyFields: keyFields, - Def: schemaType, - ResolverName: resolverName, - Requires: requires, + Name: schemaType.Name, + KeyFields: keyFields, + Def: schemaType, + Requires: requires, } + + // Let's process custom entity resolver settings. + dir = schemaType.Directives.ForName("entityResolver") + if dir != nil { + if dirArg := dir.Arguments.ForName("multi"); dirArg != nil { + if dirVal, err := dirArg.Value.Value(nil); err == nil { + e.Multi = dirVal.(bool) + } + } + } + // 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 @@ -297,8 +318,21 @@ func (f *federation) setEntities(schema *ast.Schema) { // extend TypeDefinedInOtherService @key(fields: "id") { // id: ID @external // } - if e.allFieldsAreExternal() { - e.ResolverName = "" + if !e.allFieldsAreExternal() { + resolverFields := []string{} + for _, f := range e.KeyFields { + resolverFields = append(resolverFields, f.Field.ToGo()) + } + + resolverFieldsToGo := schemaType.Name + "By" + strings.Join(resolverFields, "And") + if e.Multi { + resolverFieldsToGo += "s" // Pluralize for better API readability + e.ResolverName = fmt.Sprintf("findMany%s", resolverFieldsToGo) + } else { + e.ResolverName = fmt.Sprintf("find%s", resolverFieldsToGo) + } + + e.InputType = resolverFieldsToGo + "Input" } f.Entities = append(f.Entities, e) diff --git a/plugin/federation/federation.gotpl b/plugin/federation/federation.gotpl index 7dbb00535d..e888acd7d9 100644 --- a/plugin/federation/federation.gotpl +++ b/plugin/federation/federation.gotpl @@ -28,7 +28,47 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. {{if .Entities}} func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) []fedruntime.Entity { list := make([]fedruntime.Entity, len(representations)) - resolveEntity := func(ctx context.Context, i int, rep map[string]interface{}) (err error) { + + repsMap := map[string]struct { + i []int + r []map[string]interface{} + }{} + + // We group entities by typename so that we can parallelize their resolution. + // This is particularly helpful when there are entity groups in multi mode. + buildRepresentationGroups := func(reps []map[string]interface{}) { + for i, rep := range reps { + typeName, ok := rep["__typename"].(string) + if !ok { + // If there is no __typename, we just skip the representation; + // we just won't be resolving these unknown types. + ec.Error(ctx, errors.New("__typename must be an existing string")) + continue + } + + _r := repsMap[typeName] + _r.i = append(_r.i, i) + _r.r = append(_r.r, rep) + repsMap[typeName] = _r + } + } + + isMulti := func(typeName string) bool { + switch typeName { + {{- range .Entities -}} + {{- if .ResolverName -}} + {{- if .Multi -}} + case "{{.Def.Name}}": + return true + {{ end }} + {{- end -}} + {{- end -}} + default: + return false + } + } + + resolveEntity := func(ctx context.Context, typeName string, rep map[string]interface{}, idx []int, i int) (err error) { // we need to do our own panic handling, because we may be called in a // goroutine, where the usual panic handling can't catch us defer func () { @@ -37,13 +77,11 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } }() - typeName, ok := rep["__typename"].(string) - if !ok { - return errors.New("__typename must be an existing string") - } switch typeName { {{ range .Entities }} {{ if .ResolverName }} + {{ if not .Multi -}} + case "{{.Def.Name}}": {{ range $i, $keyField := .KeyFields -}} id{{$i}}, err := ec.{{.Type.UnmarshalFunc}}(ctx, rep["{{.Field.Join `"].(map[string]interface{})["`}}"]) @@ -64,40 +102,122 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } {{ end }} - list[i] = entity + + list[idx[i]] = entity return nil + {{ end }} {{ end }} - {{ end }} + {{- end }} + default: + return errors.New("unknown type: "+typeName) + } + } + + resolveManyEntities := func(ctx context.Context, typeName string, reps []map[string]interface{}, idx []int) (err error) { + // we need to do our own panic handling, because we may be called in a + // goroutine, where the usual panic handling can't catch us + defer func () { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + } + }() + + switch typeName { + {{ range .Entities -}} + {{ if .ResolverName -}} + {{ if .Multi -}} + case "{{.Def.Name}}": + _reps := make([]*{{.InputType}}, len(reps)) + + for i, rep := range reps { + {{ range $i, $keyField := .KeyFields -}} + id{{$i}}, err := ec.{{.Type.UnmarshalFunc}}(ctx, rep["{{.Field.Join `"].(map[string]interface{})["`}}"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "{{.Definition.Name}}")) + } + {{end}} + + _reps[i] = &{{.InputType}} { + {{ range $i, $keyField := .KeyFields -}} + {{$keyField.Field.ToGo}}: id{{$i}}, + {{end}} + } + } + + entities, err := ec.resolvers.Entity().{{.ResolverName | go}}(ctx, _reps) + if err != nil { + return err + } + + for i, entity := range entities { + {{- range .Requires -}} + {{- range .Fields -}} + entity.{{.NameGo}}, err = ec.{{.TypeReference.UnmarshalFunc}}(ctx, reps[i]["{{.Name}}"]) + if err != nil { + return err + } + {{- end -}} + {{- end -}} + list[idx[i]] = entity + } + {{ end }} + {{- end }} + {{- end }} default: return errors.New("unknown type: "+typeName) } + + return nil } - // if there are multiple entities to resolve, parallelize (similar to - // graphql.FieldSet.Dispatch) - switch len(representations) { + resolveEntityGroup := func(typeName string, reps []map[string]interface{}, idx []int) { + if isMulti(typeName) { + err := resolveManyEntities(ctx, typeName, reps, idx) + if err != nil { + ec.Error(ctx, err) + } + } else { + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + var e sync.WaitGroup + e.Add(len(reps)) + for i, rep := range reps { + i, rep := i, rep + go func(i int, rep map[string]interface{}) { + err := resolveEntity(ctx, typeName, rep, idx, i) + if err != nil { + ec.Error(ctx, err) + } + e.Done() + }(i, rep) + } + e.Wait() + } + } + + buildRepresentationGroups(representations) + + switch len(repsMap) { case 0: return list case 1: - err := resolveEntity(ctx, 0, representations[0]) - if err != nil { - ec.Error(ctx, err) + for typeName, reps := range repsMap { + resolveEntityGroup(typeName, reps.r, reps.i) } return list default: var g sync.WaitGroup - g.Add(len(representations)) - for i, rep := range representations { - go func(i int, rep map[string]interface{}) { - err := resolveEntity(ctx, i, rep) - if err != nil { - ec.Error(ctx, err) - } + g.Add(len(repsMap)) + for typeName, reps := range repsMap { + go func(typeName string, reps []map[string]interface{}, idx []int) { + resolveEntityGroup(typeName, reps, idx) g.Done() - }(i, rep) + }(typeName, reps.r, reps.i) } g.Wait() return list } + + return list } {{end}} diff --git a/plugin/federation/federation_entityresolver_test.go b/plugin/federation/federation_entityresolver_test.go index 780b8a7632..cc0158e6dd 100644 --- a/plugin/federation/federation_entityresolver_test.go +++ b/plugin/federation/federation_entityresolver_test.go @@ -20,7 +20,7 @@ func TestEntityResolver(t *testing.T) { Resolvers: &entityresolver.Resolver{}}), )) - t.Run("Hello entities - single federation key", func(t *testing.T) { + t.Run("Hello entities", func(t *testing.T) { representations := []map[string]interface{}{ { "__typename": "Hello", @@ -50,7 +50,7 @@ func TestEntityResolver(t *testing.T) { require.Equal(t, resp.Entities[1].Name, "first name - 2") }) - t.Run("HelloWithError entities - single federation key", func(t *testing.T) { + t.Run("HelloWithError entities", func(t *testing.T) { representations := []map[string]interface{}{ { "__typename": "HelloWithErrors", @@ -104,7 +104,7 @@ func TestEntityResolver(t *testing.T) { require.Equal(t, resp.Entities[4].Name, "") }) - t.Run("World entity with nested key", func(t *testing.T) { + t.Run("World entities with nested key", func(t *testing.T) { representations := []map[string]interface{}{ { "__typename": "World", @@ -231,6 +231,123 @@ func TestEntityResolver(t *testing.T) { }) } +func TestMultiEntityResolver(t *testing.T) { + c := client.New(handler.NewDefaultServer( + generated.NewExecutableSchema(generated.Config{ + Resolvers: &entityresolver.Resolver{}}), + )) + + t.Run("MultiHello entities", func(t *testing.T) { + itemCount := 10 + representations := []map[string]interface{}{} + + for i := 0; i < itemCount; i++ { + representations = append(representations, map[string]interface{}{ + "__typename": "MultiHello", + "name": "world name - " + strconv.Itoa(i), + }) + } + + var resp struct { + Entities []struct { + Name string `json:"name"` + } `json:"_entities"` + } + + err := c.Post( + entityQuery([]string{ + "MultiHello {name}", + }), + &resp, + client.Var("representations", representations), + ) + + require.NoError(t, err) + + for i := 0; i < itemCount; i++ { + require.Equal(t, resp.Entities[i].Name, "world name - "+strconv.Itoa(i)+" - from multiget") + } + }) + + t.Run("MultiHello and Hello (heterogeneous) entities", func(t *testing.T) { + itemCount := 20 + representations := []map[string]interface{}{} + + for i := 0; i < itemCount; i++ { + // Let's interleve the representations to test ordering of the + // responses from the entity query + if i%2 == 0 { + representations = append(representations, map[string]interface{}{ + "__typename": "MultiHello", + "name": "world name - " + strconv.Itoa(i), + }) + } else { + representations = append(representations, map[string]interface{}{ + "__typename": "Hello", + "name": "hello - " + strconv.Itoa(i), + }) + } + } + + var resp struct { + Entities []struct { + Name string `json:"name"` + } `json:"_entities"` + } + + err := c.Post( + entityQuery([]string{ + "MultiHello {name}", + "Hello {name}", + }), + &resp, + client.Var("representations", representations), + ) + + require.NoError(t, err) + + for i := 0; i < itemCount; i++ { + if i%2 == 0 { + require.Equal(t, resp.Entities[i].Name, "world name - "+strconv.Itoa(i)+" - from multiget") + } else { + require.Equal(t, resp.Entities[i].Name, "hello - "+strconv.Itoa(i)) + } + } + }) + + t.Run("MultiHelloWithError entities", func(t *testing.T) { + itemCount := 10 + representations := []map[string]interface{}{} + + for i := 0; i < itemCount; i++ { + representations = append(representations, map[string]interface{}{ + "__typename": "MultiHelloWithError", + "name": "world name - " + strconv.Itoa(i), + }) + } + + var resp struct { + Entities []struct { + Name string `json:"name"` + } `json:"_entities"` + } + + err := c.Post( + entityQuery([]string{ + "MultiHelloWithError {name}", + }), + &resp, + client.Var("representations", representations), + ) + + require.Error(t, err) + entityErrors, err := getEntityErrors(err) + require.NoError(t, err) + require.Len(t, entityErrors, 1) + require.Contains(t, entityErrors[0].Message, "error resolving MultiHelloWorldWithError") + }) +} + func entityQuery(queries []string) string { // What we want! // query($representations:[_Any!]!){_entities(representations:$representations){ ...on Hello{secondary} }} @@ -242,13 +359,13 @@ func entityQuery(queries []string) string { return "query($representations:[_Any!]!){_entities(representations:$representations){" + strings.Join(entityQueries, "") + "}}" } -type entityResolverErrors []struct { +type entityResolverError struct { Message string `json:"message"` Path []string `json:"path"` } -func getEntityErrors(err error) (entityResolverErrors, error) { - var errors entityResolverErrors +func getEntityErrors(err error) ([]*entityResolverError, error) { + var errors []*entityResolverError err = json.Unmarshal([]byte(err.Error()), &errors) return errors, err } diff --git a/plugin/federation/readme.md b/plugin/federation/readme.md index 47fc34c9a2..e2156e668f 100644 --- a/plugin/federation/readme.md +++ b/plugin/federation/readme.md @@ -15,3 +15,25 @@ Running entity resolver tests. # Architecture TODO(miguel): add details. + +# Entity resolvers - GetMany entities + +The federation plugin implements `GetMany` semantics in which entity resolvers get the entire list of representations that need to be resolved. This functionality is currently optin tho, and to enable it you need to specify the directive `@entityResolver` in the federated entity you want this feature for. E.g. + +``` +directive @entityResolver(multi: Boolean) on OBJECT + +type MultiHello @key(fields: "name") @entityResolver(multi: true) { + name: String! +} +``` + +That allows the federation plugin to generate `GetMany` resolver function that can take a list of representations to be resolved. + +From that entity type, the resolver function would be + +``` +func (r *entityResolver) FindManyMultiHellosByName(ctx context.Context, reps []*generated.EntityResolverfindManyMultiHellosByNameInput) ([]*generated.MultiHello, error) { + /// +} +``` diff --git a/plugin/federation/testdata/entityresolver/entity.resolvers.go b/plugin/federation/testdata/entityresolver/entity.resolvers.go index 5d3d1f884b..3036a4e1e1 100644 --- a/plugin/federation/testdata/entityresolver/entity.resolvers.go +++ b/plugin/federation/testdata/entityresolver/entity.resolvers.go @@ -28,6 +28,22 @@ func (r *entityResolver) FindHelloWithErrorsByName(ctx context.Context, name str }, nil } +func (r *entityResolver) FindManyMultiHelloByNames(ctx context.Context, reps []*generated.MultiHelloByNamesInput) ([]*generated.MultiHello, error) { + results := []*generated.MultiHello{} + + for _, item := range reps { + results = append(results, &generated.MultiHello{ + Name: item.Name + " - from multiget", + }) + } + + return results, nil +} + +func (r *entityResolver) FindManyMultiHelloWithErrorByNames(ctx context.Context, reps []*generated.MultiHelloWithErrorByNamesInput) ([]*generated.MultiHelloWithError, error) { + return nil, fmt.Errorf("error resolving MultiHelloWorldWithError") +} + func (r *entityResolver) FindPlanetRequiresByName(ctx context.Context, name string) (*generated.PlanetRequires, error) { return &generated.PlanetRequires{ Name: name, diff --git a/plugin/federation/testdata/entityresolver/generated/exec.go b/plugin/federation/testdata/entityresolver/generated/exec.go index a6f5d82e23..feb8e4dbe1 100644 --- a/plugin/federation/testdata/entityresolver/generated/exec.go +++ b/plugin/federation/testdata/entityresolver/generated/exec.go @@ -37,19 +37,21 @@ type Config struct { type ResolverRoot interface { Entity() EntityResolver - Query() QueryResolver } type DirectiveRoot struct { + EntityResolver func(ctx context.Context, obj interface{}, next graphql.Resolver, multi *bool) (res interface{}, err error) } type ComplexityRoot struct { Entity struct { - FindHelloByName func(childComplexity int, name string) int - FindHelloWithErrorsByName func(childComplexity int, name string) int - FindPlanetRequiresByName func(childComplexity int, name string) int - FindWorldByHelloNameAndFoo func(childComplexity int, helloName string, foo string) int - FindWorldNameByName func(childComplexity int, name string) int + FindHelloByName func(childComplexity int, name string) int + FindHelloWithErrorsByName func(childComplexity int, name string) int + FindManyMultiHelloByNames func(childComplexity int, reps []*MultiHelloByNamesInput) int + FindManyMultiHelloWithErrorByNames func(childComplexity int, reps []*MultiHelloWithErrorByNamesInput) int + FindPlanetRequiresByName func(childComplexity int, name string) int + FindWorldByHelloNameAndFoo func(childComplexity int, helloName string, foo string) int + FindWorldNameByName func(childComplexity int, name string) int } Hello struct { @@ -61,6 +63,14 @@ type ComplexityRoot struct { Name func(childComplexity int) int } + MultiHello struct { + Name func(childComplexity int) int + } + + MultiHelloWithError struct { + Name func(childComplexity int) int + } + PlanetRequires struct { Diameter func(childComplexity int) int Name func(childComplexity int) int @@ -68,8 +78,6 @@ type ComplexityRoot struct { } Query struct { - Hello func(childComplexity int) int - World func(childComplexity int) int __resolve__service func(childComplexity int) int __resolve_entities func(childComplexity int, representations []map[string]interface{}) int } @@ -92,14 +100,12 @@ type ComplexityRoot struct { type EntityResolver interface { FindHelloByName(ctx context.Context, name string) (*Hello, error) FindHelloWithErrorsByName(ctx context.Context, name string) (*HelloWithErrors, error) + FindManyMultiHelloByNames(ctx context.Context, reps []*MultiHelloByNamesInput) ([]*MultiHello, error) + FindManyMultiHelloWithErrorByNames(ctx context.Context, reps []*MultiHelloWithErrorByNamesInput) ([]*MultiHelloWithError, error) FindPlanetRequiresByName(ctx context.Context, name string) (*PlanetRequires, error) FindWorldByHelloNameAndFoo(ctx context.Context, helloName string, foo string) (*World, error) FindWorldNameByName(ctx context.Context, name string) (*WorldName, error) } -type QueryResolver interface { - Hello(ctx context.Context) (*Hello, error) - World(ctx context.Context) (*World, error) -} type executableSchema struct { resolvers ResolverRoot @@ -140,6 +146,30 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Entity.FindHelloWithErrorsByName(childComplexity, args["name"].(string)), true + case "Entity.findManyMultiHelloByNames": + if e.complexity.Entity.FindManyMultiHelloByNames == nil { + break + } + + args, err := ec.field_Entity_findManyMultiHelloByNames_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Entity.FindManyMultiHelloByNames(childComplexity, args["reps"].([]*MultiHelloByNamesInput)), true + + case "Entity.findManyMultiHelloWithErrorByNames": + if e.complexity.Entity.FindManyMultiHelloWithErrorByNames == nil { + break + } + + args, err := ec.field_Entity_findManyMultiHelloWithErrorByNames_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Entity.FindManyMultiHelloWithErrorByNames(childComplexity, args["reps"].([]*MultiHelloWithErrorByNamesInput)), true + case "Entity.findPlanetRequiresByName": if e.complexity.Entity.FindPlanetRequiresByName == nil { break @@ -197,6 +227,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.HelloWithErrors.Name(childComplexity), true + case "MultiHello.name": + if e.complexity.MultiHello.Name == nil { + break + } + + return e.complexity.MultiHello.Name(childComplexity), true + + case "MultiHelloWithError.name": + if e.complexity.MultiHelloWithError.Name == nil { + break + } + + return e.complexity.MultiHelloWithError.Name(childComplexity), true + case "PlanetRequires.diameter": if e.complexity.PlanetRequires.Diameter == nil { break @@ -218,20 +262,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PlanetRequires.Size(childComplexity), true - case "Query.hello": - if e.complexity.Query.Hello == nil { - break - } - - return e.complexity.Query.Hello(childComplexity), true - - case "Query.world": - if e.complexity.Query.World == nil { - break - } - - return e.complexity.Query.World(childComplexity), true - case "Query._service": if e.complexity.Query.__resolve__service == nil { break @@ -336,7 +366,9 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er } var sources = []*ast.Source{ - {Name: "testdata/entityresolver/schema.graphql", Input: `type Hello @key(fields: "name") { + {Name: "testdata/entityresolver/schema.graphql", Input: `directive @entityResolver(multi: Boolean) on OBJECT + +type Hello @key(fields: "name") { name: String! secondary: String! } @@ -361,9 +393,12 @@ type PlanetRequires @key(fields: "name") { diameter: Int! } -type Query { - hello: Hello! - world: World! +type MultiHello @key(fields: "name") @entityResolver(multi: true) { + name: String! +} + +type MultiHelloWithError @key(fields: "name") @entityResolver(multi: true) { + name: String! } `, BuiltIn: false}, {Name: "federation/directives.graphql", Input: ` @@ -378,12 +413,20 @@ directive @extends on OBJECT | INTERFACE `, BuiltIn: true}, {Name: "federation/entity.graphql", Input: ` # a union of all types that use the @key directive -union _Entity = Hello | HelloWithErrors | PlanetRequires | World | WorldName +union _Entity = Hello | HelloWithErrors | MultiHello | MultiHelloWithError | PlanetRequires | World | WorldName +input MultiHelloByNamesInput { + Name: String! +} +input MultiHelloWithErrorByNamesInput { + Name: String! +} # fake type to build resolver interfaces for users to implement type Entity { findHelloByName(name: String!,): Hello! findHelloWithErrorsByName(name: String!,): HelloWithErrors! + findManyMultiHelloByNames(reps: [MultiHelloByNamesInput!]!): [MultiHello] + findManyMultiHelloWithErrorByNames(reps: [MultiHelloWithErrorByNamesInput!]!): [MultiHelloWithError] findPlanetRequiresByName(name: String!,): PlanetRequires! findWorldByHelloNameAndFoo(helloName: String!,foo: String!,): World! findWorldNameByName(name: String!,): WorldName! @@ -406,6 +449,21 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) dir_entityResolver_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 *bool + if tmp, ok := rawArgs["multi"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("multi")) + arg0, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) + if err != nil { + return nil, err + } + } + args["multi"] = arg0 + return args, nil +} + func (ec *executionContext) field_Entity_findHelloByName_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -436,6 +494,36 @@ func (ec *executionContext) field_Entity_findHelloWithErrorsByName_args(ctx cont return args, nil } +func (ec *executionContext) field_Entity_findManyMultiHelloByNames_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 []*MultiHelloByNamesInput + if tmp, ok := rawArgs["reps"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("reps")) + arg0, err = ec.unmarshalNMultiHelloByNamesInput2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloByNamesInputᚄ(ctx, tmp) + if err != nil { + return nil, err + } + } + args["reps"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Entity_findManyMultiHelloWithErrorByNames_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 []*MultiHelloWithErrorByNamesInput + if tmp, ok := rawArgs["reps"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("reps")) + arg0, err = ec.unmarshalNMultiHelloWithErrorByNamesInput2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloWithErrorByNamesInputᚄ(ctx, tmp) + if err != nil { + return nil, err + } + } + args["reps"] = arg0 + return args, nil +} + func (ec *executionContext) field_Entity_findPlanetRequiresByName_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -642,6 +730,132 @@ func (ec *executionContext) _Entity_findHelloWithErrorsByName(ctx context.Contex return ec.marshalNHelloWithErrors2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐHelloWithErrors(ctx, field.Selections, res) } +func (ec *executionContext) _Entity_findManyMultiHelloByNames(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Entity", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Entity_findManyMultiHelloByNames_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Entity().FindManyMultiHelloByNames(rctx, args["reps"].([]*MultiHelloByNamesInput)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + multi, err := ec.unmarshalOBoolean2ᚖbool(ctx, true) + if err != nil { + return nil, err + } + if ec.directives.EntityResolver == nil { + return nil, errors.New("directive entityResolver is not implemented") + } + return ec.directives.EntityResolver(ctx, nil, directive0, multi) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*MultiHello); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/99designs/gqlgen/plugin/federation/testdata/entityresolver/generated.MultiHello`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*MultiHello) + fc.Result = res + return ec.marshalOMultiHello2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHello(ctx, field.Selections, res) +} + +func (ec *executionContext) _Entity_findManyMultiHelloWithErrorByNames(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Entity", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Entity_findManyMultiHelloWithErrorByNames_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Entity().FindManyMultiHelloWithErrorByNames(rctx, args["reps"].([]*MultiHelloWithErrorByNamesInput)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + multi, err := ec.unmarshalOBoolean2ᚖbool(ctx, true) + if err != nil { + return nil, err + } + if ec.directives.EntityResolver == nil { + return nil, errors.New("directive entityResolver is not implemented") + } + return ec.directives.EntityResolver(ctx, nil, directive0, multi) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*MultiHelloWithError); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/99designs/gqlgen/plugin/federation/testdata/entityresolver/generated.MultiHelloWithError`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*MultiHelloWithError) + fc.Result = res + return ec.marshalOMultiHelloWithError2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloWithError(ctx, field.Selections, res) +} + func (ec *executionContext) _Entity_findPlanetRequiresByName(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -873,7 +1087,7 @@ func (ec *executionContext) _HelloWithErrors_name(ctx context.Context, field gra return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _PlanetRequires_name(ctx context.Context, field graphql.CollectedField, obj *PlanetRequires) (ret graphql.Marshaler) { +func (ec *executionContext) _MultiHello_name(ctx context.Context, field graphql.CollectedField, obj *MultiHello) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -881,7 +1095,7 @@ func (ec *executionContext) _PlanetRequires_name(ctx context.Context, field grap } }() fc := &graphql.FieldContext{ - Object: "PlanetRequires", + Object: "MultiHello", Field: field, Args: nil, IsMethod: false, @@ -908,7 +1122,7 @@ func (ec *executionContext) _PlanetRequires_name(ctx context.Context, field grap return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _PlanetRequires_size(ctx context.Context, field graphql.CollectedField, obj *PlanetRequires) (ret graphql.Marshaler) { +func (ec *executionContext) _MultiHelloWithError_name(ctx context.Context, field graphql.CollectedField, obj *MultiHelloWithError) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -916,7 +1130,7 @@ func (ec *executionContext) _PlanetRequires_size(ctx context.Context, field grap } }() fc := &graphql.FieldContext{ - Object: "PlanetRequires", + Object: "MultiHelloWithError", Field: field, Args: nil, IsMethod: false, @@ -926,7 +1140,7 @@ func (ec *executionContext) _PlanetRequires_size(ctx context.Context, field grap ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Size, nil + return obj.Name, nil }) if err != nil { ec.Error(ctx, err) @@ -938,12 +1152,12 @@ func (ec *executionContext) _PlanetRequires_size(ctx context.Context, field grap } return graphql.Null } - res := resTmp.(int) + res := resTmp.(string) fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) + return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _PlanetRequires_diameter(ctx context.Context, field graphql.CollectedField, obj *PlanetRequires) (ret graphql.Marshaler) { +func (ec *executionContext) _PlanetRequires_name(ctx context.Context, field graphql.CollectedField, obj *PlanetRequires) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -961,7 +1175,7 @@ func (ec *executionContext) _PlanetRequires_diameter(ctx context.Context, field ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Diameter, nil + return obj.Name, nil }) if err != nil { ec.Error(ctx, err) @@ -973,12 +1187,12 @@ func (ec *executionContext) _PlanetRequires_diameter(ctx context.Context, field } return graphql.Null } - res := resTmp.(int) + res := resTmp.(string) fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) + return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _Query_hello(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _PlanetRequires_size(ctx context.Context, field graphql.CollectedField, obj *PlanetRequires) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -986,17 +1200,17 @@ func (ec *executionContext) _Query_hello(ctx context.Context, field graphql.Coll } }() fc := &graphql.FieldContext{ - Object: "Query", + Object: "PlanetRequires", Field: field, Args: nil, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, } ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Hello(rctx) + return obj.Size, nil }) if err != nil { ec.Error(ctx, err) @@ -1008,12 +1222,12 @@ func (ec *executionContext) _Query_hello(ctx context.Context, field graphql.Coll } return graphql.Null } - res := resTmp.(*Hello) + res := resTmp.(int) fc.Result = res - return ec.marshalNHello2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐHello(ctx, field.Selections, res) + return ec.marshalNInt2int(ctx, field.Selections, res) } -func (ec *executionContext) _Query_world(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _PlanetRequires_diameter(ctx context.Context, field graphql.CollectedField, obj *PlanetRequires) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -1021,17 +1235,17 @@ func (ec *executionContext) _Query_world(ctx context.Context, field graphql.Coll } }() fc := &graphql.FieldContext{ - Object: "Query", + Object: "PlanetRequires", Field: field, Args: nil, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, } ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().World(rctx) + return obj.Diameter, nil }) if err != nil { ec.Error(ctx, err) @@ -1043,9 +1257,9 @@ func (ec *executionContext) _Query_world(ctx context.Context, field graphql.Coll } return graphql.Null } - res := resTmp.(*World) + res := resTmp.(int) fc.Result = res - return ec.marshalNWorld2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐWorld(ctx, field.Selections, res) + return ec.marshalNInt2int(ctx, field.Selections, res) } func (ec *executionContext) _Query__entities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -2487,6 +2701,52 @@ func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.Co // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputMultiHelloByNamesInput(ctx context.Context, obj interface{}) (MultiHelloByNamesInput, error) { + var it MultiHelloByNamesInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "Name": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("Name")) + it.Name, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputMultiHelloWithErrorByNamesInput(ctx context.Context, obj interface{}) (MultiHelloWithErrorByNamesInput, error) { + var it MultiHelloWithErrorByNamesInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "Name": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("Name")) + it.Name, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** @@ -2509,6 +2769,20 @@ func (ec *executionContext) __Entity(ctx context.Context, sel ast.SelectionSet, return graphql.Null } return ec._HelloWithErrors(ctx, sel, obj) + case MultiHello: + return ec._MultiHello(ctx, sel, &obj) + case *MultiHello: + if obj == nil { + return graphql.Null + } + return ec._MultiHello(ctx, sel, obj) + case MultiHelloWithError: + return ec._MultiHelloWithError(ctx, sel, &obj) + case *MultiHelloWithError: + if obj == nil { + return graphql.Null + } + return ec._MultiHelloWithError(ctx, sel, obj) case PlanetRequires: return ec._PlanetRequires(ctx, sel, &obj) case *PlanetRequires: @@ -2601,6 +2875,46 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) } + out.Concurrently(i, func() graphql.Marshaler { + return rrm(innerCtx) + }) + case "findManyMultiHelloByNames": + field := field + + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Entity_findManyMultiHelloByNames(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) + } + + out.Concurrently(i, func() graphql.Marshaler { + return rrm(innerCtx) + }) + case "findManyMultiHelloWithErrorByNames": + field := field + + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Entity_findManyMultiHelloWithErrorByNames(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) + } + out.Concurrently(i, func() graphql.Marshaler { return rrm(innerCtx) }) @@ -2756,6 +3070,68 @@ func (ec *executionContext) _HelloWithErrors(ctx context.Context, sel ast.Select return out } +var multiHelloImplementors = []string{"MultiHello", "_Entity"} + +func (ec *executionContext) _MultiHello(ctx context.Context, sel ast.SelectionSet, obj *MultiHello) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, multiHelloImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("MultiHello") + case "name": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._MultiHello_name(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + +var multiHelloWithErrorImplementors = []string{"MultiHelloWithError", "_Entity"} + +func (ec *executionContext) _MultiHelloWithError(ctx context.Context, sel ast.SelectionSet, obj *MultiHelloWithError) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, multiHelloWithErrorImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("MultiHelloWithError") + case "name": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._MultiHelloWithError_name(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var planetRequiresImplementors = []string{"PlanetRequires", "_Entity"} func (ec *executionContext) _PlanetRequires(ctx context.Context, sel ast.SelectionSet, obj *PlanetRequires) graphql.Marshaler { @@ -2826,52 +3202,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Query") - case "hello": - field := field - - innerFunc := func(ctx context.Context) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Query_hello(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&invalids, 1) - } - return res - } - - rrm := func(ctx context.Context) graphql.Marshaler { - return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) - } - - out.Concurrently(i, func() graphql.Marshaler { - return rrm(innerCtx) - }) - case "world": - field := field - - innerFunc := func(ctx context.Context) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Query_world(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&invalids, 1) - } - return res - } - - rrm := func(ctx context.Context) graphql.Marshaler { - return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) - } - - out.Concurrently(i, func() graphql.Marshaler { - return rrm(innerCtx) - }) case "_entities": field := field @@ -3517,6 +3847,58 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) unmarshalNMultiHelloByNamesInput2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloByNamesInputᚄ(ctx context.Context, v interface{}) ([]*MultiHelloByNamesInput, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]*MultiHelloByNamesInput, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNMultiHelloByNamesInput2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloByNamesInput(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) unmarshalNMultiHelloByNamesInput2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloByNamesInput(ctx context.Context, v interface{}) (*MultiHelloByNamesInput, error) { + res, err := ec.unmarshalInputMultiHelloByNamesInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNMultiHelloWithErrorByNamesInput2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloWithErrorByNamesInputᚄ(ctx context.Context, v interface{}) ([]*MultiHelloWithErrorByNamesInput, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]*MultiHelloWithErrorByNamesInput, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNMultiHelloWithErrorByNamesInput2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloWithErrorByNamesInput(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) unmarshalNMultiHelloWithErrorByNamesInput2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloWithErrorByNamesInput(ctx context.Context, v interface{}) (*MultiHelloWithErrorByNamesInput, error) { + res, err := ec.unmarshalInputMultiHelloWithErrorByNamesInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNPlanetRequires2githubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐPlanetRequires(ctx context.Context, sel ast.SelectionSet, v PlanetRequires) graphql.Marshaler { return ec._PlanetRequires(ctx, sel, &v) } @@ -3978,6 +4360,102 @@ func (ec *executionContext) marshalOHello2ᚖgithubᚗcomᚋ99designsᚋgqlgen return ec._Hello(ctx, sel, v) } +func (ec *executionContext) marshalOMultiHello2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHello(ctx context.Context, sel ast.SelectionSet, v []*MultiHello) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOMultiHello2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHello(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + +func (ec *executionContext) marshalOMultiHello2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHello(ctx context.Context, sel ast.SelectionSet, v *MultiHello) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._MultiHello(ctx, sel, v) +} + +func (ec *executionContext) marshalOMultiHelloWithError2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloWithError(ctx context.Context, sel ast.SelectionSet, v []*MultiHelloWithError) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOMultiHelloWithError2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloWithError(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + +func (ec *executionContext) marshalOMultiHelloWithError2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋtestdataᚋentityresolverᚋgeneratedᚐMultiHelloWithError(ctx context.Context, sel ast.SelectionSet, v *MultiHelloWithError) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._MultiHelloWithError(ctx, sel, v) +} + func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/plugin/federation/testdata/entityresolver/generated/federation.go b/plugin/federation/testdata/entityresolver/generated/federation.go index 87524f0589..ce813f278e 100644 --- a/plugin/federation/testdata/entityresolver/generated/federation.go +++ b/plugin/federation/testdata/entityresolver/generated/federation.go @@ -33,7 +33,43 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]interface{}) []fedruntime.Entity { list := make([]fedruntime.Entity, len(representations)) - resolveEntity := func(ctx context.Context, i int, rep map[string]interface{}) (err error) { + + repsMap := map[string]struct { + i []int + r []map[string]interface{} + }{} + + // We group entities by typename so that we can parallelize their resolution. + // This is particularly helpful when there are entity groups in multi mode. + buildRepresentationGroups := func(reps []map[string]interface{}) { + for i, rep := range reps { + typeName, ok := rep["__typename"].(string) + if !ok { + // If there is no __typename, we just skip the representation; + // we just won't be resolving these unknown types. + ec.Error(ctx, errors.New("__typename must be an existing string")) + continue + } + + _r := repsMap[typeName] + _r.i = append(_r.i, i) + _r.r = append(_r.r, rep) + repsMap[typeName] = _r + } + } + + isMulti := func(typeName string) bool { + switch typeName { + case "MultiHello": + return true + case "MultiHelloWithError": + return true + default: + return false + } + } + + resolveEntity := func(ctx context.Context, typeName string, rep map[string]interface{}, idx []int, i int) (err error) { // we need to do our own panic handling, because we may be called in a // goroutine, where the usual panic handling can't catch us defer func() { @@ -42,10 +78,6 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } }() - typeName, ok := rep["__typename"].(string) - if !ok { - return errors.New("__typename must be an existing string") - } switch typeName { case "Hello": @@ -60,7 +92,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil case "HelloWithErrors": @@ -75,7 +107,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil case "PlanetRequires": @@ -95,7 +127,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil case "World": @@ -114,7 +146,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil case "WorldName": @@ -129,7 +161,7 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } - list[i] = entity + list[idx[i]] = entity return nil default: @@ -137,30 +169,115 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } } - // if there are multiple entities to resolve, parallelize (similar to - // graphql.FieldSet.Dispatch) - switch len(representations) { + resolveManyEntities := func(ctx context.Context, typeName string, reps []map[string]interface{}, idx []int) (err error) { + // we need to do our own panic handling, because we may be called in a + // goroutine, where the usual panic handling can't catch us + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + } + }() + + switch typeName { + case "MultiHello": + _reps := make([]*MultiHelloByNamesInput, len(reps)) + + for i, rep := range reps { + id0, err := ec.unmarshalNString2string(ctx, rep["name"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "name")) + } + + _reps[i] = &MultiHelloByNamesInput{ + Name: id0, + } + } + + entities, err := ec.resolvers.Entity().FindManyMultiHelloByNames(ctx, _reps) + if err != nil { + return err + } + + for i, entity := range entities { + list[idx[i]] = entity + } + case "MultiHelloWithError": + _reps := make([]*MultiHelloWithErrorByNamesInput, len(reps)) + + for i, rep := range reps { + id0, err := ec.unmarshalNString2string(ctx, rep["name"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "name")) + } + + _reps[i] = &MultiHelloWithErrorByNamesInput{ + Name: id0, + } + } + + entities, err := ec.resolvers.Entity().FindManyMultiHelloWithErrorByNames(ctx, _reps) + if err != nil { + return err + } + + for i, entity := range entities { + list[idx[i]] = entity + } + + default: + return errors.New("unknown type: " + typeName) + } + + return nil + } + + resolveEntityGroup := func(typeName string, reps []map[string]interface{}, idx []int) { + if isMulti(typeName) { + err := resolveManyEntities(ctx, typeName, reps, idx) + if err != nil { + ec.Error(ctx, err) + } + } else { + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + var e sync.WaitGroup + e.Add(len(reps)) + for i, rep := range reps { + i, rep := i, rep + go func(i int, rep map[string]interface{}) { + err := resolveEntity(ctx, typeName, rep, idx, i) + if err != nil { + ec.Error(ctx, err) + } + e.Done() + }(i, rep) + } + e.Wait() + } + } + + buildRepresentationGroups(representations) + + switch len(repsMap) { case 0: return list case 1: - err := resolveEntity(ctx, 0, representations[0]) - if err != nil { - ec.Error(ctx, err) + for typeName, reps := range repsMap { + resolveEntityGroup(typeName, reps.r, reps.i) } return list default: var g sync.WaitGroup - g.Add(len(representations)) - for i, rep := range representations { - go func(i int, rep map[string]interface{}) { - err := resolveEntity(ctx, i, rep) - if err != nil { - ec.Error(ctx, err) - } + g.Add(len(repsMap)) + for typeName, reps := range repsMap { + go func(typeName string, reps []map[string]interface{}, idx []int) { + resolveEntityGroup(typeName, reps, idx) g.Done() - }(i, rep) + }(typeName, reps.r, reps.i) } g.Wait() return list } + + return list } diff --git a/plugin/federation/testdata/entityresolver/generated/models.go b/plugin/federation/testdata/entityresolver/generated/models.go index ec3ae12665..4057d1d6d0 100644 --- a/plugin/federation/testdata/entityresolver/generated/models.go +++ b/plugin/federation/testdata/entityresolver/generated/models.go @@ -15,6 +15,26 @@ type HelloWithErrors struct { func (HelloWithErrors) IsEntity() {} +type MultiHello struct { + Name string `json:"name"` +} + +func (MultiHello) IsEntity() {} + +type MultiHelloByNamesInput struct { + Name string `json:"Name"` +} + +type MultiHelloWithError struct { + Name string `json:"name"` +} + +func (MultiHelloWithError) IsEntity() {} + +type MultiHelloWithErrorByNamesInput struct { + Name string `json:"Name"` +} + type PlanetRequires struct { Name string `json:"name"` Size int `json:"size"` diff --git a/plugin/federation/testdata/entityresolver/schema.graphql b/plugin/federation/testdata/entityresolver/schema.graphql index b8b1e5071b..7facd046a6 100644 --- a/plugin/federation/testdata/entityresolver/schema.graphql +++ b/plugin/federation/testdata/entityresolver/schema.graphql @@ -1,3 +1,5 @@ +directive @entityResolver(multi: Boolean) on OBJECT + type Hello @key(fields: "name") { name: String! secondary: String! @@ -23,7 +25,10 @@ type PlanetRequires @key(fields: "name") { diameter: Int! } -type Query { - hello: Hello! - world: World! +type MultiHello @key(fields: "name") @entityResolver(multi: true) { + name: String! +} + +type MultiHelloWithError @key(fields: "name") @entityResolver(multi: true) { + name: String! } diff --git a/plugin/federation/testdata/entityresolver/schema.resolvers.go b/plugin/federation/testdata/entityresolver/schema.resolvers.go index 64e0c1d6fd..2f55aa7019 100644 --- a/plugin/federation/testdata/entityresolver/schema.resolvers.go +++ b/plugin/federation/testdata/entityresolver/schema.resolvers.go @@ -2,23 +2,3 @@ package entityresolver // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. - -import ( - "context" - "fmt" - - "github.com/99designs/gqlgen/plugin/federation/testdata/entityresolver/generated" -) - -func (r *queryResolver) Hello(ctx context.Context) (*generated.Hello, error) { - panic(fmt.Errorf("not implemented")) -} - -func (r *queryResolver) World(ctx context.Context) (*generated.World, error) { - panic(fmt.Errorf("not implemented")) -} - -// Query returns generated.QueryResolver implementation. -func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } - -type queryResolver struct{ *Resolver }