Repository: jensneuse/graphql-go-tools (go)

Issue:
`@requires` fields not included in entity representations
graphq-go-tools doesn't not include `@requires` fields in representations when making entity queries.

Consider the following schema:
```
# service one

extend type Query {
    serviceOne: ServiceOneType
}

type ServiceOneType @key(fields: "id"){
    id: ID!
    serviceOneFieldOne: String!
    serviceOneFieldTwo: String!
}

# service two

extend type ServiceOneType @key(fields: "id") {
    id: ID! @external
    serviceOneFieldOne: String! @external
    serviceOneFieldTwo: String! @external
    serviceTwoFieldOne: String! @requires(fields: "serviceOneFieldOne")
    serviceTwoFieldTwo: String! @requires(fields: "serviceOneFieldTwo")
}
```
When performing the following query:
```
{
    serviceOne {
        serviceTwoFieldOne
        serviceTwoFieldTwo
    }
}
```
the following upstream queries are expected:
```
# service one
{"query": "{ serviceOne { id serviceOneFieldOne serviceOneFieldTwo } }"}

#service two
{
    "query": "query($representations: [_Any!]!) { _entities(representations: $representations) {... on ServiceOneType { serviceTwoFieldOne serviceTwoFieldTwo } } }",
    "variables": {
        "representations": [
            {"id": 1, "serviceOneFieldOne": "someValue", "serviceOneFieldTwo": "someOtherValue"}
        ]
    }
}
```
This isn't what graphql-go-tools sends. Instead, it sends representations that don't include the required fields, i.e. `{"representations": [{"id": 1}]}`.

See the attached file for a failing test case. [testcase.txt](https://github.com/jensneuse/graphql-go-tools/files/7786893/testcase.txt)

Also, while investigating this issue I noticed that the Planner.handleFederation function adds values for each primary key to representations.

This doesn't align with the [GraphQL federation specification](https://www.apollographql.com/docs/federation/entities/#referencing). The specification states that keys for types extended by a service "must match the value of exactly one @key defined in the entity's originating subgraph".

I mention this because the representation logic will need to be updated to fix the `@requires` issue, and the tests in graphql_datastore_test.go related to multiple federated keys should probably be updated to align with the spec as well, e.g. https://github.com/jensneuse/graphql-go-tools/blob/master/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go#L2744 should not include "name" (and it isn't valid for the product service schema to specify "name" as an additional key).

Pull request description:
Include `@requires` fields when constructing entity representations
This PR fixes #314.

graphql-go-tools currently doesn't include `@requires` fields in entity
representations. This PR updates the GraphQL datasource planner to
include `@requires` fields when constructing representations for an
upstream operation.

The representations logic has been updated to use RequiresFields from
plan.FieldConfiguration. This field already includes the necessary
federation keys and required fields.

Due to the order the Planner visitor calls LeaveDocument, the
representations are updated each time a field is visited instead of
being set once during LeaveDocument. This is because ConfigureFetch is
called before LeaveDocument. It would be possible to move the code to
LeaveDocument if the visit order was reversed for the Leave* methods,
but that is a large change so it wasn't made in this PR.

Note that the use of RequiresFields also fixes a bug where
graphql-go-tools didn't align with the federation spec with respect to
federation keys on extended types. The existing representations code
includes all the keys defined on a type, but the spec states that
exactly one key must match in a subgraph[1]. The RequiresFields logic
aligns with this requirement. The GraphQL datasource tests have been
updated accordingly.

[1] https://www.apollographql.com/docs/federation/entities/#referencing

Patch:
diff --git a/pkg/engine/datasource/graphql_datasource/graphql_datasource.go b/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
index 889e4e0c..c085a723 100644
--- a/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
+++ b/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"strings"
 
 	"github.com/jensneuse/graphql-go-tools/pkg/asttransform"
 	"github.com/tidwall/sjson"
@@ -30,6 +29,7 @@ type Planner struct {
 	config                             Configuration
 	upstreamOperation                  *ast.Document
 	upstreamVariables                  []byte
+	representationsJson                []byte
 	nodes                              []ast.Node
 	variables                          resolve.Variables
 	lastFieldEnclosingTypeName         string
@@ -86,7 +86,6 @@ func (p *Planner) addDirectiveToNode(directiveRef int, node ast.Node) {
 }
 
 func (p *Planner) DownstreamResponseFieldAlias(downstreamFieldRef int) (alias string, exists bool) {
-
 	// If there's no alias but the downstream Query re-uses the same path on different root fields,
 	// we rewrite the downstream Query using an alias so that we can have an aliased Query to the upstream
 	// while keeping a non aliased Query to the downstream but with a path rewrite on an existing root field.
@@ -177,7 +176,6 @@ func (p *Planner) Register(visitor *plan.Visitor, configuration plan.DataSourceC
 }
 
 func (p *Planner) ConfigureFetch() plan.FetchConfiguration {
-
 	var input []byte
 	input = httpclient.SetInputBodyWithPath(input, p.upstreamVariables, "variables")
 	input = httpclient.SetInputBodyWithPath(input, p.printOperation(), "query")
@@ -215,7 +213,6 @@ func (p *Planner) ConfigureFetch() plan.FetchConfiguration {
 }
 
 func (p *Planner) ConfigureSubscription() plan.SubscriptionConfiguration {
-
 	input := httpclient.SetInputBodyWithPath(nil, p.upstreamVariables, "variables")
 	input = httpclient.SetInputBodyWithPath(input, p.printOperation(), "query")
 	input = httpclient.SetInputURL(input, []byte(p.config.Subscription.URL))
@@ -285,7 +282,6 @@ func (p *Planner) LeaveSelectionSet(ref int) {
 }
 
 func (p *Planner) EnterInlineFragment(ref int) {
-
 	typeCondition := p.visitor.Operation.InlineFragmentTypeConditionName(ref)
 	if typeCondition == nil {
 		return
@@ -324,7 +320,6 @@ func (p *Planner) LeaveInlineFragment(ref int) {
 }
 
 func (p *Planner) EnterField(ref int) {
-
 	fieldName := p.visitor.Operation.FieldNameString(ref)
 
 	// store root field name and ref
@@ -339,16 +334,22 @@ func (p *Planner) EnterField(ref int) {
 
 	p.lastFieldEnclosingTypeName = p.visitor.Walker.EnclosingTypeDefinition.NameString(p.visitor.Definition)
 
-	p.handleFederation()
-	p.addField(ref)
-
-	upstreamFieldRef := p.nodes[len(p.nodes)-1].Ref
 	typeName := p.lastFieldEnclosingTypeName
 
 	fieldConfiguration := p.visitor.Config.Fields.ForTypeField(typeName, fieldName)
 	if fieldConfiguration == nil {
+		p.addField(ref)
 		return
 	}
+
+	// Note: federated fields always have a field configuration because at
+	// least the federation key for the type the field lives on is required
+	// (and required fields are specified in the configuration).
+	p.handleFederation(fieldConfiguration)
+	p.addField(ref)
+
+	upstreamFieldRef := p.nodes[len(p.nodes)-1].Ref
+
 	for i := range fieldConfiguration.Arguments {
 		argumentConfiguration := fieldConfiguration.Arguments[i]
 		p.configureArgument(upstreamFieldRef, ref, *fieldConfiguration, argumentConfiguration)
@@ -361,7 +362,6 @@ func (p *Planner) LeaveField(ref int) {
 }
 
 func (p *Planner) EnterArgument(ref int) {
-
 }
 
 func (p *Planner) EnterDocument(operation, definition *ast.Document) {
@@ -373,6 +373,7 @@ func (p *Planner) EnterDocument(operation, definition *ast.Document) {
 	p.nodes = p.nodes[:0]
 	p.upstreamVariables = nil
 	p.variables = p.variables[:0]
+	p.representationsJson = p.representationsJson[:0]
 	p.disallowSingleFlight = false
 	p.hasFederationRoot = false
 	p.extractEntities = false
@@ -407,24 +408,39 @@ func (p *Planner) EnterDocument(operation, definition *ast.Document) {
 }
 
 func (p *Planner) LeaveDocument(operation, definition *ast.Document) {
-
 }
 
-func (p *Planner) handleFederation() {
-	if !p.config.Federation.Enabled || // federation must be enabled
-		p.hasFederationRoot || // should not already have federation root field
-		!p.isNestedRequest() { // must be nested, otherwise it's a regular query
+func (p *Planner) handleFederation(fieldConfig *plan.FieldConfiguration) {
+	if !p.config.Federation.Enabled { // federation must be enabled
+		return
+	}
+	// If there's no federation root and this isn't a nested request, this
+	// isn't a federated field and there's nothing to do.
+	if !p.hasFederationRoot && !p.isNestedRequest() {
+		return
+	}
+	// If a federated root is already present, the representations variable has
+	// already been added. Update it to include information for the additional
+	// field. NOTE: only the first federated field has isNestedRequest set to
+	// true. Subsequent fields use hasFederationRoot to determine federation
+	// status.
+	if p.hasFederationRoot {
+		// Ideally the "representations" variable could be set once in
+		// LeaveDocument, but ConfigureFetch is called before this visitor's
+		// LeaveDocument is called. (Updating the visitor logic to call
+		// LeaveDocument in reverse registration order would fix this issue.)
+		p.updateRepresentationsVariable(fieldConfig)
 		return
 	}
 	p.hasFederationRoot = true
 	// query($representations: [_Any!]!){_entities(representations: $representations){... on Product
-	p.addRepresentationsVariableDefinition() // $representations: [_Any!]!
-	p.addEntitiesSelectionSet()              // {_entities(representations: $representations)
-	p.addOneTypeInlineFragment()             // ... on Product
-	p.addRepresentationsVariable()           // "variables\":{\"representations\":[{\"upc\":\"$$0$$\",\"__typename\":\"Product\"}]}}
+	p.addRepresentationsVariableDefinition()     // $representations: [_Any!]!
+	p.addEntitiesSelectionSet()                  // {_entities(representations: $representations)
+	p.addOneTypeInlineFragment()                 // ... on Product
+	p.updateRepresentationsVariable(fieldConfig) // "variables\":{\"representations\":[{\"upc\":\"$$0$$\",\"__typename\":\"Product\"}]}}
 }
 
-func (p *Planner) addRepresentationsVariable() {
+func (p *Planner) updateRepresentationsVariable(fieldConfig *plan.FieldConfiguration) {
 	// "variables\":{\"representations\":[{\"upc\":\$$0$$\,\"__typename\":\"Product\"}]}}
 	parser := astparser.NewParser()
 	doc := ast.NewDocument()
@@ -436,14 +452,19 @@ func (p *Planner) addRepresentationsVariable() {
 		return
 	}
 
-	fields := p.selectFederationKeyFields(doc)
+	// RequiresFields includes `@requires` fields as well as federation keys
+	// for the type containing the field currently being visited.
+	fields := fieldConfig.RequiresFields
 	if len(fields) == 0 {
 		return
 	}
 
 	onTypeName := p.visitor.Config.Types.RenameTypeNameOnMatchStr(p.lastFieldEnclosingTypeName)
 
-	representationsJson, _ := sjson.SetRawBytes(nil, "__typename", []byte("\""+onTypeName+"\""))
+	if len(p.representationsJson) == 0 {
+		p.representationsJson, _ = sjson.SetRawBytes(nil, "__typename", []byte("\""+onTypeName+"\""))
+	}
+
 	for i := range fields {
 		objectVariable := &resolve.ObjectVariable{
 			Path: []string{fields[i]},
@@ -452,7 +473,7 @@ func (p *Planner) addRepresentationsVariable() {
 		if fieldDef == nil {
 			continue
 		}
-		renderer, err := resolve.NewJSONVariableRendererWithValidationFromTypeRef(p.visitor.Definition,p.visitor.Definition, fieldDef.Type)
+		renderer, err := resolve.NewJSONVariableRendererWithValidationFromTypeRef(p.visitor.Definition, p.visitor.Definition, fieldDef.Type)
 		if err != nil {
 			continue
 		}
@@ -461,61 +482,13 @@ func (p *Planner) addRepresentationsVariable() {
 		if exists {
 			continue
 		}
-		representationsJson, _ = sjson.SetRawBytes(representationsJson, fields[i], []byte(variable))
+		p.representationsJson, _ = sjson.SetRawBytes(p.representationsJson, fields[i], []byte(variable))
 	}
-	representationsJson = append([]byte("["), append(representationsJson, []byte("]")...)...)
+	representationsJson := append([]byte("["), append(p.representationsJson, []byte("]")...)...)
 	p.upstreamVariables, _ = sjson.SetRawBytes(p.upstreamVariables, "representations", representationsJson)
 	p.extractEntities = true
 }
 
-func (p *Planner) selectFederationKeyFields(doc *ast.Document) []string {
-	var directiveRefs []int
-
-	onTypeName := p.visitor.Config.Types.RenameTypeNameOnMatchStr(p.lastFieldEnclosingTypeName)
-
-	for i := range doc.ObjectTypeExtensions {
-		if onTypeName == doc.ObjectTypeExtensionNameString(i) {
-			for _, j := range doc.ObjectTypeExtensions[i].Directives.Refs {
-				if doc.DirectiveNameString(j) == "key" {
-					directiveRefs = append(directiveRefs, j)
-				}
-			}
-			break
-		}
-	}
-
-	for i := range doc.ObjectTypeDefinitions {
-		if onTypeName == doc.ObjectTypeDefinitionNameString(i) {
-			for _, j := range doc.ObjectTypeDefinitions[i].Directives.Refs {
-				if doc.DirectiveNameString(j) == "key" {
-					directiveRefs = append(directiveRefs, j)
-				}
-			}
-			break
-		}
-	}
-
-	if len(directiveRefs) == 0 {
-		return nil
-	}
-
-	var fields []string
-
-	for _, directiveRef := range directiveRefs {
-		value, exists := doc.DirectiveArgumentValueByName(directiveRef, []byte("fields"))
-		if !exists {
-			continue
-		}
-		if value.Kind != ast.ValueKindString {
-			continue
-		}
-
-		fieldsStr := doc.StringValueContentString(value.Ref)
-		fields = append(fields, strings.Split(fieldsStr, " ")...)
-	}
-
-	return fields
-}
 func (p *Planner) fieldDefinition(fieldName, typeName string) *ast.FieldDefinition {
 	node, ok := p.visitor.Definition.Index.FirstNodeByNameStr(typeName)
 	if !ok {
@@ -547,7 +520,6 @@ func (p *Planner) addOneTypeInlineFragment() {
 }
 
 func (p *Planner) addEntitiesSelectionSet() {
-
 	// $representations
 	representationsLiteral := p.upstreamOperation.Input.AppendInputString("representations")
 	representationsVariable := p.upstreamOperation.AddVariableValue(ast.VariableValue{
@@ -763,7 +735,7 @@ func (p *Planner) addVariableDefinitionsRecursively(value ast.Value, sourcePath
 	variableDefinitionTypeName := p.visitor.Operation.ResolveTypeNameString(p.visitor.Operation.VariableDefinitions[variableDefinition].Type)
 	variableDefinitionTypeName = p.visitor.Config.Types.RenameTypeNameOnMatchStr(variableDefinitionTypeName)
 
-	importedVariableDefinition := p.visitor.Importer.ImportVariableDefinitionWithRename(variableDefinition, p.visitor.Operation, p.upstreamOperation,variableDefinitionTypeName)
+	importedVariableDefinition := p.visitor.Importer.ImportVariableDefinitionWithRename(variableDefinition, p.visitor.Operation, p.upstreamOperation, variableDefinitionTypeName)
 	p.upstreamOperation.AddImportedVariableDefinitionToOperationDefinition(p.nodes[0].Ref, importedVariableDefinition)
 
 	fieldType := p.resolveNestedArgumentType(fieldName)
@@ -837,7 +809,6 @@ const (
 
 // printOperation - prints normalized upstream operation
 func (p *Planner) printOperation() []byte {
-
 	buf := &bytes.Buffer{}
 
 	err := astprinter.Print(p.upstreamOperation, nil, buf)
@@ -1024,7 +995,6 @@ func (p *Planner) replaceQueryType(definition *ast.Document) {
 
 // normalizeOperation - normalizes operation against definition.
 func (p *Planner) normalizeOperation(operation, definition *ast.Document, report *operationreport.Report) (ok bool) {
-
 	report.Reset()
 	normalizer := astnormalization.NewWithOpts(
 		astnormalization.WithExtractVariables(),


Test patch:
diff --git a/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go b/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
index eca5bfc4..ddc67dd1 100644
--- a/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
+++ b/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
@@ -2741,17 +2741,13 @@ func TestGraphQLDataSource(t *testing.T) {
 																	&resolve.BatchFetch{
 																		Fetch: &resolve.SingleFetch{
 																			BufferId:   2,
-																			Input:      `{"method":"POST","url":"http://product.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Product {name price}}}","variables":{"representations":[{"name":$$1$$,"upc":$$0$$,"__typename":"Product"}]}}}`,
+																			Input:      `{"method":"POST","url":"http://product.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Product {name price}}}","variables":{"representations":[{"upc":$$0$$,"__typename":"Product"}]}}}`,
 																			DataSource: &Source{},
 																			Variables: resolve.NewVariables(
 																				&resolve.ObjectVariable{
 																					Path:     []string{"upc"},
 																					Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
 																				},
-																				&resolve.ObjectVariable{
-																					Path:     []string{"name"},
-																					Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
-																				},
 																			),
 																			DataSourceIdentifier: []byte("graphql_datasource.Source"),
 																			ProcessResponseConfig: resolve.ProcessResponseConfig{
@@ -2764,16 +2760,12 @@ func TestGraphQLDataSource(t *testing.T) {
 																	&resolve.BatchFetch{
 																		Fetch: &resolve.SingleFetch{
 																			BufferId: 3,
-																			Input:    `{"method":"POST","url":"http://review.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Product {reviews {body author {id username}}}}}","variables":{"representations":[{"name":$$1$$,"upc":$$0$$,"__typename":"Product"}]}}}`,
+																			Input:    `{"method":"POST","url":"http://review.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Product {reviews {body author {id username}}}}}","variables":{"representations":[{"upc":$$0$$,"__typename":"Product"}]}}}`,
 																			Variables: resolve.NewVariables(
 																				&resolve.ObjectVariable{
 																					Path:     []string{"upc"},
 																					Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
 																				},
-																				&resolve.ObjectVariable{
-																					Path:     []string{"name"},
-																					Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
-																				},
 																			),
 																			DataSource:           &Source{},
 																			DataSourceIdentifier: []byte("graphql_datasource.Source"),
@@ -2941,7 +2933,7 @@ func TestGraphQLDataSource(t *testing.T) {
 						},
 						Federation: FederationConfiguration{
 							Enabled:    true,
-							ServiceSDL: "extend type Query {topProducts(first: Int = 5): [Product]} type Product @key(fields: \"upc\") @key(fields: \"name\"){upc: String! name: String! price: Int!}",
+							ServiceSDL: "extend type Query {topProducts(first: Int = 5): [Product]} type Product @key(fields: \"upc\") {upc: String! price: Int!}",
 						},
 					}),
 					Factory: federationFactory,
@@ -2978,7 +2970,7 @@ func TestGraphQLDataSource(t *testing.T) {
 						},
 						Federation: FederationConfiguration{
 							Enabled:    true,
-							ServiceSDL: "type Review { body: String! author: User! @provides(fields: \"username\") product: Product! } extend type User @key(fields: \"id\") { id: ID! @external reviews: [Review] } extend type Product @key(fields: \"upc\") @key(fields: \"name\") { upc: String! @external name: String! reviews: [Review] }",
+							ServiceSDL: "type Review { body: String! author: User! @provides(fields: \"username\") product: Product! } extend type User @key(fields: \"id\") { id: ID! @external reviews: [Review] } extend type Product @key(fields: \"upc\") { upc: String! @external reviews: [Review] }",
 						},
 					}),
 				},
@@ -3017,6 +3009,162 @@ func TestGraphQLDataSource(t *testing.T) {
 			},
 		}))
 
+	t.Run("federated entity with requires", RunTest(requiredFieldTestSchema,
+		`	query QueryWithRequiredFields {
+						serviceOne {
+							serviceTwoFieldOne  # @requires(fields: "serviceOneFieldOne")
+							serviceTwoFieldTwo  # @requires(fields: "serviceOneFieldTwo")
+						}
+					}`,
+		"QueryWithRequiredFields",
+		&plan.SynchronousResponsePlan{
+			Response: &resolve.GraphQLResponse{
+				Data: &resolve.Object{
+					Fetch: &resolve.SingleFetch{
+						BufferId: 0,
+						// Should fetch the federation key as well as all the required fields.
+						Input:                 `{"method":"POST","url":"http://one.service","body":{"query":"{serviceOne {id serviceOneFieldOne serviceOneFieldTwo}}"}}`,
+						DataSource:            &Source{},
+						DataSourceIdentifier:  []byte("graphql_datasource.Source"),
+						ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true},
+					},
+					Fields: []*resolve.Field{
+						{
+							HasBuffer: true,
+							BufferID:  0,
+							Name:      []byte("serviceOne"),
+							Position: resolve.Position{
+								Line:   2,
+								Column: 7,
+							},
+							Value: &resolve.Object{
+								Fetch: &resolve.BatchFetch{
+									Fetch: &resolve.SingleFetch{
+										BufferId: 1,
+										// The required fields are present in the representations.
+										Input: `{"method":"POST","url":"http://two.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on ServiceOneType {serviceTwoFieldOne serviceTwoFieldTwo}}}","variables":{"representations":[{"serviceOneFieldTwo":$$2$$,"serviceOneFieldOne":$$1$$,"id":$$0$$,"__typename":"ServiceOneType"}]}}}`,
+										Variables: resolve.NewVariables(
+											&resolve.ObjectVariable{
+												Path:     []string{"id"},
+												Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
+											},
+											&resolve.ObjectVariable{
+												Path:     []string{"serviceOneFieldOne"},
+												Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
+											},
+											&resolve.ObjectVariable{
+												Path:     []string{"serviceOneFieldTwo"},
+												Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
+											},
+										),
+										DataSource:           &Source{},
+										DataSourceIdentifier: []byte("graphql_datasource.Source"),
+										ProcessResponseConfig: resolve.ProcessResponseConfig{
+											ExtractGraphqlResponse:    true,
+											ExtractFederationEntities: true,
+										},
+									},
+									BatchFactory: batchFactory,
+								},
+								Path:     []string{"serviceOne"},
+								Nullable: true,
+								Fields: []*resolve.Field{
+									{
+										HasBuffer: true,
+										BufferID:  1,
+										Name:      []byte("serviceTwoFieldOne"),
+										Position: resolve.Position{
+											Line:   3,
+											Column: 8,
+										},
+										Value: &resolve.String{
+											Path: []string{"serviceTwoFieldOne"},
+										},
+									},
+									{
+										HasBuffer: true,
+										BufferID:  1,
+										Name:      []byte("serviceTwoFieldTwo"),
+										Position: resolve.Position{
+											Line:   4,
+											Column: 8,
+										},
+										Value: &resolve.String{
+											Path: []string{"serviceTwoFieldTwo"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		plan.Configuration{
+			DataSources: []plan.DataSourceConfiguration{
+				{
+					RootNodes: []plan.TypeField{
+						{
+							TypeName:   "Query",
+							FieldNames: []string{"serviceOne"},
+						},
+					},
+					ChildNodes: []plan.TypeField{
+						{
+							TypeName:   "ServiceOneType",
+							FieldNames: []string{"id", "serviceOneFieldOne", "serviceOneFieldTwo"},
+						},
+					},
+					Custom: ConfigJson(Configuration{
+						Fetch: FetchConfiguration{
+							URL: "http://one.service",
+						},
+						Federation: FederationConfiguration{
+							Enabled:    true,
+							ServiceSDL: "extend type Query {serviceOne: ServiceOneType} type ServiceOneType @key(fields: \"id\"){ id: ID! serviceOneFieldOne: String! serviceOneFieldTwo: String!}",
+						},
+					}),
+					Factory: federationFactory,
+				},
+				{
+					RootNodes: []plan.TypeField{
+						{
+							TypeName:   "ServiceOneType",
+							FieldNames: []string{"serviceTwoFieldOne", "serviceTwoFieldTwo"},
+						},
+					},
+					ChildNodes: []plan.TypeField{
+						{
+							TypeName:   "ServiceOneType",
+							FieldNames: []string{"id", "serviceOneFieldOne", "serviceOneFieldTwo"},
+						},
+					},
+					Custom: ConfigJson(Configuration{
+						Fetch: FetchConfiguration{
+							URL: "http://two.service",
+						},
+						Federation: FederationConfiguration{
+							Enabled:    true,
+							ServiceSDL: "extend type ServiceOneType @key(fields: \"id\") { id: ID! @external serviceOneFieldOne: String! @external serviceOneFieldTwo: String! @external serviceTwoFieldOne: String! @requires(fields: \"serviceOneFieldOne\") serviceTwoFieldTwo: String! @requires(fields: \"serviceOneFieldTwo\")}",
+						},
+					}),
+					Factory: federationFactory,
+				},
+			},
+			Fields: []plan.FieldConfiguration{
+				{
+					TypeName:       "ServiceOneType",
+					FieldName:      "serviceTwoFieldOne",
+					RequiresFields: []string{"id", "serviceOneFieldOne"},
+				},
+				{
+					TypeName:       "ServiceOneType",
+					FieldName:      "serviceTwoFieldTwo",
+					RequiresFields: []string{"id", "serviceOneFieldTwo"},
+				},
+			},
+		}))
+
 	t.Run("federation with renamed schema", RunTest(renamedFederationTestSchema,
 		`	query MyReviews {
 						api_me {
@@ -3174,17 +3322,13 @@ func TestGraphQLDataSource(t *testing.T) {
 																	&resolve.BatchFetch{
 																		Fetch: &resolve.SingleFetch{
 																			BufferId:   2,
-																			Input:      `{"method":"POST","url":"http://product.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Product {name price}}}","variables":{"representations":[{"name":$$1$$,"upc":$$0$$,"__typename":"Product"}]}}}`,
+																			Input:      `{"method":"POST","url":"http://product.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Product {name price}}}","variables":{"representations":[{"upc":$$0$$,"__typename":"Product"}]}}}`,
 																			DataSource: &Source{},
 																			Variables: resolve.NewVariables(
 																				&resolve.ObjectVariable{
 																					Path:     []string{"upc"},
 																					Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
 																				},
-																				&resolve.ObjectVariable{
-																					Path:     []string{"name"},
-																					Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
-																				},
 																			),
 																			DataSourceIdentifier: []byte("graphql_datasource.Source"),
 																			ProcessResponseConfig: resolve.ProcessResponseConfig{
@@ -3197,16 +3341,12 @@ func TestGraphQLDataSource(t *testing.T) {
 																	&resolve.BatchFetch{
 																		Fetch: &resolve.SingleFetch{
 																			BufferId: 3,
-																			Input:    `{"method":"POST","url":"http://review.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Product {reviews {body author {id username}}}}}","variables":{"representations":[{"name":$$1$$,"upc":$$0$$,"__typename":"Product"}]}}}`,
+																			Input:    `{"method":"POST","url":"http://review.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Product {reviews {body author {id username}}}}}","variables":{"representations":[{"upc":$$0$$,"__typename":"Product"}]}}}`,
 																			Variables: resolve.NewVariables(
 																				&resolve.ObjectVariable{
 																					Path:     []string{"upc"},
 																					Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
 																				},
-																				&resolve.ObjectVariable{
-																					Path:     []string{"name"},
-																					Renderer: resolve.NewJSONVariableRendererWithValidation(`{"type":"string"}`),
-																				},
 																			),
 																			DataSource:           &Source{},
 																			DataSourceIdentifier: []byte("graphql_datasource.Source"),
@@ -3375,7 +3515,7 @@ func TestGraphQLDataSource(t *testing.T) {
 						},
 						Federation: FederationConfiguration{
 							Enabled:    true,
-							ServiceSDL: "extend type Query {topProducts(first: Int = 5): [Product]} type Product @key(fields: \"upc\") @key(fields: \"name\"){upc: String! name: String! price: Int!}",
+							ServiceSDL: "extend type Query {topProducts(first: Int = 5): [Product]} type Product @key(fields: \"upc\") {upc: String! price: Int!}",
 						},
 						UpstreamSchema: federationTestSchema,
 					}),
@@ -3413,7 +3553,7 @@ func TestGraphQLDataSource(t *testing.T) {
 						},
 						Federation: FederationConfiguration{
 							Enabled:    true,
-							ServiceSDL: "type Review { body: String! author: User! @provides(fields: \"username\") product: Product! } extend type User @key(fields: \"id\") { id: ID! @external reviews: [Review] } extend type Product @key(fields: \"upc\") @key(fields: \"name\") { upc: String! @external name: String! reviews: [Review] }",
+							ServiceSDL: "type Review { body: String! author: User! @provides(fields: \"username\") product: Product! } extend type User @key(fields: \"id\") { id: ID! @external reviews: [Review] } extend type Product @key(fields: \"upc\") { upc: String! @external reviews: [Review] }",
 						},
 						UpstreamSchema: federationTestSchema,
 					}),
@@ -3746,7 +3886,6 @@ func runTestOnTestDefinition(operation, operationName string, expectedPlan plan.
 }
 
 func BenchmarkFederationBatching(b *testing.B) {
-
 	userService := FakeDataSource(`{"data":{"me": {"id": "1234","username": "Me","__typename": "User"}}}`)
 	reviewsService := FakeDataSource(`{"data":{"_entities":[{"reviews": [{"body": "A highly effective form of birth control.","product": {"upc": "top-1","__typename": "Product"}},{"body": "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","product": {"upc": "top-2","__typename": "Product"}}]}]}}`)
 	productsService := FakeDataSource(`{"data":{"_entities":[{"name": "Trilby"},{"name": "Fedora"}]}}`)
@@ -4703,6 +4842,28 @@ type User_api {
 }
 `
 
+const requiredFieldTestSchema = `
+scalar String
+scalar ID
+
+schema {
+	query: Query
+}
+
+type Query {
+  serviceOne: ServiceOneType
+}
+
+type ServiceOneType {
+  id: ID!
+  serviceOneFieldOne: String!
+  serviceOneFieldTwo: String!
+
+  serviceTwoFieldOne: String!
+  serviceTwoFieldTwo: String!
+}
+`
+
 const subgraphTestSchema = `
 directive @external on FIELD_DEFINITION
 directive @requires(fields: _FieldSet!) on FIELD_DEFINITION