Skip to content

Commit 0ac544a

Browse files
authored
Resolve Node IDs in dependent steps & run response middleware on partial success (#216)
* Add failing test for partial success with unresolved Node IDs * Resolve Node IDs in dependent steps, even on partial successes * Add failing test for partial success not scrubbing unrequested fields from response * Fix builtin middleware not running on partial success Fixes #214
1 parent 634cff6 commit 0ac544a

File tree

3 files changed

+163
-17
lines changed

3 files changed

+163
-17
lines changed

execute.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,16 +256,12 @@ func executeOneStep(
256256
}
257257

258258
// fire the query
259-
err := queryer.Query(ctx.RequestContext, &graphql.QueryInput{
259+
queryErr := queryer.Query(ctx.RequestContext, &graphql.QueryInput{
260260
Query: step.QueryString,
261261
QueryDocument: step.QueryDocument,
262262
Variables: variables,
263263
OperationName: operationName,
264264
}, &queryResult)
265-
if err != nil {
266-
ctx.logger.Warn("Network Error: ", err)
267-
return queryResult, nil, err
268-
}
269265

270266
// NOTE: this insertion point could point to a list of values. If it did, we have to have
271267
// passed it to the this invocation of this function. It is safe to trust this
@@ -313,7 +309,7 @@ func executeOneStep(
313309
}
314310
}
315311
}
316-
return queryResult, dependentSteps, nil
312+
return queryResult, dependentSteps, queryErr
317313
}
318314

319315
func max(a, b int) int {

gateway.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,9 @@ func (g *Gateway) Execute(ctx *RequestContext, plans QueryPlanList) (map[string]
9090

9191
// TODO: handle plans of more than one query
9292
// execute the plan and return the results
93-
result, err := g.executor.Execute(executionContext)
94-
if err != nil {
95-
if len(result) == 0 {
96-
return nil, err
97-
}
98-
return result, err
93+
result, executeErr := g.executor.Execute(executionContext)
94+
if executeErr != nil && len(result) == 0 {
95+
result = nil
9996
}
10097

10198
// now that we have our response, throw it through the list of middlewarse
@@ -106,7 +103,7 @@ func (g *Gateway) Execute(ctx *RequestContext, plans QueryPlanList) (map[string]
106103
}
107104

108105
// we're done here
109-
return result, nil
106+
return result, executeErr
110107
}
111108

112109
func (g *Gateway) internalSchema() (*ast.Schema, error) {
@@ -189,7 +186,8 @@ func New(sources []*graphql.RemoteSchema, configs ...Option) (*Gateway, error) {
189186
{
190187
URL: internalSchemaLocation,
191188
Schema: internal,
192-
}},
189+
},
190+
},
193191
false,
194192
),
195193
)
@@ -337,23 +335,20 @@ func fieldURLs(schemas []*graphql.RemoteSchema, stripInternal bool) FieldURLMap
337335
for _, remoteSchema := range schemas {
338336
// each type defined by the schema can be found at remoteSchema.URL
339337
for name, typeDef := range remoteSchema.Schema.Types {
340-
341338
// if the type is part of the introspection (and can't be left up to the backing services)
342339
if !strings.HasPrefix(typeDef.Name, "__") || !stripInternal {
343340
// you can ask for __typename at any service that defines the type
344341
locations.RegisterURL(name, "__typename", remoteSchema.URL)
345342

346343
// each field of each type can be found here
347344
for _, fieldDef := range typeDef.Fields {
348-
349345
// if the field is not an introspection field
350346
if !(name == typeNameQuery && strings.HasPrefix(fieldDef.Name, "__")) {
351347
locations.RegisterURL(name, fieldDef.Name, remoteSchema.URL)
352348
} else if !stripInternal { // its an introspection name
353349
// register the location for the field
354350
locations.RegisterURL(name, fieldDef.Name, remoteSchema.URL)
355351
}
356-
357352
}
358353
}
359354
}

gateway_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ type User {
754754
`, resp.Body.String())
755755
}
756756

757+
// TestDataAndErrorsBothReturnFromOneServicePartialSuccess verifies fix for https://github.com/nautilus/gateway/issues/212
757758
func TestDataAndErrorsBothReturnFromOneServicePartialSuccess(t *testing.T) {
758759
t.Parallel()
759760
schema, err := graphql.LoadSchema(`
@@ -801,3 +802,157 @@ type Query {
801802
}
802803
`, resp.Body.String())
803804
}
805+
806+
// TestGatewayRunsResponseMiddlewaresOnError verifies part of fix for https://github.com/nautilus/gateway/issues/212
807+
// The issue included the 'id' field not getting scrubbed when an error was returned, and scrubbing is a builtin response middleware.
808+
func TestGatewayRunsResponseMiddlewaresOnError(t *testing.T) {
809+
t.Parallel()
810+
schema, err := graphql.LoadSchema(`
811+
type Query {
812+
foo: String
813+
}
814+
`)
815+
require.NoError(t, err)
816+
queryerFactory := QueryerFactory(func(ctx *PlanningContext, url string) graphql.Queryer {
817+
return graphql.QueryerFunc(func(input *graphql.QueryInput) (interface{}, error) {
818+
return map[string]interface{}{
819+
"foo": nil,
820+
}, graphql.ErrorList{
821+
&graphql.Error{
822+
Message: "foo is broken",
823+
Path: []interface{}{"foo"},
824+
},
825+
}
826+
})
827+
})
828+
executedResponseMiddleware := false
829+
gateway, err := New([]*graphql.RemoteSchema{
830+
{Schema: schema, URL: "boo"},
831+
}, WithQueryerFactory(&queryerFactory), WithMiddlewares(ResponseMiddleware(func(*ExecutionContext, map[string]interface{}) error {
832+
executedResponseMiddleware = true
833+
return nil
834+
})))
835+
require.NoError(t, err)
836+
837+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"query": "query { foo }"}`))
838+
resp := httptest.NewRecorder()
839+
gateway.GraphQLHandler(resp, req)
840+
assert.Equal(t, http.StatusOK, resp.Code)
841+
assert.JSONEq(t, `
842+
{
843+
"data": {
844+
"foo": null
845+
},
846+
"errors": [
847+
{
848+
"message": "foo is broken",
849+
"path": ["foo"],
850+
"extensions": null
851+
}
852+
]
853+
}
854+
`, resp.Body.String())
855+
assert.True(t, executedResponseMiddleware, "All response middleware should run, even on responses with errors")
856+
}
857+
858+
// TestPartialSuccessAlsoResolvesValidNodeIDs verifies fix for https://github.com/nautilus/gateway/issues/214
859+
func TestPartialSuccessAlsoResolvesValidNodeIDs(t *testing.T) {
860+
t.Parallel()
861+
schemaFoo, err := graphql.LoadSchema(`
862+
type Query {
863+
foo: Foo
864+
}
865+
866+
type Foo {
867+
bar: Bar
868+
boo: String
869+
}
870+
871+
interface Node {
872+
id: ID!
873+
}
874+
875+
type Bar implements Node {
876+
id: ID!
877+
}
878+
`)
879+
require.NoError(t, err)
880+
schemaBar, err := graphql.LoadSchema(`
881+
type Query {
882+
node(id: ID!): Node
883+
}
884+
885+
interface Node {
886+
id: ID!
887+
}
888+
889+
type Bar implements Node {
890+
id: ID!
891+
baz: String
892+
}
893+
`)
894+
require.NoError(t, err)
895+
const query = `
896+
query {
897+
foo {
898+
bar {
899+
baz
900+
}
901+
}
902+
}
903+
`
904+
queryerFactory := QueryerFactory(func(ctx *PlanningContext, url string) graphql.Queryer {
905+
return graphql.QueryerFunc(func(input *graphql.QueryInput) (interface{}, error) {
906+
t.Log("Received request:", input.Query)
907+
if strings.Contains(input.Query, "node(") {
908+
return map[string]interface{}{
909+
"node": map[string]interface{}{
910+
"baz": "biff",
911+
},
912+
}, nil
913+
}
914+
return map[string]interface{}{
915+
"foo": map[string]interface{}{
916+
"bar": map[string]interface{}{
917+
"id": "bar-id",
918+
},
919+
"boo": nil,
920+
},
921+
}, graphql.ErrorList{
922+
&graphql.Error{
923+
Message: "boo is broken",
924+
Path: []interface{}{"foo", "boo"},
925+
},
926+
}
927+
})
928+
})
929+
gateway, err := New([]*graphql.RemoteSchema{
930+
{Schema: schemaFoo, URL: "foo"},
931+
{Schema: schemaBar, URL: "bar"},
932+
}, WithQueryerFactory(&queryerFactory))
933+
require.NoError(t, err)
934+
935+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(fmt.Sprintf(`{"query": %q}`, query)))
936+
resp := httptest.NewRecorder()
937+
gateway.GraphQLHandler(resp, req)
938+
assert.Equal(t, http.StatusOK, resp.Code)
939+
assert.JSONEq(t, `
940+
{
941+
"data": {
942+
"foo": {
943+
"bar": {
944+
"baz": "biff"
945+
},
946+
"boo": null
947+
}
948+
},
949+
"errors": [
950+
{
951+
"message": "boo is broken",
952+
"path": ["foo", "boo"],
953+
"extensions": null
954+
}
955+
]
956+
}
957+
`, resp.Body.String())
958+
}

0 commit comments

Comments
 (0)