diff --git a/execution_test.go b/execution_test.go index 161935ee..abb19e46 100644 --- a/execution_test.go +++ b/execution_test.go @@ -1479,6 +1479,239 @@ func TestTrimInsertionPointForNestedBoundaryQuery(t *testing.T) { require.Equal(t, expected, result) } +func TestNestingNullableBoundaryTypes(t *testing.T) { + t.Run("nested boundary types are all null", func(t *testing.T) { + f := &queryExecutionFixture{ + services: []testService{ + { + schema: `directive @boundary on OBJECT | FIELD_DEFINITION + type Gizmo @boundary { + id: ID! + } + type Query { + tastyGizmos: [Gizmo!]! + gizmo(ids: [ID!]!): [Gizmo]! @boundary + }`, + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(` + { + "data": { + "tastyGizmos": [ + { + "_bramble_id": "beehasknees", + "id": "beehasknees" + }, + { + "_bramble_id": "umlaut", + "id": "umlaut" + }, + { + "_bramble_id": "probanana", + "id": "probanana" + } + ] + } + } + `)) + }), + }, + { + schema: `directive @boundary on OBJECT | FIELD_DEFINITION + type Gizmo @boundary { + id: ID! + wizzle: Wizzle + } + type Wizzle @boundary { + id: ID! + } + type Query { + wizzles(ids: [ID!]): [Wizzle]! @boundary + gizmo(ids: [ID!]): [Gizmo]! @boundary + }`, + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ + "data": { + "_result": [null, null, null] + } + }`)) + }), + }, + { + schema: `directive @boundary on OBJECT | FIELD_DEFINITION + type Wizzle @boundary { + id: ID! + bazingaFactor: Int + } + type Query { + wizzles(ids: [ID!]): [Wizzle]! @boundary + }`, + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`should not be called...`)) + }), + }, + }, + + query: `{ + tastyGizmos { + id + wizzle { + id + bazingaFactor + } + } + }`, + expected: `{ + "tastyGizmos": [ + { + "id": "beehasknees", + "wizzle": null + }, + { + "id": "umlaut", + "wizzle": null + }, + { + "id": "probanana", + "wizzle": null + } + ] + }`, + } + + f.checkSuccess(t) + }) + + t.Run("nested boundary types sometimes null", func(t *testing.T) { + f := &queryExecutionFixture{ + services: []testService{ + { + schema: `directive @boundary on OBJECT | FIELD_DEFINITION + type Gizmo @boundary { + id: ID! + } + type Query { + tastyGizmos: [Gizmo!]! + gizmo(ids: [ID!]!): [Gizmo]! @boundary + }`, + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(` + { + "data": { + "tastyGizmos": [ + { + "_bramble_id": "beehasknees", + "id": "beehasknees" + }, + { + "_bramble_id": "umlaut", + "id": "umlaut" + }, + { + "_bramble_id": "probanana", + "id": "probanana" + } + ] + } + } + `)) + }), + }, + { + schema: `directive @boundary on OBJECT | FIELD_DEFINITION + type Gizmo @boundary { + id: ID! + wizzle: Wizzle + } + type Wizzle @boundary { + id: ID! + } + type Query { + wizzles(ids: [ID!]): [Wizzle]! @boundary + gizmos(ids: [ID!]): [Gizmo]! @boundary + }`, + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ + "data": { + "_result": [ + null, + { + "_bramble_id": "umlaut", + "id": "umlaut", + "wizzle": null + }, + { + "_bramble_id": "probanana", + "id": "probanana", + "wizzle": { + "_bramble_id": "bananawizzle", + "id": "bananawizzle" + } + } + ] + } + }`)) + }), + }, + { + schema: `directive @boundary on OBJECT | FIELD_DEFINITION + type Wizzle @boundary { + id: ID! + bazingaFactor: Int + } + type Query { + wizzles(ids: [ID!]): [Wizzle]! @boundary + }`, + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ + "data": { + "_result": [ + { + "_bramble_id": "bananawizzle", + "id": "bananawizzle", + "bazingaFactor": 4 + } + ] + } + }`)) + }), + }, + }, + + query: `{ + tastyGizmos { + id + wizzle { + id + bazingaFactor + } + } + }`, + expected: `{ + "tastyGizmos": [ + { + "id": "beehasknees", + "wizzle": null + }, + { + "id": "umlaut", + "wizzle": null + }, + { + "id": "probanana", + "wizzle": { + "id": "bananawizzle", + "bazingaFactor": 4 + } + } + ] + }`, + } + + f.checkSuccess(t) + }) + +} + func TestBuildBoundaryQueryDocuments(t *testing.T) { ddl := ` type Gizmo { diff --git a/query_execution.go b/query_execution.go index 80e31f2b..cec8ba08 100644 --- a/query_execution.go +++ b/query_execution.go @@ -172,13 +172,15 @@ func (q *queryExecution) executeChildStep(step *QueryPlanStep, boundaryIDs []str q.writeExecutionResult(step, data, nil) - if len(data) > 0 { + nonNillBoundaryResults := extractNonNilBoundaryResults(data) + + if len(nonNillBoundaryResults) > 0 { for _, childStep := range step.Then { - boundaryResultInsertionPoint, err := trimInsertionPointForNestedBoundaryStep(data, childStep.InsertionPoint) + boundaryResultInsertionPoint, err := trimInsertionPointForNestedBoundaryStep(nonNillBoundaryResults, childStep.InsertionPoint) if err != nil { return err } - boundaryIDs, err := extractAndDedupeBoundaryIDs(data, boundaryResultInsertionPoint) + boundaryIDs, err := extractAndDedupeBoundaryIDs(nonNillBoundaryResults, boundaryResultInsertionPoint) if err != nil { return err } @@ -195,6 +197,18 @@ func (q *queryExecution) executeChildStep(step *QueryPlanStep, boundaryIDs []str return nil } +func extractNonNilBoundaryResults(data []interface{}) []interface{} { + var nonNilResults []interface{} + for _, d := range data { + if d != nil { + nonNilResults = append(nonNilResults, d) + } + + } + + return nonNilResults +} + func (q *queryExecution) executeBoundaryQuery(documents []string, serviceURL string, boundaryFieldGetter BoundaryField) ([]interface{}, error) { output := make([]interface{}, 0) if !boundaryFieldGetter.Array {