Skip to content

Commit

Permalink
Input field default values (#380)
Browse files Browse the repository at this point in the history
* Merge pull request #271 from TykTechnologies/fix/TT-5563-input-default

Fix/tt 5563 input default

* fixed bug with nullable field

* fix inject input default and support list of custom scalar

* gofmt changes
  • Loading branch information
kofoworola committed Jun 29, 2022
1 parent 0983996 commit be7d24d
Show file tree
Hide file tree
Showing 5 changed files with 509 additions and 2 deletions.
6 changes: 5 additions & 1 deletion pkg/astnormalization/astnormalization.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,11 @@ func (o *OperationNormalizer) setupOperationWalkers() {
if o.options.removeUnusedVariables {
deleteUnusedVariables(&other)
}
o.operationWalkers = append(o.operationWalkers, &fragmentInline, &extractVariablesWalker, &other)

injectInputDefaultWalker := astvisitor.NewWalker(48)
injectInputFieldDefaults(&injectInputDefaultWalker)

o.operationWalkers = append(o.operationWalkers, &fragmentInline, &extractVariablesWalker, &other, &injectInputDefaultWalker)
}

func (o *OperationNormalizer) prepareDefinition(definition *ast.Document, report *operationreport.Report) {
Expand Down
27 changes: 27 additions & 0 deletions pkg/astnormalization/astnormalization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ func TestNormalizeOperation(t *testing.T) {
disallowedSecondRootField
}`, "", "")
})
t.Run("inject default", func(t *testing.T) {
run(t,
injectDefaultValueDefinition, `
query{elQuery(input:{fieldB: "dupa"})}`,
`query($a: elInput){elQuery(input: $a)}`, "",
`{"a":{"fieldB":"dupa","fieldA":"VALUE_A"}}`,
)
})
t.Run("fragments", func(t *testing.T) {
run(t, testDefinition, `
query conflictingBecauseAlias ($unused: String) {
Expand Down Expand Up @@ -690,3 +698,22 @@ extend type Subscription {
textCounter: String
}
`
const injectDefaultValueDefinition = `
type Query {
elQuery(input: elInput): Boolean!
}
type Mutation{
elMutation(input: elInput!): Boolean!
}
input elInput{
fieldA: MyEnum! = VALUE_A
fieldB: String
}
enum MyEnum {
VALUE_A
VALUE_B
}
`
210 changes: 210 additions & 0 deletions pkg/astnormalization/inject_input_default_values.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package astnormalization

import (
"errors"
"fmt"
"github.com/buger/jsonparser"
"github.com/wundergraph/graphql-go-tools/pkg/ast"
"github.com/wundergraph/graphql-go-tools/pkg/astvisitor"
)

func injectInputFieldDefaults(walker *astvisitor.Walker) *inputFieldDefaultInjectionVisitor {
visitor := &inputFieldDefaultInjectionVisitor{
Walker: walker,
jsonPath: make([]string, 0),
}
walker.RegisterEnterDocumentVisitor(visitor)
walker.RegisterVariableDefinitionVisitor(visitor)
return visitor
}

type inputFieldDefaultInjectionVisitor struct {
*astvisitor.Walker

operation *ast.Document
definition *ast.Document

variableName string
jsonPath []string
}

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

func (v *inputFieldDefaultInjectionVisitor) EnterVariableDefinition(ref int) {
v.variableName = v.operation.VariableDefinitionNameString(ref)

variableVal, _, _, err := jsonparser.Get(v.operation.Input.Variables, v.variableName)
if err == jsonparser.KeyPathNotFoundError {
return
}
if err != nil {
v.StopWithInternalErr(err)
return
}

typeRef := v.operation.VariableDefinitions[ref].Type
if v.isScalarTypeOrExtension(typeRef, v.operation) {
return
}
newVal, err := v.processObjectOrListInput(typeRef, variableVal, v.operation)
if err != nil {
v.StopWithInternalErr(err)
return
}
newVariables, err := jsonparser.Set(v.operation.Input.Variables, newVal, v.variableName)
if err != nil {
v.StopWithInternalErr(err)
return
}
v.operation.Input.Variables = newVariables
}

func (v *inputFieldDefaultInjectionVisitor) recursiveInjectInputFields(inputObjectRef int, varValue []byte) ([]byte, error) {
finalVal := varValue
objectDef := v.definition.InputObjectTypeDefinitions[inputObjectRef]
if !objectDef.HasInputFieldsDefinition {
return varValue, nil
}
for _, ref := range objectDef.InputFieldsDefinition.Refs {
valDef := v.definition.InputValueDefinitions[ref]
fieldName := v.definition.InputValueDefinitionNameString(ref)
isTypeScalarOrEnum := v.isScalarTypeOrExtension(valDef.Type, v.definition)
hasDefault := valDef.DefaultValue.IsDefined

varVal, _, _, err := jsonparser.Get(varValue, fieldName)
if err != nil && err != jsonparser.KeyPathNotFoundError {
v.StopWithInternalErr(err)
return nil, err
}
existsInVal := err != jsonparser.KeyPathNotFoundError

if !isTypeScalarOrEnum {
var valToUse []byte
if existsInVal {
valToUse = varVal
} else if hasDefault {
defVal, err := v.definition.ValueToJSON(valDef.DefaultValue.Value)
if err != nil {
return nil, err
}
valToUse = defVal
} else {
continue
}
fieldValue, err := v.processObjectOrListInput(valDef.Type, valToUse, v.definition)
if err != nil {
return nil, err
}
finalVal, err = jsonparser.Set(finalVal, fieldValue, fieldName)
if err != nil {
return nil, err
}
continue
}

if !hasDefault && isTypeScalarOrEnum {
continue
}
if existsInVal {
continue
}
defVal, err := v.definition.ValueToJSON(valDef.DefaultValue.Value)
if err != nil {
return nil, err
}

finalVal, err = jsonparser.Set(finalVal, defVal, fieldName)
if err != nil {
return nil, err
}
}
return finalVal, nil
}

func (v *inputFieldDefaultInjectionVisitor) isScalarTypeOrExtension(typeRef int, typeDoc *ast.Document) bool {
if typeDoc.TypeIsScalar(typeRef, v.definition) || typeDoc.TypeIsEnum(typeRef, v.definition) {
return true
}
typeName := typeDoc.TypeNameBytes(typeRef)
node, found := v.definition.Index.FirstNonExtensionNodeByNameBytes(typeName)
if !found {
return false
}
switch node.Kind {
case ast.NodeKindScalarTypeDefinition, ast.NodeKindEnumTypeDefinition:
return true
}
return false
}

func (v *inputFieldDefaultInjectionVisitor) processObjectOrListInput(fieldType int, defaultValue []byte, typeDoc *ast.Document) ([]byte, error) {
finalVal := defaultValue
fieldIsList := typeDoc.TypeIsList(fieldType)
varVal, valType, _, err := jsonparser.Get(defaultValue)
if err != nil {
return nil, err

}
node, found := v.definition.Index.FirstNodeByNameBytes(typeDoc.ResolveTypeNameBytes(fieldType))
if !found {
return finalVal, nil
}
if node.Kind == ast.NodeKindScalarTypeDefinition {
return finalVal, nil
}
valIsList := valType == jsonparser.Array
if fieldIsList && valIsList {
_, err := jsonparser.ArrayEach(varVal, v.jsonWalker(typeDoc.ResolveListOrNameType(fieldType), defaultValue, &node, typeDoc, &finalVal))
if err != nil {
return nil, err

}
} else if !fieldIsList && !valIsList {
finalVal, err = v.recursiveInjectInputFields(node.Ref, defaultValue)
if err != nil {
return nil, err
}
} else {
return nil, errors.New("mismatched input value")
}
return finalVal, nil
}

func (v *inputFieldDefaultInjectionVisitor) jsonWalker(fieldType int, defaultValue []byte, node *ast.Node, typeDoc *ast.Document, finalVal *[]byte) func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
i := 0
listOfList := typeDoc.TypeIsList(typeDoc.Types[fieldType].OfType)
return func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
if err != nil {
return
}
if listOfList && dataType == jsonparser.Array {
newVal, err := v.processObjectOrListInput(typeDoc.Types[fieldType].OfType, value, typeDoc)
if err != nil {
return
}
*finalVal, err = jsonparser.Set(defaultValue, newVal, fmt.Sprintf("[%d]", i))
if err != nil {
return
}
} else if !listOfList && dataType == jsonparser.Object {
newVal, err := v.recursiveInjectInputFields(node.Ref, value)
if err != nil {
return
}
*finalVal, err = jsonparser.Set(defaultValue, newVal, fmt.Sprintf("[%d]", i))
if err != nil {
return
}
} else {
return
}
i++
}

}
func (v *inputFieldDefaultInjectionVisitor) LeaveVariableDefinition(ref int) {
v.variableName = ""
v.jsonPath = make([]string, 0)
}
Loading

0 comments on commit be7d24d

Please sign in to comment.