Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-8944] Refactor: move variable validation logic to separate step #351

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pkg/graphql/execution_engine_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ func (e *ExecutionEngineV2) ValidateForSchema(operation *Request) error {
return nil
}

func (e *ExecutionEngineV2) InputValidation(operation *Request) error {
result, err := operation.ValidateInput(e.config.schema)
if err != nil {
return err
}
if !result.Valid {
return result.Errors
}
return nil
}

func (e *ExecutionEngineV2) Setup(ctx context.Context, postProcessor *postprocess.Processor, resolveContext *resolve.Context, operation *Request, options ...ExecutionOptionsV2) {
for i := range options {
options[i](postProcessor, resolveContext)
Expand Down
21 changes: 17 additions & 4 deletions pkg/graphql/execution_engine_v2_custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type CustomExecutionEngineV2ValidatorStage interface {
ValidateForSchema(operation *Request) error
}

type CustomExecutionEngineV2InputValidationStage interface {
InputValidation(operation *Request) error
}

type CustomExecutionEngineV2ResolverStage interface {
Setup(ctx context.Context, postProcessor *postprocess.Processor, resolveContext *resolve.Context, operation *Request, options ...ExecutionOptionsV2)
Plan(postProcessor *postprocess.Processor, operation *Request, report *operationreport.Report) (plan.Plan, error)
Expand All @@ -34,6 +38,7 @@ type CustomExecutionEngineV2 interface {
CustomExecutionEngineV2NormalizerStage
CustomExecutionEngineV2ValidatorStage
CustomExecutionEngineV2ResolverStage
CustomExecutionEngineV2InputValidationStage
}

type ExecutionEngineV2Executor interface {
Expand All @@ -54,8 +59,9 @@ type CustomExecutionEngineV2RequiredStages struct {
}

type CustomExecutionEngineV2OptionalStages struct {
NormalizerStage CustomExecutionEngineV2NormalizerStage
ValidatorStage CustomExecutionEngineV2ValidatorStage
NormalizerStage CustomExecutionEngineV2NormalizerStage
ValidatorStage CustomExecutionEngineV2ValidatorStage
InputValidationStage CustomExecutionEngineV2InputValidationStage
}

type CustomExecutionEngineV2Executor struct {
Expand All @@ -69,8 +75,9 @@ func NewCustomExecutionEngineV2Executor(executionEngineV2 CustomExecutionEngineV
ResolverStage: executionEngineV2,
},
OptionalStages: &CustomExecutionEngineV2OptionalStages{
NormalizerStage: executionEngineV2,
ValidatorStage: executionEngineV2,
NormalizerStage: executionEngineV2,
ValidatorStage: executionEngineV2,
InputValidationStage: executionEngineV2,
},
}

Expand Down Expand Up @@ -117,6 +124,12 @@ func (c *CustomExecutionEngineV2Executor) Execute(ctx context.Context, operation
}
}

if c.ExecutionStages.OptionalStages != nil && c.ExecutionStages.OptionalStages.InputValidationStage != nil {
if err := c.ExecutionStages.OptionalStages.InputValidationStage.InputValidation(operation); err != nil {
return err
}
}

execContext := c.getExecutionCtx()
defer c.putExecutionCtx(execContext)
execContext.prepare(ctx, operation.Variables, operation.request)
Expand Down
44 changes: 44 additions & 0 deletions pkg/graphql/input_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package graphql

import (
"github.com/TykTechnologies/graphql-go-tools/pkg/operationreport"
"github.com/TykTechnologies/graphql-go-tools/pkg/variablevalidator"
)

type InputValidationResult struct {
Valid bool
Errors Errors
}

func inputValidationResultFromReport(report operationreport.Report) (InputValidationResult, error) {
result := InputValidationResult{
Valid: false,
Errors: nil,
}

if !report.HasErrors() {
result.Valid = true
return result, nil
}

result.Errors = RequestErrorsFromOperationReport(report)

var err error
if len(report.InternalErrors) > 0 {
err = report.InternalErrors[0]
}

return result, err
}

func (r *Request) ValidateInput(schema *Schema) (InputValidationResult, error) {
validator := variablevalidator.NewVariableValidator()

report := r.parseQueryOnce()
if report.HasErrors() {
return inputValidationResultFromReport(report)
}
validator.Validate(&r.document, &schema.document, []byte(r.OperationName), r.Variables, &report)

return inputValidationResultFromReport(report)
}
30 changes: 30 additions & 0 deletions pkg/graphql/input_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package graphql

import (
"github.com/TykTechnologies/graphql-go-tools/pkg/starwars"
"github.com/stretchr/testify/assert"
"testing"
)

func TestRequest_ValidateInput(t *testing.T) {
t.Run("Should pass input validation", func(t *testing.T) {
schema := starwarsSchema(t)
request := requestForQuery(t, starwars.FileDroidWithArgAndVarQuery)
request.Variables = []byte(`{"droidID":"testID"}`)

result, err := request.ValidateInput(schema)
assert.NoError(t, err)
assert.True(t, result.Valid)
assert.Nil(t, result.Errors)
})

t.Run("Should fail input validation", func(t *testing.T) {
schema := starwarsSchema(t)
request := requestForQuery(t, starwars.FileDroidWithArgAndVarQuery)

result, err := request.ValidateInput(schema)
assert.NoError(t, err)
assert.False(t, result.Valid)
assert.Equal(t, `Required variable "$droidID" was not provided, locations: [{Line:1 Column:13}], path: [query]`, result.Errors.Error())
})
}
15 changes: 15 additions & 0 deletions pkg/operationreport/externalerror.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const (
UnknownFieldOfInputObjectErrMsg = `Field "%s" is not defined by type "%s".`
DuplicatedFieldInputObjectErrMsg = `There can be only one input field named "%s".`
ValueIsNotAnInputObjectTypeErrMsg = `Expected value of type "%s", found %s.`
VariableNotProvidedErrMsg = `Required variable "$%s" was not provided`
VariableValidationFailedErrMsg = `Validation for variable "%s" failed: %s`
)

type ExternalError struct {
Expand Down Expand Up @@ -242,6 +244,19 @@ func ErrValueIsNotAnInputObjectType(value, inputType ast.ByteSlice, position pos
return err
}

func ErrVariableNotProvided(name ast.ByteSlice, position position.Position) (err ExternalError) {
err.Message = fmt.Sprintf(VariableNotProvidedErrMsg, name)
err.Locations = LocationsFromPosition(position)

return err
}

func ErrVariableValidationFailed(name ast.ByteSlice, message string, position position.Position) (err ExternalError) {
err.Message = fmt.Sprintf(VariableValidationFailedErrMsg, name, message)
err.Locations = LocationsFromPosition(position)
return err
}

func ErrValueDoesntSatisfyString(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) {
err.Message = fmt.Sprintf(NotStringErrMsg, inputType, value)
err.Locations = LocationsFromPosition(position)
Expand Down
107 changes: 107 additions & 0 deletions pkg/variablevalidator/variablevalidator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package variablevalidator

import (
"bytes"
"context"
"errors"
"fmt"
"github.com/TykTechnologies/graphql-go-tools/pkg/ast"
"github.com/TykTechnologies/graphql-go-tools/pkg/astvisitor"
"github.com/TykTechnologies/graphql-go-tools/pkg/graphqljsonschema"
"github.com/TykTechnologies/graphql-go-tools/pkg/operationreport"
"github.com/buger/jsonparser"
)

type VariableValidator struct {
walker *astvisitor.Walker
visitor *validatorVisitor
}

func NewVariableValidator() *VariableValidator {
walker := astvisitor.Walker{}
validator := VariableValidator{
walker: &walker,
visitor: &validatorVisitor{
Walker: &walker,
currentOperation: ast.InvalidRef,
},
}

validator.walker.RegisterEnterDocumentVisitor(validator.visitor)
validator.walker.RegisterEnterOperationVisitor(validator.visitor)
validator.walker.RegisterLeaveOperationVisitor(validator.visitor)
validator.walker.RegisterEnterVariableDefinitionVisitor(validator.visitor)

return &validator
}

type validatorVisitor struct {
*astvisitor.Walker

operationName, variables []byte
currentOperation int
operation, definition *ast.Document
}

func (v *validatorVisitor) EnterDocument(operation, definition *ast.Document) {
v.operation, v.definition = operation, definition
}

func (v *validatorVisitor) EnterVariableDefinition(ref int) {
if v.currentOperation == ast.InvalidRef {
return
}
typeRef := v.operation.VariableDefinitions[ref].Type

variableName := v.operation.VariableDefinitionNameBytes(ref)
variable, t, _, err := jsonparser.Get(v.variables, string(variableName))
if t == jsonparser.NotExist && v.operation.TypeIsNonNull(typeRef) {
v.StopWithExternalErr(operationreport.ErrVariableNotProvided(variableName, v.operation.VariableDefinitions[ref].VariableValue.Position))
return
}
if err != nil {
v.StopWithInternalErr(errors.New("error parsing variables"))
return
}

if t == jsonparser.String {
variable = []byte(fmt.Sprintf(`"%s"`, string(variable)))
}

jsonSchema := graphqljsonschema.FromTypeRef(v.operation, v.definition, typeRef)
schemaValidator, err := graphqljsonschema.NewValidatorFromSchema(jsonSchema)
if err != nil {
v.StopWithInternalErr(err)
return
}
if err := schemaValidator.Validate(context.Background(), variable); err != nil {
v.StopWithExternalErr(operationreport.ErrVariableValidationFailed(variableName, err.Error(), v.operation.VariableDefinitions[ref].VariableValue.Position))
return
}
}

func (v *validatorVisitor) EnterOperationDefinition(ref int) {
if len(v.operationName) == 0 {
v.currentOperation = ref
return
}

if bytes.Equal(v.operationName, v.operation.OperationDefinitionNameBytes(ref)) {
v.currentOperation = ref
}
}

func (v *validatorVisitor) LeaveOperationDefinition(ref int) {
if v.currentOperation == ref {
v.Stop()
}
}

func (v *VariableValidator) Validate(operation, definition *ast.Document, operationName, variables []byte, report *operationreport.Report) {
if v.visitor != nil {
v.visitor.operationName = operationName
v.visitor.variables = variables
}

v.walker.Walk(operation, definition, report)
}
Loading