Skip to content

Commit

Permalink
fix aliased GraphQL operations (#246)
Browse files Browse the repository at this point in the history
* fix idle pconn fatal error

* add index method remove node by name
add ast method replace root operation type definition
add ast method remove object type definition

* add normalization with retry to grapqhl datasource

* add code for extracting nested children and arguments (#95)

* move code for determine child nodes

* add function to find all field arguments

* add lookup functionality

* fix linter errors

* add recursion stop condition (#96)

* fix review comments

Co-authored-by: Sergey Petrunin <[email protected]>

* fix being unable to reuse arguments for rest

* refactor to planning behavior struct

* fix error when no planner is provided

* remove unused code

* enforce dot as first non-space character in templates (TT-1769) (#98)

* enforce dot as first non-space character in templates

* extend test cases

* change request.header to request.headers

* add a failing test for alias in schema

* add logic to allow DataSourcePlanners to rewrite downstream field aliases

Co-authored-by: Sergey Petrunin <[email protected]>
Co-authored-by: Sergey Petrunin <[email protected]>
Co-authored-by: Patric Vormstein <[email protected]>
  • Loading branch information
4 people authored Mar 10, 2021
1 parent 6c3e11b commit b0ff0fb
Show file tree
Hide file tree
Showing 15 changed files with 1,274 additions and 78 deletions.
21 changes: 21 additions & 0 deletions pkg/ast/ast_object_type_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,24 @@ func (d *Document) ImportObjectTypeDefinition(name, description string, fieldRef

return
}

func (d *Document) RemoveObjectTypeDefinition(name ByteSlice) bool {
node, ok := d.Index.FirstNodeByNameBytes(name)
if !ok {
return false
}

if node.Kind != NodeKindObjectTypeDefinition {
return false
}

for i := range d.RootNodes {
if d.RootNodes[i].Kind == NodeKindObjectTypeDefinition && d.RootNodes[i].Ref == node.Ref {
d.RootNodes = append(d.RootNodes[:i], d.RootNodes[i+1:]...)
break
}
}

d.Index.RemoveNodeByName(name)
return true
}
30 changes: 28 additions & 2 deletions pkg/ast/ast_root_operation_type_definition.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ast

import "github.com/jensneuse/graphql-go-tools/pkg/lexer/position"
import (
"github.com/jensneuse/graphql-go-tools/pkg/lexer/position"
)

type RootOperationTypeDefinitionList struct {
LBrace position.Position // {
Expand Down Expand Up @@ -74,12 +76,36 @@ func (d *Document) AddRootOperationTypeDefinition(rootOperationTypeDefinition Ro
}

func (d *Document) ImportRootOperationTypeDefinition(name string, operationType OperationType) (ref int) {
nameBytes := []byte(name)

switch operationType {
case OperationTypeQuery:
d.Index.QueryTypeName = nameBytes
case OperationTypeMutation:
d.Index.MutationTypeName = nameBytes
case OperationTypeSubscription:
d.Index.SubscriptionTypeName = nameBytes
default:
return -1
}

operationTypeDefinition := RootOperationTypeDefinition{
OperationType: operationType,
NamedType: Type{
Name: d.Input.AppendInputString(name),
Name: d.Input.AppendInputBytes(nameBytes),
},
}

return d.AddRootOperationTypeDefinition(operationTypeDefinition)
}

func (d *Document) ReplaceRootOperationTypeDefinition(name string, operationType OperationType) (ref int) {
for i := range d.RootOperationTypeDefinitions {
if d.RootOperationTypeDefinitions[i].OperationType == operationType {
d.RootOperationTypeDefinitions = append(d.RootOperationTypeDefinitions[:i], d.RootOperationTypeDefinitions[i+1:]...)
break
}
}

return d.ImportRootOperationTypeDefinition(name, operationType)
}
23 changes: 22 additions & 1 deletion pkg/ast/index.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package ast

import "github.com/cespare/xxhash"
import (
"bytes"

"github.com/cespare/xxhash"
)

// Index is a struct to easily look up objects in a document, e.g. find Nodes (type/interface/union definitions) by name
type Index struct {
Expand Down Expand Up @@ -83,3 +87,20 @@ func (i *Index) FirstNodeByNameBytes(name []byte) (Node, bool) {
}
return node[0], true
}

func (i *Index) RemoveNodeByName(name []byte) {
hash := xxhash.Sum64(name)
delete(i.nodes, hash)

if bytes.Equal(i.QueryTypeName, name) {
i.QueryTypeName = nil
}

if bytes.Equal(i.MutationTypeName, name) {
i.MutationTypeName = nil
}

if bytes.Equal(i.SubscriptionTypeName, name) {
i.SubscriptionTypeName = nil
}
}
105 changes: 90 additions & 15 deletions pkg/engine/datasource/graphql_datasource/graphql_datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,43 @@ type Planner struct {
hasFederationRoot bool
extractEntities bool
client httpclient.Client
isNested bool
isNested bool
rootTypeName string
}

func (p *Planner) DownstreamResponseFieldAlias(downstreamFieldRef int) (alias string, exists bool) {

// If there's no alias but the downstream Query re-uses the same path on different root fields,
// we rewrite the downstream Query using an alias so that we can have an aliased Query to the upstream
// while keeping a non aliased Query to the downstream but with a path rewrite on an existing root field.

fieldName := p.visitor.Operation.FieldNameString(downstreamFieldRef)

if p.visitor.Operation.FieldAliasIsDefined(downstreamFieldRef) {
return "", false
}

typeName := p.visitor.Walker.EnclosingTypeDefinition.NameString(p.visitor.Definition)
for i := range p.visitor.Config.Fields {
if p.visitor.Config.Fields[i].TypeName == typeName &&
p.visitor.Config.Fields[i].FieldName == fieldName &&
len(p.visitor.Config.Fields[i].Path) == 1 {

if p.visitor.Config.Fields[i].Path[0] != fieldName {
aliasBytes := p.visitor.Operation.FieldNameBytes(downstreamFieldRef)
return string(aliasBytes), true
}
break
}
}
return "", false
}

func (p *Planner) DataSourcePlanningBehavior() plan.DataSourcePlanningBehavior {
return plan.DataSourcePlanningBehavior{
MergeAliasedRootNodes: true,
OverrideFieldPathFromAlias: true,
}
}

type Configuration struct {
Expand Down Expand Up @@ -179,14 +215,16 @@ func (p *Planner) LeaveSelectionSet(ref int) {

func (p *Planner) EnterField(ref int) {

if p.rootTypeName == "" {
p.rootTypeName = p.visitor.Walker.EnclosingTypeDefinition.NameString(p.visitor.Definition)
}

p.lastFieldEnclosingTypeName = p.visitor.Walker.EnclosingTypeDefinition.NameString(p.visitor.Definition)

p.handleFederation(ref)

p.addField(ref)

// fmt.Printf("Planner::%s::%s::EnterField::%s::%d\n", p.id, p.visitor.Walker.Path.DotDelimitedString(), p.visitor.Operation.FieldNameString(ref), ref)

upstreamFieldRef := p.nodes[len(p.nodes)-1].Ref
typeName := p.lastFieldEnclosingTypeName
fieldName := p.visitor.Operation.FieldNameString(ref)
Expand Down Expand Up @@ -221,6 +259,7 @@ func (p *Planner) EnterDocument(operation, definition *ast.Document) {
p.disallowSingleFlight = false
p.hasFederationRoot = false
p.extractEntities = false
p.rootTypeName = ""
}

func (p *Planner) LeaveDocument(operation, definition *ast.Document) {
Expand Down Expand Up @@ -526,6 +565,11 @@ func (p *Planner) configureObjectFieldSource(upstreamFieldRef, downstreamFieldRe
}
}

const (
normalizationFailedErrMsg = "printOperation: normalization failed"
parseDocumentFailedErrMsg = "printOperation: parse %s failed"
)

func (p *Planner) printOperation() []byte {

buf := &bytes.Buffer{}
Expand Down Expand Up @@ -558,38 +602,61 @@ func (p *Planner) printOperation() []byte {

parser.Parse(operation, report)
if report.HasErrors() {
p.visitor.Walker.StopWithInternalErr(fmt.Errorf("printOperation: parse operation failed"))
p.stopWithError(parseDocumentFailedErrMsg, "operation")
return nil
}

parser.Parse(definition, report)
if report.HasErrors() {
p.visitor.Walker.StopWithInternalErr(fmt.Errorf("printOperation: parse definition failed"))
p.stopWithError(parseDocumentFailedErrMsg, "definition")
return nil
}

operationStr, _ := astprinter.PrintStringIndent(operation, definition, " ")
schemaStr, _ := astprinter.PrintStringIndent(definition, nil, " ")
_, _ = schemaStr, operationStr

normalizer := astnormalization.NewNormalizer(true, true)
normalizer.NormalizeOperation(operation, definition, report)

if report.HasErrors() {
p.visitor.Walker.StopWithInternalErr(fmt.Errorf("normalization failed"))
if !p.normalizeOperation(operation, definition, report, true) {
p.stopWithError(normalizationFailedErrMsg)
return nil
}

buf.Reset()

err = astprinter.Print(operation, p.visitor.Definition, buf)
if err != nil {
p.visitor.Walker.StopWithInternalErr(fmt.Errorf("normalization failed"))
p.stopWithError(normalizationFailedErrMsg)
return nil
}

return buf.Bytes()
}

func (p *Planner) stopWithError(msg string, args ...interface{}) {
p.visitor.Walker.StopWithInternalErr(fmt.Errorf(msg, args...))
}

// normalizeOperation - tries to normalize operation against definition.
//
// withRetry - temporary flag which will replace query type with corresponding root type and reruns normalization.
//
// TODO: implement less hacky solution to normalize upstream queries
func (p *Planner) normalizeOperation(operation, definition *ast.Document, report *operationreport.Report, withRetry bool) (ok bool) {

report.Reset()
normalizer := astnormalization.NewNormalizer(true, true)
normalizer.NormalizeOperation(operation, definition, report)

if !report.HasErrors() {
return true
}

if withRetry && p.isNested && !p.config.Federation.Enabled {
definition.RemoveObjectTypeDefinition(definition.Index.QueryTypeName)
definition.ReplaceRootOperationTypeDefinition(p.rootTypeName, ast.OperationTypeQuery)

return p.normalizeOperation(operation, definition, report, false)
}

return false
}

func (p *Planner) addField(ref int) {

fieldName := p.visitor.Operation.FieldNameString(ref)
Expand All @@ -608,7 +675,15 @@ func (p *Planner) addField(ref int) {
if p.visitor.Config.Fields[i].TypeName == typeName &&
p.visitor.Config.Fields[i].FieldName == fieldName &&
len(p.visitor.Config.Fields[i].Path) == 1 {

if p.visitor.Config.Fields[i].Path[0] != fieldName && !alias.IsDefined {
alias.IsDefined = true
aliasBytes := p.visitor.Operation.FieldNameBytes(ref)
alias.Name = p.upstreamOperation.Input.AppendInputBytes(aliasBytes)
}

fieldName = p.visitor.Config.Fields[i].Path[0]

break
}
}
Expand Down
Loading

0 comments on commit b0ff0fb

Please sign in to comment.