Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.idea/*
.vscode/*
*.out
*.test
.DS_Store
pkg/parser/testdata/lotto.graphql
*node_modules*
*vendor*
*vendor*
64 changes: 44 additions & 20 deletions v2/pkg/ast/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ func (p Path) Equals(another Path) bool {
return true
}

func (p Path) Overlaps(other Path) bool {
for i, el := range p {
switch {
case i >= len(other):
return true
case el.Kind != other[i].Kind:
return false
case el.FragmentRef != other[i].FragmentRef:
return false
case el.Kind == ArrayIndex && el.ArrayIndex != other[i].ArrayIndex:
return false
case !bytes.Equal(el.FieldName, other[i].FieldName):
return false
}
}
return true
}

func (p Path) EndsWithFragment() bool {
if len(p) == 0 {
return false
Expand All @@ -77,29 +95,35 @@ func (p Path) WithoutInlineFragmentNames() Path {
return out
}

func (p Path) StringSlice() []string {
ret := make([]string, len(p))
for i, item := range p {
ret[i] = item.String()
}
return ret
}

func (p Path) String() string {
out := "["
for i := range p {
if i != 0 {
out += ","
}
switch p[i].Kind {
case ArrayIndex:
out += strconv.Itoa(p[i].ArrayIndex)
case FieldName:
if len(p[i].FieldName) == 0 {
out += "query"
} else {
out += unsafebytes.BytesToString(p[i].FieldName)
}
case InlineFragmentName:
out += InlineFragmentPathPrefix
out += strconv.Itoa(p[i].FragmentRef)
out += unsafebytes.BytesToString(p[i].FieldName)
return "[" + strings.Join(p.StringSlice(), ",") + "]"
}

func (p PathItem) String() string {
switch p.Kind {
case ArrayIndex:
return strconv.Itoa(p.ArrayIndex)
case FieldName:
out := "query"
if len(p.FieldName) != 0 {
out = unsafebytes.BytesToString(p.FieldName)
}
return out
case InlineFragmentName:
out := InlineFragmentPathPrefix
out += strconv.Itoa(p.FragmentRef)
out += unsafebytes.BytesToString(p.FieldName)
return out
}
out += "]"
return out
return ""
}

func (p Path) DotDelimitedString() string {
Expand Down
106 changes: 106 additions & 0 deletions v2/pkg/ast/path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package ast_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
)

func TestPath_Overlaps(t *testing.T) {
tests := []struct {
name string
a, b ast.Path
expect bool
}{
{
name: "both empty",
a: ast.Path{},
b: ast.Path{},
expect: true,
},
{
name: "same single field",
a: ast.Path{{Kind: ast.FieldName, FieldName: []byte("foo")}},
b: ast.Path{{Kind: ast.FieldName, FieldName: []byte("foo")}},
expect: true,
},
{
name: "one empty",
a: ast.Path{},
b: ast.Path{{Kind: ast.FieldName, FieldName: []byte("foo")}},
expect: true,
},
{
name: "different single field",
a: ast.Path{{Kind: ast.FieldName, FieldName: []byte("foo")}},
b: ast.Path{{Kind: ast.FieldName, FieldName: []byte("bar")}},
expect: false,
},
{
name: "prefix matches but one is shorter",
a: ast.Path{{Kind: ast.FieldName, FieldName: []byte("foo")}},
b: ast.Path{
{Kind: ast.FieldName, FieldName: []byte("foo")},
{Kind: ast.FieldName, FieldName: []byte("bar")},
},
expect: true,
},
{
name: "same index array overlap",
a: ast.Path{{Kind: ast.ArrayIndex, ArrayIndex: 0}},
b: ast.Path{{Kind: ast.ArrayIndex, ArrayIndex: 0}},
expect: true,
},
{
name: "different index array overlap",
a: ast.Path{{Kind: ast.ArrayIndex, ArrayIndex: 1}},
b: ast.Path{{Kind: ast.ArrayIndex, ArrayIndex: 2}},
expect: false,
},
{
name: "fragment mismatch",
a: ast.Path{{Kind: ast.InlineFragmentName, FragmentRef: 1, FieldName: []byte("FragA")}},
b: ast.Path{{Kind: ast.InlineFragmentName, FragmentRef: 2, FieldName: []byte("FragA")}},
expect: false,
},
{
name: "fragment match",
a: ast.Path{{Kind: ast.InlineFragmentName, FragmentRef: 1, FieldName: []byte("FragA")}},
b: ast.Path{{Kind: ast.InlineFragmentName, FragmentRef: 1, FieldName: []byte("FragA")}},
expect: true,
},
{
name: "mixed path partial overlap",
a: ast.Path{
{Kind: ast.FieldName, FieldName: []byte("foo")},
{Kind: ast.ArrayIndex, ArrayIndex: 1},
},
b: ast.Path{
{Kind: ast.FieldName, FieldName: []byte("foo")},
{Kind: ast.ArrayIndex, ArrayIndex: 1},
{Kind: ast.FieldName, FieldName: []byte("extra")},
},
expect: true,
},
{
name: "mixed path no overlap at second item",
a: ast.Path{
{Kind: ast.FieldName, FieldName: []byte("foo")},
{Kind: ast.ArrayIndex, ArrayIndex: 2},
},
b: ast.Path{
{Kind: ast.FieldName, FieldName: []byte("foo")},
{Kind: ast.ArrayIndex, ArrayIndex: 3},
},
expect: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.a.Overlaps(tt.b), tt.expect)
assert.Equal(t, tt.b.Overlaps(tt.a), tt.expect)
})
}
}
16 changes: 16 additions & 0 deletions v2/pkg/asttransform/baseschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,22 @@ directive @skip(
"Skipped when true."
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"Directs the executor to defer this fragment when the if argument is true or undefined."
directive @defer(
"A unique identifier for the results."
label: String
"Controls whether the fragment will be deferred, usually via a variable."
if: Boolean! = true
) on FRAGMENT_SPREAD | INLINE_FRAGMENT
"Directs the executor to stream this array field when the if argument is true or undefined."
directive @stream(
"A unique identifier for the results."
label: String
"Controls streaming, usually via a variable."
if: Boolean! = true
"The number of results to include in the initial (non-streamed) response."
initialCount: Int = 0
) on FIELD
"Marks an element of a GraphQL schema as no longer supported."
directive @deprecated(
"""
Expand Down
12 changes: 10 additions & 2 deletions v2/pkg/engine/plan/analyze_plan_kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,19 @@ func (p *planKindVisitor) EnterDirective(ref int) {
switch ancestor.Kind {
case ast.NodeKindField:
switch directiveName {
case "defer":
p.hasDeferDirective = true
case "stream":
p.hasStreamDirective = true
}
case ast.NodeKindInlineFragment:
switch directiveName {
case "defer":
p.hasDeferDirective = true
}
case ast.NodeKindFragmentSpread:
switch directiveName {
case "defer":
p.hasDeferDirective = true
}
}
}

Expand Down
12 changes: 9 additions & 3 deletions v2/pkg/engine/plan/analyze_plan_kind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ func TestAnalyzePlanKind(t *testing.T) {
name
}
primaryFunction
favoriteEpisode @defer
... @defer {
favoriteEpisode
}
}
}`,
"MyQuery",
Expand Down Expand Up @@ -146,7 +148,9 @@ func TestAnalyzePlanKind(t *testing.T) {
name
}
primaryFunction
favoriteEpisode @defer
... @defer {
favoriteEpisode
}
}
}`,
"OperationNameNotExists",
Expand All @@ -167,7 +171,9 @@ func TestAnalyzePlanKind(t *testing.T) {
subscription NewReviews {
newReviews {
id
stars @defer
... @defer {
stars
}
}
}`,
"NewReviews",
Expand Down
47 changes: 47 additions & 0 deletions v2/pkg/engine/plan/configuration_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/argument_templates"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve"
"github.com/wundergraph/graphql-go-tools/v2/pkg/lexer/literal"
)

// configurationVisitor - walks through the operation multiple times to collect plannings paths
Expand All @@ -36,6 +37,8 @@ type configurationVisitor struct {
nodeSuggestions *NodeSuggestions // nodeSuggestions holds information about suggested data sources for each field
nodeSuggestionHints []NodeSuggestionHint // nodeSuggestionHints holds information about suggested data sources for key fields

targetDefer *resolve.DeferInfo // deferPath directs the planning to the specified path.

parentTypeNodes []ast.Node // parentTypeNodes is a stack of parent type nodes - used to determine if the parent is abstract
arrayFields []arrayField // arrayFields is a stack of array fields - used to plan nested queries
selectionSetRefs []int // selectionSetRefs is a stack of selection set refs - used to add a required fields
Expand Down Expand Up @@ -110,6 +113,7 @@ type objectFetchConfiguration struct {
dependsOnFetchIDs []int
rootFields []resolve.GraphCoordinate
operationType ast.OperationType
deferInfo *resolve.DeferInfo
}

func (c *configurationVisitor) currentSelectionSet() int {
Expand Down Expand Up @@ -364,13 +368,55 @@ func (c *configurationVisitor) LeaveSelectionSet(ref int) {
c.parentTypeNodes = c.parentTypeNodes[:len(c.parentTypeNodes)-1]
}

func (c *configurationVisitor) EnterInlineFragment(ref int) {
c.deleteDeferAndSkipIfNeeded(ref)
}

func (c *configurationVisitor) EnterFragmentSpread(ref int) {
c.deleteDeferAndSkipIfNeeded(ref)
}

func (c *configurationVisitor) inDeferPath(item ast.PathItem) bool {
if c.targetDefer == nil {
return true
}
fullPath := append(make([]ast.PathItem, 0, len(c.walker.Path)+1), c.walker.Path...)
fullPath = append(fullPath, item)

return c.targetDefer.Overlaps(fullPath)
}

func (c *configurationVisitor) deleteDeferAndSkipIfNeeded(ref int) {
directives := c.operation.InlineFragments[ref].Directives.Refs
deferRef, found := c.operation.DirectiveWithNameBytes(directives, literal.DEFER)

if !found {
// No defer directive here.
return
}
// Don't pass the directive on in any case.
if idx := slices.Index(directives, deferRef); idx >= 0 {
c.operation.InlineFragments[ref].Directives.Refs = slices.Delete(directives, idx, idx+1)
}

// If target is nil or doesn't match, we skip the deferred fragment.
if c.targetDefer == nil || !c.targetDefer.Equals(&resolve.DeferInfo{Path: c.walker.Path}) {
c.walker.SkipNode()
}
}

func (c *configurationVisitor) EnterField(fieldRef int) {
fieldName := c.operation.FieldNameUnsafeString(fieldRef)
fieldAliasOrName := c.operation.FieldAliasOrNameString(fieldRef)
typeName := c.walker.EnclosingTypeDefinition.NameString(c.definition)

c.debugPrint("EnterField ref:", fieldRef, "fieldName:", fieldName, "typeName:", typeName)

if !c.inDeferPath(ast.PathItem{Kind: ast.FieldName, FieldName: c.operation.FieldAliasOrNameBytes(fieldRef)}) {
c.debugPrint(" ...", fieldName, "skipped")
c.walker.SkipNode()
return
}
parentPath := c.walker.Path.DotDelimitedString()
// we need to also check preceding path for inline fragments
// as for the field within inline fragment the parent path will include type condition in a path
Expand Down Expand Up @@ -835,6 +881,7 @@ func (c *configurationVisitor) addNewPlanner(fieldRef int, typeName, fieldName,
sourceName: dsConfig.Name(),
operationType: c.resolveRootFieldOperationType(typeName),
filter: c.resolveSubscriptionFilterCondition(typeName, fieldName),
deferInfo: c.targetDefer,
}

plannerPathConfig := newPlannerPathsConfiguration(
Expand Down
Loading