diff --git a/examples/federation/gateway/main.go b/examples/federation/gateway/main.go index e213b2d4b..28be367ed 100644 --- a/examples/federation/gateway/main.go +++ b/examples/federation/gateway/main.go @@ -19,7 +19,6 @@ import ( ) // It's just a simple example of graphql federation gateway server, it's NOT a production ready code. -// func logger() log.Logger { logger, err := zap.NewDevelopmentConfig().Build() if err != nil { @@ -59,7 +58,7 @@ func startServer() { datasourceWatcher := NewDatasourcePoller(httpClient, DatasourcePollerConfig{ Services: []ServiceConfig{ - {Name: "accounts", URL: "http://localhost:4008/query", Fallback: fallback}, + {Name: "accounts", URL: "http://localhost:4001/query", Fallback: fallback}, {Name: "products", URL: "http://localhost:4002/query", WS: "ws://localhost:4002/query"}, {Name: "reviews", URL: "http://localhost:4003/query"}, }, diff --git a/examples/federation/go.sum b/examples/federation/go.sum index 623bb1c8d..ed8f963e3 100644 --- a/examples/federation/go.sum +++ b/examples/federation/go.sum @@ -57,7 +57,7 @@ github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= @@ -188,8 +188,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9 h1:frX3nT9RkKybPnjyI+yvZh6ZucTZatCCEm9D47sZ2zo= @@ -198,8 +196,8 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -207,7 +205,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= @@ -226,7 +223,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -237,8 +233,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= @@ -251,8 +247,8 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/ast/ast_fragment_definition.go b/pkg/ast/ast_fragment_definition.go index 99b78e2d4..669156c1f 100644 --- a/pkg/ast/ast_fragment_definition.go +++ b/pkg/ast/ast_fragment_definition.go @@ -7,6 +7,8 @@ import ( "github.com/wundergraph/graphql-go-tools/pkg/lexer/position" ) +type FragmentDefinitionRef int + // TypeCondition // example: // on User @@ -17,11 +19,12 @@ type TypeCondition struct { // FragmentDefinition // example: -// fragment friendFields on User { -// id -// name -// profilePic(size: 50) -// } +// +// fragment friendFields on User { +// id +// name +// profilePic(size: 50) +// } type FragmentDefinition struct { FragmentLiteral position.Position // fragment Name ByteSliceReference // Name but not on, e.g. friendFields @@ -32,30 +35,30 @@ type FragmentDefinition struct { HasSelections bool } -func (d *Document) FragmentDefinitionRef(byName ByteSlice) (ref int, exists bool) { +func (d *Document) FragmentDefinitionRef(byName ByteSlice) (ref FragmentDefinitionRef, exists bool) { for i := range d.FragmentDefinitions { if bytes.Equal(byName, d.Input.ByteSlice(d.FragmentDefinitions[i].Name)) { - return i, true + return FragmentDefinitionRef(i), true } } return -1, false } -func (d *Document) FragmentDefinitionTypeName(ref int) ByteSlice { +func (d *Document) FragmentDefinitionTypeName(ref FragmentDefinitionRef) ByteSlice { return d.ResolveTypeNameBytes(d.FragmentDefinitions[ref].TypeCondition.Type) } -func (d *Document) FragmentDefinitionNameBytes(ref int) ByteSlice { +func (d *Document) FragmentDefinitionNameBytes(ref FragmentDefinitionRef) ByteSlice { return d.Input.ByteSlice(d.FragmentDefinitions[ref].Name) } -func (d *Document) FragmentDefinitionNameString(ref int) string { +func (d *Document) FragmentDefinitionNameString(ref FragmentDefinitionRef) string { return unsafebytes.BytesToString(d.Input.ByteSlice(d.FragmentDefinitions[ref].Name)) } -func (d *Document) FragmentDefinitionIsLastRootNode(ref int) bool { +func (d *Document) FragmentDefinitionIsLastRootNode(ref FragmentDefinitionRef) bool { for i := range d.RootNodes { - if d.RootNodes[i].Kind == NodeKindFragmentDefinition && d.RootNodes[i].Ref == ref { + if d.RootNodes[i].Kind == NodeKindFragmentDefinition && d.RootNodes[i].Ref == int(ref) { return len(d.RootNodes)-1 == i } } diff --git a/pkg/ast/ast_fragment_spread.go b/pkg/ast/ast_fragment_spread.go index 7b091c7d0..868e8c4b3 100644 --- a/pkg/ast/ast_fragment_spread.go +++ b/pkg/ast/ast_fragment_spread.go @@ -5,6 +5,8 @@ import ( "github.com/wundergraph/graphql-go-tools/pkg/lexer/position" ) +type FragmentSpreadRef int + // FragmentSpread // example: // ...MyFragment @@ -15,7 +17,7 @@ type FragmentSpread struct { Directives DirectiveList // optional, e.g. @foo } -func (d *Document) CopyFragmentSpread(ref int) int { +func (d *Document) CopyFragmentSpread(ref FragmentSpreadRef) FragmentSpreadRef { var directives DirectiveList if d.FragmentSpreads[ref].HasDirectives { directives = d.CopyDirectiveList(d.FragmentSpreads[ref].Directives) @@ -27,16 +29,16 @@ func (d *Document) CopyFragmentSpread(ref int) int { }) } -func (d *Document) AddFragmentSpread(spread FragmentSpread) int { +func (d *Document) AddFragmentSpread(spread FragmentSpread) FragmentSpreadRef { d.FragmentSpreads = append(d.FragmentSpreads, spread) - return len(d.FragmentSpreads) - 1 + return FragmentSpreadRef(len(d.FragmentSpreads) - 1) } -func (d *Document) FragmentSpreadNameBytes(ref int) ByteSlice { +func (d *Document) FragmentSpreadNameBytes(ref FragmentSpreadRef) ByteSlice { return d.Input.ByteSlice(d.FragmentSpreads[ref].FragmentName) } -func (d *Document) FragmentSpreadNameString(ref int) string { +func (d *Document) FragmentSpreadNameString(ref FragmentSpreadRef) string { return unsafebytes.BytesToString(d.FragmentSpreadNameBytes(ref)) } @@ -45,20 +47,20 @@ func (d *Document) FragmentSpreadNameString(ref int) string { // possible problems: changing directives or sub selections will affect both fields with the same id // simple solution: run normalization deduplicate fields // as part of the normalization flow this problem will be handled automatically -// just be careful in case you use this function outside of the normalization package -func (d *Document) ReplaceFragmentSpread(selectionSet int, spreadRef int, replaceWithSelectionSet int) { +// just be careful in case you use this function outside the normalization package +func (d *Document) ReplaceFragmentSpread(selectionSet int, spreadRef FragmentSpreadRef, replaceWithSelectionSet int) { for i, j := range d.SelectionSets[selectionSet].SelectionRefs { - if d.Selections[j].Kind == SelectionKindFragmentSpread && d.Selections[j].Ref == spreadRef { + if d.Selections[j].Kind == SelectionKindFragmentSpread && d.Selections[j].Ref == int(spreadRef) { d.SelectionSets[selectionSet].SelectionRefs = append(d.SelectionSets[selectionSet].SelectionRefs[:i], append(d.SelectionSets[replaceWithSelectionSet].SelectionRefs, d.SelectionSets[selectionSet].SelectionRefs[i+1:]...)...) - d.Index.ReplacedFragmentSpreads = append(d.Index.ReplacedFragmentSpreads, spreadRef) + d.Index.ReplacedFragmentSpreads = append(d.Index.ReplacedFragmentSpreads, int(spreadRef)) return } } } -// ReplaceFragmentSpreadWithInlineFragment replaces a given fragment spread with a inline fragment +// ReplaceFragmentSpreadWithInlineFragment replaces a given fragment spread with an inline fragment // attention! the same rules apply as for 'ReplaceFragmentSpread', look above! -func (d *Document) ReplaceFragmentSpreadWithInlineFragment(selectionSet int, spreadRef int, replaceWithSelectionSet int, typeCondition TypeCondition) { +func (d *Document) ReplaceFragmentSpreadWithInlineFragment(selectionSet int, spreadRef FragmentSpreadRef, replaceWithSelectionSet int, typeCondition TypeCondition) { d.InlineFragments = append(d.InlineFragments, InlineFragment{ TypeCondition: typeCondition, SelectionSet: replaceWithSelectionSet, @@ -71,9 +73,9 @@ func (d *Document) ReplaceFragmentSpreadWithInlineFragment(selectionSet int, spr }) selectionRef := len(d.Selections) - 1 for i, j := range d.SelectionSets[selectionSet].SelectionRefs { - if d.Selections[j].Kind == SelectionKindFragmentSpread && d.Selections[j].Ref == spreadRef { + if d.Selections[j].Kind == SelectionKindFragmentSpread && d.Selections[j].Ref == int(spreadRef) { d.SelectionSets[selectionSet].SelectionRefs = append(d.SelectionSets[selectionSet].SelectionRefs[:i], append([]int{selectionRef}, d.SelectionSets[selectionSet].SelectionRefs[i+1:]...)...) - d.Index.ReplacedFragmentSpreads = append(d.Index.ReplacedFragmentSpreads, spreadRef) + d.Index.ReplacedFragmentSpreads = append(d.Index.ReplacedFragmentSpreads, int(spreadRef)) return } } diff --git a/pkg/astvalidation/operation_rule_fragments.go b/pkg/astvalidation/operation_rule_fragments.go index 791611727..06392efb3 100644 --- a/pkg/astvalidation/operation_rule_fragments.go +++ b/pkg/astvalidation/operation_rule_fragments.go @@ -29,14 +29,23 @@ type fragmentsVisitor struct { fragmentDefinitionsVisited []ast.ByteSlice } -func (f *fragmentsVisitor) EnterFragmentSpread(ref int) { - if f.Ancestors[0].Kind == ast.NodeKindOperationDefinition { - spreadName := f.operation.FragmentSpreadNameBytes(ref) - f.StopWithExternalErr(operationreport.ErrFragmentSpreadFormsCycle(spreadName)) +func (f *fragmentsVisitor) EnterFragmentSpread(ref ast.FragmentSpreadRef) { + fragmentName := f.operation.FragmentSpreadNameBytes(ref) + defer func() { + if r := recover(); r != nil { + f.StopWithExternalErr(operationreport.ErrFragmentUndefined(fragmentName)) + } + }() + fragmentTypeName := f.operation.FragmentDefinitionTypeName(ref) + enclosingTypeName := f.EnclosingTypeDefinition.NameBytes(f.definition) + if !bytes.Equal(fragmentTypeName, enclosingTypeName) { + f.StopWithExternalErr(operationreport.ErrInvalidFragmentSpread(fragmentName, fragmentTypeName, enclosingTypeName)) + } else if f.Ancestors[0].Kind == ast.NodeKindOperationDefinition { + f.StopWithExternalErr(operationreport.ErrFragmentSpreadFormsCycle(fragmentName)) } } -func (f *fragmentsVisitor) LeaveDocument(operation, definition *ast.Document) { +func (f *fragmentsVisitor) LeaveDocument(_, _ *ast.Document) { for i := range f.fragmentDefinitionsVisited { if !f.operation.FragmentDefinitionIsUsed(f.fragmentDefinitionsVisited[i]) { fragmentName := f.fragmentDefinitionsVisited[i] @@ -89,7 +98,7 @@ func (f *fragmentsVisitor) EnterDocument(operation, definition *ast.Document) { f.fragmentDefinitionsVisited = f.fragmentDefinitionsVisited[:0] } -func (f *fragmentsVisitor) EnterFragmentDefinition(ref int) { +func (f *fragmentsVisitor) EnterFragmentDefinition(ref ast.FragmentDefinitionRef) { fragmentDefinitionName := f.operation.FragmentDefinitionNameBytes(ref) typeName := f.operation.FragmentDefinitionTypeName(ref) diff --git a/pkg/astvalidation/operation_validation_test.go b/pkg/astvalidation/operation_validation_test.go index 252712c5c..b59833e0e 100644 --- a/pkg/astvalidation/operation_validation_test.go +++ b/pkg/astvalidation/operation_validation_test.go @@ -2547,7 +2547,7 @@ func TestExecutionValidation(t *testing.T) { ...undefinedFragment } }`, - Fragments(), Invalid, withExpectNormalizationError()) + Fragments(), Invalid, withExpectNormalizationError(), withValidationErrors("undefinedFragment undefined")) }) }) t.Run("5.5.2.2 Fragment spreads must not form cycles", func(t *testing.T) { @@ -2566,7 +2566,7 @@ func TestExecutionValidation(t *testing.T) { barkVolume ...nameFragment }`, - Fragments(), Invalid) + Fragments(), Invalid, withValidationErrors("external: fragment spread: barkVolumeFragment forms fragment cycle")) }) t.Run("136", func(t *testing.T) { run(t, ` @@ -2669,6 +2669,18 @@ func TestExecutionValidation(t *testing.T) { }`, Fragments(), Valid) }) + t.Run("Spreading a fragment on an invalid type returns ErrInvalidFragmentSpread", func(t *testing.T) { + run(t, ` + { + dog { + ...invalidCatFragment + } + } + fragment invalidCatFragment on Cat { + meowVolume + }`, + Fragments(), Invalid, withValidationErrors("external: fragment spread: fragment invalidCatFragment must be spread on type Cat and not type Dog")) + }) }) t.Run("5.5.2.3.2 Abstract Spreads in Object Scope", func(t *testing.T) { t.Run("139", func(t *testing.T) { diff --git a/pkg/astvisitor/visitor.go b/pkg/astvisitor/visitor.go index 89ed77909..52e382391 100644 --- a/pkg/astvisitor/visitor.go +++ b/pkg/astvisitor/visitor.go @@ -131,13 +131,13 @@ type ( EnterFragmentSpreadVisitor interface { // EnterFragmentSpread gets called when the walker enters a fragment spread // ref is the reference to the selection set on the AST - EnterFragmentSpread(ref int) + EnterFragmentSpread(ref ast.FragmentSpreadRef) } // LeaveFragmentSpreadVisitor is the callback when the walker leaves a fragment spread LeaveFragmentSpreadVisitor interface { // LeaveFragmentSpread gets called when the walker leaves a fragment spread // ref is the reference to the selection set on the AST - LeaveFragmentSpread(ref int) + LeaveFragmentSpread(ref ast.FragmentSpreadRef) } // FragmentSpreadVisitor is the callback when the walker enters or leaves a fragment spread FragmentSpreadVisitor interface { @@ -165,13 +165,13 @@ type ( EnterFragmentDefinitionVisitor interface { // EnterFragmentDefinition gets called when the walker enters a fragment definition // ref is the reference to the selection set on the AST - EnterFragmentDefinition(ref int) + EnterFragmentDefinition(ref ast.FragmentDefinitionRef) } // LeaveFragmentDefinitionVisitor is the callback when the walker leaves a fragment definition LeaveFragmentDefinitionVisitor interface { // LeaveFragmentDefinition gets called when the walker leaves a fragment definition // ref is the reference to the selection set on the AST - LeaveFragmentDefinition(ref int) + LeaveFragmentDefinition(ref ast.FragmentDefinitionRef) } // FragmentDefinitionVisitor is the callback when the walker enters or leaves a fragment definition FragmentDefinitionVisitor interface { @@ -1355,7 +1355,7 @@ func (w *Walker) appendAncestor(ref int, kind ast.NodeKind) { } typeName = w.document.InlineFragmentTypeConditionName(ref) case ast.NodeKindFragmentDefinition: - typeName = w.document.FragmentDefinitionTypeName(ref) + typeName = w.document.FragmentDefinitionTypeName(ast.FragmentDefinitionRef(ref)) w.Path = append(w.Path, ast.PathItem{ Kind: ast.FieldName, ArrayIndex: 0, @@ -1951,7 +1951,7 @@ func (w *Walker) walkFragmentSpread(ref int) { for i := 0; i < len(w.visitors.enterFragmentSpread); { if w.filter == nil || w.filter.AllowVisitor(EnterFragmentSpread, ref, w.visitors.enterFragmentSpread[i]) { - w.visitors.enterFragmentSpread[i].EnterFragmentSpread(ref) + w.visitors.enterFragmentSpread[i].EnterFragmentSpread(ast.FragmentSpreadRef(ref)) } if w.revisit { w.revisit = false @@ -1970,7 +1970,7 @@ func (w *Walker) walkFragmentSpread(ref int) { for i := 0; i < len(w.visitors.leaveFragmentSpread); { if w.filter == nil || w.filter.AllowVisitor(LeaveFragmentSpread, ref, w.visitors.leaveFragmentSpread[i]) { - w.visitors.leaveFragmentSpread[i].LeaveFragmentSpread(ref) + w.visitors.leaveFragmentSpread[i].LeaveFragmentSpread(ast.FragmentSpreadRef(ref)) } if w.revisit { w.revisit = false @@ -2071,7 +2071,7 @@ func (w *Walker) walkFragmentDefinition(ref int) { for i := 0; i < len(w.visitors.enterFragmentDefinition); { if w.filter == nil || w.filter.AllowVisitor(EnterFragmentDefinition, ref, w.visitors.enterFragmentDefinition[i]) { - w.visitors.enterFragmentDefinition[i].EnterFragmentDefinition(ref) + w.visitors.enterFragmentDefinition[i].EnterFragmentDefinition(ast.FragmentDefinitionRef(ref)) } if w.revisit { w.revisit = false @@ -2112,7 +2112,7 @@ func (w *Walker) walkFragmentDefinition(ref int) { for i := 0; i < len(w.visitors.leaveFragmentDefinition); { if w.filter == nil || w.filter.AllowVisitor(LeaveFragmentDefinition, ref, w.visitors.leaveFragmentDefinition[i]) { - w.visitors.leaveFragmentDefinition[i].LeaveFragmentDefinition(ref) + w.visitors.leaveFragmentDefinition[i].LeaveFragmentDefinition(ast.FragmentDefinitionRef(ref)) } if w.revisit { w.revisit = false diff --git a/pkg/graphql/execution_engine_v2_test.go b/pkg/graphql/execution_engine_v2_test.go index 1a600f7b4..9723247ff 100644 --- a/pkg/graphql/execution_engine_v2_test.go +++ b/pkg/graphql/execution_engine_v2_test.go @@ -151,7 +151,7 @@ type ExecutionEngineV2TestCase struct { } func TestExecutionEngineV2_Execute(t *testing.T) { - run := func(testCase ExecutionEngineV2TestCase, withError bool) func(t *testing.T) { + run := func(testCase ExecutionEngineV2TestCase, withError bool, expectedErrorMessage string) func(t *testing.T) { t.Helper() return func(t *testing.T) { @@ -188,6 +188,9 @@ func TestExecutionEngineV2_Execute(t *testing.T) { if withError { assert.Error(t, err) + if expectedErrorMessage != "" { + assert.Contains(t, err.Error(), expectedErrorMessage) + } } else { assert.NoError(t, err) } @@ -195,11 +198,15 @@ func TestExecutionEngineV2_Execute(t *testing.T) { } runWithError := func(testCase ExecutionEngineV2TestCase) func(t *testing.T) { - return run(testCase, true) + return run(testCase, true, "") + } + + runWithAndCompareError := func(testCase ExecutionEngineV2TestCase, expectedErrorMessage string) func(t *testing.T) { + return run(testCase, true, expectedErrorMessage) } runWithoutError := func(testCase ExecutionEngineV2TestCase) func(t *testing.T) { - return run(testCase, false) + return run(testCase, false, "") } t.Run("execute with empty request object should not panic", runWithError( @@ -1613,6 +1620,59 @@ func TestExecutionEngineV2_Execute(t *testing.T) { expectedResponse: `{"data":{"hero":"Human"}}`, }, )) + + t.Run("Spreading a fragment on an invalid type returns ErrInvalidFragmentSpread", runWithAndCompareError( + ExecutionEngineV2TestCase{ + schema: starwarsSchema(t), + operation: loadStarWarsQuery(starwars.FileInvalidFragmentsQuery, nil), + dataSources: []plan.DataSourceConfiguration{ + { + RootNodes: []plan.TypeField{ + { + TypeName: "Query", + FieldNames: []string{"droid"}, + }, + }, + ChildNodes: []plan.TypeField{ + { + TypeName: "Droid", + FieldNames: []string{"name"}, + }, + }, + Factory: &graphql_datasource.Factory{ + HTTPClient: testNetHttpClient(t, roundTripperTestCase{ + expectedHost: "example.com", + expectedPath: "/", + expectedBody: "", + sendResponseBody: `{"data":{"droid":{"name":"R2D2"}}}`, + sendStatusCode: 200, + }), + }, + Custom: graphql_datasource.ConfigJson(graphql_datasource.Configuration{ + Fetch: graphql_datasource.FetchConfiguration{ + URL: "https://example.com/", + Method: "GET", + }, + }), + }, + }, + fields: []plan.FieldConfiguration{ + { + TypeName: "Query", + FieldName: "droid", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "id", + SourceType: plan.FieldArgumentSource, + RenderConfig: plan.RenderArgumentAsGraphQLValue, + }, + }, + }, + }, + expectedResponse: ``, + }, + "fragment spread: fragment reviewFields must be spread on type Review and not type Droid", + )) } func testNetHttpClient(t *testing.T, testCase roundTripperTestCase) *http.Client { diff --git a/pkg/operationreport/externalerror.go b/pkg/operationreport/externalerror.go index 1ee1fc7b5..726a439bf 100644 --- a/pkg/operationreport/externalerror.go +++ b/pkg/operationreport/externalerror.go @@ -337,6 +337,14 @@ func ErrFragmentSpreadFormsCycle(spreadName ast.ByteSlice) (err ExternalError) { return err } +func ErrInvalidFragmentSpread(fragmentName, fragmentTypeName, enclosingName ast.ByteSlice) (err ExternalError) { + err.Message = fmt.Sprintf( + "fragment spread: fragment %s must be spread on type %s and not type %s", + fragmentName, fragmentTypeName, enclosingName, + ) + return err +} + func ErrFragmentDefinedButNotUsed(fragmentName ast.ByteSlice) (err ExternalError) { err.Message = fmt.Sprintf("fragment: %s defined but not used", fragmentName) return err diff --git a/pkg/starwars/starwars.go b/pkg/starwars/starwars.go index 9abf7b052..94f263472 100644 --- a/pkg/starwars/starwars.go +++ b/pkg/starwars/starwars.go @@ -32,6 +32,7 @@ const ( FileMultiQueries = "testdata/queries/multi_queries.query" FileMultiQueriesWithArguments = "testdata/queries/multi_queries_with_arguments.query" FileInvalidQuery = "testdata/queries/invalid.query" + FileInvalidFragmentsQuery = "testdata/queries/invalid_fragments.query" ) var ( diff --git a/pkg/starwars/testdata/queries/invalid_fragments.query b/pkg/starwars/testdata/queries/invalid_fragments.query new file mode 100644 index 000000000..e3e9f26dc --- /dev/null +++ b/pkg/starwars/testdata/queries/invalid_fragments.query @@ -0,0 +1,11 @@ +query Fragments($droidID: ID!){ + droid(id: $droidID) { + ...reviewFields + } +} + +fragment reviewFields on Review { + id + stars + commentary +} \ No newline at end of file