From 6947fe0ec9b87ed34574942448813fa95605923f Mon Sep 17 00:00:00 2001 From: Carl Dunham Date: Mon, 26 Jul 2021 16:00:26 -0700 Subject: [PATCH 1/3] formatting --- plugin/federation/test_data/schema.graphql | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/federation/test_data/schema.graphql b/plugin/federation/test_data/schema.graphql index a47f8b224aa..79c567a820f 100644 --- a/plugin/federation/test_data/schema.graphql +++ b/plugin/federation/test_data/schema.graphql @@ -1,10 +1,10 @@ type Hello @key(fields: "name") { - name: String! + name: String! } type World @key(fields: "foo bar") { - foo: String! - bar: Int! + foo: String! + bar: Int! } extend type ExternalExtension @key(fields: "upc") { @@ -13,6 +13,6 @@ extend type ExternalExtension @key(fields: "upc") { } type Query { - hello: Hello! - world: World! + hello: Hello! + world: World! } From 1bc9cbd9ed903b09e80ac6102e156eee74a23f1f Mon Sep 17 00:00:00 2001 From: Carl Dunham Date: Tue, 27 Jul 2021 11:44:18 -0700 Subject: [PATCH 2/3] update federation schema to latest Apollo spec --- .../accounts/graph/generated/generated.go | 4 ++-- .../products/graph/generated/generated.go | 4 ++-- .../reviews/graph/generated/generated.go | 4 ++-- plugin/federation/federation.go | 16 +++++++++++++--- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/example/federation/accounts/graph/generated/generated.go b/example/federation/accounts/graph/generated/generated.go index fb31df149cf..e29831b3dd1 100644 --- a/example/federation/accounts/graph/generated/generated.go +++ b/example/federation/accounts/graph/generated/generated.go @@ -212,8 +212,8 @@ scalar _FieldSet directive @external on FIELD_DEFINITION directive @requires(fields: _FieldSet!) on FIELD_DEFINITION directive @provides(fields: _FieldSet!) on FIELD_DEFINITION -directive @key(fields: _FieldSet!) on OBJECT | INTERFACE -directive @extends on OBJECT +directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE +directive @extends on OBJECT | INTERFACE `, BuiltIn: true}, {Name: "federation/entity.graphql", Input: ` # a union of all types that use the @key directive diff --git a/example/federation/products/graph/generated/generated.go b/example/federation/products/graph/generated/generated.go index 8d568c1ce03..53f0a80fe8e 100644 --- a/example/federation/products/graph/generated/generated.go +++ b/example/federation/products/graph/generated/generated.go @@ -226,8 +226,8 @@ scalar _FieldSet directive @external on FIELD_DEFINITION directive @requires(fields: _FieldSet!) on FIELD_DEFINITION directive @provides(fields: _FieldSet!) on FIELD_DEFINITION -directive @key(fields: _FieldSet!) on OBJECT | INTERFACE -directive @extends on OBJECT +directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE +directive @extends on OBJECT | INTERFACE `, BuiltIn: true}, {Name: "federation/entity.graphql", Input: ` # a union of all types that use the @key directive diff --git a/example/federation/reviews/graph/generated/generated.go b/example/federation/reviews/graph/generated/generated.go index 2ba1cd32126..f4a44be1a1b 100644 --- a/example/federation/reviews/graph/generated/generated.go +++ b/example/federation/reviews/graph/generated/generated.go @@ -275,8 +275,8 @@ scalar _FieldSet directive @external on FIELD_DEFINITION directive @requires(fields: _FieldSet!) on FIELD_DEFINITION directive @provides(fields: _FieldSet!) on FIELD_DEFINITION -directive @key(fields: _FieldSet!) on OBJECT | INTERFACE -directive @extends on OBJECT +directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE +directive @extends on OBJECT | INTERFACE `, BuiltIn: true}, {Name: "federation/entity.graphql", Input: ` # a union of all types that use the @key directive diff --git a/plugin/federation/federation.go b/plugin/federation/federation.go index cdcea8c4f0c..f472325c363 100644 --- a/plugin/federation/federation.go +++ b/plugin/federation/federation.go @@ -75,8 +75,8 @@ scalar _FieldSet directive @external on FIELD_DEFINITION directive @requires(fields: _FieldSet!) on FIELD_DEFINITION directive @provides(fields: _FieldSet!) on FIELD_DEFINITION -directive @key(fields: _FieldSet!) on OBJECT | INTERFACE -directive @extends on OBJECT +directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE +directive @extends on OBJECT | INTERFACE `, BuiltIn: true, } @@ -226,8 +226,18 @@ func (f *federation) getKeyField(keyFields []*KeyField, fieldName string) *KeyFi func (f *federation) setEntities(schema *ast.Schema) { for _, schemaType := range schema.Types { + if schemaType.Kind == ast.Interface { + // TODO: support @key and @extends for interfaces + if dir := schemaType.Directives.ForName("key"); dir != nil { + panic("@key directive is not currently supported for interfaces.") + } + if dir := schemaType.Directives.ForName("extends"); dir != nil { + panic("@extends directive is not currently supported for interfaces.") + } + continue + } if schemaType.Kind == ast.Object { - dir := schemaType.Directives.ForName("key") // TODO: interfaces + dir := schemaType.Directives.ForName("key") if dir != nil { if len(dir.Arguments) > 1 { panic("Multiple arguments are not currently supported in @key declaration.") From bb4dae11ed25a8ebe066f2608560048e30e9a017 Mon Sep 17 00:00:00 2001 From: Carl Dunham Date: Tue, 27 Jul 2021 12:37:34 -0700 Subject: [PATCH 3/3] add nested FieldSet support to @key and @requires directives also: handle extra spaces in FieldSet upgrade deps in federation integration tests --- .../accounts/graph/entity.resolvers.go | 7 + .../accounts/graph/generated/federation.go | 15 + .../accounts/graph/generated/generated.go | 365 +++++++++++++- .../accounts/graph/model/models_gen.go | 13 +- .../federation/accounts/graph/schema.graphqls | 7 + .../accounts/graph/schema.resolvers.go | 7 +- example/federation/integration-test.js | 4 +- example/federation/package.json | 6 +- .../products/graph/entity.resolvers.go | 15 +- .../products/graph/generated/federation.go | 27 +- .../products/graph/generated/generated.go | 410 +++++++++++++++- .../products/graph/model/models_gen.go | 15 +- example/federation/products/graph/products.go | 15 + .../federation/products/graph/schema.graphqls | 9 +- .../reviews/graph/entity.resolvers.go | 18 +- .../reviews/graph/generated/federation.go | 22 +- .../reviews/graph/generated/generated.go | 459 ++++++++++++++++-- .../federation/reviews/graph/model/models.go | 9 +- .../reviews/graph/model/models_gen.go | 12 + example/federation/reviews/graph/reviews.go | 6 +- .../federation/reviews/graph/schema.graphqls | 17 +- .../reviews/graph/schema.resolvers.go | 24 +- plugin/federation/federation.go | 123 +++-- plugin/federation/federation.gotpl | 18 +- plugin/federation/federation_test.go | 54 ++- plugin/federation/fieldset/fieldset.go | 189 ++++++++ plugin/federation/fieldset/fieldset_test.go | 77 +++ plugin/federation/test_data/schema.graphql | 26 +- 28 files changed, 1754 insertions(+), 215 deletions(-) create mode 100644 plugin/federation/fieldset/fieldset.go create mode 100644 plugin/federation/fieldset/fieldset_test.go diff --git a/example/federation/accounts/graph/entity.resolvers.go b/example/federation/accounts/graph/entity.resolvers.go index 79f530cc046..aa6cfb3a9da 100644 --- a/example/federation/accounts/graph/entity.resolvers.go +++ b/example/federation/accounts/graph/entity.resolvers.go @@ -10,6 +10,13 @@ import ( "github.com/99designs/gqlgen/example/federation/accounts/graph/model" ) +func (r *entityResolver) FindEmailHostByID(ctx context.Context, id string) (*model.EmailHost, error) { + return &model.EmailHost{ + ID: id, + Name: "Email Host " + id, + }, nil +} + func (r *entityResolver) FindUserByID(ctx context.Context, id string) (*model.User, error) { name := "User " + id if id == "1234" { diff --git a/example/federation/accounts/graph/generated/federation.go b/example/federation/accounts/graph/generated/federation.go index 095c6ab8eb9..1d7ee7ca52a 100644 --- a/example/federation/accounts/graph/generated/federation.go +++ b/example/federation/accounts/graph/generated/federation.go @@ -48,6 +48,21 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } switch typeName { + case "EmailHost": + id0, err := ec.unmarshalNString2string(ctx, rep["id"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) + } + + entity, err := ec.resolvers.Entity().FindEmailHostByID(ctx, + id0) + if err != nil { + return err + } + + list[i] = entity + return nil + case "User": id0, err := ec.unmarshalNID2string(ctx, rep["id"]) if err != nil { diff --git a/example/federation/accounts/graph/generated/generated.go b/example/federation/accounts/graph/generated/generated.go index e29831b3dd1..d1598942b42 100644 --- a/example/federation/accounts/graph/generated/generated.go +++ b/example/federation/accounts/graph/generated/generated.go @@ -45,8 +45,14 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + EmailHost struct { + ID func(childComplexity int) int + Name func(childComplexity int) int + } + Entity struct { - FindUserByID func(childComplexity int, id string) int + FindEmailHostByID func(childComplexity int, id string) int + FindUserByID func(childComplexity int, id string) int } Query struct { @@ -56,6 +62,8 @@ type ComplexityRoot struct { } User struct { + Email func(childComplexity int) int + Host func(childComplexity int) int ID func(childComplexity int) int Username func(childComplexity int) int } @@ -66,6 +74,7 @@ type ComplexityRoot struct { } type EntityResolver interface { + FindEmailHostByID(ctx context.Context, id string) (*model.EmailHost, error) FindUserByID(ctx context.Context, id string) (*model.User, error) } type QueryResolver interface { @@ -87,6 +96,32 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "EmailHost.id": + if e.complexity.EmailHost.ID == nil { + break + } + + return e.complexity.EmailHost.ID(childComplexity), true + + case "EmailHost.name": + if e.complexity.EmailHost.Name == nil { + break + } + + return e.complexity.EmailHost.Name(childComplexity), true + + case "Entity.findEmailHostByID": + if e.complexity.Entity.FindEmailHostByID == nil { + break + } + + args, err := ec.field_Entity_findEmailHostByID_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Entity.FindEmailHostByID(childComplexity, args["id"].(string)), true + case "Entity.findUserByID": if e.complexity.Entity.FindUserByID == nil { break @@ -125,6 +160,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.__resolve_entities(childComplexity, args["representations"].([]map[string]interface{})), true + case "User.email": + if e.complexity.User.Email == nil { + break + } + + return e.complexity.User.Email(childComplexity), true + + case "User.host": + if e.complexity.User.Host == nil { + break + } + + return e.complexity.User.Host(childComplexity), true + case "User.id": if e.complexity.User.ID == nil { break @@ -200,8 +249,15 @@ var sources = []*ast.Source{ me: User } +type EmailHost @key(fields: "id") { + id: String! + name: String! +} + type User @key(fields: "id") { id: ID! + host: EmailHost! + email: String! username: String! } `, BuiltIn: false}, @@ -217,11 +273,12 @@ directive @extends on OBJECT | INTERFACE `, BuiltIn: true}, {Name: "federation/entity.graphql", Input: ` # a union of all types that use the @key directive -union _Entity = User +union _Entity = EmailHost | User # fake type to build resolver interfaces for users to implement type Entity { - findUserByID(id: ID!,): User! + findEmailHostByID(id: String!,): EmailHost! + findUserByID(id: ID!,): User! } @@ -241,6 +298,21 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Entity_findEmailHostByID_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + func (ec *executionContext) field_Entity_findUserByID_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -324,6 +396,118 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** +func (ec *executionContext) _EmailHost_id(ctx context.Context, field graphql.CollectedField, obj *model.EmailHost) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "EmailHost", + Field: field, + Args: nil, + 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 obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _EmailHost_name(ctx context.Context, field graphql.CollectedField, obj *model.EmailHost) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "EmailHost", + Field: field, + Args: nil, + 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 obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Entity_findEmailHostByID(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_findEmailHostByID_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) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Entity().FindEmailHostByID(rctx, args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.EmailHost) + fc.Result = res + return ec.marshalNEmailHost2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋaccountsᚋgraphᚋmodelᚐEmailHost(ctx, field.Selections, res) +} + func (ec *executionContext) _Entity_findUserByID(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -581,6 +765,76 @@ func (ec *executionContext) _User_id(ctx context.Context, field graphql.Collecte return ec.marshalNID2string(ctx, field.Selections, res) } +func (ec *executionContext) _User_host(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + Field: field, + Args: nil, + 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 obj.Host, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.EmailHost) + fc.Result = res + return ec.marshalNEmailHost2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋaccountsᚋgraphᚋmodelᚐEmailHost(ctx, field.Selections, res) +} + +func (ec *executionContext) _User_email(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + Field: field, + Args: nil, + 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 obj.Email, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -1778,6 +2032,13 @@ func (ec *executionContext) __Entity(ctx context.Context, sel ast.SelectionSet, switch obj := (obj).(type) { case nil: return graphql.Null + case model.EmailHost: + return ec._EmailHost(ctx, sel, &obj) + case *model.EmailHost: + if obj == nil { + return graphql.Null + } + return ec._EmailHost(ctx, sel, obj) case model.User: return ec._User(ctx, sel, &obj) case *model.User: @@ -1794,6 +2055,47 @@ func (ec *executionContext) __Entity(ctx context.Context, sel ast.SelectionSet, // region **************************** object.gotpl **************************** +var emailHostImplementors = []string{"EmailHost", "_Entity"} + +func (ec *executionContext) _EmailHost(ctx context.Context, sel ast.SelectionSet, obj *model.EmailHost) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, emailHostImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("EmailHost") + case "id": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._EmailHost_id(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "name": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._EmailHost_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 entityImplementors = []string{"Entity"} func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -1813,6 +2115,29 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Entity") + case "findEmailHostByID": + 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_findEmailHostByID(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 "findUserByID": field := field @@ -1974,6 +2299,26 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = innerFunc(ctx) + if out.Values[i] == graphql.Null { + invalids++ + } + case "host": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._User_host(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "email": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._User_email(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + if out.Values[i] == graphql.Null { invalids++ } @@ -2450,6 +2795,20 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) marshalNEmailHost2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋaccountsᚋgraphᚋmodelᚐEmailHost(ctx context.Context, sel ast.SelectionSet, v model.EmailHost) graphql.Marshaler { + return ec._EmailHost(ctx, sel, &v) +} + +func (ec *executionContext) marshalNEmailHost2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋaccountsᚋgraphᚋmodelᚐEmailHost(ctx context.Context, sel ast.SelectionSet, v *model.EmailHost) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._EmailHost(ctx, sel, v) +} + func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalID(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/example/federation/accounts/graph/model/models_gen.go b/example/federation/accounts/graph/model/models_gen.go index 67853e2ba3d..8ccceef61fd 100644 --- a/example/federation/accounts/graph/model/models_gen.go +++ b/example/federation/accounts/graph/model/models_gen.go @@ -2,9 +2,18 @@ package model +type EmailHost struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func (EmailHost) IsEntity() {} + type User struct { - ID string `json:"id"` - Username string `json:"username"` + ID string `json:"id"` + Host *EmailHost `json:"host"` + Email string `json:"email"` + Username string `json:"username"` } func (User) IsEntity() {} diff --git a/example/federation/accounts/graph/schema.graphqls b/example/federation/accounts/graph/schema.graphqls index 29581a5d29a..01c7c2e11b7 100644 --- a/example/federation/accounts/graph/schema.graphqls +++ b/example/federation/accounts/graph/schema.graphqls @@ -2,7 +2,14 @@ extend type Query { me: User } +type EmailHost @key(fields: "id") { + id: String! + name: String! +} + type User @key(fields: "id") { id: ID! + host: EmailHost! + email: String! username: String! } diff --git a/example/federation/accounts/graph/schema.resolvers.go b/example/federation/accounts/graph/schema.resolvers.go index 52184482275..266cc3b3b01 100644 --- a/example/federation/accounts/graph/schema.resolvers.go +++ b/example/federation/accounts/graph/schema.resolvers.go @@ -12,7 +12,12 @@ import ( func (r *queryResolver) Me(ctx context.Context) (*model.User, error) { return &model.User{ - ID: "1234", + ID: "1234", + Host: &model.EmailHost{ + ID: "4567", + Name: "Email Host 4567", + }, + Email: "me@example.com", Username: "Me", }, nil } diff --git a/example/federation/integration-test.js b/example/federation/integration-test.js index 17b8b5bd9bc..399177b919f 100644 --- a/example/federation/integration-test.js +++ b/example/federation/integration-test.js @@ -47,8 +47,8 @@ describe('Json', () => { "body": "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.", "product": { "__typename": "Product", - "name": "Trilby", - "upc": "top-1" + "name": "Fedora", + "upc": "top-2" } } ] diff --git a/example/federation/package.json b/example/federation/package.json index bc79f98efcc..aa2093e8c62 100644 --- a/example/federation/package.json +++ b/example/federation/package.json @@ -9,9 +9,9 @@ "author": "", "license": "ISC", "dependencies": { - "@apollo/gateway": "^0.11.7", - "apollo-server": "^2.9.16", - "graphql": "^14.6.0" + "@apollo/gateway": "^0.42.0", + "apollo-server": "^3.3.0", + "graphql": "^15.6.1" }, "devDependencies": { "apollo-cache-inmemory": "^1.6.5", diff --git a/example/federation/products/graph/entity.resolvers.go b/example/federation/products/graph/entity.resolvers.go index 54c94a7c002..1af00473615 100644 --- a/example/federation/products/graph/entity.resolvers.go +++ b/example/federation/products/graph/entity.resolvers.go @@ -10,10 +10,17 @@ import ( "github.com/99designs/gqlgen/example/federation/products/graph/model" ) -func (r *entityResolver) FindProductByUpc(ctx context.Context, upc string) (*model.Product, error) { - for _, h := range hats { - if h.Upc == upc { - return h, nil +func (r *entityResolver) FindManufacturerByID(ctx context.Context, id string) (*model.Manufacturer, error) { + return &model.Manufacturer{ + ID: id, + Name: "Millinery " + id, + }, nil +} + +func (r *entityResolver) FindProductByManufacturerIDAndID(ctx context.Context, manufacturerID string, id string) (*model.Product, error) { + for _, hat := range hats { + if hat.ID == id && hat.Manufacturer.ID == manufacturerID { + return hat, nil } } return nil, nil diff --git a/example/federation/products/graph/generated/federation.go b/example/federation/products/graph/generated/federation.go index b92e6c227ca..410d467a82d 100644 --- a/example/federation/products/graph/generated/federation.go +++ b/example/federation/products/graph/generated/federation.go @@ -48,13 +48,13 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati } switch typeName { - case "Product": - id0, err := ec.unmarshalNString2string(ctx, rep["upc"]) + case "Manufacturer": + id0, err := ec.unmarshalNString2string(ctx, rep["id"]) if err != nil { - return errors.New(fmt.Sprintf("Field %s undefined in schema.", "upc")) + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) } - entity, err := ec.resolvers.Entity().FindProductByUpc(ctx, + entity, err := ec.resolvers.Entity().FindManufacturerByID(ctx, id0) if err != nil { return err @@ -63,6 +63,25 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati list[i] = entity return nil + case "Product": + id0, err := ec.unmarshalNString2string(ctx, rep["manufacturer"].(map[string]interface{})["id"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "manufacturerID")) + } + id1, err := ec.unmarshalNString2string(ctx, rep["id"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) + } + + entity, err := ec.resolvers.Entity().FindProductByManufacturerIDAndID(ctx, + id0, id1) + if err != nil { + return err + } + + list[i] = entity + return nil + default: return errors.New("unknown type: " + typeName) } diff --git a/example/federation/products/graph/generated/generated.go b/example/federation/products/graph/generated/generated.go index 53f0a80fe8e..db9b6e3a572 100644 --- a/example/federation/products/graph/generated/generated.go +++ b/example/federation/products/graph/generated/generated.go @@ -46,13 +46,21 @@ type DirectiveRoot struct { type ComplexityRoot struct { Entity struct { - FindProductByUpc func(childComplexity int, upc string) int + FindManufacturerByID func(childComplexity int, id string) int + FindProductByManufacturerIDAndID func(childComplexity int, manufacturerID string, id string) int + } + + Manufacturer struct { + ID func(childComplexity int) int + Name func(childComplexity int) int } Product struct { - Name func(childComplexity int) int - Price func(childComplexity int) int - Upc func(childComplexity int) int + ID func(childComplexity int) int + Manufacturer func(childComplexity int) int + Name func(childComplexity int) int + Price func(childComplexity int) int + Upc func(childComplexity int) int } Query struct { @@ -67,7 +75,8 @@ type ComplexityRoot struct { } type EntityResolver interface { - FindProductByUpc(ctx context.Context, upc string) (*model.Product, error) + FindManufacturerByID(ctx context.Context, id string) (*model.Manufacturer, error) + FindProductByManufacturerIDAndID(ctx context.Context, manufacturerID string, id string) (*model.Product, error) } type QueryResolver interface { TopProducts(ctx context.Context, first *int) ([]*model.Product, error) @@ -88,17 +97,57 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { - case "Entity.findProductByUpc": - if e.complexity.Entity.FindProductByUpc == nil { + case "Entity.findManufacturerByID": + if e.complexity.Entity.FindManufacturerByID == nil { + break + } + + args, err := ec.field_Entity_findManufacturerByID_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Entity.FindManufacturerByID(childComplexity, args["id"].(string)), true + + case "Entity.findProductByManufacturerIDAndID": + if e.complexity.Entity.FindProductByManufacturerIDAndID == nil { break } - args, err := ec.field_Entity_findProductByUpc_args(context.TODO(), rawArgs) + args, err := ec.field_Entity_findProductByManufacturerIDAndID_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Entity.FindProductByUpc(childComplexity, args["upc"].(string)), true + return e.complexity.Entity.FindProductByManufacturerIDAndID(childComplexity, args["manufacturerID"].(string), args["id"].(string)), true + + case "Manufacturer.id": + if e.complexity.Manufacturer.ID == nil { + break + } + + return e.complexity.Manufacturer.ID(childComplexity), true + + case "Manufacturer.name": + if e.complexity.Manufacturer.Name == nil { + break + } + + return e.complexity.Manufacturer.Name(childComplexity), true + + case "Product.id": + if e.complexity.Product.ID == nil { + break + } + + return e.complexity.Product.ID(childComplexity), true + + case "Product.manufacturer": + if e.complexity.Product.Manufacturer == nil { + break + } + + return e.complexity.Product.Manufacturer(childComplexity), true case "Product.name": if e.complexity.Product.Name == nil { @@ -213,7 +262,14 @@ var sources = []*ast.Source{ topProducts(first: Int = 5): [Product] } -type Product @key(fields: "upc") { +type Manufacturer @key(fields: "id") { + id: String! + name: String! +} + +type Product @key(fields: "manufacturer { id } id") { + id: String! + manufacturer: Manufacturer! upc: String! name: String! price: Int! @@ -231,11 +287,12 @@ directive @extends on OBJECT | INTERFACE `, BuiltIn: true}, {Name: "federation/entity.graphql", Input: ` # a union of all types that use the @key directive -union _Entity = Product +union _Entity = Manufacturer | Product # fake type to build resolver interfaces for users to implement type Entity { - findProductByUpc(upc: String!,): Product! + findManufacturerByID(id: String!,): Manufacturer! + findProductByManufacturerIDAndID(manufacturerID: String!,id: String!,): Product! } @@ -255,18 +312,42 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** -func (ec *executionContext) field_Entity_findProductByUpc_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { +func (ec *executionContext) field_Entity_findManufacturerByID_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Entity_findProductByManufacturerIDAndID_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} var arg0 string - if tmp, ok := rawArgs["upc"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("upc")) + if tmp, ok := rawArgs["manufacturerID"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("manufacturerID")) arg0, err = ec.unmarshalNString2string(ctx, tmp) if err != nil { return nil, err } } - args["upc"] = arg0 + args["manufacturerID"] = arg0 + var arg1 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg1, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg1 return args, nil } @@ -353,7 +434,7 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** -func (ec *executionContext) _Entity_findProductByUpc(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _Entity_findManufacturerByID(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -370,7 +451,7 @@ func (ec *executionContext) _Entity_findProductByUpc(ctx context.Context, field ctx = graphql.WithFieldContext(ctx, fc) rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Entity_findProductByUpc_args(ctx, rawArgs) + args, err := ec.field_Entity_findManufacturerByID_args(ctx, rawArgs) if err != nil { ec.Error(ctx, err) return graphql.Null @@ -378,7 +459,49 @@ func (ec *executionContext) _Entity_findProductByUpc(ctx context.Context, field fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Entity().FindProductByUpc(rctx, args["upc"].(string)) + return ec.resolvers.Entity().FindManufacturerByID(rctx, args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Manufacturer) + fc.Result = res + return ec.marshalNManufacturer2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋproductsᚋgraphᚋmodelᚐManufacturer(ctx, field.Selections, res) +} + +func (ec *executionContext) _Entity_findProductByManufacturerIDAndID(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_findProductByManufacturerIDAndID_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) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Entity().FindProductByManufacturerIDAndID(rctx, args["manufacturerID"].(string), args["id"].(string)) }) if err != nil { ec.Error(ctx, err) @@ -395,6 +518,146 @@ func (ec *executionContext) _Entity_findProductByUpc(ctx context.Context, field return ec.marshalNProduct2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋproductsᚋgraphᚋmodelᚐProduct(ctx, field.Selections, res) } +func (ec *executionContext) _Manufacturer_id(ctx context.Context, field graphql.CollectedField, obj *model.Manufacturer) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Manufacturer", + Field: field, + Args: nil, + 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 obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Manufacturer_name(ctx context.Context, field graphql.CollectedField, obj *model.Manufacturer) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Manufacturer", + Field: field, + Args: nil, + 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 obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Product_id(ctx context.Context, field graphql.CollectedField, obj *model.Product) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Product", + Field: field, + Args: nil, + 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 obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Product_manufacturer(ctx context.Context, field graphql.CollectedField, obj *model.Product) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Product", + Field: field, + Args: nil, + 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 obj.Manufacturer, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Manufacturer) + fc.Result = res + return ec.marshalNManufacturer2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋproductsᚋgraphᚋmodelᚐManufacturer(ctx, field.Selections, res) +} + func (ec *executionContext) _Product_upc(ctx context.Context, field graphql.CollectedField, obj *model.Product) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -1849,6 +2112,13 @@ func (ec *executionContext) __Entity(ctx context.Context, sel ast.SelectionSet, switch obj := (obj).(type) { case nil: return graphql.Null + case model.Manufacturer: + return ec._Manufacturer(ctx, sel, &obj) + case *model.Manufacturer: + if obj == nil { + return graphql.Null + } + return ec._Manufacturer(ctx, sel, obj) case model.Product: return ec._Product(ctx, sel, &obj) case *model.Product: @@ -1884,7 +2154,7 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Entity") - case "findProductByUpc": + case "findManufacturerByID": field := field innerFunc := func(ctx context.Context) (res graphql.Marshaler) { @@ -1893,7 +2163,30 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Entity_findProductByUpc(ctx, field) + res = ec._Entity_findManufacturerByID(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 "findProductByManufacturerIDAndID": + 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_findProductByManufacturerIDAndID(ctx, field) if res == graphql.Null { atomic.AddUint32(&invalids, 1) } @@ -1918,6 +2211,47 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g return out } +var manufacturerImplementors = []string{"Manufacturer", "_Entity"} + +func (ec *executionContext) _Manufacturer(ctx context.Context, sel ast.SelectionSet, obj *model.Manufacturer) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, manufacturerImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Manufacturer") + case "id": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._Manufacturer_id(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "name": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._Manufacturer_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 productImplementors = []string{"Product", "_Entity"} func (ec *executionContext) _Product(ctx context.Context, sel ast.SelectionSet, obj *model.Product) graphql.Marshaler { @@ -1928,6 +2262,26 @@ func (ec *executionContext) _Product(ctx context.Context, sel ast.SelectionSet, switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Product") + case "id": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._Product_id(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "manufacturer": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._Product_manufacturer(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + + if out.Values[i] == graphql.Null { + invalids++ + } case "upc": innerFunc := func(ctx context.Context) (res graphql.Marshaler) { return ec._Product_upc(ctx, field, obj) @@ -2546,6 +2900,20 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) marshalNManufacturer2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋproductsᚋgraphᚋmodelᚐManufacturer(ctx context.Context, sel ast.SelectionSet, v model.Manufacturer) graphql.Marshaler { + return ec._Manufacturer(ctx, sel, &v) +} + +func (ec *executionContext) marshalNManufacturer2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋproductsᚋgraphᚋmodelᚐManufacturer(ctx context.Context, sel ast.SelectionSet, v *model.Manufacturer) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._Manufacturer(ctx, sel, v) +} + func (ec *executionContext) marshalNProduct2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋproductsᚋgraphᚋmodelᚐProduct(ctx context.Context, sel ast.SelectionSet, v model.Product) graphql.Marshaler { return ec._Product(ctx, sel, &v) } diff --git a/example/federation/products/graph/model/models_gen.go b/example/federation/products/graph/model/models_gen.go index c18b34dc211..3e86a4cc86e 100644 --- a/example/federation/products/graph/model/models_gen.go +++ b/example/federation/products/graph/model/models_gen.go @@ -2,10 +2,19 @@ package model +type Manufacturer struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func (Manufacturer) IsEntity() {} + type Product struct { - Upc string `json:"upc"` - Name string `json:"name"` - Price int `json:"price"` + ID string `json:"id"` + Manufacturer *Manufacturer `json:"manufacturer"` + Upc string `json:"upc"` + Name string `json:"name"` + Price int `json:"price"` } func (Product) IsEntity() {} diff --git a/example/federation/products/graph/products.go b/example/federation/products/graph/products.go index 7c23ba7fc03..ebfdf00a612 100644 --- a/example/federation/products/graph/products.go +++ b/example/federation/products/graph/products.go @@ -4,16 +4,31 @@ import "github.com/99designs/gqlgen/example/federation/products/graph/model" var hats = []*model.Product{ { + ID: "111", + Manufacturer: &model.Manufacturer{ + ID: "1234", + Name: "Millinery 1234", + }, Upc: "top-1", Name: "Trilby", Price: 11, }, { + ID: "222", + Manufacturer: &model.Manufacturer{ + ID: "2345", + Name: "Millinery 2345", + }, Upc: "top-2", Name: "Fedora", Price: 22, }, { + ID: "333", + Manufacturer: &model.Manufacturer{ + ID: "2345", + Name: "Millinery 2345", + }, Upc: "top-3", Name: "Boater", Price: 33, diff --git a/example/federation/products/graph/schema.graphqls b/example/federation/products/graph/schema.graphqls index 02ea017a700..7e55ac996eb 100644 --- a/example/federation/products/graph/schema.graphqls +++ b/example/federation/products/graph/schema.graphqls @@ -2,7 +2,14 @@ extend type Query { topProducts(first: Int = 5): [Product] } -type Product @key(fields: "upc") { +type Manufacturer @key(fields: "id") { + id: String! + name: String! +} + +type Product @key(fields: "manufacturer { id } id") { + id: String! + manufacturer: Manufacturer! upc: String! name: String! price: Int! diff --git a/example/federation/reviews/graph/entity.resolvers.go b/example/federation/reviews/graph/entity.resolvers.go index 37c280df920..5f447f2bea0 100644 --- a/example/federation/reviews/graph/entity.resolvers.go +++ b/example/federation/reviews/graph/entity.resolvers.go @@ -10,15 +10,27 @@ import ( "github.com/99designs/gqlgen/example/federation/reviews/graph/model" ) -func (r *entityResolver) FindProductByUpc(ctx context.Context, upc string) (*model.Product, error) { +func (r *entityResolver) FindProductByManufacturerIDAndID(ctx context.Context, manufacturerID string, id string) (*model.Product, error) { + var productReviews []*model.Review + + for _, review := range reviews { + if review.Product.ID == id && review.Product.Manufacturer.ID == manufacturerID { + productReviews = append(productReviews, review) + } + } return &model.Product{ - Upc: upc, + ID: id, + Manufacturer: &model.Manufacturer{ + ID: manufacturerID, + }, + Reviews: productReviews, }, nil } func (r *entityResolver) FindUserByID(ctx context.Context, id string) (*model.User, error) { return &model.User{ - ID: id, + ID: id, + Host: &model.EmailHost{}, }, nil } diff --git a/example/federation/reviews/graph/generated/federation.go b/example/federation/reviews/graph/generated/federation.go index 3dff729b252..55391a46eae 100644 --- a/example/federation/reviews/graph/generated/federation.go +++ b/example/federation/reviews/graph/generated/federation.go @@ -49,13 +49,17 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati switch typeName { case "Product": - id0, err := ec.unmarshalNString2string(ctx, rep["upc"]) + id0, err := ec.unmarshalNString2string(ctx, rep["manufacturer"].(map[string]interface{})["id"]) if err != nil { - return errors.New(fmt.Sprintf("Field %s undefined in schema.", "upc")) + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "manufacturerID")) + } + id1, err := ec.unmarshalNString2string(ctx, rep["id"]) + if err != nil { + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) } - entity, err := ec.resolvers.Entity().FindProductByUpc(ctx, - id0) + entity, err := ec.resolvers.Entity().FindProductByManufacturerIDAndID(ctx, + id0, id1) if err != nil { return err } @@ -75,6 +79,16 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return err } + entity.Host.ID, err = ec.unmarshalNString2string(ctx, rep["hostID"]) + if err != nil { + return err + } + + entity.Email, err = ec.unmarshalNString2string(ctx, rep["email"]) + if err != nil { + return err + } + list[i] = entity return nil diff --git a/example/federation/reviews/graph/generated/generated.go b/example/federation/reviews/graph/generated/generated.go index f4a44be1a1b..ee3d560a206 100644 --- a/example/federation/reviews/graph/generated/generated.go +++ b/example/federation/reviews/graph/generated/generated.go @@ -38,7 +38,6 @@ type Config struct { type ResolverRoot interface { Entity() EntityResolver - Product() ProductResolver User() UserResolver } @@ -46,14 +45,23 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + EmailHost struct { + ID func(childComplexity int) int + } + Entity struct { - FindProductByUpc func(childComplexity int, upc string) int - FindUserByID func(childComplexity int, id string) int + FindProductByManufacturerIDAndID func(childComplexity int, manufacturerID string, id string) int + FindUserByID func(childComplexity int, id string) int + } + + Manufacturer struct { + ID func(childComplexity int) int } Product struct { - Reviews func(childComplexity int) int - Upc func(childComplexity int) int + ID func(childComplexity int) int + Manufacturer func(childComplexity int) int + Reviews func(childComplexity int) int } Query struct { @@ -68,6 +76,8 @@ type ComplexityRoot struct { } User struct { + Email func(childComplexity int) int + Host func(childComplexity int) int ID func(childComplexity int) int Reviews func(childComplexity int) int } @@ -78,12 +88,9 @@ type ComplexityRoot struct { } type EntityResolver interface { - FindProductByUpc(ctx context.Context, upc string) (*model.Product, error) + FindProductByManufacturerIDAndID(ctx context.Context, manufacturerID string, id string) (*model.Product, error) FindUserByID(ctx context.Context, id string) (*model.User, error) } -type ProductResolver interface { - Reviews(ctx context.Context, obj *model.Product) ([]*model.Review, error) -} type UserResolver interface { Reviews(ctx context.Context, obj *model.User) ([]*model.Review, error) } @@ -103,17 +110,24 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { - case "Entity.findProductByUpc": - if e.complexity.Entity.FindProductByUpc == nil { + case "EmailHost.id": + if e.complexity.EmailHost.ID == nil { break } - args, err := ec.field_Entity_findProductByUpc_args(context.TODO(), rawArgs) + return e.complexity.EmailHost.ID(childComplexity), true + + case "Entity.findProductByManufacturerIDAndID": + if e.complexity.Entity.FindProductByManufacturerIDAndID == nil { + break + } + + args, err := ec.field_Entity_findProductByManufacturerIDAndID_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Entity.FindProductByUpc(childComplexity, args["upc"].(string)), true + return e.complexity.Entity.FindProductByManufacturerIDAndID(childComplexity, args["manufacturerID"].(string), args["id"].(string)), true case "Entity.findUserByID": if e.complexity.Entity.FindUserByID == nil { @@ -127,19 +141,33 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Entity.FindUserByID(childComplexity, args["id"].(string)), true - case "Product.reviews": - if e.complexity.Product.Reviews == nil { + case "Manufacturer.id": + if e.complexity.Manufacturer.ID == nil { break } - return e.complexity.Product.Reviews(childComplexity), true + return e.complexity.Manufacturer.ID(childComplexity), true + + case "Product.id": + if e.complexity.Product.ID == nil { + break + } - case "Product.upc": - if e.complexity.Product.Upc == nil { + return e.complexity.Product.ID(childComplexity), true + + case "Product.manufacturer": + if e.complexity.Product.Manufacturer == nil { break } - return e.complexity.Product.Upc(childComplexity), true + return e.complexity.Product.Manufacturer(childComplexity), true + + case "Product.reviews": + if e.complexity.Product.Reviews == nil { + break + } + + return e.complexity.Product.Reviews(childComplexity), true case "Query._service": if e.complexity.Query.__resolve__service == nil { @@ -181,6 +209,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Review.Product(childComplexity), true + case "User.email": + if e.complexity.User.Email == nil { + break + } + + return e.complexity.User.Email(childComplexity), true + + case "User.host": + if e.complexity.User.Host == nil { + break + } + + return e.complexity.User.Host(childComplexity), true + case "User.id": if e.complexity.User.ID == nil { break @@ -258,13 +300,24 @@ var sources = []*ast.Source{ product: Product! } +extend type EmailHost @key(fields: "id") { + id: String! @external +} + extend type User @key(fields: "id") { id: ID! @external - reviews: [Review] + host: EmailHost! @external + email: String! @external + reviews: [Review] @requires(fields: "host {id} email") +} + +extend type Manufacturer @key(fields: "id") { + id: String! @external } -extend type Product @key(fields: "upc") { - upc: String! @external +extend type Product @key(fields: " manufacturer{ id} id") { + id: String! @external + manufacturer: Manufacturer! @external reviews: [Review] } `, BuiltIn: false}, @@ -280,11 +333,11 @@ directive @extends on OBJECT | INTERFACE `, BuiltIn: true}, {Name: "federation/entity.graphql", Input: ` # a union of all types that use the @key directive -union _Entity = Product | User +union _Entity = EmailHost | Manufacturer | Product | User # fake type to build resolver interfaces for users to implement type Entity { - findProductByUpc(upc: String!,): Product! + findProductByManufacturerIDAndID(manufacturerID: String!,id: String!,): Product! findUserByID(id: ID!,): User! } @@ -305,18 +358,27 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** -func (ec *executionContext) field_Entity_findProductByUpc_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { +func (ec *executionContext) field_Entity_findProductByManufacturerIDAndID_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} var arg0 string - if tmp, ok := rawArgs["upc"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("upc")) + if tmp, ok := rawArgs["manufacturerID"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("manufacturerID")) arg0, err = ec.unmarshalNString2string(ctx, tmp) if err != nil { return nil, err } } - args["upc"] = arg0 + args["manufacturerID"] = arg0 + var arg1 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg1, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg1 return args, nil } @@ -403,7 +465,42 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** -func (ec *executionContext) _Entity_findProductByUpc(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _EmailHost_id(ctx context.Context, field graphql.CollectedField, obj *model.EmailHost) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "EmailHost", + Field: field, + Args: nil, + 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 obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Entity_findProductByManufacturerIDAndID(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -420,7 +517,7 @@ func (ec *executionContext) _Entity_findProductByUpc(ctx context.Context, field ctx = graphql.WithFieldContext(ctx, fc) rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Entity_findProductByUpc_args(ctx, rawArgs) + args, err := ec.field_Entity_findProductByManufacturerIDAndID_args(ctx, rawArgs) if err != nil { ec.Error(ctx, err) return graphql.Null @@ -428,7 +525,7 @@ func (ec *executionContext) _Entity_findProductByUpc(ctx context.Context, field fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Entity().FindProductByUpc(rctx, args["upc"].(string)) + return ec.resolvers.Entity().FindProductByManufacturerIDAndID(rctx, args["manufacturerID"].(string), args["id"].(string)) }) if err != nil { ec.Error(ctx, err) @@ -487,7 +584,42 @@ func (ec *executionContext) _Entity_findUserByID(ctx context.Context, field grap return ec.marshalNUser2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋreviewsᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) } -func (ec *executionContext) _Product_upc(ctx context.Context, field graphql.CollectedField, obj *model.Product) (ret graphql.Marshaler) { +func (ec *executionContext) _Manufacturer_id(ctx context.Context, field graphql.CollectedField, obj *model.Manufacturer) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Manufacturer", + Field: field, + Args: nil, + 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 obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Product_id(ctx context.Context, field graphql.CollectedField, obj *model.Product) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -505,7 +637,7 @@ func (ec *executionContext) _Product_upc(ctx context.Context, field graphql.Coll 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.Upc, nil + return obj.ID, nil }) if err != nil { ec.Error(ctx, err) @@ -522,6 +654,41 @@ func (ec *executionContext) _Product_upc(ctx context.Context, field graphql.Coll return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _Product_manufacturer(ctx context.Context, field graphql.CollectedField, obj *model.Product) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Product", + Field: field, + Args: nil, + 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 obj.Manufacturer, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Manufacturer) + fc.Result = res + return ec.marshalNManufacturer2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋreviewsᚋgraphᚋmodelᚐManufacturer(ctx, field.Selections, res) +} + func (ec *executionContext) _Product_reviews(ctx context.Context, field graphql.CollectedField, obj *model.Product) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -533,14 +700,14 @@ func (ec *executionContext) _Product_reviews(ctx context.Context, field graphql. Object: "Product", 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.Product().Reviews(rctx, obj) + return obj.Reviews, nil }) if err != nil { ec.Error(ctx, err) @@ -842,6 +1009,76 @@ func (ec *executionContext) _User_id(ctx context.Context, field graphql.Collecte return ec.marshalNID2string(ctx, field.Selections, res) } +func (ec *executionContext) _User_host(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + Field: field, + Args: nil, + 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 obj.Host, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.EmailHost) + fc.Result = res + return ec.marshalNEmailHost2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋreviewsᚋgraphᚋmodelᚐEmailHost(ctx, field.Selections, res) +} + +func (ec *executionContext) _User_email(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + Field: field, + Args: nil, + 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 obj.Email, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + func (ec *executionContext) _User_reviews(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2036,6 +2273,20 @@ func (ec *executionContext) __Entity(ctx context.Context, sel ast.SelectionSet, switch obj := (obj).(type) { case nil: return graphql.Null + case model.EmailHost: + return ec._EmailHost(ctx, sel, &obj) + case *model.EmailHost: + if obj == nil { + return graphql.Null + } + return ec._EmailHost(ctx, sel, obj) + case model.Manufacturer: + return ec._Manufacturer(ctx, sel, &obj) + case *model.Manufacturer: + if obj == nil { + return graphql.Null + } + return ec._Manufacturer(ctx, sel, obj) case model.Product: return ec._Product(ctx, sel, &obj) case *model.Product: @@ -2059,6 +2310,37 @@ func (ec *executionContext) __Entity(ctx context.Context, sel ast.SelectionSet, // region **************************** object.gotpl **************************** +var emailHostImplementors = []string{"EmailHost", "_Entity"} + +func (ec *executionContext) _EmailHost(ctx context.Context, sel ast.SelectionSet, obj *model.EmailHost) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, emailHostImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("EmailHost") + case "id": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._EmailHost_id(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 entityImplementors = []string{"Entity"} func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -2078,7 +2360,7 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Entity") - case "findProductByUpc": + case "findProductByManufacturerIDAndID": field := field innerFunc := func(ctx context.Context) (res graphql.Marshaler) { @@ -2087,7 +2369,7 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Entity_findProductByUpc(ctx, field) + res = ec._Entity_findProductByManufacturerIDAndID(ctx, field) if res == graphql.Null { atomic.AddUint32(&invalids, 1) } @@ -2135,6 +2417,37 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g return out } +var manufacturerImplementors = []string{"Manufacturer", "_Entity"} + +func (ec *executionContext) _Manufacturer(ctx context.Context, sel ast.SelectionSet, obj *model.Manufacturer) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, manufacturerImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Manufacturer") + case "id": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._Manufacturer_id(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 productImplementors = []string{"Product", "_Entity"} func (ec *executionContext) _Product(ctx context.Context, sel ast.SelectionSet, obj *model.Product) graphql.Marshaler { @@ -2145,33 +2458,33 @@ func (ec *executionContext) _Product(ctx context.Context, sel ast.SelectionSet, switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Product") - case "upc": + case "id": innerFunc := func(ctx context.Context) (res graphql.Marshaler) { - return ec._Product_upc(ctx, field, obj) + return ec._Product_id(ctx, field, obj) } out.Values[i] = innerFunc(ctx) if out.Values[i] == graphql.Null { - atomic.AddUint32(&invalids, 1) + invalids++ + } + case "manufacturer": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._Product_manufacturer(ctx, field, obj) } - case "reviews": - field := field + out.Values[i] = innerFunc(ctx) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "reviews": innerFunc := func(ctx context.Context) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Product_reviews(ctx, field, obj) - return res + return ec._Product_reviews(ctx, field, obj) } - out.Concurrently(i, func() graphql.Marshaler { - return innerFunc(ctx) + out.Values[i] = innerFunc(ctx) - }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -2341,6 +2654,26 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = innerFunc(ctx) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + case "host": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._User_host(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + + if out.Values[i] == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + case "email": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._User_email(ctx, field, obj) + } + + out.Values[i] = innerFunc(ctx) + if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } @@ -2824,6 +3157,16 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) marshalNEmailHost2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋreviewsᚋgraphᚋmodelᚐEmailHost(ctx context.Context, sel ast.SelectionSet, v *model.EmailHost) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._EmailHost(ctx, sel, v) +} + func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalID(v) return res, graphql.ErrorOnPath(ctx, err) @@ -2839,6 +3182,16 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } +func (ec *executionContext) marshalNManufacturer2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋreviewsᚋgraphᚋmodelᚐManufacturer(ctx context.Context, sel ast.SelectionSet, v *model.Manufacturer) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._Manufacturer(ctx, sel, v) +} + func (ec *executionContext) marshalNProduct2githubᚗcomᚋ99designsᚋgqlgenᚋexampleᚋfederationᚋreviewsᚋgraphᚋmodelᚐProduct(ctx context.Context, sel ast.SelectionSet, v model.Product) graphql.Marshaler { return ec._Product(ctx, sel, &v) } diff --git a/example/federation/reviews/graph/model/models.go b/example/federation/reviews/graph/model/models.go index 51394035234..af4eb24755a 100644 --- a/example/federation/reviews/graph/model/models.go +++ b/example/federation/reviews/graph/model/models.go @@ -1,7 +1,9 @@ package model type Product struct { - Upc string `json:"upc"` + ID string `json:"id"` + Manufacturer *Manufacturer `json:"manufacturer"` + Reviews []*Review `json:"reviews"` } func (Product) IsEntity() {} @@ -13,7 +15,10 @@ type Review struct { } type User struct { - ID string `json:"id"` + ID string `json:"id"` + Host *EmailHost `json:"host"` + Email string `json:"email"` + // Reviews []*Review `json:"reviews"` } func (User) IsEntity() {} diff --git a/example/federation/reviews/graph/model/models_gen.go b/example/federation/reviews/graph/model/models_gen.go index 8e0d251dce1..5cd133b3330 100644 --- a/example/federation/reviews/graph/model/models_gen.go +++ b/example/federation/reviews/graph/model/models_gen.go @@ -1,3 +1,15 @@ // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package model + +type EmailHost struct { + ID string `json:"id"` +} + +func (EmailHost) IsEntity() {} + +type Manufacturer struct { + ID string `json:"id"` +} + +func (Manufacturer) IsEntity() {} diff --git a/example/federation/reviews/graph/reviews.go b/example/federation/reviews/graph/reviews.go index c30a84fe319..922ed1446f2 100644 --- a/example/federation/reviews/graph/reviews.go +++ b/example/federation/reviews/graph/reviews.go @@ -5,17 +5,17 @@ import "github.com/99designs/gqlgen/example/federation/reviews/graph/model" var reviews = []*model.Review{ { Body: "A highly effective form of birth control.", - Product: &model.Product{Upc: "top-1"}, + Product: &model.Product{ID: "111", Manufacturer: &model.Manufacturer{ID: "1234"}}, Author: &model.User{ID: "1234"}, }, { Body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.", - Product: &model.Product{Upc: "top-1"}, + Product: &model.Product{ID: "222", Manufacturer: &model.Manufacturer{ID: "2345"}}, Author: &model.User{ID: "1234"}, }, { Body: "This is the last straw. Hat you will wear. 11/10", - Product: &model.Product{Upc: "top-1"}, + Product: &model.Product{ID: "333", Manufacturer: &model.Manufacturer{ID: "2345"}}, Author: &model.User{ID: "7777"}, }, } diff --git a/example/federation/reviews/graph/schema.graphqls b/example/federation/reviews/graph/schema.graphqls index 72cfcf3a3c8..fde407128ed 100644 --- a/example/federation/reviews/graph/schema.graphqls +++ b/example/federation/reviews/graph/schema.graphqls @@ -4,12 +4,23 @@ type Review { product: Product! } +extend type EmailHost @key(fields: "id") { + id: String! @external +} + extend type User @key(fields: "id") { id: ID! @external - reviews: [Review] + host: EmailHost! @external + email: String! @external + reviews: [Review] @requires(fields: "host {id} email") +} + +extend type Manufacturer @key(fields: "id") { + id: String! @external } -extend type Product @key(fields: "upc") { - upc: String! @external +extend type Product @key(fields: " manufacturer{ id} id") { + id: String! @external + manufacturer: Manufacturer! @external reviews: [Review] } diff --git a/example/federation/reviews/graph/schema.resolvers.go b/example/federation/reviews/graph/schema.resolvers.go index 572a6be6e8d..5876306ecb5 100644 --- a/example/federation/reviews/graph/schema.resolvers.go +++ b/example/federation/reviews/graph/schema.resolvers.go @@ -10,35 +10,17 @@ import ( "github.com/99designs/gqlgen/example/federation/reviews/graph/model" ) -func (r *productResolver) Reviews(ctx context.Context, obj *model.Product) ([]*model.Review, error) { - var res []*model.Review - - for _, review := range reviews { - if review.Product.Upc == obj.Upc { - res = append(res, review) - } - } - - return res, nil -} - func (r *userResolver) Reviews(ctx context.Context, obj *model.User) ([]*model.Review, error) { - var res []*model.Review - + var productReviews []*model.Review for _, review := range reviews { if review.Author.ID == obj.ID { - res = append(res, review) + productReviews = append(productReviews, review) } } - - return res, nil + return productReviews, nil } -// Product returns generated.ProductResolver implementation. -func (r *Resolver) Product() generated.ProductResolver { return &productResolver{r} } - // User returns generated.UserResolver implementation. func (r *Resolver) User() generated.UserResolver { return &userResolver{r} } -type productResolver struct{ *Resolver } type userResolver struct{ *Resolver } diff --git a/plugin/federation/federation.go b/plugin/federation/federation.go index f472325c363..dd1b8ca38ed 100644 --- a/plugin/federation/federation.go +++ b/plugin/federation/federation.go @@ -3,7 +3,6 @@ package federation import ( "fmt" "sort" - "strings" "github.com/vektah/gqlparser/v2/ast" @@ -11,6 +10,7 @@ import ( "github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/templates" "github.com/99designs/gqlgen/plugin" + "github.com/99designs/gqlgen/plugin/federation/fieldset" ) type federation struct { @@ -97,12 +97,11 @@ func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source { if e.ResolverName != "" { resolverArgs := "" - for _, field := range e.KeyFields { - resolverArgs += fmt.Sprintf("%s: %s,", field.Field.Name, field.Field.Type.String()) + 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.Def.Name) } - } if len(f.Entities) == 0 { @@ -152,22 +151,16 @@ type Entity struct { } type KeyField struct { - Field *ast.FieldDefinition - TypeReference *config.TypeReference // The Go representation of that field type + Definition *ast.FieldDefinition + Field fieldset.Field // len > 1 for nested fields + Type *config.TypeReference // The Go representation of that field type } // Requires represents an @requires clause type Requires struct { - Name string // the name of the field - Fields []*RequireField // the name of the sibling fields -} - -// RequireField is similar to an entity but it is a field not -// an object -type RequireField struct { - Name string // The same name as the type declaration - NameGo string // The Go struct field name - TypeReference *config.TypeReference // The Go representation of that field type + Name string // the name of the field + Field fieldset.Field // source Field, len > 1 for nested fields + Type *config.TypeReference // The Go representation of that field type } func (e *Entity) allFieldsAreExternal() bool { @@ -186,22 +179,27 @@ func (f *federation) GenerateCode(data *codegen.Data) error { } for _, e := range f.Entities { obj := data.Objects.ByName(e.Def.Name) - for _, field := range obj.Fields { - // Storing key fields in a slice rather than a map - // to preserve insertion order at the tradeoff of higher - // lookup complexity. - keyField := f.getKeyField(e.KeyFields, field.Name) - if keyField != nil { - keyField.TypeReference = field.TypeReference + + // fill in types for key fields + // + for _, keyField := range e.KeyFields { + if len(keyField.Field) == 0 { + fmt.Println("skipping key field " + keyField.Definition.Name + " in " + e.Def.Name) + continue } - for _, r := range e.Requires { - for _, rf := range r.Fields { - if rf.Name == field.Name { - rf.TypeReference = field.TypeReference - rf.NameGo = field.GoFieldName - } - } + cgField := keyField.Field.TypeReference(obj, data.Objects) + keyField.Type = cgField.TypeReference + } + + // fill in types for requires fields + // + for _, reqField := range e.Requires { + if len(reqField.Field) == 0 { + fmt.Println("skipping requires field " + reqField.Name + " in " + e.Def.Name) + continue } + cgField := reqField.Field.TypeReference(obj, data.Objects) + reqField.Type = cgField.TypeReference } } } @@ -215,15 +213,6 @@ func (f *federation) GenerateCode(data *codegen.Data) error { }) } -func (f *federation) getKeyField(keyFields []*KeyField, fieldName string) *KeyField { - for _, field := range keyFields { - if field.Field.Name == fieldName { - return field - } - } - return nil -} - func (f *federation) setEntities(schema *ast.Schema) { for _, schemaType := range schema.Types { if schemaType.Kind == ast.Interface { @@ -237,52 +226,50 @@ func (f *federation) setEntities(schema *ast.Schema) { continue } if schemaType.Kind == ast.Object { - dir := schemaType.Directives.ForName("key") - if dir != nil { - if len(dir.Arguments) > 1 { - panic("Multiple arguments are not currently supported in @key declaration.") - } - fieldName := dir.Arguments[0].Value.Raw // TODO: multiple arguments - if strings.Contains(fieldName, "{") { - panic("Nested fields are not currently supported in @key declaration.") + keys := schemaType.Directives.ForNames("key") + if len(keys) > 1 { + // TODO: support multiple keys -- multiple resolvers per Entity + panic("only one @key directive currently supported") + } + + if len(keys) > 0 { + dir := keys[0] + if len(dir.Arguments) != 1 || dir.Arguments[0].Name != "fields" { + panic("Exactly one `fields` argument needed for @key declaration.") } + arg := dir.Arguments[0] + keyFieldSet := fieldset.New(arg.Value.Raw, nil) + // TODO: why is this nested inside the @key handling? -- because it's per-Entity, and we make one per @key requires := []*Requires{} for _, f := range schemaType.Fields { dir := f.Directives.ForName("requires") if dir == nil { continue } - args := dir.Arguments[0].Value.Raw - if strings.Contains(args, "{") { - // TODO: see. https://github.com/99designs/gqlgen/issues/1138 - panic("Nested fields are not currently supported in @requires declaration.") - } - fields := strings.Split(args, " ") - requireFields := []*RequireField{} - for _, f := range fields { - requireFields = append(requireFields, &RequireField{ - Name: f, + requiresFieldSet := fieldset.New(dir.Arguments[0].Value.Raw, nil) + for _, field := range requiresFieldSet { + requires = append(requires, &Requires{ + Name: field.ToGoPrivate(), + Field: field, }) } - requires = append(requires, &Requires{ - Name: f.Name, - Fields: requireFields, - }) } - fieldNames := strings.Split(fieldName, " ") - keyFields := make([]*KeyField, len(fieldNames)) + keyFields := make([]*KeyField, len(keyFieldSet)) resolverName := fmt.Sprintf("find%sBy", schemaType.Name) - for i, f := range fieldNames { - field := schemaType.Fields.ForName(f) + for i, field := range keyFieldSet { + def := field.FieldDefinition(schemaType, schema) + + if def == nil { + panic(fmt.Sprintf("no field for %v", field)) + } - keyFields[i] = &KeyField{Field: field} + keyFields[i] = &KeyField{Definition: def, Field: field} if i > 0 { resolverName += "And" } - resolverName += templates.ToGo(f) - + resolverName += field.ToGo() } e := &Entity{ diff --git a/plugin/federation/federation.gotpl b/plugin/federation/federation.gotpl index 33427987f01..7dbb00535d7 100644 --- a/plugin/federation/federation.gotpl +++ b/plugin/federation/federation.gotpl @@ -46,25 +46,23 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati {{ if .ResolverName }} case "{{.Def.Name}}": {{ range $i, $keyField := .KeyFields -}} - id{{$i}}, err := ec.{{.TypeReference.UnmarshalFunc}}(ctx, rep["{{$keyField.Field.Name}}"]) + 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.", "{{$keyField.Field.Name}}")) + return errors.New(fmt.Sprintf("Field %s undefined in schema.", "{{.Definition.Name}}")) } {{end}} - + entity, err := ec.resolvers.Entity().{{.ResolverName | go}}(ctx, {{ range $i, $_ := .KeyFields -}} id{{$i}}, {{end}}) if err != nil { return err } - + {{ range .Requires }} - {{ range .Fields}} - entity.{{.NameGo}}, err = ec.{{.TypeReference.UnmarshalFunc}}(ctx, rep["{{.Name}}"]) - if err != nil { - return err - } - {{ end }} + entity.{{.Field.JoinGo `.`}}, err = ec.{{.Type.UnmarshalFunc}}(ctx, rep["{{.Name}}"]) + if err != nil { + return err + } {{ end }} list[i] = entity return nil diff --git a/plugin/federation/federation_test.go b/plugin/federation/federation_test.go index f6a164e89ed..20578cf7a7a 100644 --- a/plugin/federation/federation_test.go +++ b/plugin/federation/federation_test.go @@ -10,13 +10,63 @@ import ( func TestWithEntities(t *testing.T) { f, cfg := load(t, "test_data/gqlgen.yml") - require.Equal(t, []string{"ExternalExtension", "Hello", "World"}, cfg.Schema.Types["_Entity"].Types) + require.Equal(t, []string{"ExternalExtension", "Hello", "MoreNesting", "NestedKey", "VeryNestedKey", "World"}, cfg.Schema.Types["_Entity"].Types) + + require.Len(t, cfg.Schema.Types["Entity"].Fields, 5) 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, "findNestedKeyByIDAndHelloName", cfg.Schema.Types["Entity"].Fields[2].Name) + require.Equal(t, "findVeryNestedKeyByIDAndHelloNameAndWorldFooAndWorldBarAndMoreWorldFoo", cfg.Schema.Types["Entity"].Fields[3].Name) + require.Equal(t, "findWorldByFooAndBar", cfg.Schema.Types["Entity"].Fields[4].Name) require.NoError(t, f.MutateConfig(cfg)) + + require.Equal(t, "ExternalExtension", f.Entities[0].Name) + require.Len(t, f.Entities[0].KeyFields, 1) + require.Equal(t, "upc", f.Entities[0].KeyFields[0].Definition.Name) + require.Equal(t, "String", f.Entities[0].KeyFields[0].Definition.Type.Name()) + + require.Equal(t, "Hello", f.Entities[1].Name) + require.Len(t, f.Entities[1].KeyFields, 1) + require.Equal(t, "name", f.Entities[1].KeyFields[0].Definition.Name) + require.Equal(t, "String", f.Entities[1].KeyFields[0].Definition.Type.Name()) + + require.Equal(t, "MoreNesting", f.Entities[2].Name) + require.Len(t, f.Entities[2].KeyFields, 1) + require.Equal(t, "id", f.Entities[2].KeyFields[0].Definition.Name) + require.Equal(t, "String", f.Entities[2].KeyFields[0].Definition.Type.Name()) + + require.Equal(t, "NestedKey", f.Entities[3].Name) + require.Len(t, f.Entities[3].KeyFields, 2) + require.Equal(t, "id", f.Entities[3].KeyFields[0].Definition.Name) + require.Equal(t, "String", f.Entities[3].KeyFields[0].Definition.Type.Name()) + require.Equal(t, "helloName", f.Entities[3].KeyFields[1].Definition.Name) + require.Equal(t, "String", f.Entities[3].KeyFields[1].Definition.Type.Name()) + + require.Equal(t, "VeryNestedKey", f.Entities[4].Name) + require.Len(t, f.Entities[4].KeyFields, 5) + require.Equal(t, "id", f.Entities[4].KeyFields[0].Definition.Name) + require.Equal(t, "String", f.Entities[4].KeyFields[0].Definition.Type.Name()) + require.Equal(t, "helloName", f.Entities[4].KeyFields[1].Definition.Name) + require.Equal(t, "String", f.Entities[4].KeyFields[1].Definition.Type.Name()) + require.Equal(t, "worldFoo", f.Entities[4].KeyFields[2].Definition.Name) + require.Equal(t, "String", f.Entities[4].KeyFields[2].Definition.Type.Name()) + require.Equal(t, "worldBar", f.Entities[4].KeyFields[3].Definition.Name) + require.Equal(t, "Int", f.Entities[4].KeyFields[3].Definition.Type.Name()) + require.Equal(t, "moreWorldFoo", f.Entities[4].KeyFields[4].Definition.Name) + require.Equal(t, "String", f.Entities[4].KeyFields[4].Definition.Type.Name()) + + require.Len(t, f.Entities[4].Requires, 2) + require.Equal(t, f.Entities[4].Requires[0].Name, "id") + require.Equal(t, f.Entities[4].Requires[1].Name, "helloSecondary") + + require.Equal(t, "World", f.Entities[5].Name) + require.Len(t, f.Entities[5].KeyFields, 2) + require.Equal(t, "foo", f.Entities[5].KeyFields[0].Definition.Name) + require.Equal(t, "String", f.Entities[5].KeyFields[0].Definition.Type.Name()) + require.Equal(t, "bar", f.Entities[5].KeyFields[1].Definition.Name) + require.Equal(t, "Int", f.Entities[5].KeyFields[1].Definition.Type.Name()) } func TestNoEntities(t *testing.T) { diff --git a/plugin/federation/fieldset/fieldset.go b/plugin/federation/fieldset/fieldset.go new file mode 100644 index 00000000000..a7157ed673e --- /dev/null +++ b/plugin/federation/fieldset/fieldset.go @@ -0,0 +1,189 @@ +package fieldset + +import ( + "fmt" + "strings" + + "github.com/99designs/gqlgen/codegen" + "github.com/99designs/gqlgen/codegen/templates" + "github.com/vektah/gqlparser/v2/ast" +) + +// Set represents a FieldSet that is used in federation directives @key and @requires. +// Would be happier to reuse FieldSet parsing from gqlparser, but this suits for now. +// +type Set []Field + +// Field represents a single field in a FieldSet +// +type Field []string + +// New parses a FieldSet string into a TinyFieldSet. +// +func New(raw string, prefix []string) Set { + if !strings.Contains(raw, "{") { + return parseUnnestedKeyFieldSet(raw, prefix) + } + + var ( + ret = Set{} + subPrefix = prefix + ) + before, during, after := extractSubs(raw) + + if before != "" { + befores := New(before, prefix) + if len(befores) > 0 { + subPrefix = befores[len(befores)-1] + ret = append(ret, befores[:len(befores)-1]...) + } + } + if during != "" { + ret = append(ret, New(during, subPrefix)...) + } + if after != "" { + ret = append(ret, New(after, prefix)...) + } + return ret +} + +// FieldDefinition looks up a field in the type. +// +func (f Field) FieldDefinition(schemaType *ast.Definition, schema *ast.Schema) *ast.FieldDefinition { + objType := schemaType + def := objType.Fields.ForName(f[0]) + + for _, part := range f[1:] { + if objType.Kind != ast.Object { + panic(fmt.Sprintf(`invalid sub-field reference "%s" in %v: `, objType.Name, f)) + } + x := def.Type.Name() + objType = schema.Types[x] + if objType == nil { + panic("invalid schema type: " + x) + } + def = objType.Fields.ForName(part) + } + if def == nil { + return nil + } + ret := *def // shallow copy + ret.Name = f.ToGoPrivate() + + return &ret +} + +// TypeReference looks up the type of a field. +// +func (f Field) TypeReference(obj *codegen.Object, objects codegen.Objects) *codegen.Field { + var def *codegen.Field + + for _, part := range f { + def = fieldByName(obj, part) + if def == nil { + panic("unable to find field " + f[0]) + } + obj = objects.ByName(def.TypeReference.Definition.Name) + } + return def +} + +// ToGo converts a (possibly nested) field into a proper public Go name. +// +func (f Field) ToGo() string { + var ret string + + for _, field := range f { + ret += templates.ToGo(field) + } + return ret +} + +// ToGoPrivate converts a (possibly nested) field into a proper private Go name. +// +func (f Field) ToGoPrivate() string { + var ret string + + for i, field := range f { + if i == 0 { + ret += templates.ToGoPrivate(field) + continue + } + ret += templates.ToGo(field) + } + return ret +} + +// Join concatenates the field parts with a string separator between. Useful in templates. +// +func (f Field) Join(str string) string { + return strings.Join(f, str) +} + +// JoinGo concatenates the Go name of field parts with a string separator between. Useful in templates. +// +func (f Field) JoinGo(str string) string { + strs := []string{} + + for _, s := range f { + strs = append(strs, templates.ToGo(s)) + } + return strings.Join(strs, str) +} + +// local functions + +// parseUnnestedKeyFieldSet // handles simple case where none of the fields are nested. +// +func parseUnnestedKeyFieldSet(raw string, prefix []string) Set { + ret := Set{} + + for _, s := range strings.Fields(raw) { + next := append(prefix[:], s) //nolint:gocritic // slicing out on purpose + ret = append(ret, next) + } + return ret +} + +// extractSubs splits out and trims sub-expressions from before, inside, and after "{}". +// +func extractSubs(str string) (string, string, string) { + start := strings.Index(str, "{") + end := matchingBracketIndex(str, start) + + if start < 0 || end < 0 { + panic("invalid key fieldSet: " + str) + } + return strings.TrimSpace(str[:start]), strings.TrimSpace(str[start+1 : end]), strings.TrimSpace(str[end+1:]) +} + +// matchingBracketIndex returns the index of the closing bracket, assuming an open bracket at start. +// +func matchingBracketIndex(str string, start int) int { + if start < 0 || len(str) <= start+1 { + return -1 + } + var depth int + + for i, c := range str[start+1:] { + switch c { + case '{': + depth++ + case '}': + if depth == 0 { + return start + 1 + i + } + depth-- + } + } + return -1 +} + +func fieldByName(obj *codegen.Object, name string) *codegen.Field { + for _, field := range obj.Fields { + if field.Name == name { + return field + } + } + return nil +} diff --git a/plugin/federation/fieldset/fieldset_test.go b/plugin/federation/fieldset/fieldset_test.go new file mode 100644 index 00000000000..dd4b9046e62 --- /dev/null +++ b/plugin/federation/fieldset/fieldset_test.go @@ -0,0 +1,77 @@ +package fieldset + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUnnestedWithoutPrefix(t *testing.T) { + fieldSet := New("foo bar", nil) + + require.Len(t, fieldSet, 2) + + require.Len(t, fieldSet[0], 1) + require.Equal(t, "foo", fieldSet[0][0]) + + require.Len(t, fieldSet[1], 1) + require.Equal(t, "bar", fieldSet[1][0]) +} + +func TestNestedWithoutPrefix(t *testing.T) { + fieldSet := New("foo bar { baz} a b {c{d}}e", nil) + + require.Len(t, fieldSet, 5) + + require.Len(t, fieldSet[0], 1) + require.Equal(t, "foo", fieldSet[0][0]) + + require.Len(t, fieldSet[1], 2) + require.Equal(t, "bar", fieldSet[1][0]) + require.Equal(t, "baz", fieldSet[1][1]) + + require.Len(t, fieldSet[2], 1) + require.Equal(t, "a", fieldSet[2][0]) + + require.Len(t, fieldSet[3], 3) + require.Equal(t, "b", fieldSet[3][0]) + require.Equal(t, "c", fieldSet[3][1]) + require.Equal(t, "d", fieldSet[3][2]) + + require.Len(t, fieldSet[4], 1) + require.Equal(t, "e", fieldSet[4][0]) +} + +func TestWithPrefix(t *testing.T) { + fieldSet := New("foo bar{id}", []string{"prefix"}) + + require.Len(t, fieldSet, 2) + + require.Len(t, fieldSet[0], 2) + require.Equal(t, "prefix", fieldSet[0][0]) + require.Equal(t, "foo", fieldSet[0][1]) + + require.Len(t, fieldSet[1], 3) + require.Equal(t, "prefix", fieldSet[1][0]) + require.Equal(t, "bar", fieldSet[1][1]) + require.Equal(t, "id", fieldSet[1][2]) +} + +func TestInvalid(t *testing.T) { + + require.Panics(t, func() { + New("foo bar{baz", nil) + }) +} + +func TestToGo(t *testing.T) { + require.Equal(t, Field{"foo"}.ToGo(), "Foo") + require.Equal(t, Field{"foo", "bar"}.ToGo(), "FooBar") + require.Equal(t, Field{"bar", "id"}.ToGo(), "BarID") +} + +func TestToGoPrivate(t *testing.T) { + require.Equal(t, Field{"foo"}.ToGoPrivate(), "foo") + require.Equal(t, Field{"foo", "bar"}.ToGoPrivate(), "fooBar") + require.Equal(t, Field{"bar", "id"}.ToGoPrivate(), "barID") +} diff --git a/plugin/federation/test_data/schema.graphql b/plugin/federation/test_data/schema.graphql index 79c567a820f..cf5a72004e5 100644 --- a/plugin/federation/test_data/schema.graphql +++ b/plugin/federation/test_data/schema.graphql @@ -1,17 +1,39 @@ type Hello @key(fields: "name") { name: String! + secondary: String! } -type World @key(fields: "foo bar") { +type World @key(fields: " foo bar ") { foo: String! bar: Int! } -extend type ExternalExtension @key(fields: "upc") { +extend type ExternalExtension @key(fields: " upc ") { upc: String! @external reviews: [World] } +extend type NestedKey @key(fields: "id hello { name}") { + id: String! @external + hello: Hello +} + +extend type MoreNesting @key(fields: "id") { + id: String! @external + world: World! @external +} + +extend type VeryNestedKey + @key( + fields: "id hello { name} world {foo } world{bar} more { world { foo }}" + ) { + id: String! @external + hello: Hello + world: World + nested: NestedKey @requires(fields: "id hello {secondary }") + more: MoreNesting +} + type Query { hello: Hello! world: World!