Repository: jensneuse/graphql-go-tools
Pull request #246 (2021-03-10 12:44:38)

fix aliased GraphQL operations


Patch:
diff --git a/pkg/ast/ast_object_type_definition.go b/pkg/ast/ast_object_type_definition.go
index 59b50dde..23fe2383 100644
--- a/pkg/ast/ast_object_type_definition.go
+++ b/pkg/ast/ast_object_type_definition.go
@@ -88,3 +88,24 @@ func (d *Document) ImportObjectTypeDefinition(name, description string, fieldRef
 
 	return
 }
+
+func (d *Document) RemoveObjectTypeDefinition(name ByteSlice) bool {
+	node, ok := d.Index.FirstNodeByNameBytes(name)
+	if !ok {
+		return false
+	}
+
+	if node.Kind != NodeKindObjectTypeDefinition {
+		return false
+	}
+
+	for i := range d.RootNodes {
+		if d.RootNodes[i].Kind == NodeKindObjectTypeDefinition && d.RootNodes[i].Ref == node.Ref {
+			d.RootNodes = append(d.RootNodes[:i], d.RootNodes[i+1:]...)
+			break
+		}
+	}
+
+	d.Index.RemoveNodeByName(name)
+	return true
+}
diff --git a/pkg/ast/ast_root_operation_type_definition.go b/pkg/ast/ast_root_operation_type_definition.go
index 3769e41e..5ccda8bb 100644
--- a/pkg/ast/ast_root_operation_type_definition.go
+++ b/pkg/ast/ast_root_operation_type_definition.go
@@ -1,6 +1,8 @@
 package ast
 
-import "github.com/jensneuse/graphql-go-tools/pkg/lexer/position"
+import (
+	"github.com/jensneuse/graphql-go-tools/pkg/lexer/position"
+)
 
 type RootOperationTypeDefinitionList struct {
 	LBrace position.Position // {
@@ -74,12 +76,36 @@ func (d *Document) AddRootOperationTypeDefinition(rootOperationTypeDefinition Ro
 }
 
 func (d *Document) ImportRootOperationTypeDefinition(name string, operationType OperationType) (ref int) {
+	nameBytes := []byte(name)
+
+	switch operationType {
+	case OperationTypeQuery:
+		d.Index.QueryTypeName = nameBytes
+	case OperationTypeMutation:
+		d.Index.MutationTypeName = nameBytes
+	case OperationTypeSubscription:
+		d.Index.SubscriptionTypeName = nameBytes
+	default:
+		return -1
+	}
+
 	operationTypeDefinition := RootOperationTypeDefinition{
 		OperationType: operationType,
 		NamedType: Type{
-			Name: d.Input.AppendInputString(name),
+			Name: d.Input.AppendInputBytes(nameBytes),
 		},
 	}
 
 	return d.AddRootOperationTypeDefinition(operationTypeDefinition)
 }
+
+func (d *Document) ReplaceRootOperationTypeDefinition(name string, operationType OperationType) (ref int) {
+	for i := range d.RootOperationTypeDefinitions {
+		if d.RootOperationTypeDefinitions[i].OperationType == operationType {
+			d.RootOperationTypeDefinitions = append(d.RootOperationTypeDefinitions[:i], d.RootOperationTypeDefinitions[i+1:]...)
+			break
+		}
+	}
+
+	return d.ImportRootOperationTypeDefinition(name, operationType)
+}
diff --git a/pkg/ast/index.go b/pkg/ast/index.go
index 1910b546..cc6ca267 100644
--- a/pkg/ast/index.go
+++ b/pkg/ast/index.go
@@ -1,6 +1,10 @@
 package ast
 
-import "github.com/cespare/xxhash"
+import (
+	"bytes"
+
+	"github.com/cespare/xxhash"
+)
 
 // Index is a struct to easily look up objects in a document, e.g. find Nodes (type/interface/union definitions) by name
 type Index struct {
@@ -83,3 +87,20 @@ func (i *Index) FirstNodeByNameBytes(name []byte) (Node, bool) {
 	}
 	return node[0], true
 }
+
+func (i *Index) RemoveNodeByName(name []byte) {
+	hash := xxhash.Sum64(name)
+	delete(i.nodes, hash)
+
+	if bytes.Equal(i.QueryTypeName, name) {
+		i.QueryTypeName = nil
+	}
+
+	if bytes.Equal(i.MutationTypeName, name) {
+		i.MutationTypeName = nil
+	}
+
+	if bytes.Equal(i.SubscriptionTypeName, name) {
+		i.SubscriptionTypeName = nil
+	}
+}
diff --git a/pkg/engine/datasource/graphql_datasource/graphql_datasource.go b/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
index e3478f56..0199eca3 100644
--- a/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
+++ b/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
@@ -42,7 +42,43 @@ type Planner struct {
 	hasFederationRoot          bool
 	extractEntities            bool
 	client                     httpclient.Client
-	isNested bool
+	isNested                   bool
+	rootTypeName               string
+}
+
+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.
+
+	fieldName := p.visitor.Operation.FieldNameString(downstreamFieldRef)
+
+	if p.visitor.Operation.FieldAliasIsDefined(downstreamFieldRef) {
+		return "", false
+	}
+
+	typeName := p.visitor.Walker.EnclosingTypeDefinition.NameString(p.visitor.Definition)
+	for i := range p.visitor.Config.Fields {
+		if p.visitor.Config.Fields[i].TypeName == typeName &&
+			p.visitor.Config.Fields[i].FieldName == fieldName &&
+			len(p.visitor.Config.Fields[i].Path) == 1 {
+
+			if p.visitor.Config.Fields[i].Path[0] != fieldName {
+				aliasBytes := p.visitor.Operation.FieldNameBytes(downstreamFieldRef)
+				return string(aliasBytes), true
+			}
+			break
+		}
+	}
+	return "", false
+}
+
+func (p *Planner) DataSourcePlanningBehavior() plan.DataSourcePlanningBehavior {
+	return plan.DataSourcePlanningBehavior{
+		MergeAliasedRootNodes:      true,
+		OverrideFieldPathFromAlias: true,
+	}
 }
 
 type Configuration struct {
@@ -179,14 +215,16 @@ func (p *Planner) LeaveSelectionSet(ref int) {
 
 func (p *Planner) EnterField(ref int) {
 
+	if p.rootTypeName == "" {
+		p.rootTypeName = p.visitor.Walker.EnclosingTypeDefinition.NameString(p.visitor.Definition)
+	}
+
 	p.lastFieldEnclosingTypeName = p.visitor.Walker.EnclosingTypeDefinition.NameString(p.visitor.Definition)
 
 	p.handleFederation(ref)
 
 	p.addField(ref)
 
-	// fmt.Printf("Planner::%s::%s::EnterField::%s::%d\n", p.id, p.visitor.Walker.Path.DotDelimitedString(), p.visitor.Operation.FieldNameString(ref), ref)
-
 	upstreamFieldRef := p.nodes[len(p.nodes)-1].Ref
 	typeName := p.lastFieldEnclosingTypeName
 	fieldName := p.visitor.Operation.FieldNameString(ref)
@@ -221,6 +259,7 @@ func (p *Planner) EnterDocument(operation, definition *ast.Document) {
 	p.disallowSingleFlight = false
 	p.hasFederationRoot = false
 	p.extractEntities = false
+	p.rootTypeName = ""
 }
 
 func (p *Planner) LeaveDocument(operation, definition *ast.Document) {
@@ -526,6 +565,11 @@ func (p *Planner) configureObjectFieldSource(upstreamFieldRef, downstreamFieldRe
 	}
 }
 
+const (
+	normalizationFailedErrMsg = "printOperation: normalization failed"
+	parseDocumentFailedErrMsg = "printOperation: parse %s failed"
+)
+
 func (p *Planner) printOperation() []byte {
 
 	buf := &bytes.Buffer{}
@@ -558,25 +602,18 @@ func (p *Planner) printOperation() []byte {
 
 	parser.Parse(operation, report)
 	if report.HasErrors() {
-		p.visitor.Walker.StopWithInternalErr(fmt.Errorf("printOperation: parse operation failed"))
+		p.stopWithError(parseDocumentFailedErrMsg, "operation")
 		return nil
 	}
 
 	parser.Parse(definition, report)
 	if report.HasErrors() {
-		p.visitor.Walker.StopWithInternalErr(fmt.Errorf("printOperation: parse definition failed"))
+		p.stopWithError(parseDocumentFailedErrMsg, "definition")
 		return nil
 	}
 
-	operationStr, _ := astprinter.PrintStringIndent(operation, definition, "  ")
-	schemaStr, _ := astprinter.PrintStringIndent(definition, nil, "  ")
-	_, _ = schemaStr, operationStr
-
-	normalizer := astnormalization.NewNormalizer(true, true)
-	normalizer.NormalizeOperation(operation, definition, report)
-
-	if report.HasErrors() {
-		p.visitor.Walker.StopWithInternalErr(fmt.Errorf("normalization failed"))
+	if !p.normalizeOperation(operation, definition, report, true) {
+		p.stopWithError(normalizationFailedErrMsg)
 		return nil
 	}
 
@@ -584,12 +621,42 @@ func (p *Planner) printOperation() []byte {
 
 	err = astprinter.Print(operation, p.visitor.Definition, buf)
 	if err != nil {
-		p.visitor.Walker.StopWithInternalErr(fmt.Errorf("normalization failed"))
+		p.stopWithError(normalizationFailedErrMsg)
 		return nil
 	}
+
 	return buf.Bytes()
 }
 
+func (p *Planner) stopWithError(msg string, args ...interface{}) {
+	p.visitor.Walker.StopWithInternalErr(fmt.Errorf(msg, args...))
+}
+
+// normalizeOperation - tries to normalize operation against definition.
+//
+// withRetry - temporary flag which will replace query type with corresponding root type and reruns normalization.
+//
+// TODO: implement less hacky solution to normalize upstream queries
+func (p *Planner) normalizeOperation(operation, definition *ast.Document, report *operationreport.Report, withRetry bool) (ok bool) {
+
+	report.Reset()
+	normalizer := astnormalization.NewNormalizer(true, true)
+	normalizer.NormalizeOperation(operation, definition, report)
+
+	if !report.HasErrors() {
+		return true
+	}
+
+	if withRetry && p.isNested && !p.config.Federation.Enabled {
+		definition.RemoveObjectTypeDefinition(definition.Index.QueryTypeName)
+		definition.ReplaceRootOperationTypeDefinition(p.rootTypeName, ast.OperationTypeQuery)
+
+		return p.normalizeOperation(operation, definition, report, false)
+	}
+
+	return false
+}
+
 func (p *Planner) addField(ref int) {
 
 	fieldName := p.visitor.Operation.FieldNameString(ref)
@@ -608,7 +675,15 @@ func (p *Planner) addField(ref int) {
 		if p.visitor.Config.Fields[i].TypeName == typeName &&
 			p.visitor.Config.Fields[i].FieldName == fieldName &&
 			len(p.visitor.Config.Fields[i].Path) == 1 {
+
+			if p.visitor.Config.Fields[i].Path[0] != fieldName && !alias.IsDefined {
+				alias.IsDefined = true
+				aliasBytes := p.visitor.Operation.FieldNameBytes(ref)
+				alias.Name = p.upstreamOperation.Input.AppendInputBytes(aliasBytes)
+			}
+
 			fieldName = p.visitor.Config.Fields[i].Path[0]
+
 			break
 		}
 	}
diff --git a/pkg/engine/datasource/rest_datasource/rest_datasource.go b/pkg/engine/datasource/rest_datasource/rest_datasource.go
index 23965ebc..6af8ef5a 100644
--- a/pkg/engine/datasource/rest_datasource/rest_datasource.go
+++ b/pkg/engine/datasource/rest_datasource/rest_datasource.go
@@ -28,6 +28,18 @@ type Planner struct {
 	operationDefinition int
 }
 
+func (p *Planner) DownstreamResponseFieldAlias(downstreamFieldRef int) (alias string, exists bool) {
+	// the REST DataSourcePlanner doesn't rewrite upstream fields: skip
+	return
+}
+
+func (p *Planner) DataSourcePlanningBehavior() plan.DataSourcePlanningBehavior {
+	return plan.DataSourcePlanningBehavior{
+		MergeAliasedRootNodes:      false,
+		OverrideFieldPathFromAlias: false,
+	}
+}
+
 func (p *Planner) EnterOperationDefinition(ref int) {
 	p.operationDefinition = ref
 }
diff --git a/pkg/engine/datasource/staticdatasource/static_datasource.go b/pkg/engine/datasource/staticdatasource/static_datasource.go
index 0247e296..a0da3a83 100644
--- a/pkg/engine/datasource/staticdatasource/static_datasource.go
+++ b/pkg/engine/datasource/staticdatasource/static_datasource.go
@@ -31,6 +31,18 @@ type Planner struct {
 	config Configuration
 }
 
+func (p *Planner) DownstreamResponseFieldAlias(downstreamFieldRef int) (alias string, exists bool) {
+	// skip, not required
+	return
+}
+
+func (p *Planner) DataSourcePlanningBehavior() plan.DataSourcePlanningBehavior {
+	return plan.DataSourcePlanningBehavior{
+		MergeAliasedRootNodes:      false,
+		OverrideFieldPathFromAlias: false,
+	}
+}
+
 func (p *Planner) Register(visitor *plan.Visitor, customConfiguration json.RawMessage, isNested bool) error {
 	return json.Unmarshal(customConfiguration, &p.config)
 }
diff --git a/pkg/engine/plan/plan.go b/pkg/engine/plan/plan.go
index 6894e6c7..8b043f4f 100644
--- a/pkg/engine/plan/plan.go
+++ b/pkg/engine/plan/plan.go
@@ -42,13 +42,12 @@ func (f FieldConfigurations) ForTypeField(typeName, fieldName string) *FieldConf
 }
 
 type FieldConfiguration struct {
-	TypeName                          string
-	FieldName                         string
-	DisableDefaultMapping             bool
-	Path                              []string
-	RespectOverrideFieldPathFromAlias bool
-	Arguments                         ArgumentsConfigurations
-	RequiresFields                    []string
+	TypeName              string
+	FieldName             string
+	DisableDefaultMapping bool
+	Path                  []string
+	Arguments      ArgumentsConfigurations
+	RequiresFields []string
 }
 
 type ArgumentsConfigurations []ArgumentConfiguration
@@ -76,11 +75,10 @@ type ArgumentConfiguration struct {
 }
 
 type DataSourceConfiguration struct {
-	RootNodes                  []TypeField
-	ChildNodes                 []TypeField
-	Factory                    PlannerFactory
-	OverrideFieldPathFromAlias bool
-	Custom                     json.RawMessage
+	RootNodes  []TypeField
+	ChildNodes []TypeField
+	Factory    PlannerFactory
+	Custom     json.RawMessage
 }
 
 func (d *DataSourceConfiguration) HasRootNode(typeName, fieldName string) bool {
@@ -107,11 +105,10 @@ type TypeField struct {
 }
 
 type FieldMapping struct {
-	TypeName                          string
-	FieldName                         string
-	DisableDefaultMapping             bool
-	Path                              []string
-	RespectOverrideFieldPathFromAlias bool
+	TypeName              string
+	FieldName             string
+	DisableDefaultMapping bool
+	Path                  []string
 }
 
 func NewPlanner(config Configuration) *Planner {
@@ -325,16 +322,6 @@ func (v *Visitor) currentFullPath() string {
 	return path
 }
 
-func (v *Visitor) currentPlannerConfiguration() plannerConfiguration {
-	path := v.currentFullPath()
-	for i := range v.planners {
-		if v.planners[i].hasPath(path) {
-			return v.planners[i]
-		}
-	}
-	return plannerConfiguration{}
-}
-
 func (v *Visitor) EnterDirective(ref int) {
 	directiveName := v.Operation.DirectiveNameString(ref)
 	ancestor := v.Walker.Ancestors[len(v.Walker.Ancestors)-1]
@@ -570,14 +557,22 @@ func (v *Visitor) EnterOperationDefinition(ref int) {
 func (v *Visitor) resolveFieldPath(ref int) []string {
 	typeName := v.Walker.EnclosingTypeDefinition.NameString(v.Definition)
 	fieldName := v.Operation.FieldNameString(ref)
-
-	config := v.currentPlannerConfiguration()
-	aliasOverride := config.dataSourceConfiguration.OverrideFieldPathFromAlias
+	config := v.currentOrParentPlannerConfiguration()
+	aliasOverride := false
+	if config.planner != nil {
+		aliasOverride = config.planner.DataSourcePlanningBehavior().OverrideFieldPathFromAlias
+	}
 
 	for i := range v.Config.Fields {
 		if v.Config.Fields[i].TypeName == typeName && v.Config.Fields[i].FieldName == fieldName {
-			if aliasOverride && v.Config.Fields[i].RespectOverrideFieldPathFromAlias {
-				return []string{v.Operation.FieldAliasOrNameString(ref)}
+			if aliasOverride {
+				override,exists := config.planner.DownstreamResponseFieldAlias(ref)
+				if exists {
+					return []string{override}
+				}
+			}
+			if aliasOverride && v.Operation.FieldAliasIsDefined(ref) {
+				return []string{v.Operation.FieldAliasString(ref)}
 			}
 			if v.Config.Fields[i].DisableDefaultMapping {
 				return nil
@@ -612,9 +607,43 @@ func (v *Visitor) LeaveDocument(operation, definition *ast.Document) {
 
 var (
 	templateRegex = regexp.MustCompile(`{{.*?}}`)
-	selectorRegex = regexp.MustCompile(`{{\s*(.*?)\s*}}`)
+	selectorRegex = regexp.MustCompile(`{{\s*\.(.*?)\s*}}`)
 )
 
+func (v *Visitor) currentOrParentPlannerConfiguration() plannerConfiguration {
+	const none = -1
+	currentPath := v.currentFullPath()
+	plannerIndex := none
+	plannerPathDeepness := none
+
+	for i := range v.planners {
+		for _, plannerPath := range v.planners[i].paths {
+			if v.isCurrentOrParentPath(currentPath, plannerPath.path) {
+				currentPlannerPathDeepness := v.pathDeepness(plannerPath.path)
+				if currentPlannerPathDeepness > plannerPathDeepness {
+					plannerPathDeepness = currentPlannerPathDeepness
+					plannerIndex = i
+					break
+				}
+			}
+		}
+	}
+
+	if plannerIndex != none {
+		return v.planners[plannerIndex]
+	}
+
+	return plannerConfiguration{}
+}
+
+func (v *Visitor) isCurrentOrParentPath(currentPath string, parentPath string) bool {
+	return strings.HasPrefix(currentPath, parentPath)
+}
+
+func (v *Visitor) pathDeepness(path string) int {
+	return strings.Count(path, ".")
+}
+
 func (v *Visitor) resolveInputTemplates(config objectFetchConfiguration, input *string, variables *resolve.Variables) {
 	*input = templateRegex.ReplaceAllStringFunc(*input, func(s string) string {
 		selectors := selectorRegex.FindStringSubmatch(s)
@@ -657,7 +686,7 @@ func (v *Visitor) resolveInputTemplates(config objectFetchConfiguration, input *
 				break
 			}
 			switch path[0] {
-			case "header":
+			case "headers":
 				key := path[1]
 				variableName, _ = variables.AddVariable(&resolve.HeaderVariable{
 					Path: []string{key},
@@ -761,10 +790,55 @@ func (_ *SubscriptionResponsePlan) PlanKind() Kind {
 	return SubscriptionResponseKind
 }
 
+type DataSourcePlanningBehavior struct {
+	// MergeAliasedRootNodes will reuse a data source for multiple root fields with aliases if true.
+	// Example:
+	//  {
+	//    rootField
+	//    alias: rootField
+	//  }
+	// On dynamic data sources (e.g. GraphQL, SQL, ...) this should return true and for
+	// static data sources (e.g. REST, static, GRPC...) it should be false.
+	MergeAliasedRootNodes bool
+	// OverrideFieldPathFromAlias will let the planner know if the response path should also be aliased (= true)
+	// or not (= false)
+	// Example:
+	//  {
+	//    rootField
+	//    alias: original
+	//  }
+	// When true expected response will be { "rootField": ..., "alias": ... }
+	// When false expected response will be { "rootField": ..., "original": ... }
+	OverrideFieldPathFromAlias bool
+}
+
 type DataSourcePlanner interface {
 	Register(visitor *Visitor, customConfiguration json.RawMessage, isNested bool) error
 	ConfigureFetch() FetchConfiguration
 	ConfigureSubscription() SubscriptionConfiguration
+	DataSourcePlanningBehavior() DataSourcePlanningBehavior
+	// DownstreamResponseFieldAlias allows the DataSourcePlanner to overwrite the response path with an alias
+	// It's required to set OverrideFieldPathFromAlias to true
+	// This function is useful in the following scenario
+	// 1. The downstream Query doesn't contain an alias
+	// 2. The path configuration rewrites the field to an existing field
+	// 3. The DataSourcePlanner is using an alias to the upstream
+	// Example:
+	//
+	// type Query {
+	//		country: Country
+	//		countryAlias: Country
+	// }
+	//
+	// Both, country and countryAlias have a path in the FieldConfiguration of "country"
+	// In theory, they would be treated as the same field
+	// However, by using DownstreamResponseFieldAlias, it's possible for the DataSourcePlanner to use an alias for countryAlias.
+	// In this case, the response would contain both, country and countryAlias fields in the response.
+	// At the same time, the downstream Query would only expect the response on the path "country",
+	// as both country and countryAlias have a mapping to the path "country".
+	// The DataSourcePlanner could keep track that it rewrites the upstream query and use DownstreamResponseFieldAlias
+	// to indicate to the Planner to expect the response for countryAlias on the path "countryAlias" instead of "country".
+	DownstreamResponseFieldAlias(downstreamFieldRef int) (alias string, exists bool)
 }
 
 type SubscriptionConfiguration struct {
@@ -912,7 +986,7 @@ func (c *configurationVisitor) EnterField(ref int) {
 	}
 	isSubscription := c.isSubscription(root.Ref, current)
 	for i, planner := range c.planners {
-		if planner.hasParent(parent) && planner.hasRootNode(typeName, fieldName) {
+		if planner.hasParent(parent) && planner.hasRootNode(typeName, fieldName) && planner.planner.DataSourcePlanningBehavior().MergeAliasedRootNodes {
 			// same parent + root node = root sibling
 			c.planners[i].paths = append(c.planners[i].paths, pathConfiguration{path: current})
 			c.fieldBuffers[ref] = planner.bufferID
diff --git a/pkg/graphql/lookup.go b/pkg/graphql/lookup.go
new file mode 100644
index 00000000..c3ed7adf
--- /dev/null
+++ b/pkg/graphql/lookup.go
@@ -0,0 +1,24 @@
+package graphql
+
+import (
+	"fmt"
+)
+
+type TypeFieldLookupKey string
+
+func CreateTypeFieldLookupKey(typeName string, fieldName string) TypeFieldLookupKey {
+	return TypeFieldLookupKey(fmt.Sprintf("%s.%s", typeName, fieldName))
+}
+
+func CreateTypeFieldArgumentsLookupMap(typeFieldArgs []TypeFieldArguments) map[TypeFieldLookupKey]TypeFieldArguments {
+	if len(typeFieldArgs) == 0 {
+		return nil
+	}
+
+	lookupMap := make(map[TypeFieldLookupKey]TypeFieldArguments)
+	for _, currentTypeFieldArgs := range typeFieldArgs {
+		lookupMap[CreateTypeFieldLookupKey(currentTypeFieldArgs.TypeName, currentTypeFieldArgs.FieldName)] = currentTypeFieldArgs
+	}
+
+	return lookupMap
+}
diff --git a/pkg/graphql/schema.go b/pkg/graphql/schema.go
index 61d54971..8f3c9860 100644
--- a/pkg/graphql/schema.go
+++ b/pkg/graphql/schema.go
@@ -5,15 +5,28 @@ import (
 	"encoding/json"
 	"io"
 	"io/ioutil"
+	"strings"
 
 	"github.com/jensneuse/graphql-go-tools/pkg/ast"
 	"github.com/jensneuse/graphql-go-tools/pkg/astparser"
 	"github.com/jensneuse/graphql-go-tools/pkg/asttransform"
 	"github.com/jensneuse/graphql-go-tools/pkg/astvalidation"
+	"github.com/jensneuse/graphql-go-tools/pkg/engine/plan"
 	"github.com/jensneuse/graphql-go-tools/pkg/introspection"
 	"github.com/jensneuse/graphql-go-tools/pkg/operationreport"
 )
 
+type TypeFields struct {
+	TypeName   string
+	FieldNames []string
+}
+
+type TypeFieldArguments struct {
+	TypeName      string
+	FieldName     string
+	ArgumentNames []string
+}
+
 type Schema struct {
 	rawInput []byte
 	document ast.Document
@@ -119,6 +132,184 @@ func (s *Schema) IntrospectionResponse(out io.Writer) error {
 	return json.NewEncoder(out).Encode(introspectionData)
 }
 
+func (s *Schema) GetAllFieldArguments(skipFieldFuncs ...SkipFieldFunc) []TypeFieldArguments {
+	objectTypeExtensions := make(map[string]ast.ObjectTypeExtension)
+	for _, objectTypeExtension := range s.document.ObjectTypeExtensions {
+		typeName, ok := s.typeNameOfObjectTypeIfHavingFields(objectTypeExtension.ObjectTypeDefinition)
+		if !ok {
+			continue
+		}
+
+		objectTypeExtensions[typeName] = objectTypeExtension
+	}
+
+	typeFieldArguments := make([]TypeFieldArguments, 0)
+	for _, objectType := range s.document.ObjectTypeDefinitions {
+		typeName, ok := s.typeNameOfObjectTypeIfHavingFields(objectType)
+		if !ok {
+			continue
+		}
+
+		for _, fieldRef := range objectType.FieldsDefinition.Refs {
+			fieldName, skip := s.determineIfFieldWithFieldNameShouldBeSkipped(fieldRef, typeName, skipFieldFuncs...)
+			if skip {
+				continue
+			}
+
+			s.addTypeFieldArgsForFieldRef(fieldRef, typeName, fieldName, &typeFieldArguments)
+		}
+
+		objectTypeExt, ok := objectTypeExtensions[typeName]
+		if !ok {
+			continue
+		}
+
+		for _, fieldRef := range objectTypeExt.FieldsDefinition.Refs {
+			fieldName, skip := s.determineIfFieldWithFieldNameShouldBeSkipped(fieldRef, typeName, skipFieldFuncs...)
+			if skip {
+				continue
+			}
+
+			s.addTypeFieldArgsForFieldRef(fieldRef, typeName, fieldName, &typeFieldArguments)
+		}
+	}
+
+	return typeFieldArguments
+}
+
+func (s *Schema) typeNameOfObjectTypeIfHavingFields(objectType ast.ObjectTypeDefinition) (typeName string, ok bool) {
+	if !objectType.HasFieldDefinitions {
+		return "", false
+	}
+
+	return s.document.Input.ByteSliceString(objectType.Name), true
+}
+
+func (s *Schema) fieldNameOfFieldDefinitionIfHavingArguments(field ast.FieldDefinition, ref int) (fieldName string, ok bool) {
+	if !field.HasArgumentsDefinitions {
+		return "", false
+	}
+
+	return s.document.FieldDefinitionNameString(ref), true
+}
+
+func (s *Schema) determineIfFieldWithFieldNameShouldBeSkipped(ref int, typeName string, skipFieldFuncs ...SkipFieldFunc) (fieldName string, skip bool) {
+	field := s.document.FieldDefinitions[ref]
+	fieldName, ok := s.fieldNameOfFieldDefinitionIfHavingArguments(field, ref)
+	if !ok {
+		return fieldName, true
+	}
+
+	for _, skipFieldFunc := range skipFieldFuncs {
+		if skipFieldFunc != nil && skipFieldFunc(typeName, fieldName, s.document) {
+			skip = true
+			break
+		}
+	}
+
+	return fieldName, skip
+}
+
+func (s *Schema) addTypeFieldArgsForFieldRef(ref int, typeName string, fieldName string, fieldArguments *[]TypeFieldArguments) {
+	currentTypeFieldArgs := TypeFieldArguments{
+		TypeName:      typeName,
+		FieldName:     fieldName,
+		ArgumentNames: make([]string, 0),
+	}
+
+	for _, argRef := range s.document.FieldDefinitions[ref].ArgumentsDefinition.Refs {
+		argName := s.document.InputValueDefinitionNameString(argRef)
+		currentTypeFieldArgs.ArgumentNames = append(currentTypeFieldArgs.ArgumentNames, string(argName))
+	}
+
+	*fieldArguments = append(*fieldArguments, currentTypeFieldArgs)
+}
+
+func (s *Schema) GetAllNestedFieldChildrenFromTypeField(typeName string, fieldName string, skipFieldFuncs ...SkipFieldFunc) []TypeFields {
+	fields := s.nodeFieldRefs(typeName)
+	if len(fields) == 0 {
+		return nil
+	}
+	for _, ref := range fields {
+		if fieldName == s.document.FieldDefinitionNameString(ref) {
+			fieldTypeName := s.document.FieldDefinitionTypeNode(ref).NameString(&s.document)
+			childNodes := make([]TypeFields, 0)
+			s.findNestedFieldChildren(fieldTypeName, &childNodes, skipFieldFuncs...)
+			return childNodes
+		}
+	}
+
+	return nil
+}
+
+func (s *Schema) findNestedFieldChildren(typeName string, childNodes *[]TypeFields, skipFieldFuncs ...SkipFieldFunc) {
+	fields := s.nodeFieldRefs(typeName)
+	if len(fields) == 0 {
+		return
+	}
+	for _, ref := range fields {
+		fieldName := s.document.FieldDefinitionNameString(ref)
+		if len(skipFieldFuncs) > 0 {
+			skip := false
+			for _, skipFieldFunc := range skipFieldFuncs {
+				if skipFieldFunc != nil && skipFieldFunc(typeName, fieldName, s.document) {
+					skip = true
+					break
+				}
+			}
+
+			if skip {
+				continue
+			}
+		}
+
+		if added := s.putChildNode(childNodes, typeName, fieldName); !added {
+			continue
+		}
+
+		fieldTypeName := s.document.FieldDefinitionTypeNode(ref).NameString(&s.document)
+		s.findNestedFieldChildren(fieldTypeName, childNodes, skipFieldFuncs...)
+	}
+}
+
+func (s *Schema) nodeFieldRefs(typeName string) []int {
+	node, exists := s.document.Index.FirstNodeByNameStr(typeName)
+	if !exists {
+		return nil
+	}
+	var fields []int
+	switch node.Kind {
+	case ast.NodeKindObjectTypeDefinition:
+		fields = s.document.ObjectTypeDefinitions[node.Ref].FieldsDefinition.Refs
+	case ast.NodeKindInterfaceTypeDefinition:
+		fields = s.document.InterfaceTypeDefinitions[node.Ref].FieldsDefinition.Refs
+	default:
+		return nil
+	}
+
+	return fields
+}
+
+func (s *Schema) putChildNode(nodes *[]TypeFields, typeName, fieldName string) (added bool) {
+	for i := range *nodes {
+		if typeName != (*nodes)[i].TypeName {
+			continue
+		}
+		for j := range (*nodes)[i].FieldNames {
+			if fieldName == (*nodes)[i].FieldNames[j] {
+				return false
+			}
+		}
+		(*nodes)[i].FieldNames = append((*nodes)[i].FieldNames, fieldName)
+		return true
+	}
+	*nodes = append(*nodes, TypeFields{
+		TypeName:   typeName,
+		FieldNames: []string{fieldName},
+	})
+	return true
+}
+
 func createSchema(schemaContent []byte) (*Schema, error) {
 	document, report := astparser.ParseGraphqlDocumentBytes(schemaContent)
 	if report.HasErrors() {
@@ -141,3 +332,30 @@ func SchemaIntrospection(schema *Schema) (*ExecutionResult, error) {
 	err := schema.IntrospectionResponse(&buf)
 	return &ExecutionResult{&buf}, err
 }
+
+type SkipFieldFunc func(typeName, fieldName string, definition ast.Document) bool
+
+func NewIsDataSourceConfigV2RootFieldSkipFunc(dataSources []plan.DataSourceConfiguration) SkipFieldFunc {
+	return func(typeName, fieldName string, _ ast.Document) bool {
+		for i := range dataSources {
+			for j := range dataSources[i].RootNodes {
+				if typeName != dataSources[i].RootNodes[j].TypeName {
+					continue
+				}
+				for k := range dataSources[i].RootNodes[j].FieldNames {
+					if fieldName == dataSources[i].RootNodes[j].FieldNames[k] {
+						return true
+					}
+				}
+			}
+		}
+		return false
+	}
+}
+
+func NewSkipReservedNamesFunc() SkipFieldFunc {
+	return func(typeName, fieldName string, _ ast.Document) bool {
+		prefix := "__"
+		return strings.HasPrefix(typeName, prefix) || strings.HasPrefix(fieldName, prefix)
+	}
+}


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 e3044252..fef1c2ff 100644
--- a/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
+++ b/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
@@ -32,7 +32,7 @@ func TestGraphQLDataSource(t *testing.T) {
 				Fetch: &resolve.SingleFetch{
 					DataSource: &Source{},
 					BufferId:   0,
-					Input:      `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$1$$"]},"body":{"query":"query($id: ID!){droid(id: $id){name aliased: name friends {name} primaryFunction} hero {name} stringList nestedStringList}","variables":{"id":"$$0$$"}}}`,
+					Input:      `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$1$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($id: ID!){droid(id: $id){name aliased: name friends {name} primaryFunction} hero {name} stringList nestedStringList}","variables":{"id":"$$0$$"}}}`,
 					Variables: resolve.NewVariables(
 						&resolve.ContextVariable{
 							Path: []string{"id"},
@@ -156,13 +156,13 @@ func TestGraphQLDataSource(t *testing.T) {
 						FieldNames: []string{"name", "primaryFunction", "friends"},
 					},
 				},
-				Factory:                    &Factory{},
-				OverrideFieldPathFromAlias: true,
+				Factory: &Factory{},
 				Custom: ConfigJson(Configuration{
 					Fetch: FetchConfiguration{
 						URL: "https://swapi.com/graphql",
 						Header: http.Header{
-							"Authorization": []string{"{{ .request.header.Authorization }}"},
+							"Authorization":    []string{"{{ .request.headers.Authorization }}"},
+							"Invalid-Template": []string{"{{ request.headers.Authorization }}"},
 						},
 					},
 				}),
@@ -191,6 +191,7 @@ func TestGraphQLDataSource(t *testing.T) {
 			},
 		},
 	}))
+
 	t.Run("simple mutation", RunTest(`
 		type Mutation {
 			addFriend(name: String!):Friend!
@@ -257,7 +258,6 @@ func TestGraphQLDataSource(t *testing.T) {
 							FieldNames: []string{"id", "name"},
 						},
 					},
-					OverrideFieldPathFromAlias: true,
 					Custom: ConfigJson(Configuration{
 						Fetch: FetchConfiguration{
 							URL: "https://service.one",
@@ -281,6 +281,7 @@ func TestGraphQLDataSource(t *testing.T) {
 			},
 		},
 	))
+
 	t.Run("nested resolvers of same upstream", RunTest(`
 		type Query {
 			foo(bar: String):Baz
@@ -352,8 +353,7 @@ func TestGraphQLDataSource(t *testing.T) {
 							FieldNames: []string{"bar"},
 						},
 					},
-					Factory:                    &Factory{},
-					OverrideFieldPathFromAlias: true,
+					Factory: &Factory{},
 					Custom: ConfigJson(Configuration{
 						Fetch: FetchConfiguration{
 							URL: "https://foo.service",
@@ -385,6 +385,249 @@ func TestGraphQLDataSource(t *testing.T) {
 			},
 		},
 	))
+
+	t.Run("same upstream with alias in query", RunTest(
+		countriesSchema,
+		`
+		query QueryWithAlias {
+			country(code: "AD") {
+				name
+			}
+			alias: country(code: "AE") {
+				name
+            }
+		}
+		`,
+		"QueryWithAlias",
+		&plan.SynchronousResponsePlan{
+			Response: &resolve.GraphQLResponse{
+				Data: &resolve.Object{
+					Fetch: &resolve.SingleFetch{
+						BufferId:   0,
+						Input:      `{"method":"POST","url":"https://countries.service","body":{"query":"query($a: ID!, $b: ID!){country(code: $a){name} alias: country(code: $b){name}}","variables":{"b":"$$1$$","a":"$$0$$"}}}`,
+						DataSource: &Source{},
+						Variables: resolve.NewVariables(
+							&resolve.ContextVariable{
+								Path: []string{"a"},
+							},
+							&resolve.ContextVariable{
+								Path: []string{"b"},
+							},
+						),
+						DisallowSingleFlight: false,
+					},
+					Fields: []*resolve.Field{
+						{
+							BufferID:  0,
+							HasBuffer: true,
+							Name:      []byte("country"),
+							Value: &resolve.Object{
+								Nullable: true,
+								Path:     []string{"country"},
+								Fields: []*resolve.Field{
+									{
+										Name: []byte("name"),
+										Value: &resolve.String{
+											Nullable: false,
+											Path:     []string{"name"},
+										},
+									},
+								},
+							},
+						},
+						{
+							BufferID:  0,
+							HasBuffer: true,
+							Name:      []byte("alias"),
+							Value: &resolve.Object{
+								Nullable: true,
+								Path:     []string{"alias"},
+								Fields: []*resolve.Field{
+									{
+										Name: []byte("name"),
+										Value: &resolve.String{
+											Nullable: false,
+											Path:     []string{"name"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		plan.Configuration{
+			DataSources: []plan.DataSourceConfiguration{
+				{
+					RootNodes: []plan.TypeField{
+						{
+							TypeName:   "Query",
+							FieldNames: []string{"country", "countryAlias"},
+						},
+					},
+					ChildNodes: []plan.TypeField{
+						{
+							TypeName:   "Country",
+							FieldNames: []string{"name", "code"},
+						},
+					},
+					Factory: &Factory{},
+					Custom: ConfigJson(Configuration{
+						Fetch: FetchConfiguration{
+							URL: "https://countries.service",
+						},
+					}),
+				},
+			},
+			Fields: []plan.FieldConfiguration{
+				{
+					TypeName:  "Query",
+					FieldName: "country",
+					Path:      []string{"country"},
+					Arguments: []plan.ArgumentConfiguration{
+						{
+							Name:       "code",
+							SourceType: plan.FieldArgumentSource,
+						},
+					},
+				},
+				{
+					TypeName:  "Query",
+					FieldName: "countryAlias",
+					Path:      []string{"country"},
+					Arguments: []plan.ArgumentConfiguration{
+						{
+							Name:       "code",
+							SourceType: plan.FieldArgumentSource,
+						},
+					},
+				},
+			},
+		},
+	))
+
+	t.Run("same upstream with alias in schema", RunTest(
+		countriesSchema,
+		`
+		query QueryWithSchemaAlias {
+			country(code: "AD") {
+				name
+			}
+			countryAlias(code: "AE") {
+				name
+            }
+		}
+		`,
+		"QueryWithSchemaAlias",
+		&plan.SynchronousResponsePlan{
+			Response: &resolve.GraphQLResponse{
+				Data: &resolve.Object{
+					Fetch: &resolve.SingleFetch{
+						BufferId:   0,
+						Input:      `{"method":"POST","url":"https://countries.service","body":{"query":"query($a: ID!, $b: ID!){country(code: $a){name} countryAlias: country(code: $b){name}}","variables":{"b":"$$1$$","a":"$$0$$"}}}`,
+						DataSource: &Source{},
+						Variables: resolve.NewVariables(
+							&resolve.ContextVariable{
+								Path: []string{"a"},
+							},
+							&resolve.ContextVariable{
+								Path: []string{"b"},
+							},
+						),
+						DisallowSingleFlight: false,
+					},
+					Fields: []*resolve.Field{
+						{
+							BufferID:  0,
+							HasBuffer: true,
+							Name:      []byte("country"),
+							Value: &resolve.Object{
+								Nullable: true,
+								Path:     []string{"country"},
+								Fields: []*resolve.Field{
+									{
+										Name: []byte("name"),
+										Value: &resolve.String{
+											Nullable: false,
+											Path:     []string{"name"},
+										},
+									},
+								},
+							},
+						},
+						{
+							BufferID:  0,
+							HasBuffer: true,
+							Name:      []byte("countryAlias"),
+							Value: &resolve.Object{
+								Nullable: true,
+								Path:     []string{"countryAlias"},
+								Fields: []*resolve.Field{
+									{
+										Name: []byte("name"),
+										Value: &resolve.String{
+											Nullable: false,
+											Path:     []string{"name"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		plan.Configuration{
+			DataSources: []plan.DataSourceConfiguration{
+				{
+					RootNodes: []plan.TypeField{
+						{
+							TypeName:   "Query",
+							FieldNames: []string{"country", "countryAlias"},
+						},
+					},
+					ChildNodes: []plan.TypeField{
+						{
+							TypeName:   "Country",
+							FieldNames: []string{"name", "code"},
+						},
+					},
+					Factory: &Factory{},
+					Custom: ConfigJson(Configuration{
+						Fetch: FetchConfiguration{
+							URL: "https://countries.service",
+						},
+					}),
+				},
+			},
+			Fields: []plan.FieldConfiguration{
+				{
+					TypeName:  "Query",
+					FieldName: "country",
+					Path:      []string{"country"},
+					Arguments: []plan.ArgumentConfiguration{
+						{
+							Name:       "code",
+							SourceType: plan.FieldArgumentSource,
+						},
+					},
+				},
+				{
+					TypeName:  "Query",
+					FieldName: "countryAlias",
+					Path:      []string{"country"},
+					Arguments: []plan.ArgumentConfiguration{
+						{
+							Name:       "code",
+							SourceType: plan.FieldArgumentSource,
+						},
+					},
+				},
+			},
+		},
+	))
+
 	nestedGraphQLEngineFactory := &Factory{}
 	t.Run("nested graphql engines", RunTest(`
 		type Query {
@@ -396,16 +639,23 @@ func TestGraphQLDataSource(t *testing.T) {
 		}
 		type ServiceOneResponse {
 			fieldOne: String!
+			countries: [Country!]!
 		}
 		type ServiceTwoResponse {
 			fieldTwo: String
 			serviceOneField: String
 			serviceOneResponse: ServiceOneResponse
 		}
+		type Country {
+			name: String!
+        }
 	`, `
 		query NestedQuery ($firstArg: String, $secondArg: Boolean, $thirdArg: Int, $fourthArg: Float){
 			serviceOne(serviceOneArg: $firstArg) {
 				fieldOne
+				countries {
+					name
+				}
 			}
 			serviceTwo(serviceTwoArg: $secondArg){
 				fieldTwo
@@ -444,8 +694,7 @@ func TestGraphQLDataSource(t *testing.T) {
 								),
 							},
 							{
-								BufferId: 1,
-								// 			 {"method":"POST","url":"https://service.two","body":{"query":"query($secondArg: Boolean, $fourthArg: Float){serviceTwo(serviceTwoArg: $secondArg){fieldTwo} secondServiceTwo(secondServiceTwoArg: $fourthArg){fieldTwo serviceOneField}}","variables":{"fourthArg":$$1$$,"secondArg":$$0$$}}}
+								BufferId: 2,
 								Input:      `{"method":"POST","url":"https://service.two","body":{"query":"query($secondArg: Boolean, $fourthArg: Float){serviceTwo(serviceTwoArg: $secondArg){fieldTwo serviceOneField} secondServiceTwo(secondServiceTwoArg: $fourthArg){fieldTwo serviceOneField}}","variables":{"fourthArg":$$1$$,"secondArg":$$0$$}}}`,
 								DataSource: &Source{},
 								Variables: resolve.NewVariables(
@@ -467,6 +716,13 @@ func TestGraphQLDataSource(t *testing.T) {
 							Value: &resolve.Object{
 								Nullable: true,
 								Path:     []string{"serviceOne"},
+
+								Fetch: &resolve.SingleFetch{
+									BufferId:   1,
+									DataSource: &Source{},
+									Input:      `{"method":"POST","url":"https://country.service","body":{"query":"{countries {name}}"}}`,
+								},
+
 								Fields: []*resolve.Field{
 									{
 										Name: []byte("fieldOne"),
@@ -474,20 +730,38 @@ func TestGraphQLDataSource(t *testing.T) {
 											Path: []string{"fieldOne"},
 										},
 									},
+									{
+										Name:      []byte("countries"),
+										HasBuffer: true,
+										BufferID:  1,
+										Value: &resolve.Array{
+											Path: []string{"countries"},
+											Item: &resolve.Object{
+												Fields: []*resolve.Field{
+													{
+														Name: []byte("name"),
+														Value: &resolve.String{
+															Path: []string{"name"},
+														},
+													},
+												},
+											},
+										},
+									},
 								},
 							},
 						},
 						{
 							HasBuffer: true,
-							BufferID:  1,
+							BufferID:  2,
 							Name:      []byte("serviceTwo"),
 							Value: &resolve.Object{
 								Nullable: true,
 								Path:     []string{"serviceTwo"},
 								Fetch: &resolve.SingleFetch{
-									BufferId:   2,
+									BufferId:   3,
 									DataSource: &Source{},
-									Input:      `{"method":"POST","url":"https://service.one","body":{"query":"query($a: String){serviceOne(serviceOneArg: $a){fieldOne}}","variables":{"a":"$$0$$"}}}`,
+									Input:      `{"method":"POST","url":"https://service.one","body":{"query":"query($a: String){serviceOneResponse: serviceOne(serviceOneArg: $a){fieldOne}}","variables":{"a":"$$0$$"}}}`,
 									Variables: resolve.NewVariables(
 										&resolve.ObjectVariable{
 											Path: []string{"serviceOneField"},
@@ -504,11 +778,11 @@ func TestGraphQLDataSource(t *testing.T) {
 									},
 									{
 										HasBuffer: true,
-										BufferID:  2,
+										BufferID:  3,
 										Name:      []byte("serviceOneResponse"),
 										Value: &resolve.Object{
 											Nullable: true,
-											Path:     []string{"serviceOne"},
+											Path:     []string{"serviceOneResponse"},
 											Fields: []*resolve.Field{
 												{
 													Name: []byte("fieldOne"),
@@ -540,7 +814,7 @@ func TestGraphQLDataSource(t *testing.T) {
 							},
 						},
 						{
-							BufferID:  1,
+							BufferID:  2,
 							HasBuffer: true,
 							Name:      []byte("secondServiceTwo"),
 							Value: &resolve.Object{
@@ -609,8 +883,7 @@ func TestGraphQLDataSource(t *testing.T) {
 							URL: "https://service.one",
 						},
 					}),
-					Factory:                    nestedGraphQLEngineFactory,
-					OverrideFieldPathFromAlias: true,
+					Factory: nestedGraphQLEngineFactory,
 				},
 				{
 					RootNodes: []plan.TypeField{
@@ -630,8 +903,27 @@ func TestGraphQLDataSource(t *testing.T) {
 							URL: "https://service.two",
 						},
 					}),
-					Factory:                    nestedGraphQLEngineFactory,
-					OverrideFieldPathFromAlias: true,
+					Factory: nestedGraphQLEngineFactory,
+				},
+				{
+					RootNodes: []plan.TypeField{
+						{
+							TypeName:   "ServiceOneResponse",
+							FieldNames: []string{"countries"},
+						},
+					},
+					ChildNodes: []plan.TypeField{
+						{
+							TypeName:   "Country",
+							FieldNames: []string{"name"},
+						},
+					},
+					Custom: ConfigJson(Configuration{
+						Fetch: FetchConfiguration{
+							URL: "https://country.service",
+						},
+					}),
+					Factory: nestedGraphQLEngineFactory,
 				},
 			},
 			Fields: []plan.FieldConfiguration{
@@ -701,6 +993,7 @@ func TestGraphQLDataSource(t *testing.T) {
 			},
 		},
 	))
+
 	t.Run("mutation with variables in array object argument", RunTest(
 		todoSchema,
 		`mutation AddTask($title: String!, $completed: Boolean!, $name: String! @fromClaim(name: "sub")) {
@@ -820,6 +1113,7 @@ func TestGraphQLDataSource(t *testing.T) {
 			},
 		},
 	))
+
 	t.Run("inline object value with arguments", RunTest(`
 			schema {
 				mutation: Mutation
@@ -958,6 +1252,7 @@ func TestGraphQLDataSource(t *testing.T) {
 			},
 		},
 	))
+
 	t.Run("subscription", RunTest(testDefinition, `
 		subscription RemainingJedis {
 			remainingJedis
@@ -1000,6 +1295,7 @@ func TestGraphQLDataSource(t *testing.T) {
 			},
 		},
 	}))
+
 	federationFactory := &Factory{}
 	t.Run("federation", RunTest(federationTestSchema,
 		`	query MyReviews {
@@ -1967,3 +2263,23 @@ type User {
   reviews: [Review]
 }
 `
+
+const countriesSchema = `
+scalar String
+scalar Int
+scalar ID
+
+schema {
+	query: Query
+}
+
+type Country {
+  name: String!
+  code: ID!
+}
+
+type Query {
+  country(code: ID!): Country
+  countryAlias(code: ID!): Country
+}
+`
diff --git a/pkg/engine/datasource/rest_datasource/rest_datasource_test.go b/pkg/engine/datasource/rest_datasource/rest_datasource_test.go
index ae0eb79f..8d62e20a 100644
--- a/pkg/engine/datasource/rest_datasource/rest_datasource_test.go
+++ b/pkg/engine/datasource/rest_datasource/rest_datasource_test.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 
+	"github.com/jensneuse/graphql-go-tools/pkg/ast"
 	"github.com/jensneuse/graphql-go-tools/pkg/engine/datasource/httpclient"
 	"github.com/jensneuse/graphql-go-tools/pkg/engine/datasourcetesting"
 	"github.com/jensneuse/graphql-go-tools/pkg/engine/plan"
@@ -33,6 +34,7 @@ const (
 		type Friend {
 			name: String
 			pet: Pet
+			phone(name: String!): String
 		}
 
 		type Pet {
@@ -68,6 +70,20 @@ const (
 		}
 	`
 
+	duplicatedArgumentOperationWithAlias = `
+		query ArgumentQuery($idVariable: String!) {
+			withArgument(id: $idVariable, name: "foo") {
+				name
+				homePhone: phone(name: "home")
+				officePhone: phone(name: "office")
+			}
+
+			aliased: withArgument(id: $idVariable, name: "bar") {
+				name
+			}
+		}
+	`
+
 	argumentWithoutVariablesOperation = `
 		query ArgumentWithoutVariablesQuery {
 			withArgument(id: "123abc", name: "foo") {
@@ -272,6 +288,166 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 			},
 		},
 	))
+	t.Run("get request with duplicated argument and alias", datasourcetesting.RunTest(schema, duplicatedArgumentOperationWithAlias, "ArgumentQuery",
+		&plan.SynchronousResponsePlan{
+			Response: &resolve.GraphQLResponse{
+				Data: &resolve.Object{
+					Fetch: &resolve.ParallelFetch{
+						Fetches: []*resolve.SingleFetch{
+							{
+								BufferId:   0,
+								Input:      `{"method":"GET","url":"https://example.com/$$0$$/$$1$$"}`,
+								DataSource: &Source{},
+								Variables: resolve.NewVariables(
+									&resolve.ContextVariable{
+										Path: []string{"idVariable"},
+									},
+									&resolve.ContextVariable{
+										Path: []string{"a"},
+									},
+								),
+							},
+							{
+								BufferId:   3,
+								Input:      `{"method":"GET","url":"https://example.com/$$0$$/$$1$$"}`,
+								DataSource: &Source{},
+								Variables: resolve.NewVariables(
+									&resolve.ContextVariable{
+										Path: []string{"idVariable"},
+									},
+									&resolve.ContextVariable{
+										Path: []string{"d"},
+									},
+								),
+							},
+						},
+					},
+					Fields: []*resolve.Field{
+						{
+							BufferID:  0,
+							HasBuffer: true,
+							Name:      []byte("withArgument"),
+							Value: &resolve.Object{
+								Nullable: true,
+								Fetch: &resolve.ParallelFetch{
+									Fetches: []*resolve.SingleFetch{
+										{
+											BufferId:   1,
+											Input:      `{"method":"GET","url":"https://example.com/friends/phone/$$0$$"}`,
+											DataSource: &Source{},
+											Variables: resolve.NewVariables(
+												&resolve.ContextVariable{
+													Path: []string{"b"},
+												},
+											),
+										},
+										{
+											BufferId:   2,
+											Input:      `{"method":"GET","url":"https://example.com/friends/phone/$$0$$"}`,
+											DataSource: &Source{},
+											Variables: resolve.NewVariables(
+												&resolve.ContextVariable{
+													Path: []string{"c"},
+												},
+											),
+										},
+									},
+								},
+								Fields: []*resolve.Field{
+									{
+										Name: []byte("name"),
+										Value: &resolve.String{
+											Path:     []string{"name"},
+											Nullable: true,
+										},
+									},
+									{
+										BufferID:  1,
+										HasBuffer: true,
+										Name:      []byte("homePhone"),
+										Value: &resolve.String{
+											Path:     []string{"phone"},
+											Nullable: true,
+										},
+									},
+									{
+										BufferID:  2,
+										HasBuffer: true,
+										Name:      []byte("officePhone"),
+										Value: &resolve.String{
+											Path:     []string{"phone"},
+											Nullable: true,
+										},
+									},
+								},
+							},
+						},
+						{
+							BufferID:  3,
+							HasBuffer: true,
+							Name:      []byte("aliased"),
+							Value: &resolve.Object{
+								Nullable: true,
+								Fields: []*resolve.Field{
+									{
+										Name: []byte("name"),
+										Value: &resolve.String{
+											Path:     []string{"name"},
+											Nullable: true,
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		plan.Configuration{
+			DataSources: []plan.DataSourceConfiguration{
+				{
+					RootNodes: []plan.TypeField{
+						{
+							TypeName:   "Query",
+							FieldNames: []string{"withArgument"},
+						},
+					},
+					Custom: ConfigJSON(Configuration{
+						Fetch: FetchConfiguration{
+							URL:    "https://example.com/{{ .arguments.id }}/{{ .arguments.name }}",
+							Method: "GET",
+						},
+					}),
+					Factory: &Factory{},
+				},
+				{
+					RootNodes: []plan.TypeField{
+						{
+							TypeName:   "Friend",
+							FieldNames: []string{"phone"},
+						},
+					},
+					Custom: ConfigJSON(Configuration{
+						Fetch: FetchConfiguration{
+							URL:    "https://example.com/friends/phone/{{ .arguments.name }}",
+							Method: "GET",
+						},
+					}),
+					Factory: &Factory{},
+				},
+			},
+			Fields: []plan.FieldConfiguration{
+				{
+					TypeName:              "Query",
+					FieldName:             "withArgument",
+					DisableDefaultMapping: true,
+				},
+			},
+		},
+		func(t *testing.T, op ast.Document, actualPlan plan.Plan) {
+			assert.Equal(t, `{"d":"bar","c":"office","b":"home","a":"foo"}`, string(op.Input.Variables))
+		},
+	))
 	t.Run("get request with argument using templates with and without spaces", datasourcetesting.RunTest(schema, argumentWithoutVariablesOperation, "ArgumentWithoutVariablesQuery",
 		&plan.SynchronousResponsePlan{
 			Response: &resolve.GraphQLResponse{
@@ -473,7 +649,7 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 				Data: &resolve.Object{
 					Fetch: &resolve.SingleFetch{
 						BufferId:   0,
-						Input:      `{"header":{"Authorization":["Bearer 123"],"Token":["Bearer $$0$$"],"X-API-Key":["456"]},"method":"GET","url":"https://example.com/friend"}`,
+						Input:      `{"header":{"Authorization":["Bearer 123"],"Invalid-Template":["{{ request.headers.Authorization }}"],"Token":["Bearer $$0$$"],"X-API-Key":["456"]},"method":"GET","url":"https://example.com/friend"}`,
 						DataSource: &Source{},
 						Variables: []resolve.Variable{
 							&resolve.HeaderVariable{
@@ -517,9 +693,10 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 							URL:    "https://example.com/friend",
 							Method: "GET",
 							Header: http.Header{
-								"Authorization": []string{"Bearer 123"},
-								"X-API-Key":     []string{"456"},
-								"Token":         []string{"Bearer {{ .request.header.Authorization }}"},
+								"Authorization":    []string{"Bearer 123"},
+								"X-API-Key":        []string{"456"},
+								"Token":            []string{"Bearer {{ .request.headers.Authorization }}"},
+								"Invalid-Template": []string{"{{ request.headers.Authorization }}"},
 							},
 						},
 					}),
diff --git a/pkg/engine/datasourcetesting/datasourcetesting.go b/pkg/engine/datasourcetesting/datasourcetesting.go
index b1f4e7d2..5d172122 100644
--- a/pkg/engine/datasourcetesting/datasourcetesting.go
+++ b/pkg/engine/datasourcetesting/datasourcetesting.go
@@ -6,6 +6,7 @@ import (
 	"github.com/stretchr/testify/assert"
 
 	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafeparser"
+	"github.com/jensneuse/graphql-go-tools/pkg/ast"
 	"github.com/jensneuse/graphql-go-tools/pkg/astnormalization"
 	"github.com/jensneuse/graphql-go-tools/pkg/astprinter"
 	"github.com/jensneuse/graphql-go-tools/pkg/asttransform"
@@ -14,7 +15,9 @@ import (
 	"github.com/jensneuse/graphql-go-tools/pkg/operationreport"
 )
 
-func RunTest(definition, operation, operationName string, expectedPlan plan.Plan, config plan.Configuration) func(t *testing.T) {
+type CheckFunc func(t *testing.T, op ast.Document, actualPlan plan.Plan)
+
+func RunTest(definition, operation, operationName string, expectedPlan plan.Plan, config plan.Configuration, extraChecks ...CheckFunc) func(t *testing.T) {
 	return func(t *testing.T) {
 		def := unsafeparser.ParseGraphqlDocumentString(definition)
 		op := unsafeparser.ParseGraphqlDocumentString(operation)
@@ -41,5 +44,10 @@ func RunTest(definition, operation, operationName string, expectedPlan plan.Plan
 			t.Fatal(report.Error())
 		}
 		assert.Equal(t, expectedPlan, actualPlan)
+
+		for _, extraCheck := range extraChecks {
+			extraCheck(t, op, actualPlan)
+		}
+
 	}
 }
diff --git a/pkg/graphql/execution_engine_v2_test.go b/pkg/graphql/execution_engine_v2_test.go
index cb8856b8..694e2c98 100644
--- a/pkg/graphql/execution_engine_v2_test.go
+++ b/pkg/graphql/execution_engine_v2_test.go
@@ -212,7 +212,7 @@ func TestExecutionEngineV2_Execute(t *testing.T) {
 					},
 					Custom: rest_datasource.ConfigJSON(rest_datasource.Configuration{
 						Fetch: rest_datasource.FetchConfiguration{
-							URL:    "https://example.com/{{ .request.header.Authorization }}",
+							URL:    "https://example.com/{{ .request.headers.Authorization }}",
 							Method: "GET",
 						},
 					}),
diff --git a/pkg/graphql/lookup_test.go b/pkg/graphql/lookup_test.go
new file mode 100644
index 00000000..a4089207
--- /dev/null
+++ b/pkg/graphql/lookup_test.go
@@ -0,0 +1,32 @@
+package graphql
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCreateTypeFieldLookupKey(t *testing.T) {
+	lookupKey := CreateTypeFieldLookupKey("Query", "hello")
+	assert.Equal(t, TypeFieldLookupKey("Query.hello"), lookupKey)
+}
+
+func TestCreateTypeFieldArgumentsLookupMap(t *testing.T) {
+	t.Run("should return nil if slice is empty", func(t *testing.T) {
+		lookupMap := CreateTypeFieldArgumentsLookupMap([]TypeFieldArguments{})
+		assert.Nil(t, lookupMap)
+	})
+
+	t.Run("should return a lookup map", func(t *testing.T) {
+		typeFieldArgs := []TypeFieldArguments{
+			{
+				TypeName:      "Query",
+				FieldName:     "hello",
+				ArgumentNames: []string{"name"},
+			},
+		}
+
+		lookupMap := CreateTypeFieldArgumentsLookupMap(typeFieldArgs)
+		assert.Equal(t, lookupMap[TypeFieldLookupKey("Query.hello")], typeFieldArgs[0])
+	})
+}
diff --git a/pkg/graphql/schema_test.go b/pkg/graphql/schema_test.go
index b072e3b8..e52bf07f 100644
--- a/pkg/graphql/schema_test.go
+++ b/pkg/graphql/schema_test.go
@@ -8,6 +8,8 @@ import (
 	"github.com/sebdah/goldie"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
+	"github.com/jensneuse/graphql-go-tools/pkg/engine/plan"
 )
 
 func TestNewSchemaFromReader(t *testing.T) {
@@ -383,10 +385,188 @@ func TestSchemaIntrospection(t *testing.T) {
 	goldie.Assert(t, "introspection_response", bodyBytes)
 }
 
+func TestSchema_GetAllFieldArguments(t *testing.T) {
+	schema, err := NewSchemaFromString(schemaWithChildren)
+	require.NoError(t, err)
+
+	t.Run("should get all field arguments without skip function", func(t *testing.T) {
+		fieldArguments := schema.GetAllFieldArguments()
+		expectedFieldArguments := []TypeFieldArguments{
+			{
+				TypeName:      "Query",
+				FieldName:     "singleArgLevel1",
+				ArgumentNames: []string{"lvl"},
+			},
+			{
+				TypeName:      "Query",
+				FieldName:     "__type",
+				ArgumentNames: []string{"name"},
+			},
+			{
+				TypeName:      "Query",
+				FieldName:     "multiArgLevel1",
+				ArgumentNames: []string{"lvl", "number"},
+			},
+			{
+				TypeName:      "SingleArgLevel1",
+				FieldName:     "singleArgLevel2",
+				ArgumentNames: []string{"lvl"},
+			},
+			{
+				TypeName:      "MultiArgLevel1",
+				FieldName:     "multiArgLevel2",
+				ArgumentNames: []string{"lvl", "number"},
+			},
+			{
+				TypeName:      "__Type",
+				FieldName:     "fields",
+				ArgumentNames: []string{"includeDeprecated"},
+			},
+			{
+				TypeName:      "__Type",
+				FieldName:     "enumValues",
+				ArgumentNames: []string{"includeDeprecated"},
+			},
+		}
+		assert.Equal(t, expectedFieldArguments, fieldArguments)
+	})
+
+	t.Run("should get all field arguments excluding skipped fields by skip field funcs", func(t *testing.T) {
+		fieldArguments := schema.GetAllFieldArguments(NewSkipReservedNamesFunc())
+		expectedFieldArguments := []TypeFieldArguments{
+			{
+				TypeName:      "Query",
+				FieldName:     "singleArgLevel1",
+				ArgumentNames: []string{"lvl"},
+			},
+			{
+				TypeName:      "Query",
+				FieldName:     "multiArgLevel1",
+				ArgumentNames: []string{"lvl", "number"},
+			},
+			{
+				TypeName:      "SingleArgLevel1",
+				FieldName:     "singleArgLevel2",
+				ArgumentNames: []string{"lvl"},
+			},
+			{
+				TypeName:      "MultiArgLevel1",
+				FieldName:     "multiArgLevel2",
+				ArgumentNames: []string{"lvl", "number"},
+			},
+		}
+		assert.Equal(t, expectedFieldArguments, fieldArguments)
+	})
+}
+
+func TestSchema_GetAllNestedFieldChildrenFromTypeField(t *testing.T) {
+	schema, err := NewSchemaFromString(schemaWithChildren)
+	require.NoError(t, err)
+
+	t.Run("should return nil when type or field does not exist", func(t *testing.T) {
+		typeFields := schema.GetAllNestedFieldChildrenFromTypeField("Not", "existent")
+		assert.Equal(t, []TypeFields(nil), typeFields)
+	})
+
+	t.Run("should get field children without skip function", func(t *testing.T) {
+		typeFields := schema.GetAllNestedFieldChildrenFromTypeField("Query", "withChildren")
+		expectedTypeFields := []TypeFields{
+			{
+				TypeName:   "WithChildren",
+				FieldNames: []string{"id", "name", "nested"},
+			},
+			{
+				TypeName:   "Nested",
+				FieldNames: []string{"id", "name"},
+			},
+		}
+
+		assert.Equal(t, expectedTypeFields, typeFields)
+	})
+
+	t.Run("should get field children with skip function for engine v2 data source config", func(t *testing.T) {
+		dataSources := []plan.DataSourceConfiguration{
+			{
+				RootNodes: []plan.TypeField{
+					{
+						TypeName:   "WithChildren",
+						FieldNames: []string{"nested"},
+					},
+				},
+			},
+		}
+		typeFields := schema.GetAllNestedFieldChildrenFromTypeField("Query", "withChildren", NewIsDataSourceConfigV2RootFieldSkipFunc(dataSources))
+		expectedTypeFields := []TypeFields{
+			{
+				TypeName:   "WithChildren",
+				FieldNames: []string{"id", "name"},
+			},
+		}
+
+		assert.Equal(t, expectedTypeFields, typeFields)
+	})
+
+	t.Run("should get field children from schema with recursive references", func(t *testing.T) {
+		schema, err = NewSchemaFromString(countriesSchema)
+		require.NoError(t, err)
+
+		typeFields := schema.GetAllNestedFieldChildrenFromTypeField("Query", "countries")
+		expectedTypeFields := []TypeFields{
+			{
+				TypeName:   "Country",
+				FieldNames: []string{"code", "name", "native", "phone", "continent", "capital", "currency", "languages", "emoji", "emojiU", "states"},
+			},
+			{
+				TypeName:   "Continent",
+				FieldNames: []string{"code", "name", "countries"},
+			},
+			{
+				TypeName:   "Language",
+				FieldNames: []string{"code", "name", "native", "rtl"},
+			},
+			{
+				TypeName:   "State",
+				FieldNames: []string{"code", "name", "country"},
+			},
+		}
+
+		assert.Equal(t, expectedTypeFields, typeFields)
+	})
+}
+
 var invalidSchema = `type Query {
 	foo: Bar
 }`
 
+var schemaWithChildren = `
+type Query {
+	withChildren: WithChildren
+	singleArgLevel1(lvl: int): SingleArgLevel1
+}
+
+extend type Query {
+	multiArgLevel1(lvl: int, number: int): MultiArgLevel1
+}
+
+type WithChildren { 
+	id: ID!
+	name: String
+	nested: Nested
+} 
+
+type Nested { 
+	id: ID! 
+	name: String! 
+} 
+
+type SingleArgLevel1 {
+	singleArgLevel2(lvl: int): String
+}
+
+type MultiArgLevel1 {
+	multiArgLevel2(lvl: int, number: int): String
+}`
+
 var countriesSchema = `directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE
 
 schema {