Repository: jensneuse/graphql-go-tools
Pull request #223 (2020-06-05 14:06:35)

sync Tyk master


Patch:
diff --git a/internal/pkg/quotes/quotes.go b/internal/pkg/quotes/quotes.go
new file mode 100644
index 00000000..87f2e66d
--- /dev/null
+++ b/internal/pkg/quotes/quotes.go
@@ -0,0 +1,19 @@
+package quotes
+
+const (
+	quoteStr = "\""
+)
+
+var (
+	quoteBytes = []byte(quoteStr)
+)
+
+func WrapBytes(bytes []byte) []byte {
+	cp := make([]byte, len(bytes))
+	copy(cp, bytes)
+	return append(quoteBytes, append(cp, quoteBytes...)...)
+}
+
+func WrapString(str string) string {
+	return quoteStr + str + quoteStr
+}
diff --git a/pkg/ast/ast_field_definition.go b/pkg/ast/ast_field_definition.go
index 8884bba5..c335ef4e 100644
--- a/pkg/ast/ast_field_definition.go
+++ b/pkg/ast/ast_field_definition.go
@@ -95,7 +95,6 @@ func (d *Document) AddFieldDefinition(fieldDefinition FieldDefinition) (ref int)
 }
 
 func (d *Document) ImportFieldDefinition(name, description string, typeRef int, argsRefs []int, directiveRefs []int) (ref int) {
-
 	fieldDef := FieldDefinition{
 		Name:        d.Input.AppendInputString(name),
 		Type:        typeRef,
diff --git a/pkg/ast/ast_node.go b/pkg/ast/ast_node.go
index 0a974a49..dc5be134 100644
--- a/pkg/ast/ast_node.go
+++ b/pkg/ast/ast_node.go
@@ -310,7 +310,13 @@ func (d *Document) NodeIsLastRootNode(node Node) bool {
 	if len(d.RootNodes) == 0 {
 		return false
 	}
-	return d.RootNodes[len(d.RootNodes)-1] == node
+	for i := len(d.RootNodes) - 1; i >= 0; i-- {
+		if d.RootNodes[i].Kind == NodeKindUnknown {
+			continue
+		}
+		return d.RootNodes[i] == node
+	}
+	return false
 }
 
 func (d *Document) RemoveNodeFromNode(remove, from Node) {
diff --git a/pkg/ast/ast_object_field.go b/pkg/ast/ast_object_field.go
index 585de784..b6013765 100644
--- a/pkg/ast/ast_object_field.go
+++ b/pkg/ast/ast_object_field.go
@@ -3,6 +3,7 @@ package ast
 import (
 	"bytes"
 
+	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafebytes"
 	"github.com/jensneuse/graphql-go-tools/pkg/lexer/position"
 )
 
@@ -23,6 +24,10 @@ func (d *Document) ObjectFieldNameBytes(ref int) ByteSlice {
 	return d.Input.ByteSlice(d.ObjectFields[ref].Name)
 }
 
+func (d *Document) ObjectFieldNameString(ref int) string {
+	return unsafebytes.BytesToString(d.Input.ByteSlice(d.ObjectFields[ref].Name))
+}
+
 func (d *Document) ObjectFieldValue(ref int) Value {
 	return d.ObjectFields[ref].Value
 }
diff --git a/pkg/ast/ast_operation_definition.go b/pkg/ast/ast_operation_definition.go
index 8d0a1405..2291e3ff 100644
--- a/pkg/ast/ast_operation_definition.go
+++ b/pkg/ast/ast_operation_definition.go
@@ -1,6 +1,8 @@
 package ast
 
 import (
+	"math"
+
 	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafebytes"
 	"github.com/jensneuse/graphql-go-tools/pkg/lexer/position"
 )
@@ -34,15 +36,6 @@ func (d *Document) OperationDefinitionNameString(ref int) string {
 	return unsafebytes.BytesToString(d.Input.ByteSlice(d.OperationDefinitions[ref].Name))
 }
 
-func (d *Document) OperationDefinitionIsLastRootNode(ref int) bool {
-	for i := range d.RootNodes {
-		if d.RootNodes[i].Kind == NodeKindOperationDefinition && d.RootNodes[i].Ref == ref {
-			return len(d.RootNodes)-1 == i
-		}
-	}
-	return false
-}
-
 func (d *Document) AddOperationDefinitionToRootNodes(definition OperationDefinition) Node {
 	d.OperationDefinitions = append(d.OperationDefinitions, definition)
 	node := Node{Kind: NodeKindOperationDefinition, Ref: len(d.OperationDefinitions) - 1}
@@ -67,3 +60,23 @@ func (d *Document) AddVariableDefinitionToOperationDefinition(operationDefinitio
 	d.OperationDefinitions[operationDefinitionRef].VariableDefinitions.Refs =
 		append(d.OperationDefinitions[operationDefinitionRef].VariableDefinitions.Refs, ref)
 }
+
+const (
+	alphabet = `abcdefghijklmnopqrstuvwxyz`
+)
+
+func (d *Document) GenerateUnusedVariableDefinitionName(operationDefinition int) []byte {
+	for i := 1;i<math.MaxInt64;i++{
+		out := make([]byte,i)
+		for j := range alphabet {
+			for k := 0;k<i;k++{
+				out[k] = alphabet[j]
+			}
+			_,exists := d.VariableDefinitionByNameAndOperation(operationDefinition,out)
+			if !exists {
+				return out
+			}
+		}
+	}
+	return nil
+}
\ No newline at end of file
diff --git a/pkg/ast/ast_value.go b/pkg/ast/ast_value.go
index 75d131dc..78e553b7 100644
--- a/pkg/ast/ast_value.go
+++ b/pkg/ast/ast_value.go
@@ -5,6 +5,9 @@ import (
 	"fmt"
 	"io"
 
+	"github.com/tidwall/sjson"
+
+	"github.com/jensneuse/graphql-go-tools/internal/pkg/quotes"
 	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafebytes"
 	"github.com/jensneuse/graphql-go-tools/pkg/lexer/literal"
 )
@@ -47,6 +50,64 @@ func (d *Document) ValueContentString(value Value) string {
 	return unsafebytes.BytesToString(d.ValueContentBytes(value))
 }
 
+func (d *Document) ValueToJSON(value Value) ([]byte, error) {
+	switch value.Kind {
+	case ValueKindNull:
+		return literal.NULL, nil
+	case ValueKindEnum:
+		return quotes.WrapBytes(d.EnumValueNameBytes(value.Ref)), nil
+	case ValueKindInteger:
+		intValueBytes := d.IntValueRaw(value.Ref)
+		if d.IntValueIsNegative(value.Ref) {
+			return append(literal.SUB, intValueBytes...), nil
+		}
+		return intValueBytes, nil
+	case ValueKindFloat:
+		floatValueBytes := d.FloatValueRaw(value.Ref)
+		if d.FloatValueIsNegative(value.Ref) {
+			return append(literal.SUB, floatValueBytes...), nil
+		}
+		return floatValueBytes, nil
+	case ValueKindBoolean:
+		if value.Ref == 0 {
+			return literal.FALSE, nil
+		}
+		return literal.TRUE, nil
+	case ValueKindString:
+		return quotes.WrapBytes(d.StringValueContentBytes(value.Ref)), nil
+	case ValueKindList:
+		out := []byte("[]")
+		for _, i := range d.ListValues[value.Ref].Refs {
+			item, err := d.ValueToJSON(d.Values[i])
+			if err != nil {
+				return nil, err
+			}
+			out, err = sjson.SetRawBytes(out, "-1", item)
+			if err != nil {
+				return nil, err
+			}
+		}
+		return out, nil
+	case ValueKindObject:
+		out := []byte("{}")
+		for i := len(d.ObjectValues[value.Ref].Refs) - 1; i >= 0; i-- {
+			ref := d.ObjectValues[value.Ref].Refs[i]
+			fieldNameString := d.ObjectFieldNameString(ref)
+			fieldValueBytes, err := d.ValueToJSON(d.ObjectFieldValue(ref))
+			if err != nil {
+				return nil, err
+			}
+			out, err = sjson.SetRawBytes(out, fieldNameString, fieldValueBytes)
+			if err != nil {
+				return nil, err
+			}
+		}
+		return out, nil
+	default:
+		return nil, fmt.Errorf("ValueToJSON: not implemented for kind: %s", value.Kind.String())
+	}
+}
+
 // nolint
 func (d *Document) PrintValue(value Value, w io.Writer) (err error) {
 	switch value.Kind {
diff --git a/pkg/ast/ast_variable_definition.go b/pkg/ast/ast_variable_definition.go
index 137ec373..f71fd640 100644
--- a/pkg/ast/ast_variable_definition.go
+++ b/pkg/ast/ast_variable_definition.go
@@ -28,8 +28,11 @@ func (d *Document) VariableDefinitionNameBytes(ref int) ByteSlice {
 	return d.VariableValueNameBytes(d.VariableDefinitions[ref].VariableValue.Ref)
 }
 
-func (d *Document) VariableDefinitionByName(name ByteSlice) (definition int, exists bool) {
-	for i := range d.VariableDefinitions {
+func (d *Document) VariableDefinitionByNameAndOperation(operationDefinition int, name ByteSlice) (definition int, exists bool) {
+	if !d.OperationDefinitions[operationDefinition].HasVariableDefinitions {
+		return -1, false
+	}
+	for _, i := range d.OperationDefinitions[operationDefinition].VariableDefinitions.Refs {
 		definitionName := d.VariableValueNameBytes(d.VariableDefinitions[i].VariableValue.Ref)
 		if bytes.Equal(name, definitionName) {
 			return i, true
@@ -39,9 +42,23 @@ func (d *Document) VariableDefinitionByName(name ByteSlice) (definition int, exi
 }
 
 func (d *Document) VariableDefinitionsBefore(variableDefinition int) bool {
-	return variableDefinition != 0
+	for i := range d.OperationDefinitions {
+		for j, k := range d.OperationDefinitions[i].VariableDefinitions.Refs {
+			if k == variableDefinition {
+				return j != 0
+			}
+		}
+	}
+	return false
 }
 
 func (d *Document) VariableDefinitionsAfter(variableDefinition int) bool {
-	return len(d.VariableDefinitions) != 1 && variableDefinition != len(d.VariableDefinitions)-1
+	for i := range d.OperationDefinitions {
+		for j, k := range d.OperationDefinitions[i].VariableDefinitions.Refs {
+			if k == variableDefinition {
+				return j != len(d.OperationDefinitions[i].VariableDefinitions.Refs)-1
+			}
+		}
+	}
+	return false
 }
diff --git a/pkg/ast/input.go b/pkg/ast/input.go
index 80c56382..ca705f7f 100644
--- a/pkg/ast/input.go
+++ b/pkg/ast/input.go
@@ -18,11 +18,14 @@ type Input struct {
 	InputPosition int
 	// TextPosition is the current position within the text (line and character information about the current Tokens)
 	TextPosition position.Position
+	// Variables are the json encoded variables of the operation
+	Variables []byte
 }
 
 // Reset empties the Input
 func (i *Input) Reset() {
 	i.RawBytes = i.RawBytes[:0]
+	i.Variables = i.Variables[:0]
 	i.InputPosition = 0
 	i.TextPosition.Reset()
 }
diff --git a/pkg/astnormalization/astnormalization.go b/pkg/astnormalization/astnormalization.go
index abfe701b..c59a7fa1 100644
--- a/pkg/astnormalization/astnormalization.go
+++ b/pkg/astnormalization/astnormalization.go
@@ -77,7 +77,7 @@ import (
 // In case you're using OperationNormalizer in a hot path you shouldn't be using this function.
 // Create a new OperationNormalizer using NewNormalizer() instead and re-use it.
 func NormalizeOperation(operation, definition *ast.Document, report *operationreport.Report) {
-	normalizer := NewNormalizer(false)
+	normalizer := NewNormalizer(false,false)
 	normalizer.NormalizeOperation(operation, definition, report)
 }
 
@@ -87,12 +87,14 @@ type registerNormalizeFunc func(walker *astvisitor.Walker)
 type OperationNormalizer struct {
 	walkers                   []*astvisitor.Walker
 	removeFragmentDefinitions bool
+	extractVariables          bool
 }
 
 // NewNormalizer creates a new OperationNormalizer and sets up all default rules
-func NewNormalizer(removeFragmentDefinitions bool) *OperationNormalizer {
+func NewNormalizer(removeFragmentDefinitions, extractVariables bool) *OperationNormalizer {
 	normalizer := &OperationNormalizer{
 		removeFragmentDefinitions: removeFragmentDefinitions,
+		extractVariables: extractVariables,
 	}
 	normalizer.setupWalkers()
 	return normalizer
@@ -108,6 +110,9 @@ func (o *OperationNormalizer) setupWalkers() {
 	mergeInlineFragments(&other)
 	mergeFieldSelections(&other)
 	deduplicateFields(&other)
+	if o.extractVariables {
+		extractVariables(&other)
+	}
 	if o.removeFragmentDefinitions {
 		removeFragmentDefinitions(&other)
 	}
diff --git a/pkg/astnormalization/variables_extraction.go b/pkg/astnormalization/variables_extraction.go
new file mode 100644
index 00000000..961e11ba
--- /dev/null
+++ b/pkg/astnormalization/variables_extraction.go
@@ -0,0 +1,84 @@
+package astnormalization
+
+import (
+	"github.com/tidwall/sjson"
+
+	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafebytes"
+	"github.com/jensneuse/graphql-go-tools/pkg/ast"
+	"github.com/jensneuse/graphql-go-tools/pkg/astimport"
+	"github.com/jensneuse/graphql-go-tools/pkg/astvisitor"
+)
+
+func extractVariables(walker *astvisitor.Walker) {
+	visitor := &variablesExtractionVisitor{
+		Walker: walker,
+	}
+	walker.RegisterEnterDocumentVisitor(visitor)
+	walker.RegisterEnterArgumentVisitor(visitor)
+}
+
+type variablesExtractionVisitor struct {
+	*astvisitor.Walker
+	operation, definition *ast.Document
+	importer              astimport.Importer
+}
+
+func (v *variablesExtractionVisitor) EnterArgument(ref int) {
+	if v.operation.Arguments[ref].Value.Kind == ast.ValueKindVariable {
+		return
+	}
+
+	if len(v.Ancestors) == 0 || v.Ancestors[0].Kind != ast.NodeKindOperationDefinition {
+		return
+	}
+
+	variableNameBytes := v.operation.GenerateUnusedVariableDefinitionName(v.Ancestors[0].Ref)
+	valueBytes, err := v.operation.ValueToJSON(v.operation.Arguments[ref].Value)
+	if err != nil {
+		v.StopWithInternalErr(err)
+		return
+	}
+	v.operation.Input.Variables, err = sjson.SetRawBytes(v.operation.Input.Variables, unsafebytes.BytesToString(variableNameBytes), valueBytes)
+	if err != nil {
+		v.StopWithInternalErr(err)
+		return
+	}
+
+	variable := ast.VariableValue{
+		Name: v.operation.Input.AppendInputBytes(variableNameBytes),
+	}
+
+	v.operation.VariableValues = append(v.operation.VariableValues, variable)
+
+	varRef := len(v.operation.VariableValues) - 1
+
+	v.operation.Arguments[ref].Value.Ref = varRef
+	v.operation.Arguments[ref].Value.Kind = ast.ValueKindVariable
+
+	defRef, ok := v.ArgumentInputValueDefinition(ref)
+	if !ok {
+		return
+	}
+
+	defType := v.definition.InputValueDefinitions[defRef].Type
+
+	importedDefType := v.importer.ImportType(defType, v.definition, v.operation)
+
+	v.operation.VariableDefinitions = append(v.operation.VariableDefinitions, ast.VariableDefinition{
+		VariableValue: ast.Value{
+			Kind: ast.ValueKindVariable,
+			Ref:  varRef,
+		},
+		Type: importedDefType,
+	})
+
+	newVariableRef := len(v.operation.VariableDefinitions) - 1
+
+	v.operation.OperationDefinitions[v.Ancestors[0].Ref].VariableDefinitions.Refs =
+		append(v.operation.OperationDefinitions[v.Ancestors[0].Ref].VariableDefinitions.Refs, newVariableRef)
+	v.operation.OperationDefinitions[v.Ancestors[0].Ref].HasVariableDefinitions = true
+}
+
+func (v *variablesExtractionVisitor) EnterDocument(operation, definition *ast.Document) {
+	v.operation, v.definition = operation, definition
+}
diff --git a/pkg/astprinter/astprinter.go b/pkg/astprinter/astprinter.go
index 4ae1d95e..9077c0a5 100644
--- a/pkg/astprinter/astprinter.go
+++ b/pkg/astprinter/astprinter.go
@@ -252,11 +252,10 @@ func (p *printVisitor) EnterOperationDefinition(ref int) {
 			p.write(literal.SPACE)
 		}
 	}
-
 }
 
 func (p *printVisitor) LeaveOperationDefinition(ref int) {
-	if !p.document.OperationDefinitionIsLastRootNode(ref) {
+	if !p.document.NodeIsLastRootNode(ast.Node{Kind: ast.NodeKindOperationDefinition, Ref: ref}) {
 		if p.indent != nil {
 			p.write(literal.LINETERMINATOR)
 			p.write(literal.LINETERMINATOR)
diff --git a/pkg/astvalidation/astvalidation.go b/pkg/astvalidation/astvalidation.go
index 4f081b25..d2e6c9a1 100644
--- a/pkg/astvalidation/astvalidation.go
+++ b/pkg/astvalidation/astvalidation.go
@@ -659,7 +659,7 @@ func (v *validArgumentsVisitor) enumValueSatisfiesInputValueDefinition(enumValue
 
 func (v *validArgumentsVisitor) variableValueSatisfiesInputValueDefinition(variableValue, inputValueDefinition int) bool {
 	variableName := v.operation.VariableValueNameBytes(variableValue)
-	variableDefinition, exists := v.operation.VariableDefinitionByName(variableName)
+	variableDefinition, exists := v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, variableName)
 	if !exists {
 		return false
 	}
@@ -742,7 +742,7 @@ func (v *valuesVisitor) EnterArgument(ref int) {
 	value := v.operation.ArgumentValue(ref)
 	if value.Kind == ast.ValueKindVariable {
 		variableName := v.operation.VariableValueNameBytes(value.Ref)
-		variableDefinition, exists := v.operation.VariableDefinitionByName(variableName)
+		variableDefinition, exists := v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, variableName)
 		if !exists {
 			operationName := v.operation.NodeNameBytes(v.Ancestors[0])
 			v.StopWithExternalErr(operationreport.ErrVariableNotDefinedOnOperation(variableName, operationName))
diff --git a/pkg/execution/datasource/datasource.go b/pkg/execution/datasource/datasource.go
index 3f9373e2..2bd49d44 100644
--- a/pkg/execution/datasource/datasource.go
+++ b/pkg/execution/datasource/datasource.go
@@ -130,11 +130,11 @@ type PlannerConfiguration struct {
 }
 
 type TypeFieldConfiguration struct {
-	TypeName                 string
-	FieldName                string
-	Mapping                  *MappingConfiguration
-	DataSource               SourceConfig `json:"data_source"`
-	DataSourcePlannerFactory PlannerFactory
+	TypeName                 string                `json:"type_name"`
+	FieldName                string                `json:"field_name"`
+	Mapping                  *MappingConfiguration `json:"mapping"`
+	DataSource               SourceConfig          `json:"data_source"`
+	DataSourcePlannerFactory PlannerFactory        `json:"-"`
 }
 
 type SourceConfig struct {
@@ -143,12 +143,12 @@ type SourceConfig struct {
 	Name string `json:"kind"`
 	// Config is the DataSource specific configuration object
 	// Each Planner needs to make sure to parse their Config Object correctly
-	Config json.RawMessage `json:"dataSourceConfig"`
+	Config json.RawMessage `json:"data_source_config"`
 }
 
 type MappingConfiguration struct {
-	Disabled bool
-	Path     string
+	Disabled bool   `json:"disabled"`
+	Path     string `json:"path"`
 }
 
 func (p *PlannerConfiguration) DataSourcePlannerFactoryForTypeField(typeName, fieldName string) PlannerFactory {
diff --git a/pkg/execution/datasource/datasource_graphql.go b/pkg/execution/datasource/datasource_graphql.go
index 16abd5e4..895db82b 100644
--- a/pkg/execution/datasource/datasource_graphql.go
+++ b/pkg/execution/datasource/datasource_graphql.go
@@ -32,11 +32,11 @@ type GraphqlRequest struct {
 // GraphQLDataSourceConfig is the configuration for the GraphQL DataSource
 type GraphQLDataSourceConfig struct {
 	// Host is the hostname of the upstream
-	Host string
+	Host string `json:"host"`
 	// URL is the url of the upstream
-	URL string
+	URL string `json:"url"`
 	// Method is the http.Method of the upstream, defaults to POST (optional)
-	Method *string
+	Method *string `json:"method"`
 }
 
 type GraphQLDataSourcePlanner struct {
diff --git a/pkg/execution/datasource/datasource_http_json.go b/pkg/execution/datasource/datasource_http_json.go
index 6eff9115..1f9ed65b 100644
--- a/pkg/execution/datasource/datasource_http_json.go
+++ b/pkg/execution/datasource/datasource_http_json.go
@@ -28,33 +28,33 @@ var httpJsonSchemes = []string{
 // HttpJsonDataSourceConfig is the configuration object for the HttpJsonDataSource
 type HttpJsonDataSourceConfig struct {
 	// Host is the hostname of the upstream
-	Host string
+	Host string `json:"host"`
 	// URL is the url of the upstream
-	URL string
+	URL string `json:"url"`
 	// Method is the http.Method, e.g. GET, POST, UPDATE, DELETE
 	// default is GET
-	Method *string
+	Method *string `json:"method"`
 	// Body is the http body to send
 	// default is null/nil (no body)
-	Body *string
+	Body *string `json:"body"`
 	// Headers defines the header mappings
-	Headers []HttpJsonDataSourceConfigHeader
+	Headers []HttpJsonDataSourceConfigHeader `json:"headers"`
 	// DefaultTypeName is the optional variable to define a default type name for the response object
 	// This is useful in case the response might be a Union or Interface type which uses StatusCodeTypeNameMappings
-	DefaultTypeName *string
+	DefaultTypeName *string `json:"default_type_name"`
 	// StatusCodeTypeNameMappings is a slice of mappings from http.StatusCode to GraphQL TypeName
 	// This can be used when the TypeName depends on the http.StatusCode
-	StatusCodeTypeNameMappings []StatusCodeTypeNameMapping
+	StatusCodeTypeNameMappings []StatusCodeTypeNameMapping `json:"status_code_type_name_mappings"`
 }
 
 type StatusCodeTypeNameMapping struct {
-	StatusCode int
-	TypeName   string
+	StatusCode int    `json:"status_code"`
+	TypeName   string `json:"type_name"`
 }
 
 type HttpJsonDataSourceConfigHeader struct {
-	Key   string
-	Value string
+	Key   string `json:"key"`
+	Value string `json:"value"`
 }
 
 type HttpJsonDataSourcePlannerFactoryFactory struct {
diff --git a/pkg/execution/handler.go b/pkg/execution/handler.go
index b3760f09..863ddea8 100644
--- a/pkg/execution/handler.go
+++ b/pkg/execution/handler.go
@@ -71,13 +71,13 @@ func (h *Handler) Handle(requestData, extraVariables []byte) (executor *Executor
 		err = report
 		return
 	}
-	normalizer := astnormalization.NewNormalizer(true)
+	normalizer := astnormalization.NewNormalizer(true,true)
 	normalizer.NormalizeOperation(&operationDocument, h.base.Definition, &report)
 	if report.HasErrors() {
 		err = report
 		return
 	}
-	plan := planner.Plan(&operationDocument, h.base.Definition, &report)
+	plan := planner.Plan(&operationDocument, h.base.Definition, graphqlRequest.OperationName, &report)
 	if report.HasErrors() {
 		err = report
 		return
diff --git a/pkg/execution/planning.go b/pkg/execution/planning.go
index f7ea1f57..fcae8987 100644
--- a/pkg/execution/planning.go
+++ b/pkg/execution/planning.go
@@ -36,13 +36,14 @@ func NewPlanner(base *datasource.BasePlanner) *Planner {
 		base:   base,
 	}
 
-	walker.RegisterEnterDocumentVisitor(&visitor)
+	walker.RegisterDocumentVisitor(&visitor)
 	walker.RegisterEnterFieldVisitor(&visitor)
 	walker.RegisterLeaveFieldVisitor(&visitor)
 	walker.RegisterEnterSelectionSetVisitor(&visitor)
 	walker.RegisterLeaveSelectionSetVisitor(&visitor)
 	walker.RegisterEnterInlineFragmentVisitor(&visitor)
 	walker.RegisterLeaveInlineFragmentVisitor(&visitor)
+	walker.RegisterEnterOperationVisitor(&visitor)
 
 	return &Planner{
 		walker:  &walker,
@@ -50,7 +51,8 @@ func NewPlanner(base *datasource.BasePlanner) *Planner {
 	}
 }
 
-func (p *Planner) Plan(operation, definition *ast.Document, report *operationreport.Report) RootNode {
+func (p *Planner) Plan(operation, definition *ast.Document, operationName string, report *operationreport.Report) RootNode {
+	p.visitor.operationName = operationName
 	p.walker.Walk(operation, definition, report)
 	return p.visitor.rootNode
 }
@@ -62,6 +64,9 @@ type planningVisitor struct {
 	rootNode              RootNode
 	currentNode           []Node
 	planners              []dataSourcePlannerRef
+	operationName         string
+	foundOperation        bool
+	isSingleOperation     bool
 }
 
 type dataSourcePlannerRef struct {
@@ -72,15 +77,36 @@ type dataSourcePlannerRef struct {
 
 func (p *planningVisitor) EnterDocument(operation, definition *ast.Document) {
 	p.operation, p.definition, p.base.Definition = operation, definition, definition
+	p.foundOperation = false
+	p.isSingleOperation = p.countOperationDefinitionsInRootNodes() == 1
 
 	if len(operation.OperationDefinitions) == 0 {
 		p.Walker.StopWithExternalErr(operationreport.ErrDocumentDoesntContainExecutableOperation())
 		return
 	}
 
+	p.currentNode = p.currentNode[:0]
+}
+
+func (p *planningVisitor) LeaveDocument(operation, definition *ast.Document) {
+	if !p.isSingleOperation && len(p.operationName) == 0 {
+		p.Report.AddExternalError(operationreport.ErrRequiredOperationNameIsMissing())
+	} else if !p.foundOperation {
+		p.Report.AddExternalError(operationreport.ErrOperationWithProvidedOperationNameNotFound(p.operationName))
+	}
+}
+
+func (p *planningVisitor) EnterOperationDefinition(ref int) {
+	operationName := p.operation.OperationDefinitionNameString(ref)
+	if !p.isSingleOperation && operationName != p.operationName {
+		p.SkipNode()
+		return
+	}
+
+	p.foundOperation = true
 	obj := &Object{}
 	p.rootNode = &Object{
-		operationType: operation.OperationDefinitions[0].OperationType,
+		operationType: p.operation.OperationDefinitions[ref].OperationType,
 		Fields: []Field{
 			{
 				Name:  literal.DATA,
@@ -88,7 +114,6 @@ func (p *planningVisitor) EnterDocument(operation, definition *ast.Document) {
 			},
 		},
 	}
-	p.currentNode = p.currentNode[:0]
 	p.currentNode = append(p.currentNode, obj)
 }
 
@@ -388,3 +413,13 @@ func (p *planningVisitor) pipelineTransformation(directive int) *PipelineTransfo
 		pipeline: pipeline,
 	}
 }
+
+func (p *planningVisitor) countOperationDefinitionsInRootNodes() (count int) {
+	for i := range p.operation.RootNodes {
+		if p.operation.RootNodes[i].Kind == ast.NodeKindOperationDefinition {
+			count++
+		}
+	}
+
+	return count
+}
diff --git a/pkg/graphql/execution_engine.go b/pkg/graphql/execution_engine.go
index 5403c3e0..64b24948 100644
--- a/pkg/graphql/execution_engine.go
+++ b/pkg/graphql/execution_engine.go
@@ -112,7 +112,7 @@ func (e *ExecutionEngine) ExecuteWithWriter(ctx context.Context, operation *Requ
 	}
 
 	planner := execution.NewPlanner(e.basePlanner)
-	plan := planner.Plan(&operation.document, e.basePlanner.Definition, &report)
+	plan := planner.Plan(&operation.document, e.basePlanner.Definition, operation.OperationName, &report)
 	if report.HasErrors() {
 		return report
 	}
diff --git a/pkg/graphql/request.go b/pkg/graphql/request.go
index 91a266fd..deb07572 100644
--- a/pkg/graphql/request.go
+++ b/pkg/graphql/request.go
@@ -20,7 +20,7 @@ var (
 )
 
 type Request struct {
-	OperationName string          `json:"operation_name"`
+	OperationName string          `json:"operationName"`
 	Variables     json.RawMessage `json:"variables"`
 	Query         string          `json:"query"`
 
@@ -67,13 +67,18 @@ func (r *Request) Normalize(schema *Schema) (result NormalizationResult, err err
 		return normalizationResultFromReport(report)
 	}
 
-	normalizer := astnormalization.NewNormalizer(true)
+	r.document.Input.Variables = r.Variables
+
+	normalizer := astnormalization.NewNormalizer(true, true)
 	normalizer.NormalizeOperation(&r.document, &schema.document, &report)
 	if report.HasErrors() {
 		return normalizationResultFromReport(report)
 	}
 
 	r.isNormalized = true
+
+	r.Variables = r.document.Input.Variables
+
 	return NormalizationResult{Successful: true, Errors: nil}, nil
 }
 
diff --git a/pkg/operationreport/externalerror.go b/pkg/operationreport/externalerror.go
index c0928c00..03aeb1e7 100644
--- a/pkg/operationreport/externalerror.go
+++ b/pkg/operationreport/externalerror.go
@@ -17,7 +17,7 @@ type Location struct {
 	Column uint32 `json:"column"`
 }
 
-func ErrDocumentDoesntContainExecutableOperation() (err ExternalError){
+func ErrDocumentDoesntContainExecutableOperation() (err ExternalError) {
 	err.Message = "document doesn't contain any executable operation"
 	return
 }
@@ -42,6 +42,16 @@ func ErrAnonymousOperationMustBeTheOnlyOperationInDocument() (err ExternalError)
 	return err
 }
 
+func ErrRequiredOperationNameIsMissing() (err ExternalError) {
+	err.Message = "operation name is required when providing multiple operations"
+	return err
+}
+
+func ErrOperationWithProvidedOperationNameNotFound(operationName string) (err ExternalError) {
+	err.Message = fmt.Sprintf("cannot find an operation with name: %s", operationName)
+	return err
+}
+
 func ErrSubscriptionMustOnlyHaveOneRootSelection(subscriptionName ast.ByteSlice) (err ExternalError) {
 	err.Message = fmt.Sprintf("subscription: %s must only have one root selection", subscriptionName)
 	return err
diff --git a/pkg/starwars/starwars.go b/pkg/starwars/starwars.go
index 6095c287..4e4942f9 100644
--- a/pkg/starwars/starwars.go
+++ b/pkg/starwars/starwars.go
@@ -19,6 +19,7 @@ const (
 	FileSimpleHeroQuery            = "testdata/queries/simple_hero.query"
 	FileHeroWithOperationNameQuery = "testdata/queries/hero_with_operation_name.query"
 	FileHeroWithAliasesQuery       = "testdata/queries/hero_with_aliases.query"
+	FileDroidWithArgQuery          = "testdata/queries/droid_with_arg.query"
 	FileDroidWithArgAndVarQuery    = "testdata/queries/droid_with_arg_and_var.query"
 	FileFragmentsQuery             = "testdata/queries/fragments.query"
 	FileDirectivesIncludeQuery     = "testdata/queries/directives_include.query"
@@ -28,6 +29,7 @@ const (
 	FileUnionQuery                 = "testdata/queries/inline_fragments.query"
 	FileRemainingJedisSubscription = "testdata/subscriptions/remaining_jedis.subscription"
 	FileIntrospectionQuery         = "testdata/queries/introspection.query"
+	FileMultiQueries               = "testdata/queries/multi_queries.query"
 )
 
 var (


Test patch:
diff --git a/pkg/ast/ast_value_test.go b/pkg/ast/ast_value_test.go
new file mode 100644
index 00000000..15ac81a5
--- /dev/null
+++ b/pkg/ast/ast_value_test.go
@@ -0,0 +1,175 @@
+package ast
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDocument_ValueToJSON(t *testing.T) {
+	run := func(prepareDoc func(doc *Document) Value, expectedOutput string) func(t *testing.T) {
+		operation := NewDocument()
+		return func(t *testing.T) {
+			out, err := operation.ValueToJSON(prepareDoc(operation))
+			assert.NoError(t, err)
+			assert.Equal(t, expectedOutput, string(out))
+		}
+	}
+
+	t.Run("ValueKindNull", run(func(doc *Document) Value {
+		return Value{
+			Kind: ValueKindNull,
+			Ref:  0,
+		}
+	}, `null`))
+	t.Run("ValueKindEnum", run(func(doc *Document) Value {
+		doc.EnumValues = append(doc.EnumValues, EnumValue{
+			Name: doc.Input.AppendInputString("FOO"),
+		})
+		return Value{
+			Kind: ValueKindEnum,
+			Ref:  0,
+		}
+	}, `"FOO"`))
+	t.Run("ValueKindInteger - positive", run(func(doc *Document) Value {
+		doc.IntValues = append(doc.IntValues, IntValue{
+			Raw: doc.Input.AppendInputString("123"),
+		})
+		return Value{
+			Kind: ValueKindInteger,
+			Ref:  0,
+		}
+	}, `123`))
+	t.Run("ValueKindInteger - negative", run(func(doc *Document) Value {
+		doc.IntValues = append(doc.IntValues, IntValue{
+			Raw:      doc.Input.AppendInputString("123"),
+			Negative: true,
+		})
+		return Value{
+			Kind: ValueKindInteger,
+			Ref:  0,
+		}
+	}, `-123`))
+	t.Run("ValueKindFloat - positive", run(func(doc *Document) Value {
+		doc.FloatValues = append(doc.FloatValues, FloatValue{
+			Raw: doc.Input.AppendInputString("12.34"),
+		})
+		return Value{
+			Kind: ValueKindFloat,
+			Ref:  0,
+		}
+	}, `12.34`))
+	t.Run("ValueKindFloat - negative", run(func(doc *Document) Value {
+		doc.FloatValues = append(doc.FloatValues, FloatValue{
+			Raw:      doc.Input.AppendInputString("12.34"),
+			Negative: true,
+		})
+		return Value{
+			Kind: ValueKindFloat,
+			Ref:  0,
+		}
+	}, `-12.34`))
+	t.Run("ValueKindBoolean - false", run(func(doc *Document) Value {
+		return Value{
+			Kind: ValueKindBoolean,
+			Ref:  0,
+		}
+	}, `false`))
+	t.Run("ValueKindBoolean - true", run(func(doc *Document) Value {
+		return Value{
+			Kind: ValueKindBoolean,
+			Ref:  1,
+		}
+	}, `true`))
+	t.Run("ValueKindString", run(func(doc *Document) Value {
+		doc.StringValues = append(doc.StringValues, StringValue{
+			Content: doc.Input.AppendInputString("foo"),
+		})
+		return Value{
+			Kind: ValueKindString,
+			Ref:  0,
+		}
+	}, `"foo"`))
+	t.Run("ValueKindList", run(func(doc *Document) Value {
+		doc.StringValues = append(doc.StringValues, StringValue{
+			Content: doc.Input.AppendInputString("foo"),
+		})
+		doc.StringValues = append(doc.StringValues, StringValue{
+			Content: doc.Input.AppendInputString("bar"),
+		})
+		doc.StringValues = append(doc.StringValues, StringValue{
+			Content: doc.Input.AppendInputString("baz"),
+		})
+		for i := 0; i < 3; i++ {
+			doc.Values = append(doc.Values, Value{Kind: ValueKindString, Ref: i})
+		}
+		doc.IntValues = append(doc.IntValues, IntValue{
+			Raw: doc.Input.AppendInputString("123"),
+		})
+		doc.Values = append(doc.Values, Value{Kind: ValueKindInteger, Ref: 0})
+		doc.ListValues = append(doc.ListValues, ListValue{
+			Refs: []int{0, 1, 2, 3},
+		})
+		return Value{
+			Kind: ValueKindList,
+			Ref:  0,
+		}
+	}, `["foo","bar","baz",123]`))
+	t.Run("ValueKindObject", run(func(doc *Document) Value {
+		doc.StringValues = append(doc.StringValues, StringValue{
+			Content: doc.Input.AppendInputString("bar"),
+		})
+		doc.StringValues = append(doc.StringValues, StringValue{
+			Content: doc.Input.AppendInputString("bal"),
+		})
+		doc.ObjectFields = append(doc.ObjectFields, ObjectField{
+			Name: doc.Input.AppendInputString("bat"),
+			Value: Value{
+				Kind: ValueKindString,
+				Ref:  1,
+			},
+		})
+		doc.ObjectFields = append(doc.ObjectFields,
+			ObjectField{
+				Name: doc.Input.AppendInputString("foo"),
+				Value: Value{
+					Kind: ValueKindString,
+					Ref:  0,
+				},
+			},
+			ObjectField{
+				Name: doc.Input.AppendInputString("baz"),
+				Value: Value{
+					Kind: ValueKindObject,
+					Ref:  1,
+				},
+			})
+		for i := 0; i < 3; i++ {
+			doc.IntValues = append(doc.IntValues, IntValue{
+				Raw: doc.Input.AppendInputString(strconv.Itoa(i + 1)),
+			})
+			doc.Values = append(doc.Values, Value{Kind: ValueKindInteger, Ref: i})
+		}
+		doc.ListValues = append(doc.ListValues, ListValue{
+			Refs: []int{0, 1, 2},
+		})
+		doc.ObjectFields = append(doc.ObjectFields, ObjectField{
+			Name: doc.Input.AppendInputString("list"),
+			Value: Value{
+				Kind: ValueKindList,
+				Ref:  0,
+			},
+		})
+		doc.ObjectValues = append(doc.ObjectValues, ObjectValue{
+			Refs: []int{1, 2, 3},
+		})
+		doc.ObjectValues = append(doc.ObjectValues, ObjectValue{
+			Refs: []int{0},
+		})
+		return Value{
+			Kind: ValueKindObject,
+			Ref:  0,
+		}
+	}, `{"foo":"bar","baz":{"bat":"bal"},"list":[1,2,3]}`))
+}
diff --git a/pkg/astnormalization/astnormalization_test.go b/pkg/astnormalization/astnormalization_test.go
index 5f80386b..7551419b 100644
--- a/pkg/astnormalization/astnormalization_test.go
+++ b/pkg/astnormalization/astnormalization_test.go
@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+
 	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafeparser"
 	"github.com/jensneuse/graphql-go-tools/pkg/astprinter"
 	"github.com/jensneuse/graphql-go-tools/pkg/astvisitor"
@@ -12,13 +14,19 @@ import (
 
 func TestNormalizeOperation(t *testing.T) {
 
-	run := func(definition, operation, expectedOutput string) {
+	run := func(t *testing.T, definition, operation, expectedOutput, variablesInput, expectedVariables string) {
 		definitionDocument := unsafeparser.ParseGraphqlDocumentString(definition)
 		operationDocument := unsafeparser.ParseGraphqlDocumentString(operation)
 		expectedOutputDocument := unsafeparser.ParseGraphqlDocumentString(expectedOutput)
 		report := operationreport.Report{}
 
-		NormalizeOperation(&operationDocument, &definitionDocument, &report)
+		if variablesInput != "" {
+			operationDocument.Input.Variables = []byte(variablesInput)
+		}
+
+
+		normalizer := NewNormalizer(true,true)
+		normalizer.NormalizeOperation(&operationDocument, &definitionDocument, &report)
 
 		if report.HasErrors() {
 			t.Fatal(report.Error())
@@ -27,13 +35,12 @@ func TestNormalizeOperation(t *testing.T) {
 		got := mustString(astprinter.PrintString(&operationDocument, &definitionDocument))
 		want := mustString(astprinter.PrintString(&expectedOutputDocument, &definitionDocument))
 
-		if want != got {
-			panic(fmt.Errorf("\nwant:\n%s\ngot:\n%s", want, got))
-		}
+		assert.Equal(t, want,got)
+		assert.Equal(t, expectedVariables, string(operationDocument.Input.Variables))
 	}
 
 	t.Run("complex", func(t *testing.T) {
-		run(testDefinition, `	
+		run(t, testDefinition, `	
 				subscription sub {
 					... multipleSubscriptions
 					... on Subscription {
@@ -77,21 +84,10 @@ func TestNormalizeOperation(t *testing.T) {
 						sender
 					}
 					disallowedSecondRootField
-				}
-				fragment newMessageFields on Message {
-					body
-					sender
-				}
-				fragment multipleSubscriptions on Subscription {
-						newMessage {
-							body
-							sender
-						}
-						disallowedSecondRootField
-				}`)
+				}`, "", "")
 	})
 	t.Run("fragments", func(t *testing.T) {
-		run(testDefinition, `
+		run(t, testDefinition, `
 				query conflictingBecauseAlias {
 					dog {
 						extras { ...frag }
@@ -107,9 +103,30 @@ func TestNormalizeOperation(t *testing.T) {
 							string1: string
 						}
 					}
+				}`, "", "")
+	})
+	t.Run("fragments", func(t *testing.T) {
+		run(t, variablesExtractionDefinition, `
+			mutation HttpBinPost{
+			  httpBinPost(input: {foo: "bar"}){
+				headers {
+				  userAgent
 				}
-				fragment frag on DogExtra { string1 }
-				fragment frag2 on DogExtra { string1: string }`)
+				data {
+				  foo
+				}
+			  }
+			}`, `
+			mutation HttpBinPost($a: HttpBinPostInput){
+			  httpBinPost(input: $a){
+				headers {
+				  userAgent
+				}
+				data {
+				  foo
+				}
+			  }
+			}`,``,`{"a":{"foo":"bar"}}`)
 	})
 }
 
@@ -119,7 +136,7 @@ func BenchmarkAstNormalization(b *testing.B) {
 	operation := unsafeparser.ParseGraphqlDocumentString(testOperation)
 	report := operationreport.Report{}
 
-	normalizer := NewNormalizer(false)
+	normalizer := NewNormalizer(false,false)
 
 	b.ResetTimer()
 	b.ReportAllocs()
@@ -137,6 +154,32 @@ var mustString = func(str string, err error) string {
 	return str
 }
 
+var runWithVariables = func(t *testing.T, normalizeFunc registerNormalizeFunc, definition, operation, expectedOutput, variablesInput, expectedVariables string) {
+	definitionDocument := unsafeparser.ParseGraphqlDocumentString(definition)
+	operationDocument := unsafeparser.ParseGraphqlDocumentString(operation)
+	expectedOutputDocument := unsafeparser.ParseGraphqlDocumentString(expectedOutput)
+	report := operationreport.Report{}
+	walker := astvisitor.NewWalker(48)
+
+	if variablesInput != "" {
+		operationDocument.Input.Variables = []byte(variablesInput)
+	}
+
+	normalizeFunc(&walker)
+
+	walker.Walk(&operationDocument, &definitionDocument, &report)
+
+	if report.HasErrors() {
+		panic(report.Error())
+	}
+
+	actualAST := mustString(astprinter.PrintString(&operationDocument, &definitionDocument))
+	expectedAST := mustString(astprinter.PrintString(&expectedOutputDocument, &definitionDocument))
+	assert.Equal(t, expectedAST, actualAST)
+	actualVariables := string(operationDocument.Input.Variables)
+	assert.Equal(t, expectedVariables, actualVariables)
+}
+
 var run = func(normalizeFunc registerNormalizeFunc, definition, operation, expectedOutput string) {
 
 	definitionDocument := unsafeparser.ParseGraphqlDocumentString(definition)
diff --git a/pkg/astnormalization/variables_extraction_test.go b/pkg/astnormalization/variables_extraction_test.go
new file mode 100644
index 00000000..f60bad8d
--- /dev/null
+++ b/pkg/astnormalization/variables_extraction_test.go
@@ -0,0 +1,57 @@
+package astnormalization
+
+import (
+	"testing"
+)
+
+const (
+	variablesExtractionDefinition = `
+		schema { mutation: Mutation }
+		type Mutation {
+			httpBinPost(input: HttpBinPostInput): HttpBinPostResponse
+		}
+		input HttpBinPostInput {
+			foo: String!
+		}
+		type HttpBinPostResponse {
+			headers: Headers
+			data: HttpBinPostResponseData
+		}
+		type HttpBinPostResponseData {
+			foo: String
+		}
+		type Headers {
+			userAgent: String!
+			host: String!
+			acceptEncoding: String
+			Authorization: String
+		}
+		scalar String
+	`
+)
+
+func TestVariablesExtraction(t *testing.T) {
+	t.Run("simple http bin example", func(t *testing.T) {
+		runWithVariables(t, extractVariables, variablesExtractionDefinition, `
+			mutation HttpBinPost{
+			  httpBinPost(input: {foo: "bar"}){
+				headers {
+				  userAgent
+				}
+				data {
+				  foo
+				}
+			  }
+			}`, `
+			mutation HttpBinPost($a: HttpBinPostInput){
+			  httpBinPost(input: $a){
+				headers {
+				  userAgent
+				}
+				data {
+				  foo
+				}
+			  }
+			}`,``,`{"a":{"foo":"bar"}}`)
+	})
+}
diff --git a/pkg/astprinter/astprinter_test.go b/pkg/astprinter/astprinter_test.go
index 4134c842..459c94ae 100644
--- a/pkg/astprinter/astprinter_test.go
+++ b/pkg/astprinter/astprinter_test.go
@@ -190,6 +190,25 @@ func TestPrint(t *testing.T) {
 					BAZ
 				}`, `extend enum Foo @foo {BAR BAZ}`)
 	})
+	t.Run("multiple operations with variables", func(t *testing.T) {
+		run(`
+				mutation AddToWatchlist($a: Int!, $b: String!){
+					addToWatchlist(movieID: $a, name: $b){
+						id
+						name
+						year
+					}
+				}
+
+				mutation AddWithInput($a: WatchlistInput!){
+    				addToWatchlistWithInput(input: $a){
+						id
+						name
+						year
+					}
+				}`,
+			`mutation AddToWatchlist($a: Int!, $b: String!){addToWatchlist(movieID: $a, name: $b){id name year}} mutation AddWithInput($a: WatchlistInput!){addToWatchlistWithInput(input: $a){id name year}}`)
+	})
 }
 
 func TestPrintSchemaDefinition(t *testing.T) {
diff --git a/pkg/astvalidation/overlapping_fields_test.go b/pkg/astvalidation/overlapping_fields_test.go
index 6b80d2a5..d4bcd8d2 100644
--- a/pkg/astvalidation/overlapping_fields_test.go
+++ b/pkg/astvalidation/overlapping_fields_test.go
@@ -24,7 +24,7 @@ func TestOverlappingFieldsCanBeMerged(t *testing.T) {
 		definition := unsafeparser.ParseGraphqlDocumentBytes(definitionBytes)
 		operation := unsafeparser.ParseGraphqlDocumentBytes(operationBytes)
 		report := operationreport.Report{}
-		normalizer := astnormalization.NewNormalizer(false)
+		normalizer := astnormalization.NewNormalizer(false,false)
 		validator := DefaultOperationValidator()
 
 		normalizer.NormalizeOperation(&operation, &definition, &report)
@@ -45,7 +45,7 @@ func TestOverlappingFieldsCanBeMerged(t *testing.T) {
 		definition := unsafeparser.ParseGraphqlDocumentBytes(definitionBytes)
 		operation := unsafeparser.ParseGraphqlDocumentBytes(operationBytes)
 		report := operationreport.Report{}
-		normalizer := astnormalization.NewNormalizer(false)
+		normalizer := astnormalization.NewNormalizer(false,false)
 		validator := DefaultOperationValidator()
 
 		normalizer.NormalizeOperation(&operation, &definition, &report)
@@ -70,7 +70,7 @@ func BenchmarkOverlappingFieldsCanBeMerged(b *testing.B) {
 			definition := unsafeparser.ParseGraphqlDocumentBytes(definitionBytes)
 			operation := unsafeparser.ParseGraphqlDocumentBytes(operationBytes)
 			report := operationreport.Report{}
-			normalizer := astnormalization.NewNormalizer(false)
+			normalizer := astnormalization.NewNormalizer(false,false)
 			validator := DefaultOperationValidator()
 
 			b.ReportAllocs()
@@ -96,7 +96,7 @@ func BenchmarkOverlappingFieldsCanBeMerged(b *testing.B) {
 			definition := unsafeparser.ParseGraphqlDocumentBytes(definitionBytes)
 			operation := unsafeparser.ParseGraphqlDocumentBytes(operationBytes)
 			report := operationreport.Report{}
-			normalizer := astnormalization.NewNormalizer(false)
+			normalizer := astnormalization.NewNormalizer(false,false)
 			validator := DefaultOperationValidator()
 
 			b.ReportAllocs()
diff --git a/pkg/execution/planning_test.go b/pkg/execution/planning_test.go
index d00f12ea..5181db08 100644
--- a/pkg/execution/planning_test.go
+++ b/pkg/execution/planning_test.go
@@ -14,6 +14,8 @@ import (
 	log "github.com/jensneuse/abstractlogger"
 	"github.com/jensneuse/diffview"
 	"github.com/jensneuse/pipeline/pkg/pipe"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafeparser"
 	"github.com/jensneuse/graphql-go-tools/pkg/ast"
@@ -46,7 +48,7 @@ func panicOnErr(err error) {
 	}
 }
 
-func run(definition string, operation string, configureBase func(base *datasource.BasePlanner), want Node, skip ...bool) func(t *testing.T) {
+func runWithOperationName(definition string, operation string, operationName string, configureBase func(base *datasource.BasePlanner), want Node, skip ...bool) func(t *testing.T) {
 	return func(t *testing.T) {
 
 		if len(skip) == 1 && skip[0] {
@@ -57,7 +59,7 @@ func run(definition string, operation string, configureBase func(base *datasourc
 		op := unsafeparser.ParseGraphqlDocumentString(operation)
 
 		var report operationreport.Report
-		normalizer := astnormalization.NewNormalizer(true)
+		normalizer := astnormalization.NewNormalizer(true, true)
 		normalizer.NormalizeOperation(&op, &def, &report)
 		if report.HasErrors() {
 			t.Error(report)
@@ -71,7 +73,7 @@ func run(definition string, operation string, configureBase func(base *datasourc
 		configureBase(base)
 
 		planner := NewPlanner(base)
-		got := planner.Plan(&op, &def, &report)
+		got := planner.Plan(&op, &def, operationName, &report)
 		if report.HasErrors() {
 			t.Error(report)
 		}
@@ -84,6 +86,42 @@ func run(definition string, operation string, configureBase func(base *datasourc
 	}
 }
 
+func run(definition string, operation string, configureBase func(base *datasource.BasePlanner), want Node, skip ...bool) func(t *testing.T) {
+	return runWithOperationName(definition, operation, "", configureBase, want, skip...)
+}
+
+func runAndReportExternalErrorWithOperationName(definition string, operation string, operationName string, configureBase func(base *datasource.BasePlanner), expectedError operationreport.ExternalError, skip ...bool) func(t *testing.T) {
+	return func(t *testing.T) {
+
+		if len(skip) == 1 && skip[0] {
+			return
+		}
+
+		def := unsafeparser.ParseGraphqlDocumentString(definition)
+		op := unsafeparser.ParseGraphqlDocumentString(operation)
+
+		var report operationreport.Report
+		normalizer := astnormalization.NewNormalizer(true, true)
+		normalizer.NormalizeOperation(&op, &def, &report)
+		if report.HasErrors() {
+			t.Error(report)
+		}
+
+		base, err := datasource.NewBaseDataSourcePlanner([]byte(definition), datasource.PlannerConfiguration{}, log.NoopLogger)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		configureBase(base)
+
+		planner := NewPlanner(base)
+		_ = planner.Plan(&op, &def, operationName, &report)
+		assert.Error(t, report)
+		require.Greater(t, len(report.ExternalErrors), 0)
+		assert.Equal(t, expectedError.Message, report.ExternalErrors[0].Message)
+	}
+}
+
 func TestPlanner_Plan(t *testing.T) {
 
 	t.Run("GraphQLDataSource", run(withBaseSchema(GraphQLDataSourceSchema), `
@@ -3381,6 +3419,219 @@ func TestPlanner_Plan(t *testing.T) {
 			},
 		))
 	})
+
+	t.Run("operation selection", func(t *testing.T) {
+		var allCountriesPlannerConfig = func(base *datasource.BasePlanner) {
+			base.Config = datasource.PlannerConfiguration{
+				TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
+					{
+						TypeName:  "query",
+						FieldName: "countries",
+						Mapping: &datasource.MappingConfiguration{
+							Disabled: true,
+						},
+					},
+				},
+			}
+		}
+		var addLanguagePlannerConfig = func(base *datasource.BasePlanner) {
+			base.Config = datasource.PlannerConfiguration{
+				TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
+					{
+						TypeName:  "Mutation",
+						FieldName: "addLanguage",
+						Mapping: &datasource.MappingConfiguration{
+							Disabled: true,
+						},
+					},
+				},
+			}
+		}
+		var allCountriesQueryNode = &Object{
+			operationType: ast.OperationTypeQuery,
+			Fields: []Field{
+				{
+					Name: []byte("data"),
+					Value: &Object{
+						Fields: []Field{
+							{
+								Name: []byte("countries"),
+								Value: &List{
+									Value: &Object{
+										Fields: []Field{
+											{
+												Name: []byte("code"),
+												Value: &Value{
+													ValueType: StringValueType,
+													DataResolvingConfig: DataResolvingConfig{
+														PathSelector: datasource.PathSelector{
+															Path: "code",
+														},
+													},
+												},
+											},
+											{
+												Name: []byte("name"),
+												Value: &Value{
+													ValueType: StringValueType,
+													DataResolvingConfig: DataResolvingConfig{
+														PathSelector: datasource.PathSelector{
+															Path: "name",
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		var addLanguageMutationNode = &Object{
+			operationType: ast.OperationTypeMutation,
+			Fields: []Field{
+				{
+					Name: []byte("data"),
+					Value: &Object{
+						Fields: []Field{
+							{
+								Name: []byte("addLanguage"),
+								Value: &Object{
+									Fields: []Field{
+										{
+											Name: []byte("code"),
+											Value: &Value{
+												ValueType: StringValueType,
+												DataResolvingConfig: DataResolvingConfig{
+													PathSelector: datasource.PathSelector{
+														Path: "code",
+													},
+												},
+											},
+										},
+										{
+											Name: []byte("name"),
+											Value: &Value{
+												ValueType: StringValueType,
+												DataResolvingConfig: DataResolvingConfig{
+													PathSelector: datasource.PathSelector{
+														Path: "name",
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+
+		t.Run("select single anonymous operation (case 1a in GraphQL spec)", runWithOperationName(countriesSchema, `
+			{
+				countries {
+					code
+					name
+				}
+			}`,
+			"",
+			allCountriesPlannerConfig,
+			allCountriesQueryNode,
+		))
+
+		t.Run("select single named operation without name (case 1a in GraphQL spec)", runWithOperationName(countriesSchema, `
+			query AllCountries {
+				countries {
+					code
+					name
+				}
+			}`,
+			"",
+			allCountriesPlannerConfig,
+			allCountriesQueryNode,
+		))
+
+		t.Run("select named operation / query with name (case 2a in GraphQL spec)", runWithOperationName(countriesSchema, `
+			query AllContinents {
+				continents {
+					code
+					name
+				}
+			}
+
+			query AllCountries {
+				countries {
+					code
+					name
+				}
+			}`,
+			"AllCountries",
+			allCountriesPlannerConfig,
+			allCountriesQueryNode,
+		))
+
+		t.Run("select named operation / mutation with name (case 2a in GraphQL spec)", runWithOperationName(countriesSchema, `
+			query AllContinents {
+				continents {
+					code
+					name
+				}
+			}
+
+			mutation AddLanguage {
+				addLanguage(code: "GO", name: "go") {
+					code
+					name
+				}
+			}`,
+			"AddLanguage",
+			addLanguagePlannerConfig,
+			addLanguageMutationNode,
+		))
+
+		t.Run("return error when multiple operations are available and operation name was not provided (case 1b in GraphQL spec)", runAndReportExternalErrorWithOperationName(countriesSchema, `
+			query AllContinents {
+				continents {
+					code
+					name
+				}
+			}
+
+			query AllCountries {
+				countries {
+					code
+					name
+				}
+			}`,
+			"",
+			allCountriesPlannerConfig,
+			operationreport.ErrRequiredOperationNameIsMissing(),
+		))
+
+		t.Run("return error when multiple operations are available and operation name does not match (case 2b in GraphQL spec)", runAndReportExternalErrorWithOperationName(countriesSchema, `
+			query AllContinents {
+				continents {
+					code
+					name
+				}
+			}
+
+			query AllCountries {
+				countries {
+					code
+					name
+				}
+			}`,
+			"NoQuery",
+			allCountriesPlannerConfig,
+			operationreport.ErrOperationWithProvidedOperationNameNotFound("NoQuery"),
+		))
+	})
 }
 
 func BenchmarkPlanner_Plan(b *testing.B) {
@@ -3480,7 +3731,7 @@ func BenchmarkPlanner_Plan(b *testing.B) {
 	b.ReportAllocs()
 
 	for i := 0; i < b.N; i++ {
-		planner.Plan(&op, &def, &report)
+		planner.Plan(&op, &def, "", &report)
 		if report.HasErrors() {
 			b.Fatal(report)
 		}
@@ -3766,6 +4017,94 @@ type Query {
 }
 `
 
+const countriesSchema = `directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE
+
+scalar String
+scalar ID
+scalar Boolean
+
+schema {
+	query: Query
+	mutation: Mutation
+}
+
+enum CacheControlScope {
+  PUBLIC
+  PRIVATE
+}
+
+type Continent {
+  code: ID!
+  name: String!
+  countries: [Country!]!
+}
+
+input ContinentFilterInput {
+  code: StringQueryOperatorInput
+}
+
+type Country {
+  code: ID!
+  name: String!
+  native: String!
+  phone: String!
+  continent: Continent!
+  capital: String
+  currency: String
+  languages: [Language!]!
+  emoji: String!
+  emojiU: String!
+  states: [State!]!
+}
+
+input CountryFilterInput {
+  code: StringQueryOperatorInput
+  currency: StringQueryOperatorInput
+  continent: StringQueryOperatorInput
+}
+
+type Language {
+  code: ID!
+  name: String
+  native: String
+  rtl: Boolean!
+}
+
+input LanguageFilterInput {
+  code: StringQueryOperatorInput
+}
+
+type Query {
+  continents(filter: ContinentFilterInput): [Continent!]!
+  continent(code: ID!): Continent
+  countries(filter: CountryFilterInput): [Country!]!
+  country(code: ID!): Country
+  languages(filter: LanguageFilterInput): [Language!]!
+  language(code: ID!): Language
+}
+
+type Mutation {
+	addLanguage(code: ID!, name: String!): Language!
+}
+
+type State {
+  code: String
+  name: String!
+  country: Country!
+}
+
+input StringQueryOperatorInput {
+  eq: String
+  ne: String
+  in: [String]
+  nin: [String]
+  regex: String
+  glob: String
+}
+
+"""The Upload scalar type represents a file upload."""
+scalar Upload`
+
 func withBaseSchema(input string) string {
 	return input + `
 "The 'Int' scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1."
diff --git a/pkg/graphql/execution_engine_test.go b/pkg/graphql/execution_engine_test.go
index 44aa8f43..c556d366 100644
--- a/pkg/graphql/execution_engine_test.go
+++ b/pkg/graphql/execution_engine_test.go
@@ -25,196 +25,193 @@ func (t testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
 }
 
 func TestExecutionEngine_ExecuteWithOptions(t *testing.T) {
-	schema := starwarsSchema(t)
-	extraVariables := map[string]string{
-		"request": `{"Authorization": "Bearer ey123"}`,
+	type testCase struct {
+		schema            *Schema
+		request           func(t *testing.T) Request
+		plannerConfig     datasource.PlannerConfiguration
+		roundTripper      testRoundTripper
+		preExecutionTasks func(t *testing.T, request Request, schema *Schema, engine *ExecutionEngine) // optional
+		expectedResponse  string
 	}
-	extraVariablesBytes, err := json.Marshal(extraVariables)
-	require.NoError(t, err)
 
-	t.Run("execute with custom roundtripper for simple hero query on HttpJsonDatasource", func(t *testing.T) {
-		query := starwars.LoadQuery(t, starwars.FileSimpleHeroQuery, nil)
-		request := Request{}
-		err := UnmarshalRequest(bytes.NewBuffer(query), &request)
-		require.NoError(t, err)
+	loadStarWarsQuery := func(t *testing.T, starwarsFile string, variables starwars.QueryVariables) func(t *testing.T) Request {
+		return func(t *testing.T) Request {
+			query := starwars.LoadQuery(t, starwarsFile, variables)
+			request := Request{}
+			err := UnmarshalRequest(bytes.NewBuffer(query), &request)
+			require.NoError(t, err)
 
-		plannerConfig := datasource.PlannerConfiguration{
-			TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
-				{
-					TypeName:  "Query",
-					FieldName: "hero",
-					Mapping: &datasource.MappingConfiguration{
-						Disabled: false,
-						Path:     "hero",
-					},
-					DataSource: datasource.SourceConfig{
-						Name: "HttpJsonDataSource",
-						Config: func() []byte {
-							data, _ := json.Marshal(datasource.HttpJsonDataSourceConfig{
-								Host: "example.com",
-								URL:  "/",
-								Method: func() *string {
-									method := "GET"
-									return &method
-								}(),
-								DefaultTypeName: func() *string {
-									typeName := "Hero"
-									return &typeName
-								}(),
-							})
-							return data
-						}(),
-					},
-				},
-			},
+			return request
 		}
+	}
 
-		roundTripper := testRoundTripper(func(req *http.Request) *http.Response {
-			assert.Equal(t, "example.com", req.URL.Host)
-			assert.Equal(t, "/", req.URL.Path)
+	createTestRoundTripper := func(host string, url string, response string, statusCode int) testRoundTripper {
+		return testRoundTripper(func(req *http.Request) *http.Response {
+			assert.Equal(t, host, req.URL.Host)
+			assert.Equal(t, url, req.URL.Path)
 
-			body := bytes.NewBuffer([]byte(`{"hero": {"name": "Luke Skywalker"}}`))
-			return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(body)}
+			body := bytes.NewBuffer([]byte(response))
+			return &http.Response{StatusCode: statusCode, Body: ioutil.NopCloser(body)}
 		})
+	}
 
-		httpJsonOptions := DataSourceHttpJsonOptions{
-			HttpClient: &http.Client{
-				Transport: roundTripper,
-			},
-		}
+	run := func(tc testCase, hasExecutionError bool) func(t *testing.T) {
+		return func(t *testing.T) {
+			request := tc.request(t)
 
-		engine, err := NewExecutionEngine(abstractlogger.NoopLogger, schema, plannerConfig)
-		assert.NoError(t, err)
+			extraVariables := map[string]string{
+				"request": `{"Authorization": "Bearer ey123"}`,
+			}
+			extraVariablesBytes, err := json.Marshal(extraVariables)
+			require.NoError(t, err)
 
-		err = engine.AddHttpJsonDataSourceWithOptions("HttpJsonDataSource", httpJsonOptions)
-		assert.NoError(t, err)
+			engine, err := NewExecutionEngine(abstractlogger.NoopLogger, tc.schema, tc.plannerConfig)
+			assert.NoError(t, err)
 
-		executionRes, err := engine.Execute(context.Background(), &request, ExecutionOptions{ExtraArguments: extraVariablesBytes})
-		assert.NoError(t, err)
-		assert.Equal(t, `{"data":{"hero":{"name":"Luke Skywalker"}}}`, executionRes.Buffer().String())
-	})
+			switch tc.plannerConfig.TypeFieldConfigurations[0].DataSource.Name {
+			case "HttpJsonDataSource":
+				httpJsonOptions := DataSourceHttpJsonOptions{
+					HttpClient: &http.Client{
+						Transport: tc.roundTripper,
+					},
+				}
 
-	t.Run("execute with empty request object should not panic", func(t *testing.T) {
+				err = engine.AddHttpJsonDataSourceWithOptions("HttpJsonDataSource", httpJsonOptions)
+				assert.NoError(t, err)
+			case "GraphqlDataSource":
+				graphqlOptions := DataSourceGraphqlOptions{
+					HttpClient: &http.Client{
+						Transport: tc.roundTripper,
+					},
+				}
 
-		request := Request{}
+				err = engine.AddGraphqlDataSourceWithOptions("GraphqlDataSource", graphqlOptions)
+				assert.NoError(t, err)
+			}
 
-		plannerConfig := datasource.PlannerConfiguration{
-			TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
-				{
-					TypeName:  "Query",
-					FieldName: "hero",
-					Mapping: &datasource.MappingConfiguration{
-						Disabled: false,
-						Path:     "hero",
-					},
-					DataSource: datasource.SourceConfig{
-						Name: "HttpJsonDataSource",
-						Config: func() []byte {
-							data, _ := json.Marshal(datasource.HttpJsonDataSourceConfig{
-								Host: "example.com",
-								URL:  "/",
-								Method: func() *string {
-									method := "GET"
-									return &method
-								}(),
-								DefaultTypeName: func() *string {
-									typeName := "Hero"
-									return &typeName
-								}(),
-							})
-							return data
-						}(),
-					},
-				},
-			},
-		}
+			if tc.preExecutionTasks != nil {
+				tc.preExecutionTasks(t, request, tc.schema, engine)
+			}
 
-		roundTripper := testRoundTripper(func(req *http.Request) *http.Response {
-			assert.Equal(t, "example.com", req.URL.Host)
-			assert.Equal(t, "/", req.URL.Path)
+			executionRes, err := engine.Execute(context.Background(), &request, ExecutionOptions{ExtraArguments: extraVariablesBytes})
 
-			body := bytes.NewBuffer([]byte(`{"hero": {"name": "Luke Skywalker"}}`))
-			return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(body)}
-		})
+			if hasExecutionError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
 
-		httpJsonOptions := DataSourceHttpJsonOptions{
-			HttpClient: &http.Client{
-				Transport: roundTripper,
-			},
+			assert.Equal(t, tc.expectedResponse, executionRes.Buffer().String())
 		}
+	}
 
-		engine, err := NewExecutionEngine(abstractlogger.NoopLogger, schema, plannerConfig)
-		assert.NoError(t, err)
+	runWithoutError := func(tc testCase) func(t *testing.T) {
+		return run(tc, false)
+	}
 
-		err = engine.AddHttpJsonDataSourceWithOptions("HttpJsonDataSource", httpJsonOptions)
-		assert.NoError(t, err)
+	runWithError := func(tc testCase) func(t *testing.T) {
+		return run(tc, true)
+	}
 
-		executionRes, err := engine.Execute(context.Background(), &request, ExecutionOptions{ExtraArguments: extraVariablesBytes})
-		assert.Error(t,err)
-		assert.Equal(t, ``, executionRes.Buffer().String())
-	})
+	t.Run("execute with custom roundtripper for simple hero query on HttpJsonDatasource", runWithoutError(testCase{
+		schema:           starwarsSchema(t),
+		request:          loadStarWarsQuery(t, starwars.FileSimpleHeroQuery, nil),
+		plannerConfig:    heroHttpJsonPlannerConfig,
+		roundTripper:     createTestRoundTripper("example.com", "/", `{"hero": {"name": "Luke Skywalker"}}`, 200),
+		expectedResponse: `{"data":{"hero":{"name":"Luke Skywalker"}}}`,
+	}))
 
-	t.Run("execute with custom roundtripper for simple hero query on GraphqlDataSource", func(t *testing.T) {
-		query := starwars.LoadQuery(t, starwars.FileSimpleHeroQuery, nil)
-		request := Request{}
-		err := UnmarshalRequest(bytes.NewBuffer(query), &request)
-		require.NoError(t, err)
+	t.Run("execute with empty request object should not panic", runWithError(testCase{
+		schema: starwarsSchema(t),
+		request: func(t *testing.T) Request {
+			return Request{}
+		},
+		plannerConfig:    heroHttpJsonPlannerConfig,
+		roundTripper:     createTestRoundTripper("example.com", "/", `{"hero": {"name": "Luke Skywalker"}}`, 200),
+		expectedResponse: "",
+	}))
 
-		plannerConfig := datasource.PlannerConfiguration{
-			TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
-				{
-					TypeName:  "query",
-					FieldName: "hero",
-					Mapping: &datasource.MappingConfiguration{
-						Disabled: false,
-						Path:     "hero",
-					},
-					DataSource: datasource.SourceConfig{
-						Name: "GraphqlDataSource",
-						Config: func() []byte {
-							data, _ := json.Marshal(datasource.GraphQLDataSourceConfig{
-								Host: "example.com",
-								URL:  "/",
-								Method: func() *string {
-									method := "GET"
-									return &method
-								}(),
-							})
-							return data
-						}(),
-					},
-				},
-			},
-		}
+	t.Run("execute with empty request object should not panic", runWithoutError(testCase{
+		schema:           starwarsSchema(t),
+		request:          loadStarWarsQuery(t, starwars.FileSimpleHeroQuery, nil),
+		plannerConfig:    heroGraphqlDataSource,
+		roundTripper:     createTestRoundTripper("example.com", "/", `{"data":{"hero":{"name":"Luke Skywalker"}}}`, 200),
+		expectedResponse: `{"data":{"hero":{"name":"Luke Skywalker"}}}`,
+	}))
 
-		roundTripper := testRoundTripper(func(req *http.Request) *http.Response {
-			assert.Equal(t, "example.com", req.URL.Host)
-			assert.Equal(t, "/", req.URL.Path)
+	t.Run("execute with empty request object should not panic", runWithoutError(testCase{
+		schema: starwarsSchema(t),
+		request: func(t *testing.T) Request {
+			request := loadStarWarsQuery(t, starwars.FileMultiQueries, nil)(t)
+			request.OperationName = "SingleHero"
+			return request
+		},
+		plannerConfig:    heroGraphqlDataSource,
+		roundTripper:     createTestRoundTripper("example.com", "/", `{"data":{"hero":{"name":"Luke Skywalker"}}}`, 200),
+		expectedResponse: `{"data":{"hero":{"name":"Luke Skywalker"}}}`,
+	}))
 
-			body := bytes.NewBuffer([]byte(`{"data":{"hero":{"name":"Luke Skywalker"}}}`))
-			return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(body)}
-		})
+	t.Run("execute query with variables for arguments", runWithoutError(testCase{
+		schema:            starwarsSchema(t),
+		request:           loadStarWarsQuery(t, starwars.FileDroidWithArgAndVarQuery, map[string]interface{}{"droidID": "R2D2"}),
+		plannerConfig:     droidGraphqlDataSource,
+		roundTripper:      createTestRoundTripper("example.com", "/", `{"data":{"droid":{"name":"R2D2"}}}`, 200),
+		preExecutionTasks: normalizeAndValidatePreExecutionTasks,
+		expectedResponse:  `{"data":{"droid":{"name":"R2D2"}}}`,
+	}))
 
-		graphqlOptions := DataSourceGraphqlOptions{
-			HttpClient: &http.Client{
-				Transport: roundTripper,
-			},
-		}
+	t.Run("execute query with arguments", runWithoutError(testCase{
+		schema:            starwarsSchema(t),
+		request:           loadStarWarsQuery(t, starwars.FileDroidWithArgQuery, nil),
+		plannerConfig:     droidGraphqlDataSource,
+		roundTripper:      createTestRoundTripper("example.com", "/", `{"data":{"droid":{"name":"R2D2"}}}`, 200),
+		preExecutionTasks: normalizeAndValidatePreExecutionTasks,
+		expectedResponse:  `{"data":{"droid":{"name":"R2D2"}}}`,
+	}))
 
-		engine, err := NewExecutionEngine(abstractlogger.NoopLogger, schema, plannerConfig)
-		assert.NoError(t, err)
+	t.Run("execute single mutation with arguments on document with multiple operations", runWithoutError(testCase{
+		schema: moviesSchema(t),
+		request: func(t *testing.T) Request {
+			return Request{
+				OperationName: "AddWithInput",
+				Variables:     nil,
+				Query: `mutation AddToWatchlist {
+						  addToWatchlist(movieID:3) {
+							id
+							name
+							year
+						  }
+						}
+						
+						
+						mutation AddWithInput {
+						  addToWatchlistWithInput(input: {id: 2}) {
+							id
+							name
+							year
+						  }
+						}`,
+			}
+		},
+		plannerConfig:     movieHttpJsonDataSource,
+		roundTripper:      createTestRoundTripper("example.com", "/", `{"added_movie":{"id":2, "name": "Episode V – The Empire Strikes Back", "year": 1980}}`, 200),
+		preExecutionTasks: normalizeAndValidatePreExecutionTasks,
+		expectedResponse:  `{"data":{"addToWatchlistWithInput":{"id":2,"name":"Episode V – The Empire Strikes Back","year":1980}}}`,
+	}))
+}
 
-		err = engine.AddGraphqlDataSourceWithOptions("GraphqlDataSource", graphqlOptions)
-		assert.NoError(t, err)
+func normalizeAndValidatePreExecutionTasks(t *testing.T, request Request, schema *Schema, engine *ExecutionEngine) {
+	normalizationResult, err := request.Normalize(schema)
+	require.NoError(t, err)
+	require.True(t, normalizationResult.Successful)
 
-		executionRes, err := engine.Execute(context.Background(), &request, ExecutionOptions{ExtraArguments: extraVariablesBytes})
-		assert.NoError(t, err)
-		assert.Equal(t, `{"data":{"hero":{"name":"Luke Skywalker"}}}`, executionRes.Buffer().String())
-	})
+	validationResult, err := request.ValidateForSchema(schema)
+	require.NoError(t, err)
+	require.True(t, validationResult.Valid)
 }
 
 func stringify(any interface{}) []byte {
-	out,_ := json.Marshal(any)
+	out, _ := json.Marshal(any)
 	return out
 }
 
@@ -222,18 +219,43 @@ func stringPtr(str string) *string {
 	return &str
 }
 
+func moviesSchema(t *testing.T) *Schema {
+	schemaString := `
+type Movie {
+  id: Int!
+  name: String!
+  year: Int!
+}
+
+type Mutation {
+  addToWatchlist(movieID: Int!): Movie
+  addToWatchlistWithInput(input: WatchlistInput!): Movie
+}
+
+type Query {
+  default: String
+}
+
+input WatchlistInput {
+  id: Int!
+}`
+	schema, err := NewSchemaFromString(schemaString)
+	require.NoError(t, err)
+	return schema
+}
+
 func TestExampleExecutionEngine_Concatenation(t *testing.T) {
 
-	schema,err := NewSchemaFromString(`
+	schema, err := NewSchemaFromString(`
 		schema { query: Query }
 		type Query { friend: Friend }
 		type Friend { firstName: String lastName: String fullName: String }
 	`)
 
-	assert.NoError(t,err)
+	assert.NoError(t, err)
 
-	friendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter,r *http.Request){
-		_,_ = w.Write([]byte(`{"firstName":"Jens","lastName":"Neuse"}`))
+	friendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"firstName":"Jens","lastName":"Neuse"}`))
 	}))
 
 	defer friendServer.Close()
@@ -261,19 +283,19 @@ func TestExampleExecutionEngine_Concatenation(t *testing.T) {
 				DataSource: datasource.SourceConfig{
 					Name: "HttpJsonDataSource",
 					Config: stringify(datasource.HttpJsonDataSourceConfig{
-						Host: friendServer.URL,
+						Host:   friendServer.URL,
 						Method: stringPtr("GET"),
 					}),
 				},
 			},
 			{
-				TypeName: "Friend",
+				TypeName:  "Friend",
 				FieldName: "fullName",
 				DataSource: datasource.SourceConfig{
-					Name:   "FriendFullName",
+					Name: "FriendFullName",
 					Config: stringify(datasource.PipelineDataSourceConfig{
 						ConfigString: stringPtr(pipelineConcat),
-						InputJSON: `{"firstName":"{{ .object.firstName }}","lastName":"{{ .object.lastName }}"}`,
+						InputJSON:    `{"firstName":"{{ .object.firstName }}","lastName":"{{ .object.lastName }}"}`,
 					}),
 				},
 			},
@@ -281,22 +303,22 @@ func TestExampleExecutionEngine_Concatenation(t *testing.T) {
 	}
 
 	engine, err := NewExecutionEngine(abstractlogger.NoopLogger, schema, plannerConfig)
-	assert.NoError(t,err)
+	assert.NoError(t, err)
 	err = engine.AddHttpJsonDataSource("HttpJsonDataSource")
-	assert.NoError(t,err)
-	err = engine.AddDataSource("FriendFullName",datasource.PipelineDataSourcePlannerFactoryFactory{})
-	assert.NoError(t,err)
+	assert.NoError(t, err)
+	err = engine.AddDataSource("FriendFullName", datasource.PipelineDataSourcePlannerFactoryFactory{})
+	assert.NoError(t, err)
 
 	request := &Request{
 		Query: `query { friend { firstName lastName fullName }}`,
 	}
 
 	executionRes, err := engine.Execute(context.Background(), request, ExecutionOptions{})
-	assert.NoError(t,err)
+	assert.NoError(t, err)
 
 	expected := `{"data":{"friend":{"firstName":"Jens","lastName":"Neuse","fullName":"Jens Neuse"}}}`
 	actual := executionRes.Buffer().String()
-	assert.Equal(t,expected,actual)
+	assert.Equal(t, expected, actual)
 }
 
 func BenchmarkExecutionEngine(b *testing.B) {
@@ -334,10 +356,10 @@ func BenchmarkExecutionEngine(b *testing.B) {
 	out := bytes.Buffer{}
 	err := newEngine().ExecuteWithWriter(ctx, req, &out, ExecutionOptions{})
 	assert.NoError(b, err)
-	assert.Equal(b,"{\"data\":{\"hello\":\"world\"}}",out.String())
+	assert.Equal(b, "{\"data\":{\"hello\":\"world\"}}", out.String())
 
 	pool := sync.Pool{
-		New: func() interface{}{
+		New: func() interface{} {
 			return newEngine()
 		},
 	}
@@ -354,3 +376,119 @@ func BenchmarkExecutionEngine(b *testing.B) {
 		}
 	})
 }
+
+var heroHttpJsonPlannerConfig = datasource.PlannerConfiguration{
+	TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
+		{
+			TypeName:  "Query",
+			FieldName: "hero",
+			Mapping: &datasource.MappingConfiguration{
+				Disabled: false,
+				Path:     "hero",
+			},
+			DataSource: datasource.SourceConfig{
+				Name: "HttpJsonDataSource",
+				Config: func() []byte {
+					data, _ := json.Marshal(datasource.HttpJsonDataSourceConfig{
+						Host: "example.com",
+						URL:  "/",
+						Method: func() *string {
+							method := "GET"
+							return &method
+						}(),
+						DefaultTypeName: func() *string {
+							typeName := "Hero"
+							return &typeName
+						}(),
+					})
+					return data
+				}(),
+			},
+		},
+	},
+}
+
+var movieHttpJsonDataSource = datasource.PlannerConfiguration{
+	TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
+		{
+			TypeName:  "Mutation",
+			FieldName: "addToWatchlistWithInput",
+			Mapping: &datasource.MappingConfiguration{
+				Disabled: false,
+				Path:     "added_movie",
+			},
+			DataSource: datasource.SourceConfig{
+				Name: "HttpJsonDataSource",
+				Config: func() []byte {
+					data, _ := json.Marshal(datasource.HttpJsonDataSourceConfig{
+						Host: "example.com",
+						URL:  "/",
+						Method: func() *string {
+							method := "GET"
+							return &method
+						}(),
+						DefaultTypeName: func() *string {
+							typeName := "Movie"
+							return &typeName
+						}(),
+					})
+					return data
+				}(),
+			},
+		},
+	},
+}
+
+var heroGraphqlDataSource = datasource.PlannerConfiguration{
+	TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
+		{
+			TypeName:  "query",
+			FieldName: "hero",
+			Mapping: &datasource.MappingConfiguration{
+				Disabled: false,
+				Path:     "hero",
+			},
+			DataSource: datasource.SourceConfig{
+				Name: "GraphqlDataSource",
+				Config: func() []byte {
+					data, _ := json.Marshal(datasource.GraphQLDataSourceConfig{
+						Host: "example.com",
+						URL:  "/",
+						Method: func() *string {
+							method := "GET"
+							return &method
+						}(),
+					})
+					return data
+				}(),
+			},
+		},
+	},
+}
+
+var droidGraphqlDataSource = datasource.PlannerConfiguration{
+	TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
+		{
+			TypeName:  "query",
+			FieldName: "droid",
+			Mapping: &datasource.MappingConfiguration{
+				Disabled: false,
+				Path:     "droid",
+			},
+			DataSource: datasource.SourceConfig{
+				Name: "GraphqlDataSource",
+				Config: func() []byte {
+					data, _ := json.Marshal(datasource.GraphQLDataSourceConfig{
+						Host: "example.com",
+						URL:  "/",
+						Method: func() *string {
+							method := "GET"
+							return &method
+						}(),
+					})
+					return data
+				}(),
+			},
+		},
+	},
+}
diff --git a/pkg/graphql/request_test.go b/pkg/graphql/request_test.go
index 36e20e72..b2141dd3 100644
--- a/pkg/graphql/request_test.go
+++ b/pkg/graphql/request_test.go
@@ -23,7 +23,7 @@ func TestUnmarshalRequest(t *testing.T) {
 	})
 
 	t.Run("should successfully unmarshal request", func(t *testing.T) {
-		requestBytes := []byte(`{"operation_name": "Hello", "variables": "", "query": "query Hello { hello }"}`)
+		requestBytes := []byte(`{"operationName": "Hello", "variables": "", "query": "query Hello { hello }"}`)
 		requestBuffer := bytes.NewBuffer(requestBytes)
 
 		var request Request
diff --git a/pkg/middleware/operation_complexity/operation_complexity_test.go b/pkg/middleware/operation_complexity/operation_complexity_test.go
index 649b8711..1cc45fbe 100644
--- a/pkg/middleware/operation_complexity/operation_complexity_test.go
+++ b/pkg/middleware/operation_complexity/operation_complexity_test.go
@@ -102,14 +102,14 @@ var run = func(t *testing.T, definition, operation string, expectedNodeCount, ex
 
 	astnormalization.NormalizeOperation(&op, &def, &report)
 
-	nodeCount, complexity, depth := CalculateOperationComplexity(&op, &def, &report)
+	actualNodeCount, actualComplexity, actualDepth := CalculateOperationComplexity(&op, &def, &report)
 	if report.HasErrors() {
 		require.NoError(t, report)
 	}
 
-	assert.Equal(t, nodeCount, expectedNodeCount, "unexpected node count")
-	assert.Equal(t, complexity, expectedComplexity, "unexpected complexity")
-	assert.Equal(t, depth, expectedDepth, "unexpected depth")
+	assert.Equal(t, expectedNodeCount, actualNodeCount, "unexpected node count")
+	assert.Equal(t, expectedComplexity, actualComplexity, "unexpected complexity")
+	assert.Equal(t, expectedDepth, actualDepth, "unexpected depth")
 }
 
 func BenchmarkEstimateComplexity(b *testing.B) {
diff --git a/pkg/starwars/testdata/queries/droid_with_arg.query b/pkg/starwars/testdata/queries/droid_with_arg.query
new file mode 100644
index 00000000..604f02bb
--- /dev/null
+++ b/pkg/starwars/testdata/queries/droid_with_arg.query
@@ -0,0 +1,5 @@
+{
+    droid(id: "R2D2") {
+        name
+    }
+}
\ No newline at end of file
diff --git a/pkg/starwars/testdata/queries/multi_queries.query b/pkg/starwars/testdata/queries/multi_queries.query
new file mode 100644
index 00000000..0268327c
--- /dev/null
+++ b/pkg/starwars/testdata/queries/multi_queries.query
@@ -0,0 +1,14 @@
+query MultiHeroes {
+    empireHero: hero {
+        name
+    }
+    jediHero: hero {
+        name
+    }
+}
+
+query SingleHero {
+    hero {
+        name
+    }
+}
\ No newline at end of file