Repository: jensneuse/graphql-go-tools
Pull request #222 (2020-05-29 06:21:56)

sync tyk master


Patch:
diff --git a/pkg/astvalidation/astvalidation.go b/pkg/astvalidation/astvalidation.go
index 1d5fdca6..4f081b25 100644
--- a/pkg/astvalidation/astvalidation.go
+++ b/pkg/astvalidation/astvalidation.go
@@ -22,6 +22,7 @@ func DefaultOperationValidator() *OperationValidator {
 		walker: astvisitor.NewWalker(48),
 	}
 
+	validator.RegisterRule(DocumentContainsExecutableOperation())
 	validator.RegisterRule(OperationNameUniqueness())
 	validator.RegisterRule(LoneAnonymousOperation())
 	validator.RegisterRule(SubscriptionSingleRootField())
@@ -80,6 +81,33 @@ func (o *OperationValidator) Validate(operation, definition *ast.Document, repor
 	return Valid
 }
 
+// DocumentContainsExecutableOperation validates if the document actually contains an executable Operation
+func DocumentContainsExecutableOperation() Rule {
+	return func(walker *astvisitor.Walker) {
+		visitor := &documentContainsExecutableOperation{
+			Walker: walker,
+		}
+		walker.RegisterEnterDocumentVisitor(visitor)
+	}
+}
+
+type documentContainsExecutableOperation struct {
+	*astvisitor.Walker
+}
+
+func (d *documentContainsExecutableOperation) EnterDocument(operation, definition *ast.Document) {
+	if len(operation.RootNodes) == 0 {
+		d.StopWithExternalErr(operationreport.ErrDocumentDoesntContainExecutableOperation())
+		return
+	}
+	for i := range operation.RootNodes {
+		if operation.RootNodes[i].Kind == ast.NodeKindOperationDefinition {
+			return
+		}
+	}
+	d.StopWithExternalErr(operationreport.ErrDocumentDoesntContainExecutableOperation())
+}
+
 // OperationNameUniqueness validates if all operation names are unique
 func OperationNameUniqueness() Rule {
 	return func(walker *astvisitor.Walker) {
@@ -572,7 +600,9 @@ func (v *validArgumentsVisitor) stringValueSatisfiesInputValueDefinition(value a
 	if inputType.TypeKind != ast.TypeKindNamed {
 		return false
 	}
-	if !bytes.Equal(v.definition.Input.ByteSlice(inputType.Name), literal.STRING) {
+
+	inputTypeName := v.definition.Input.ByteSlice(inputType.Name)
+	if !bytes.Equal(inputTypeName, literal.STRING) && !bytes.Equal(inputTypeName, literal.ID) {
 		return false
 	}
 	return true
@@ -869,18 +899,16 @@ func (v *valuesVisitor) objectValueSatisfiesInputValueDefinition(objectValue, in
 }
 
 func (v *valuesVisitor) valueSatisfiesScalar(value ast.Value, scalar int) bool {
-	scalarName := v.definition.ScalarTypeDefinitionNameBytes(scalar)
-	switch value.Kind {
-	case ast.ValueKindString:
-		return bytes.Equal(scalarName, literal.STRING)
-	case ast.ValueKindBoolean:
-		return bytes.Equal(scalarName, literal.BOOLEAN)
-	case ast.ValueKindInteger:
-		return bytes.Equal(scalarName, literal.INT) || bytes.Equal(scalarName, literal.FLOAT)
-	case ast.ValueKindFloat:
-		return bytes.Equal(scalarName, literal.FLOAT)
+	scalarName := v.definition.ScalarTypeDefinitionNameString(scalar)
+	switch scalarName {
+	case "Boolean":
+		return value.Kind == ast.ValueKindBoolean
+	case "Int":
+		return value.Kind == ast.ValueKindInteger
+	case "Float":
+		return value.Kind == ast.ValueKindFloat || value.Kind == ast.ValueKindInteger
 	default:
-		return false
+		return value.Kind == ast.ValueKindString
 	}
 }
 
diff --git a/pkg/execution/datasource/datasource_graphql.go b/pkg/execution/datasource/datasource_graphql.go
index e819c000..16abd5e4 100644
--- a/pkg/execution/datasource/datasource_graphql.go
+++ b/pkg/execution/datasource/datasource_graphql.go
@@ -391,6 +391,13 @@ func (g *GraphQLDataSource) Resolve(ctx context.Context, args ResolverArgs, out
 		return n, err
 	}
 
+	defer func() {
+		err := res.Body.Close()
+		if err != nil {
+			g.Log.Error("GraphQLDataSource.Resolve.Response.Body.Close", log.Error(err))
+		}
+	}()
+
 	data = bytes.ReplaceAll(data, literal.BACKSLASH, nil)
 	data, _, _, err = jsonparser.Get(data, "data")
 	if err != nil {
diff --git a/pkg/execution/datasource/datasource_http_json.go b/pkg/execution/datasource/datasource_http_json.go
index 71be7dca..6eff9115 100644
--- a/pkg/execution/datasource/datasource_http_json.go
+++ b/pkg/execution/datasource/datasource_http_json.go
@@ -15,6 +15,7 @@ import (
 	"github.com/tidwall/gjson"
 	"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/lexer/literal"
 )
@@ -317,20 +318,46 @@ func (r *HttpJsonDataSource) Resolve(ctx context.Context, args ResolverArgs, out
 		return
 	}
 
+	defer func() {
+		err := res.Body.Close()
+		if err != nil {
+			r.Log.Error("HttpJsonDataSource.Resolve.Response.Body.Close", log.Error(err))
+		}
+	}()
+
 	statusCode := strconv.Itoa(res.StatusCode)
 	statusCodeTypeName := gjson.GetBytes(typeNameArg, statusCode)
+	defaultTypeName := gjson.GetBytes(typeNameArg, "defaultTypeName")
+	var result *gjson.Result
 	if statusCodeTypeName.Exists() {
-		data, err = sjson.SetRawBytes(data, "__typename", []byte(statusCodeTypeName.Raw))
-		if err != nil {
-			r.Log.Error("HttpJsonDataSource.Resolve.setStatusCodeTypeName",
-				log.Error(err),
-			)
-			return
-		}
-	} else {
-		defaultTypeName := gjson.GetBytes(typeNameArg, "defaultTypeName")
-		if defaultTypeName.Exists() {
-			data, err = sjson.SetRawBytes(data, "__typename", []byte(defaultTypeName.Raw))
+		result = &statusCodeTypeName
+	}
+	if result == nil && defaultTypeName.Exists() {
+		result = &defaultTypeName
+	}
+
+	if result != nil {
+		parsed := gjson.ParseBytes(data)
+		if parsed.IsArray() {
+			arrayData := []byte(`[]`)
+			items := parsed.Array()
+			for i := range items {
+				item, err := sjson.SetRaw(items[i].Raw, "__typename", result.Raw)
+				if err != nil {
+					r.Log.Error("HttpJsonDataSource.Resolve.array.setDefaultTypeName",
+						log.Error(err),
+					)
+				}
+				arrayData, err = sjson.SetRawBytes(arrayData, "-1", unsafebytes.StringToBytes(item))
+				if err != nil {
+					r.Log.Error("HttpJsonDataSource.Resolve.array.setArrayItem",
+						log.Error(err),
+					)
+				}
+			}
+			data = arrayData
+		} else {
+			data, err = sjson.SetRawBytes(data, "__typename", []byte(result.Raw))
 			if err != nil {
 				r.Log.Error("HttpJsonDataSource.Resolve.setDefaultTypeName",
 					log.Error(err),
diff --git a/pkg/execution/planning.go b/pkg/execution/planning.go
index 2786c11b..f7ea1f57 100644
--- a/pkg/execution/planning.go
+++ b/pkg/execution/planning.go
@@ -72,6 +72,12 @@ type dataSourcePlannerRef struct {
 
 func (p *planningVisitor) EnterDocument(operation, definition *ast.Document) {
 	p.operation, p.definition, p.base.Definition = operation, definition, definition
+
+	if len(operation.OperationDefinitions) == 0 {
+		p.Walker.StopWithExternalErr(operationreport.ErrDocumentDoesntContainExecutableOperation())
+		return
+	}
+
 	obj := &Object{}
 	p.rootNode = &Object{
 		operationType: operation.OperationDefinitions[0].OperationType,
diff --git a/pkg/graphql/execution_engine.go b/pkg/graphql/execution_engine.go
index 5844c0a0..5403c3e0 100644
--- a/pkg/graphql/execution_engine.go
+++ b/pkg/graphql/execution_engine.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"io/ioutil"
 	"net/http"
+	"sync"
 
 	"github.com/jensneuse/abstractlogger"
 
@@ -30,24 +31,29 @@ type ExecutionOptions struct {
 }
 
 type ExecutionEngine struct {
-	logger      abstractlogger.Logger
-	basePlanner *datasource.BasePlanner
-	executor    *execution.Executor
-	schema      *Schema
+	logger       abstractlogger.Logger
+	basePlanner  *datasource.BasePlanner
+	executorPool *sync.Pool
+	schema       *Schema
 }
 
 func NewExecutionEngine(logger abstractlogger.Logger, schema *Schema, plannerConfig datasource.PlannerConfiguration) (*ExecutionEngine, error) {
-	executor := execution.NewExecutor(nil)
+	executorPool := sync.Pool{
+		New: func() interface{} {
+			return execution.NewExecutor(nil)
+		},
+	}
+
 	basePlanner, err := datasource.NewBaseDataSourcePlanner(schema.rawInput, plannerConfig, logger)
 	if err != nil {
 		return nil, err
 	}
 
 	return &ExecutionEngine{
-		logger:      logger,
-		basePlanner: basePlanner,
-		executor:    executor,
-		schema:      schema,
+		logger:       logger,
+		basePlanner:  basePlanner,
+		executorPool: &executorPool,
+		schema:       schema,
 	}, nil
 }
 
@@ -93,6 +99,7 @@ func (e *ExecutionEngine) AddDataSource(name string, plannerFactoryFactory datas
 
 func (e *ExecutionEngine) ExecuteWithWriter(ctx context.Context, operation *Request, writer io.Writer, options ExecutionOptions) error {
 	var report operationreport.Report
+
 	if !operation.IsNormalized() {
 		normalizationResult, err := operation.Normalize(e.schema)
 		if err != nil {
@@ -117,7 +124,9 @@ func (e *ExecutionEngine) ExecuteWithWriter(ctx context.Context, operation *Requ
 		ExtraArguments: extraArguments,
 	}
 
-	return e.executor.Execute(executionContext, plan, writer)
+	poolExecutor := e.executorPool.Get().(*execution.Executor)
+	defer e.executorPool.Put(poolExecutor)
+	return poolExecutor.Execute(executionContext, plan, writer)
 }
 
 func (e *ExecutionEngine) Execute(ctx context.Context, operation *Request, options ExecutionOptions) (*ExecutionResult, error) {
diff --git a/pkg/operationreport/externalerror.go b/pkg/operationreport/externalerror.go
index 8cbfe7f0..c0928c00 100644
--- a/pkg/operationreport/externalerror.go
+++ b/pkg/operationreport/externalerror.go
@@ -17,6 +17,11 @@ type Location struct {
 	Column uint32 `json:"column"`
 }
 
+func ErrDocumentDoesntContainExecutableOperation() (err ExternalError){
+	err.Message = "document doesn't contain any executable operation"
+	return
+}
+
 func ErrFieldUndefinedOnType(fieldName, typeName ast.ByteSlice) (err ExternalError) {
 	err.Message = fmt.Sprintf("field: %s not defined on type: %s", fieldName, typeName)
 	return err


Test patch:
diff --git a/pkg/astvalidation/astvalidation_test.go b/pkg/astvalidation/astvalidation_test.go
index 110dafba..675dbd5b 100644
--- a/pkg/astvalidation/astvalidation_test.go
+++ b/pkg/astvalidation/astvalidation_test.go
@@ -63,6 +63,12 @@ func TestExecutionValidation(t *testing.T) {
 		}
 	}
 
+	runManyRulesWithDefinition := func(definitionInput, operationInput string, expectation ValidationState, rules ...Rule) {
+		for _, rule := range rules {
+			runWithDefinition(definitionInput, operationInput, rule, expectation)
+		}
+	}
+
 	run := func(operationInput string, rule Rule, expectation ValidationState, expectFailedNormalization ...bool) {
 		runWithDefinition(testDefinition, operationInput, rule, expectation, expectFailedNormalization...)
 	}
@@ -2210,6 +2216,15 @@ func TestExecutionValidation(t *testing.T) {
 							}`,
 					ValidArguments(), Invalid)
 			})
+			t.Run("ID as arg given as string", func(t *testing.T) {
+				runManyRulesWithDefinition(countriesDefinition, `{
+						country(code: "DE") {
+							code
+							name
+						}
+					}`,
+					Valid, ValidArguments(), Values())
+			})
 		})
 		t.Run("5.4.2 Argument Uniqueness", func(t *testing.T) {
 			t.Run("121 variant", func(t *testing.T) {
@@ -4368,3 +4383,87 @@ type Query {
 schema {
 	query: Query
 }`
+
+const countriesDefinition = `directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE
+
+scalar String
+scalar ID
+scalar Boolean
+
+schema {
+	query: Query
+}
+
+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 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
+`
diff --git a/pkg/execution/datasource_http_json_test.go b/pkg/execution/datasource_http_json_test.go
index cb66b1e7..a8cb0555 100644
--- a/pkg/execution/datasource_http_json_test.go
+++ b/pkg/execution/datasource_http_json_test.go
@@ -21,11 +21,12 @@ const httpJsonDataSourceSchema = `
 schema {
     query: Query
 }
-
 type Query {
 	simpleType: SimpleType
 	unionType: UnionType
 	interfaceType: InterfaceType
+	listOfStrings: [String!]
+	listOfObjects: [SimpleType]
 }
 type SimpleType {
 	scalarField: String
@@ -161,6 +162,184 @@ func TestHttpJsonDataSourcePlanner_Plan(t *testing.T) {
 			},
 		},
 	))
+	t.Run("list of strings", run(withBaseSchema(httpJsonDataSourceSchema), `
+		query ListOfStrings {
+			listOfStrings
+		}
+		`,
+		func(base *datasource.BasePlanner) {
+			base.Config = datasource.PlannerConfiguration{
+				TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
+					{
+						TypeName:  "query",
+						FieldName: "listOfStrings",
+						Mapping: &datasource.MappingConfiguration{
+							Disabled: true,
+						},
+						DataSource: datasource.SourceConfig{
+							Name: "HttpJsonDataSource",
+							Config: func() []byte {
+								data, _ := json.Marshal(datasource.HttpJsonDataSourceConfig{
+									Host: "example.com",
+									URL:  "/",
+									Method: func() *string {
+										method := "GET"
+										return &method
+									}(),
+								})
+								return data
+							}(),
+						},
+					},
+				},
+			}
+			panicOnErr(base.RegisterDataSourcePlannerFactory("HttpJsonDataSource", &datasource.HttpJsonDataSourcePlannerFactoryFactory{}))
+		},
+		&Object{
+			operationType: ast.OperationTypeQuery,
+			Fields: []Field{
+				{
+					Name: literal.DATA,
+					Value: &Object{
+						Fetch: &SingleFetch{
+							BufferName: "listOfStrings",
+							Source: &DataSourceInvocation{
+								DataSource: &datasource.HttpJsonDataSource{
+									Log:    abstractlogger.Noop{},
+									Client: datasource.DefaultHttpClient(),
+								},
+								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("host"),
+										Value: []byte("example.com"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("url"),
+										Value: []byte("/"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("method"),
+										Value: []byte("GET"),
+									},
+								},
+							},
+						},
+						Fields: []Field{
+							{
+								Name:            []byte("listOfStrings"),
+								HasResolvedData: true,
+								Value: &List{
+									Value: &Value{
+										ValueType: StringValueType,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	))
+	t.Run("list of objects", run(withBaseSchema(httpJsonDataSourceSchema), `
+		query ListOfObjects {
+			listOfObjects {
+				scalarField
+			}
+		}
+		`,
+		func(base *datasource.BasePlanner) {
+			base.Config = datasource.PlannerConfiguration{
+				TypeFieldConfigurations: []datasource.TypeFieldConfiguration{
+					{
+						TypeName:  "query",
+						FieldName: "listOfObjects",
+						Mapping: &datasource.MappingConfiguration{
+							Disabled: true,
+						},
+						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 := "SimpleType"
+										return &typeName
+									}(),
+								})
+								return data
+							}(),
+						},
+					},
+				},
+			}
+			panicOnErr(base.RegisterDataSourcePlannerFactory("HttpJsonDataSource", &datasource.HttpJsonDataSourcePlannerFactoryFactory{}))
+		},
+		&Object{
+			operationType: ast.OperationTypeQuery,
+			Fields: []Field{
+				{
+					Name: literal.DATA,
+					Value: &Object{
+						Fetch: &SingleFetch{
+							BufferName: "listOfObjects",
+							Source: &DataSourceInvocation{
+								DataSource: &datasource.HttpJsonDataSource{
+									Log:    abstractlogger.Noop{},
+									Client: datasource.DefaultHttpClient(),
+								},
+								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("host"),
+										Value: []byte("example.com"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("url"),
+										Value: []byte("/"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("method"),
+										Value: []byte("GET"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("__typename"),
+										Value: []byte(`{"defaultTypeName":"SimpleType"}`),
+									},
+								},
+							},
+						},
+						Fields: []Field{
+							{
+								Name:            []byte("listOfObjects"),
+								HasResolvedData: true,
+								Value: &List{
+									Value: &Object{
+										Fields: []Field{
+											{
+												Name: []byte("scalarField"),
+												Value: &Value{
+													DataResolvingConfig: DataResolvingConfig{
+														PathSelector: datasource.PathSelector{
+															Path: "scalarField",
+														},
+													},
+													ValueType: StringValueType,
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	))
 	t.Run("unionType", run(withBaseSchema(httpJsonDataSourceSchema), `
 		query UnionTypeQuery {
 			unionType {
@@ -363,7 +542,7 @@ func TestHttpJsonDataSourcePlanner_Plan(t *testing.T) {
 							BufferName: "interfaceType",
 							Source: &DataSourceInvocation{
 								DataSource: &datasource.HttpJsonDataSource{
-									Log: abstractlogger.Noop{},
+									Log:    abstractlogger.Noop{},
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
@@ -481,7 +660,7 @@ func TestHttpJsonDataSource_Resolve(t *testing.T) {
 			}
 			buf := bytes.Buffer{}
 			source := &datasource.HttpJsonDataSource{
-				Log: abstractlogger.Noop{},
+				Log:    abstractlogger.Noop{},
 				Client: datasource.DefaultHttpClient(),
 			}
 			args := ResolvedArgs{
diff --git a/pkg/execution/execution_test.go b/pkg/execution/execution_test.go
index 9d15f0e0..51faa4a7 100644
--- a/pkg/execution/execution_test.go
+++ b/pkg/execution/execution_test.go
@@ -17,6 +17,7 @@ import (
 	log "github.com/jensneuse/abstractlogger"
 	"github.com/jensneuse/diffview"
 	"github.com/sebdah/goldie"
+	"github.com/stretchr/testify/assert"
 
 	"github.com/jensneuse/graphql-go-tools/pkg/ast"
 	"github.com/jensneuse/graphql-go-tools/pkg/execution/datasource"
@@ -267,7 +268,7 @@ func TestExecution(t *testing.T) {
 														Value: []byte("/graphql"),
 													},
 													&datasource.StaticVariableArgument{
-														Name: literal.QUERY,
+														Name:  literal.QUERY,
 														Value: []byte(`query q1($id: String!){userPets(id: $id){	__typename name nickname... on Dog {woof} ... on Cat {meow}}}`),
 													},
 													&datasource.ObjectVariableArgument{
@@ -808,7 +809,7 @@ func genField() Field {
 												Value: []byte("/graphql"),
 											},
 											&datasource.StaticVariableArgument{
-												Name: literal.QUERY,
+												Name:  literal.QUERY,
 												Value: []byte(`query q1($id: String!){userPets(id: $id){	__typename name nickname... on Dog {woof} ... on Cat {meow}}}`),
 											},
 											&datasource.ObjectVariableArgument{
@@ -2576,6 +2577,145 @@ func TestExecutor_HTTPJSONDataSourceWithBodyComplexPlayload(t *testing.T) {
 	}
 }
 
+func TestExecutor_HTTPJSONDataSource_ArrayResponse(t *testing.T) {
+
+	response := []interface{}{
+		map[string]interface{}{
+			"fieldValue": "foo",
+		},
+		map[string]interface{}{
+			"fieldValue": "bar",
+		},
+		map[string]interface{}{
+			"fieldValue": "baz",
+		},
+	}
+
+	REST1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+		responseData, err := json.Marshal(response)
+		assert.NoError(t, err)
+
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write(responseData)
+	}))
+
+	plan := &Object{
+		operationType: ast.OperationTypeQuery,
+		Fields: []Field{
+			{
+				Name: []byte("data"),
+				Value: &Object{
+					Fetch: &SingleFetch{
+						BufferName: "objects",
+						Source: &DataSourceInvocation{
+							DataSource: &datasource.HttpJsonDataSource{
+								Log:    log.NoopLogger,
+								Client: datasource.DefaultHttpClient(),
+							},
+							Args: []datasource.Argument{
+								&datasource.StaticVariableArgument{
+									Name:  []byte("host"),
+									Value: []byte(REST1.URL),
+								},
+								&datasource.StaticVariableArgument{
+									Name:  []byte("url"),
+									Value: []byte("/"),
+								},
+								&datasource.StaticVariableArgument{
+									Name:  []byte("method"),
+									Value: []byte("GET"),
+								},
+								&datasource.StaticVariableArgument{
+									Name:  []byte("__typename"),
+									Value: []byte(`{"defaultTypeName":"SimpleType"}`),
+								},
+							},
+						},
+					},
+					Fields: []Field{
+						{
+							Name:            []byte("objects"),
+							HasResolvedData: true,
+							Value: &List{
+								Value: &Object{
+									Fields: []Field{
+										{
+											Name: []byte("__typename"),
+											Value: &Value{
+												ValueType: StringValueType,
+												DataResolvingConfig: DataResolvingConfig{
+													PathSelector: datasource.PathSelector{
+														Path: "__typename",
+													},
+												},
+											},
+										},
+										{
+											Name: []byte("fieldValue"),
+											Value: &Value{
+												ValueType: StringValueType,
+												DataResolvingConfig: DataResolvingConfig{
+													PathSelector: datasource.PathSelector{
+														Path: "fieldValue",
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	out := &bytes.Buffer{}
+	ex := NewExecutor(nil)
+	ctx := Context{
+		Context: context.Background(),
+	}
+
+	err := ex.Execute(ctx, plan, out)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := map[string]interface{}{
+		"data": map[string]interface{}{
+			"objects": []interface{}{
+				map[string]interface{}{
+					"__typename": "SimpleType",
+					"fieldValue": "foo",
+				},
+				map[string]interface{}{
+					"__typename": "SimpleType",
+					"fieldValue": "bar",
+				},
+				map[string]interface{}{
+					"__typename": "SimpleType",
+					"fieldValue": "baz",
+				},
+			},
+		},
+	}
+
+	wantResult, err := json.MarshalIndent(expected, "", "  ")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := string(wantResult)
+	got := prettyJSON(out)
+
+	if want != got {
+		t.Fatalf("want: %s\ngot: %s\n", want, got)
+		return
+	}
+}
+
 func TestExecutor_HTTPJSONDataSourceWithHeaders(t *testing.T) {
 
 	REST1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -2791,7 +2931,7 @@ func TestExecutor_HTTPJSONDataSourceWithPathSelector(t *testing.T) {
 }
 
 func prettyJSON(r io.Reader) string {
-	data := map[string]interface{}{}
+	var data interface{}
 	err := json.NewDecoder(r).Decode(&data)
 	if err != nil {
 		panic(err)
diff --git a/pkg/graphql/execution_engine_test.go b/pkg/graphql/execution_engine_test.go
index 1f20b0ea..44aa8f43 100644
--- a/pkg/graphql/execution_engine_test.go
+++ b/pkg/graphql/execution_engine_test.go
@@ -94,6 +94,66 @@ func TestExecutionEngine_ExecuteWithOptions(t *testing.T) {
 		assert.Equal(t, `{"data":{"hero":{"name":"Luke Skywalker"}}}`, executionRes.Buffer().String())
 	})
 
+	t.Run("execute with empty request object should not panic", func(t *testing.T) {
+
+		request := Request{}
+
+		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
+						}(),
+					},
+				},
+			},
+		}
+
+		roundTripper := testRoundTripper(func(req *http.Request) *http.Response {
+			assert.Equal(t, "example.com", req.URL.Host)
+			assert.Equal(t, "/", req.URL.Path)
+
+			body := bytes.NewBuffer([]byte(`{"hero": {"name": "Luke Skywalker"}}`))
+			return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(body)}
+		})
+
+		httpJsonOptions := DataSourceHttpJsonOptions{
+			HttpClient: &http.Client{
+				Transport: roundTripper,
+			},
+		}
+
+		engine, err := NewExecutionEngine(abstractlogger.NoopLogger, schema, plannerConfig)
+		assert.NoError(t, err)
+
+		err = engine.AddHttpJsonDataSourceWithOptions("HttpJsonDataSource", httpJsonOptions)
+		assert.NoError(t, err)
+
+		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 GraphqlDataSource", func(t *testing.T) {
 		query := starwars.LoadQuery(t, starwars.FileSimpleHeroQuery, nil)
 		request := Request{}
@@ -154,7 +214,7 @@ func TestExecutionEngine_ExecuteWithOptions(t *testing.T) {
 }
 
 func stringify(any interface{}) []byte {
-	out, _ := json.Marshal(any)
+	out,_ := json.Marshal(any)
 	return out
 }
 
@@ -164,16 +224,16 @@ func stringPtr(str string) *string {
 
 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()
@@ -201,19 +261,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 }}"}`,
 					}),
 				},
 			},
@@ -221,22 +281,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) {
diff --git a/pkg/graphql/request_test.go b/pkg/graphql/request_test.go
index 88768b79..36e20e72 100644
--- a/pkg/graphql/request_test.go
+++ b/pkg/graphql/request_test.go
@@ -49,6 +49,18 @@ func TestRequest_ValidateForSchema(t *testing.T) {
 		assert.Equal(t, ValidationResult{Valid: false, Errors: nil}, result)
 	})
 
+	t.Run("should return gql errors no valid operation is in the the request", func(t *testing.T) {
+		request := Request{}
+
+		schema, err := NewSchemaFromString("schema { query: Query } type Query { hello: String }")
+		require.NoError(t, err)
+
+		result, err := request.ValidateForSchema(schema)
+		assert.NoError(t, err)
+		assert.False(t, result.Valid)
+		assert.Greater(t, result.Errors.Count(), 0)
+	})
+
 	t.Run("should return gql errors when validation fails", func(t *testing.T) {
 		request := Request{
 			OperationName: "Goodbye",