diff --git a/execution/engine/federation_integration_test.go b/execution/engine/federation_integration_test.go index 9b749cda2b..e0a937ed76 100644 --- a/execution/engine/federation_integration_test.go +++ b/execution/engine/federation_integration_test.go @@ -507,6 +507,19 @@ func TestFederationIntegrationTest(t *testing.T) { expected := `{"data":{"cat":{"name":"Pepper"},"me":{"id":"1234","username":"Me","realName":"User Usington","reviews":[{"body":"A highly effective form of birth control."},{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits."}],"history":[{},{"rating":5},{}]}}}` assert.Equal(t, compact(expected), string(resp)) }) + + // Regression test for https://github.com/wundergraph/cosmo/issues/2346 + // Deeply nested fields go missing when a named fragment on an interface + // contains inline fragments for multiple concrete types and the non-matching + // type's fragment is defined first. + t.Run("merge concrete type in root field and interface fragment", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + resp := gqlClient.Query(ctx, setup.GatewayServer.URL, testQueryPath("queries/merge_concrete_type_in_root_field_and_interface_fragment.graphql"), nil, t) + expected := `{"data":{"productA":{"category":{"id":"c111","displayOwner":{"name":"owner"}}}}}` + assert.Equal(t, compact(expected), string(resp)) + }) } func compact(input string) string { diff --git a/execution/federationtesting/accounts/graph/generated/generated.go b/execution/federationtesting/accounts/graph/generated/generated.go index 22fd02edc8..bfe9ab7496 100644 --- a/execution/federationtesting/accounts/graph/generated/generated.go +++ b/execution/federationtesting/accounts/graph/generated/generated.go @@ -69,6 +69,11 @@ type ComplexityRoot struct { Name func(childComplexity int) int } + Category struct { + ID func(childComplexity int) int + Owner func(childComplexity int) int + } + ConcreteListItem1 struct { Obj func(childComplexity int) int } @@ -85,10 +90,24 @@ type ComplexityRoot struct { FindUserByID func(childComplexity int, id string) int } + Owner struct { + Name func(childComplexity int) int + } + Product struct { Upc func(childComplexity int) int } + ProductA struct { + Category func(childComplexity int) int + ID func(childComplexity int) int + } + + ProductB struct { + Category func(childComplexity int) int + ID func(childComplexity int) int + } + Purchase struct { Product func(childComplexity int) int Quantity func(childComplexity int) int @@ -104,6 +123,7 @@ type ComplexityRoot struct { InterfaceUnion func(childComplexity int, which model.Which) int Me func(childComplexity int) int OtherInterfaces func(childComplexity int) int + ProductA func(childComplexity int) int SomeNestedInterfaces func(childComplexity int) int TitleName func(childComplexity int) int __resolve__service func(childComplexity int) int @@ -194,6 +214,7 @@ type QueryResolver interface { Cds(ctx context.Context) ([]model.Cd, error) OtherInterfaces(ctx context.Context) ([]model.SomeInterface, error) SomeNestedInterfaces(ctx context.Context) ([]model.SomeNestedInterface, error) + ProductA(ctx context.Context) (*model.ProductA, error) } type executableSchema struct { @@ -264,6 +285,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Cat.Name(childComplexity), true + case "Category.id": + if e.complexity.Category.ID == nil { + break + } + + return e.complexity.Category.ID(childComplexity), true + + case "Category.owner": + if e.complexity.Category.Owner == nil { + break + } + + return e.complexity.Category.Owner(childComplexity), true + case "ConcreteListItem1.obj": if e.complexity.ConcreteListItem1.Obj == nil { break @@ -297,6 +332,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Entity.FindUserByID(childComplexity, args["id"].(string)), true + case "Owner.name": + if e.complexity.Owner.Name == nil { + break + } + + return e.complexity.Owner.Name(childComplexity), true + case "Product.upc": if e.complexity.Product.Upc == nil { break @@ -304,6 +346,34 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Product.Upc(childComplexity), true + case "ProductA.category": + if e.complexity.ProductA.Category == nil { + break + } + + return e.complexity.ProductA.Category(childComplexity), true + + case "ProductA.id": + if e.complexity.ProductA.ID == nil { + break + } + + return e.complexity.ProductA.ID(childComplexity), true + + case "ProductB.category": + if e.complexity.ProductB.Category == nil { + break + } + + return e.complexity.ProductB.Category(childComplexity), true + + case "ProductB.id": + if e.complexity.ProductB.ID == nil { + break + } + + return e.complexity.ProductB.ID(childComplexity), true + case "Purchase.product": if e.complexity.Purchase.Product == nil { break @@ -386,6 +456,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Query.OtherInterfaces(childComplexity), true + case "Query.productA": + if e.complexity.Query.ProductA == nil { + break + } + + return e.complexity.Query.ProductA(childComplexity), true + case "Query.someNestedInterfaces": if e.complexity.Query.SomeNestedInterfaces == nil { break @@ -752,6 +829,7 @@ var sources = []*ast.Source{ cds: [CD] otherInterfaces: [SomeInterface] someNestedInterfaces: [SomeNestedInterface] + productA: ProductA } type Cat { @@ -922,6 +1000,30 @@ type CDerObj { first: String! middle: String! last: String! +} + +interface Base { + id: ID! + category: Category +} + +type Category { + id: ID! + owner: Owner +} + +type Owner { + name: String! +} + +type ProductA implements Base { + id: ID! + category: Category +} + +type ProductB implements Base { + id: ID! + category: Category }`, BuiltIn: false}, {Name: "../../federation/directives.graphql", Input: ` directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE @@ -1502,6 +1604,95 @@ func (ec *executionContext) fieldContext_Cat_name(_ context.Context, field graph return fc, nil } +func (ec *executionContext) _Category_id(ctx context.Context, field graphql.CollectedField, obj *model.Category) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Category_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, 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.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Category_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Category", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Category_owner(ctx context.Context, field graphql.CollectedField, obj *model.Category) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Category_owner(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Owner, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.Owner) + fc.Result = res + return ec.marshalOOwner2ᚖgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋexecutionᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐOwner(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Category_owner(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Category", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_Owner_name(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Owner", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _ConcreteListItem1_obj(ctx context.Context, field graphql.CollectedField, obj *model.ConcreteListItem1) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ConcreteListItem1_obj(ctx, field) if err != nil { @@ -1677,35 +1868,258 @@ func (ec *executionContext) fieldContext_Entity_findUserByID(ctx context.Context IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_User_id(ctx, field) - case "username": - return ec.fieldContext_User_username(ctx, field) - case "history": - return ec.fieldContext_User_history(ctx, field) - case "realName": - return ec.fieldContext_User_realName(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type User", field.Name) + switch field.Name { + case "id": + return ec.fieldContext_User_id(ctx, field) + case "username": + return ec.fieldContext_User_username(ctx, field) + case "history": + return ec.fieldContext_User_history(ctx, field) + case "realName": + return ec.fieldContext_User_realName(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type User", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Entity_findUserByID_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Owner_name(ctx context.Context, field graphql.CollectedField, obj *model.Owner) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Owner_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, 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) fieldContext_Owner_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Owner", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Product_upc(ctx context.Context, field graphql.CollectedField, obj *model.Product) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Product_upc(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Upc, 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) fieldContext_Product_upc(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Product", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ProductA_id(ctx context.Context, field graphql.CollectedField, obj *model.ProductA) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ProductA_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, 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.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ProductA_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ProductA", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ProductA_category(ctx context.Context, field graphql.CollectedField, obj *model.ProductA) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ProductA_category(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Category, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.Category) + fc.Result = res + return ec.marshalOCategory2ᚖgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋexecutionᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐCategory(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ProductA_category(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ProductA", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Category_id(ctx, field) + case "owner": + return ec.fieldContext_Category_owner(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Category", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ProductB_id(ctx context.Context, field graphql.CollectedField, obj *model.ProductB) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ProductB_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, 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.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ProductB_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ProductB", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") }, } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Entity_findUserByID_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } return fc, nil } -func (ec *executionContext) _Product_upc(ctx context.Context, field graphql.CollectedField, obj *model.Product) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Product_upc(ctx, field) +func (ec *executionContext) _ProductB_category(ctx context.Context, field graphql.CollectedField, obj *model.ProductB) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ProductB_category(ctx, field) if err != nil { return graphql.Null } @@ -1718,31 +2132,34 @@ func (ec *executionContext) _Product_upc(ctx context.Context, field graphql.Coll }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Upc, nil + return obj.Category, 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) + res := resTmp.(*model.Category) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalOCategory2ᚖgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋexecutionᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐCategory(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Product_upc(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_ProductB_category(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Product", + Object: "ProductB", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + switch field.Name { + case "id": + return ec.fieldContext_Category_id(ctx, field) + case "owner": + return ec.fieldContext_Category_owner(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Category", field.Name) }, } return fc, nil @@ -2328,6 +2745,53 @@ func (ec *executionContext) fieldContext_Query_someNestedInterfaces(_ context.Co return fc, nil } +func (ec *executionContext) _Query_productA(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_productA(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().ProductA(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.ProductA) + fc.Result = res + return ec.marshalOProductA2ᚖgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋexecutionᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐProductA(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_productA(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ProductA_id(ctx, field) + case "category": + return ec.fieldContext_ProductA_category(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ProductA", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Query__entities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query__entities(ctx, field) if err != nil { @@ -6034,6 +6498,29 @@ func (ec *executionContext) _AbstractListItem(ctx context.Context, sel ast.Selec } } +func (ec *executionContext) _Base(ctx context.Context, sel ast.SelectionSet, obj model.Base) graphql.Marshaler { + switch obj := (obj).(type) { + case nil: + return graphql.Null + case model.ProductB: + return ec._ProductB(ctx, sel, &obj) + case *model.ProductB: + if obj == nil { + return graphql.Null + } + return ec._ProductB(ctx, sel, obj) + case model.ProductA: + return ec._ProductA(ctx, sel, &obj) + case *model.ProductA: + if obj == nil { + return graphql.Null + } + return ec._ProductA(ctx, sel, obj) + default: + panic(fmt.Errorf("unexpected type %T", obj)) + } +} + func (ec *executionContext) _CD(ctx context.Context, sel ast.SelectionSet, obj model.Cd) graphql.Marshaler { switch obj := (obj).(type) { case nil: @@ -6534,6 +7021,47 @@ func (ec *executionContext) _Cat(ctx context.Context, sel ast.SelectionSet, obj return out } +var categoryImplementors = []string{"Category"} + +func (ec *executionContext) _Category(ctx context.Context, sel ast.SelectionSet, obj *model.Category) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, categoryImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Category") + case "id": + out.Values[i] = ec._Category_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "owner": + out.Values[i] = ec._Category_owner(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var concreteListItem1Implementors = []string{"ConcreteListItem1", "AbstractListItem"} func (ec *executionContext) _ConcreteListItem1(ctx context.Context, sel ast.SelectionSet, obj *model.ConcreteListItem1) graphql.Marshaler { @@ -6712,6 +7240,45 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g return out } +var ownerImplementors = []string{"Owner"} + +func (ec *executionContext) _Owner(ctx context.Context, sel ast.SelectionSet, obj *model.Owner) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, ownerImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Owner") + case "name": + out.Values[i] = ec._Owner_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var productImplementors = []string{"Product", "_Entity"} func (ec *executionContext) _Product(ctx context.Context, sel ast.SelectionSet, obj *model.Product) graphql.Marshaler { @@ -6751,6 +7318,88 @@ func (ec *executionContext) _Product(ctx context.Context, sel ast.SelectionSet, return out } +var productAImplementors = []string{"ProductA", "Base"} + +func (ec *executionContext) _ProductA(ctx context.Context, sel ast.SelectionSet, obj *model.ProductA) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, productAImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ProductA") + case "id": + out.Values[i] = ec._ProductA_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "category": + out.Values[i] = ec._ProductA_category(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var productBImplementors = []string{"ProductB", "Base"} + +func (ec *executionContext) _ProductB(ctx context.Context, sel ast.SelectionSet, obj *model.ProductB) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, productBImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ProductB") + case "id": + out.Values[i] = ec._ProductB_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "category": + out.Values[i] = ec._ProductB_category(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var purchaseImplementors = []string{"Purchase", "History", "Info"} func (ec *executionContext) _Purchase(ctx context.Context, sel ast.SelectionSet, obj *model.Purchase) graphql.Marshaler { @@ -7005,6 +7654,25 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "productA": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_productA(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "_entities": field := field @@ -8708,6 +9376,13 @@ func (ec *executionContext) marshalOCat2ᚖgithubᚗcomᚋwundergraphᚋgraphql return ec._Cat(ctx, sel, v) } +func (ec *executionContext) marshalOCategory2ᚖgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋexecutionᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐCategory(ctx context.Context, sel ast.SelectionSet, v *model.Category) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Category(ctx, sel, v) +} + func (ec *executionContext) marshalOHistory2githubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋexecutionᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐHistory(ctx context.Context, sel ast.SelectionSet, v model.History) graphql.Marshaler { if v == nil { return graphql.Null @@ -8763,6 +9438,20 @@ func (ec *executionContext) marshalOIdentifiable2githubᚗcomᚋwundergraphᚋgr return ec._Identifiable(ctx, sel, v) } +func (ec *executionContext) marshalOOwner2ᚖgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋexecutionᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐOwner(ctx context.Context, sel ast.SelectionSet, v *model.Owner) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Owner(ctx, sel, v) +} + +func (ec *executionContext) marshalOProductA2ᚖgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋexecutionᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐProductA(ctx context.Context, sel ast.SelectionSet, v *model.ProductA) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ProductA(ctx, sel, v) +} + func (ec *executionContext) marshalOSomeInterface2githubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋexecutionᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐSomeInterface(ctx context.Context, sel ast.SelectionSet, v model.SomeInterface) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/execution/federationtesting/accounts/graph/model/models_gen.go b/execution/federationtesting/accounts/graph/model/models_gen.go index 43e7569711..ddb888c887 100644 --- a/execution/federationtesting/accounts/graph/model/models_gen.go +++ b/execution/federationtesting/accounts/graph/model/models_gen.go @@ -18,6 +18,12 @@ type AbstractListItem interface { GetObj() OtherInterface } +type Base interface { + IsBase() + GetID() string + GetCategory() *Category +} + type Cd interface { IsCd() } @@ -120,6 +126,11 @@ type Cat struct { Name string `json:"name"` } +type Category struct { + ID string `json:"id"` + Owner *Owner `json:"owner,omitempty"` +} + type ConcreteListItem1 struct { Obj OtherInterface `json:"obj"` } @@ -143,12 +154,34 @@ func (D) IsCd() {} func (D) IsCDer() {} func (this D) GetName() *CDerObj { return this.Name } +type Owner struct { + Name string `json:"name"` +} + type Product struct { Upc string `json:"upc"` } func (Product) IsEntity() {} +type ProductA struct { + ID string `json:"id"` + Category *Category `json:"category,omitempty"` +} + +func (ProductA) IsBase() {} +func (this ProductA) GetID() string { return this.ID } +func (this ProductA) GetCategory() *Category { return this.Category } + +type ProductB struct { + ID string `json:"id"` + Category *Category `json:"category,omitempty"` +} + +func (ProductB) IsBase() {} +func (this ProductB) GetID() string { return this.ID } +func (this ProductB) GetCategory() *Category { return this.Category } + type Purchase struct { Product *Product `json:"product"` Wallet Wallet `json:"wallet,omitempty"` diff --git a/execution/federationtesting/accounts/graph/schema.graphqls b/execution/federationtesting/accounts/graph/schema.graphqls index 1f8806c71a..e52c144e79 100644 --- a/execution/federationtesting/accounts/graph/schema.graphqls +++ b/execution/federationtesting/accounts/graph/schema.graphqls @@ -11,6 +11,7 @@ type Query { cds: [CD] otherInterfaces: [SomeInterface] someNestedInterfaces: [SomeNestedInterface] + productA: ProductA } type Cat { @@ -181,4 +182,28 @@ type CDerObj { first: String! middle: String! last: String! +} + +interface Base { + id: ID! + category: Category +} + +type Category { + id: ID! + owner: Owner +} + +type Owner { + name: String! +} + +type ProductA implements Base { + id: ID! + category: Category +} + +type ProductB implements Base { + id: ID! + category: Category } \ No newline at end of file diff --git a/execution/federationtesting/accounts/graph/schema.resolvers.go b/execution/federationtesting/accounts/graph/schema.resolvers.go index 1b56e64752..93d93d0a94 100644 --- a/execution/federationtesting/accounts/graph/schema.resolvers.go +++ b/execution/federationtesting/accounts/graph/schema.resolvers.go @@ -211,6 +211,19 @@ func (r *queryResolver) SomeNestedInterfaces(ctx context.Context) ([]model.SomeN }, nil } +// ProductA is the resolver for the productA field. +func (r *queryResolver) ProductA(ctx context.Context) (*model.ProductA, error) { + return &model.ProductA{ + ID: "p111", + Category: &model.Category{ + ID: "c111", + Owner: &model.Owner{ + Name: "owner", + }, + }, + }, nil +} + // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } diff --git a/execution/federationtesting/testdata/queries/merge_concrete_type_in_root_field_and_interface_fragment.graphql b/execution/federationtesting/testdata/queries/merge_concrete_type_in_root_field_and_interface_fragment.graphql new file mode 100644 index 0000000000..68706a0da5 --- /dev/null +++ b/execution/federationtesting/testdata/queries/merge_concrete_type_in_root_field_and_interface_fragment.graphql @@ -0,0 +1,27 @@ +query MergeConcreteTypeInRootFieldAndInterfaceFragment { + productA { + category { + id + } + ... on Base { + ...ProductDetail + } + } +} + +fragment ProductDetail on Base { + ... on ProductB { + category { + displayOwner: owner { + name + } + } + } + ... on ProductA { + category { + displayOwner: owner { + name + } + } + } +} diff --git a/v2/pkg/engine/postprocess/merge_fields.go b/v2/pkg/engine/postprocess/merge_fields.go index e676127be5..fe7d9d994f 100644 --- a/v2/pkg/engine/postprocess/merge_fields.go +++ b/v2/pkg/engine/postprocess/merge_fields.go @@ -87,6 +87,7 @@ func (m *mergeFields) traverseNode(node resolve.Node) { for j := i + 1; j < len(n.Fields); j++ { if m.fieldsCanMerge(n.Fields[i], n.Fields[j]) { m.mergeValues(n.Fields[i], n.Fields[j]) + m.mergeParentOnTypeNames(n.Fields[i], n.Fields[j]) n.Fields = append(n.Fields[:j], n.Fields[j+1:]...) j-- } @@ -145,6 +146,14 @@ func (m *mergeFields) mergeScalars(left, right *resolve.Field) { return } left.OnTypeNames = m.deduplicateOnTypeNames(append(left.OnTypeNames, right.OnTypeNames...)) + m.mergeParentOnTypeNames(left, right) +} + +// mergeParentOnTypeNames merges the ParentOnTypeNames from right into left. +// At each depth layer, the type names are combined and deduplicated. +// This is important because resolvable.go ensures that at each depth layer, +// we have at least one matching type condition, otherwise we skip resolving the field. +func (m *mergeFields) mergeParentOnTypeNames(left, right *resolve.Field) { if left.ParentOnTypeNames == nil { left.ParentOnTypeNames = right.ParentOnTypeNames return @@ -156,15 +165,10 @@ WithNext: for i := range right.ParentOnTypeNames { for j := range left.ParentOnTypeNames { if right.ParentOnTypeNames[i].Depth == left.ParentOnTypeNames[j].Depth { - // merge all parent type conditions at the same depth - // this is important because resolvable.go ensures that at each depth layer, - // we have at least one matching type condition - // otherwise we skip resolving the field left.ParentOnTypeNames[j].Names = m.deduplicateOnTypeNames(append(left.ParentOnTypeNames[j].Names, right.ParentOnTypeNames[i].Names...)) continue WithNext } } - // if we reach this point, we have a new depth layer and just append it left.ParentOnTypeNames = append(left.ParentOnTypeNames, right.ParentOnTypeNames[i]) } } diff --git a/v2/pkg/engine/postprocess/merge_fields_test.go b/v2/pkg/engine/postprocess/merge_fields_test.go index 948f84c94c..5214fa4fb6 100644 --- a/v2/pkg/engine/postprocess/merge_fields_test.go +++ b/v2/pkg/engine/postprocess/merge_fields_test.go @@ -227,6 +227,186 @@ func TestMergeFields_Process(t *testing.T) { }, )) + // Regression test for https://github.com/wundergraph/cosmo/issues/2346 + // When merging sibling object fields from different concrete type fragments, + // ParentOnTypeNames must include all types, not just the first one processed. + t.Run("merge object fields preserves ParentOnTypeNames from both types", runTest( + &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`category`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`id`), + Value: &resolve.Scalar{}, + }, + }, + }, + }, + { + Name: []byte(`category`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`owner`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`name`), + Value: &resolve.String{}, + }, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte(`ProductB`)}, + }, + { + Name: []byte(`category`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`owner`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`name`), + Value: &resolve.String{}, + }, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte(`ProductA`)}, + }, + }, + }, + &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`category`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`id`), + Value: &resolve.Scalar{}, + }, + { + Name: []byte(`owner`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`name`), + Value: &resolve.String{}, + ParentOnTypeNames: []resolve.ParentOnTypeNames{ + {Depth: 2, Names: [][]byte{[]byte(`ProductB`), []byte(`ProductA`)}}, + }, + }, + }, + }, + ParentOnTypeNames: []resolve.ParentOnTypeNames{ + {Depth: 1, Names: [][]byte{[]byte(`ProductB`), []byte(`ProductA`)}}, + }, + }, + }, + }, + }, + }, + }, + )) + + // Same as above but with ProductA first — confirms order-independence. + t.Run("merge object fields preserves ParentOnTypeNames - reversed order", runTest( + &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`category`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`id`), + Value: &resolve.Scalar{}, + }, + }, + }, + }, + { + Name: []byte(`category`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`owner`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`name`), + Value: &resolve.String{}, + }, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte(`ProductA`)}, + }, + { + Name: []byte(`category`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`owner`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`name`), + Value: &resolve.String{}, + }, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte(`ProductB`)}, + }, + }, + }, + &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`category`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`id`), + Value: &resolve.Scalar{}, + }, + { + Name: []byte(`owner`), + Value: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte(`name`), + Value: &resolve.String{}, + ParentOnTypeNames: []resolve.ParentOnTypeNames{ + {Depth: 2, Names: [][]byte{[]byte(`ProductA`), []byte(`ProductB`)}}, + }, + }, + }, + }, + ParentOnTypeNames: []resolve.ParentOnTypeNames{ + {Depth: 1, Names: [][]byte{[]byte(`ProductA`), []byte(`ProductB`)}}, + }, + }, + }, + }, + }, + }, + }, + )) + t.Run("merge fields at the end of an object nested reverse", runTest( &resolve.Object{ Fields: []*resolve.Field{