Skip to content

Commit b92d3aa

Browse files
authored
Batching integration (#63)
* renamed NetworkQueryer to SingleRequestQueryer * applying network middlewares works on any QueryerWithMiddlewares * planner's QueryerFactory now takes the planning context * renamed configurator to option * added type declaration for queryer factory * fixed comments * added test for passing queryer factory * cleanup * comments * more comment tidying
1 parent 1ac0f74 commit b92d3aa

File tree

8 files changed

+115
-63
lines changed

8 files changed

+115
-63
lines changed

execute.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ func executeStep(
205205
// if we have middlewares
206206
if len(ctx.RequestMiddlewares) > 0 {
207207
// if the queryer is a network queryer
208-
if nQueryer, ok := queryer.(*graphql.NetworkQueryer); ok {
208+
if nQueryer, ok := queryer.(graphql.QueryerWithMiddlewares); ok {
209209
queryer = nQueryer.WithMiddlewares(ctx.RequestMiddlewares)
210210
}
211211
}

execute_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -729,10 +729,7 @@ func TestExecutor_appliesRequestMiddlewares(t *testing.T) {
729729
},
730730
QueryString: `hello`,
731731
// return a known value we can test against
732-
Queryer: &graphql.NetworkQueryer{
733-
URL: "hello",
734-
Client: httpClient,
735-
},
732+
Queryer: graphql.NewSingleRequestQueryer("hello").WithHTTPClient(httpClient),
736733
},
737734
},
738735
},

gateway.go

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,18 @@ import (
1111
"github.com/nautilus/graphql"
1212
)
1313

14-
type contextKey int
15-
1614
// Gateway is the top level entry for interacting with a gateway. It is responsible for merging a list of
1715
// remote schemas into one, generating a query plan to execute based on an incoming request, and following
1816
// that plan
1917
type Gateway struct {
20-
sources []*graphql.RemoteSchema
21-
schema *ast.Schema
22-
planner QueryPlanner
23-
executor Executor
24-
merger Merger
25-
middlewares MiddlewareList
26-
queryFields []*QueryField
18+
sources []*graphql.RemoteSchema
19+
schema *ast.Schema
20+
planner QueryPlanner
21+
executor Executor
22+
merger Merger
23+
middlewares MiddlewareList
24+
queryFields []*QueryField
25+
queryerFactory *QueryerFactory
2726

2827
// group up the list of middlewares at startup to avoid it during execution
2928
requestMiddlewares []graphql.NetworkMiddleware
@@ -94,7 +93,7 @@ func (g *Gateway) internalSchema() *ast.Schema {
9493
}
9594

9695
// New instantiates a new schema with the required stuffs.
97-
func New(sources []*graphql.RemoteSchema, configs ...Configurator) (*Gateway, error) {
96+
func New(sources []*graphql.RemoteSchema, configs ...Option) (*Gateway, error) {
9897
// if there are no source schemas
9998
if len(sources) == 0 {
10099
return nil, errors.New("a gateway must have at least one schema")
@@ -109,11 +108,19 @@ func New(sources []*graphql.RemoteSchema, configs ...Configurator) (*Gateway, er
109108
queryFields: []*QueryField{nodeField},
110109
}
111110

112-
// pass the gateway through any configurators
111+
// pass the gateway through any Options
113112
for _, config := range configs {
114113
config(gateway)
115114
}
116115

116+
// if we have a queryer factory to assign
117+
if gateway.queryerFactory != nil {
118+
// if the planner can accept the factory
119+
if planner, ok := gateway.planner.(PlannerWithQueryerFactory); ok {
120+
gateway.planner = planner.WithQueryerFactory(gateway.queryerFactory)
121+
}
122+
}
123+
117124
internal := gateway.internalSchema()
118125
// find the field URLs before we merge schemas. We need to make sure to include
119126
// the fields defined by the gateway's internal schema
@@ -174,45 +181,53 @@ func New(sources []*graphql.RemoteSchema, configs ...Configurator) (*Gateway, er
174181
return gateway, nil
175182
}
176183

177-
// Configurator is a function to be passed to New that configures the
184+
// Option is a function to be passed to New that configures the
178185
// resulting schema
179-
type Configurator func(*Gateway)
186+
type Option func(*Gateway)
180187

181-
// WithPlanner returns a Configurator that sets the planner of the gateway
182-
func WithPlanner(p QueryPlanner) Configurator {
188+
// WithPlanner returns an Option that sets the planner of the gateway
189+
func WithPlanner(p QueryPlanner) Option {
183190
return func(g *Gateway) {
184191
g.planner = p
185192
}
186193
}
187194

188-
// WithExecutor returns a Configurator that sets the executor of the gateway
189-
func WithExecutor(e Executor) Configurator {
195+
// WithExecutor returns an Option that sets the executor of the gateway
196+
func WithExecutor(e Executor) Option {
190197
return func(g *Gateway) {
191198
g.executor = e
192199
}
193200
}
194201

195-
// WithMerger returns a Configurator that sets the merger of the gateway
196-
func WithMerger(m Merger) Configurator {
202+
// WithMerger returns an Option that sets the merger of the gateway
203+
func WithMerger(m Merger) Option {
197204
return func(g *Gateway) {
198205
g.merger = m
199206
}
200207
}
201208

202-
// WithMiddlewares returns a Configurator that adds middlewares to the gateway
203-
func WithMiddlewares(middlewares ...Middleware) Configurator {
209+
// WithMiddlewares returns an Option that adds middlewares to the gateway
210+
func WithMiddlewares(middlewares ...Middleware) Option {
204211
return func(g *Gateway) {
205212
g.middlewares = append(g.middlewares, middlewares...)
206213
}
207214
}
208215

209-
// WithQueryFields returns a Configurator that adds the given query fields to the gateway
210-
func WithQueryFields(fields ...*QueryField) Configurator {
216+
// WithQueryFields returns an Option that adds the given query fields to the gateway
217+
func WithQueryFields(fields ...*QueryField) Option {
211218
return func(g *Gateway) {
212219
g.queryFields = append(g.queryFields, fields...)
213220
}
214221
}
215222

223+
// WithQueryerFactory returns an Option that changes the queryer used by the planner
224+
// when generating plans that interact with remote services.
225+
func WithQueryerFactory(factory *QueryerFactory) Option {
226+
return func(g *Gateway) {
227+
g.queryerFactory = factory
228+
}
229+
}
230+
216231
var nodeField = &QueryField{
217232
Name: "node",
218233
Type: ast.NamedType("Node", &ast.Position{}),

gateway_test.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func TestGateway(t *testing.T) {
8181
}
8282
})
8383

84-
t.Run("Variadic Configuration", func(t *testing.T) {
84+
t.Run("Options", func(t *testing.T) {
8585
// create a new schema with the sources and some configuration
8686
gateway, err := New([]*graphql.RemoteSchema{sources[0]}, func(schema *Gateway) {
8787
schema.sources = append(schema.sources, sources[1])
@@ -96,6 +96,37 @@ func TestGateway(t *testing.T) {
9696
assert.Len(t, gateway.sources, 2)
9797
})
9898

99+
t.Run("WithPlanner", func(t *testing.T) {
100+
// the planner we will assign
101+
planner := &MockPlanner{}
102+
103+
gateway, err := New(sources, WithPlanner(planner))
104+
if err != nil {
105+
t.Error(err.Error())
106+
return
107+
}
108+
109+
assert.Equal(t, planner, gateway.planner)
110+
})
111+
112+
t.Run("WithQueryerFactory", func(t *testing.T) {
113+
// the planner we will assign
114+
planner := &MinQueriesPlanner{}
115+
116+
factory := QueryerFactory(func(ctx *PlanningContext, url string) graphql.Queryer {
117+
return ctx.Gateway
118+
})
119+
120+
// instantiate the gateway
121+
gateway, err := New(sources, WithPlanner(planner), WithQueryerFactory(&factory))
122+
if err != nil {
123+
t.Error(err.Error())
124+
return
125+
}
126+
127+
assert.Equal(t, &factory, gateway.planner.(*MinQueriesPlanner).QueryerFactory)
128+
})
129+
99130
t.Run("fieldURLs ignore introspection", func(t *testing.T) {
100131
locations := fieldURLs(sources, true)
101132

@@ -111,19 +142,6 @@ func TestGateway(t *testing.T) {
111142
}
112143
})
113144

114-
t.Run("Configurator WithPlanner", func(t *testing.T) {
115-
// the planner we will assign
116-
planner := &MockPlanner{}
117-
118-
gateway, err := New(sources, WithPlanner(planner))
119-
if err != nil {
120-
t.Error(err.Error())
121-
return
122-
}
123-
124-
assert.Equal(t, planner, gateway.planner)
125-
})
126-
127145
t.Run("Response Middleware Error", func(t *testing.T) {
128146
// create a new schema with the sources and some configuration
129147
gateway, err := New(sources,

go.mod

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ require (
77
github.com/hashicorp/golang-lru v0.5.0 // indirect
88
github.com/inconshreveable/mousetrap v1.0.0 // indirect
99
github.com/mitchellh/mapstructure v1.1.2
10-
github.com/nautilus/graphql v0.0.0-20190208193207-6bb377f3952a
11-
github.com/opentracing/opentracing-go v1.0.2 // indirect
10+
github.com/nautilus/graphql v0.0.0-20190220053332-42ac2d2a1f98
1211
github.com/sirupsen/logrus v1.3.0
1312
github.com/spf13/cobra v0.0.3
1413
github.com/spf13/pflag v1.0.3 // indirect
1514
github.com/stretchr/testify v1.3.0
1615
github.com/vektah/gqlparser v1.1.0
1716
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664 // indirect
18-
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3
17+
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd
1918
golang.org/x/sys v0.0.0-20190130150945-aca44879d564 // indirect
2019
golang.org/x/tools v0.0.0-20190208185513-a3f91d6be4f3 // indirect
2120
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
1010
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1111
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
1212
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
13+
github.com/graph-gophers/dataloader v5.0.0+incompatible h1:R+yjsbrNq1Mo3aPG+Z/EKYrXrXXUNJHOgbRt+U6jOug=
14+
github.com/graph-gophers/dataloader v5.0.0+incompatible/go.mod h1:jk4jk0c5ZISbKaMe8WsVopGB5/15GvGHMdMdPtwlRp4=
1315
github.com/graph-gophers/graphql-go v0.0.0-20190108123631-d5b7dc6be53b h1:hrePtAgLPsHHKUv6l9EDI+QSH5cjPIYYzoIsIWfZ6qY=
1416
github.com/graph-gophers/graphql-go v0.0.0-20190108123631-d5b7dc6be53b/go.mod h1:aRnZGurV3LlZ1Y+ygyx1mAV6OUfq+nu6OgpJ6jKgZ3g=
1517
github.com/graphql-go/graphql v0.7.7 h1:nwEsJGwPq9N6cElOO+NYyoWuELAQZ4GuJks0Rlco5og=
@@ -30,6 +32,10 @@ github.com/nautilus/graphql v0.0.0-20190206230122-a60d56409fd3 h1:Ck8Xk1aJF/M7ms
3032
github.com/nautilus/graphql v0.0.0-20190206230122-a60d56409fd3/go.mod h1:aw58+1WuROPKYEN337vjRRaH9LfiiJKu6RwwFuF1wzw=
3133
github.com/nautilus/graphql v0.0.0-20190208193207-6bb377f3952a h1:7sDoVtsm6RWyGTlJyoIMxC1uVIUgBgscUVcCslus1y8=
3234
github.com/nautilus/graphql v0.0.0-20190208193207-6bb377f3952a/go.mod h1:aw58+1WuROPKYEN337vjRRaH9LfiiJKu6RwwFuF1wzw=
35+
github.com/nautilus/graphql v0.0.0-20190220052332-1ec821cfd939 h1:dIhr36omlDy2IMlwbEDSQj6aEWR5BzQq8hmrY0UgGZc=
36+
github.com/nautilus/graphql v0.0.0-20190220052332-1ec821cfd939/go.mod h1:58l1aeXqmU+gAfwagVHqTUv8C6LBfa63mZ6VHj1dbXQ=
37+
github.com/nautilus/graphql v0.0.0-20190220053332-42ac2d2a1f98 h1:xz+MM8U50sjgNR1qjvh7iO4SH9zF3+fbL/kZtEjtces=
38+
github.com/nautilus/graphql v0.0.0-20190220053332-42ac2d2a1f98/go.mod h1:58l1aeXqmU+gAfwagVHqTUv8C6LBfa63mZ6VHj1dbXQ=
3339
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
3440
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
3541
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -54,6 +60,8 @@ golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664 h1:YbZJ76lQ1BqNhVe7dKTSB6
5460
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
5561
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
5662
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
63+
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU=
64+
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
5765
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
5866
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
5967
golang.org/x/sys v0.0.0-20190130150945-aca44879d564 h1:o6ENHFwwr1TZ9CUPQcfo1HGvLP1OPsPOTB7xCIOPNmU=

plan.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,31 @@ type QueryPlanner interface {
5555
Plan(*PlanningContext) ([]*QueryPlan, error)
5656
}
5757

58+
// PlannerWithQueryerFactory is an interface for planners with configurable queryer factories
59+
type PlannerWithQueryerFactory interface {
60+
WithQueryerFactory(*QueryerFactory) QueryPlanner
61+
}
62+
63+
// QueryerFactory is a function that returns the queryer to use depending on the context
64+
type QueryerFactory func(ctx *PlanningContext, url string) graphql.Queryer
65+
5866
// Planner is meant to be embedded in other QueryPlanners to share configuration
5967
type Planner struct {
60-
QueryerFactory func(url string) graphql.Queryer
68+
QueryerFactory *QueryerFactory
69+
queryerCache map[string]graphql.Queryer
6170
}
6271

6372
// MinQueriesPlanner does the most basic level of query planning
6473
type MinQueriesPlanner struct {
6574
Planner
6675
}
6776

77+
// WithQueryerFactory returns a version of the planner with the factory set
78+
func (p *MinQueriesPlanner) WithQueryerFactory(factory *QueryerFactory) QueryPlanner {
79+
p.Planner.QueryerFactory = factory
80+
return p
81+
}
82+
6883
// PlanningContext is the input struct to the Plan method
6984
type PlanningContext struct {
7085
Query string
@@ -864,11 +879,11 @@ func (p *Planner) GetQueryer(ctx *PlanningContext, url string) graphql.Queryer {
864879
// if there is a queryer factory defined
865880
if p.QueryerFactory != nil {
866881
// use the factory
867-
return p.QueryerFactory(url)
882+
return (*p.QueryerFactory)(ctx, url)
868883
}
869884

870-
// otherwise return a network queryer
871-
return graphql.NewNetworkQueryer(url)
885+
// return the queryer for the url
886+
return graphql.NewSingleRequestQueryer(url)
872887
}
873888

874889
func plannerBuildQuery(parentType string, variables ast.VariableDefinitionList, selectionSet ast.SelectionSet, fragmentDefinitions ast.FragmentDefinitionList) *ast.QueryDocument {

plan_test.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ func TestPlanQuery_singleRootField(t *testing.T) {
4646
rootField := graphql.SelectedFields(root.SelectionSet)[0]
4747

4848
// make sure that the first step is pointed at the right place
49-
queryer := root.Queryer.(*graphql.NetworkQueryer)
50-
assert.Equal(t, location, queryer.URL)
49+
queryer := root.Queryer.(*graphql.SingleRequestQueryer)
50+
assert.Equal(t, location, queryer.URL())
5151

5252
// we need to be asking for Query.foo
5353
assert.Equal(t, rootField.Name, "foo")
@@ -363,10 +363,10 @@ func TestPlanQuery_nestedInlineFragmentsSameLocation(t *testing.T) {
363363
// find the steps
364364
for _, step := range steps {
365365
// look at the queryer to figure out where the request is going
366-
if queryer, ok := step.Queryer.(*graphql.NetworkQueryer); ok {
367-
if queryer.URL == loc1 {
366+
if queryer, ok := step.Queryer.(*graphql.SingleRequestQueryer); ok {
367+
if queryer.URL() == loc1 {
368368
loc1Step = step
369-
} else if queryer.URL == loc2 {
369+
} else if queryer.URL() == loc2 {
370370
loc2Step = step
371371
}
372372
} else {
@@ -500,8 +500,8 @@ func TestPlanQuery_singleRootObject(t *testing.T) {
500500
rootField := graphql.SelectedFields(rootStep.SelectionSet)[0]
501501

502502
// make sure that the first step is pointed at the right place
503-
queryer := rootStep.Queryer.(*graphql.NetworkQueryer)
504-
assert.Equal(t, location, queryer.URL)
503+
queryer := rootStep.Queryer.(*graphql.SingleRequestQueryer)
504+
assert.Equal(t, location, queryer.URL())
505505

506506
// we need to be asking for allUsers
507507
assert.Equal(t, rootField.Name, "allUsers")
@@ -617,8 +617,8 @@ func TestPlanQuery_subGraphs(t *testing.T) {
617617
}
618618
firstField := graphql.SelectedFields(firstStep.SelectionSet)[0]
619619
// it is resolved against the user service
620-
queryer := firstStep.Queryer.(*graphql.NetworkQueryer)
621-
assert.Equal(t, userLocation, queryer.URL)
620+
queryer := firstStep.Queryer.(*graphql.SingleRequestQueryer)
621+
assert.Equal(t, userLocation, queryer.URL())
622622

623623
// make sure it is for allUsers
624624
assert.Equal(t, "allUsers", firstField.Name)
@@ -655,8 +655,8 @@ func TestPlanQuery_subGraphs(t *testing.T) {
655655
// make sure we are grabbing values off of User since we asked for User.catPhotos
656656
assert.Equal(t, "User", secondStep.ParentType)
657657
// we should be going to the catePhoto servie
658-
queryer = secondStep.Queryer.(*graphql.NetworkQueryer)
659-
assert.Equal(t, catLocation, queryer.URL)
658+
queryer = secondStep.Queryer.(*graphql.SingleRequestQueryer)
659+
assert.Equal(t, catLocation, queryer.URL())
660660
// we should only want one field selected
661661
if len(secondStep.SelectionSet) != 1 {
662662
t.Errorf("Did not have the right number of subfields of User.catPhotos: %v", len(secondStep.SelectionSet))
@@ -687,8 +687,8 @@ func TestPlanQuery_subGraphs(t *testing.T) {
687687
// make sure we are grabbing values off of User since we asked for User.catPhotos
688688
assert.Equal(t, "CatPhoto", thirdStep.ParentType)
689689
// we should be going to the catePhoto service
690-
queryer = thirdStep.Queryer.(*graphql.NetworkQueryer)
691-
assert.Equal(t, userLocation, queryer.URL)
690+
queryer = thirdStep.Queryer.(*graphql.SingleRequestQueryer)
691+
assert.Equal(t, userLocation, queryer.URL())
692692
// make sure we will insert the step in the right place
693693
assert.Equal(t, []string{"allUsers", "catPhotos"}, thirdStep.InsertionPoint)
694694

@@ -1031,7 +1031,7 @@ func TestPlanQuery_nodeField(t *testing.T) {
10311031
var url1Step *QueryPlanStep
10321032
var url2Step *QueryPlanStep
10331033
for _, step := range internalStep.Then {
1034-
if queryer, ok := step.Queryer.(*graphql.NetworkQueryer); ok && queryer.URL == "url1" {
1034+
if queryer, ok := step.Queryer.(*graphql.SingleRequestQueryer); ok && queryer.URL() == "url1" {
10351035
url1Step = step
10361036
} else {
10371037
url2Step = step

0 commit comments

Comments
 (0)