Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
27 changes: 27 additions & 0 deletions v2/pkg/engine/resolve/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,12 @@ func (l *Loader) mergeResult(fetchItem *FetchItem, res *result, items []*astjson

// Check if data needs processing.
if res.postProcessing.SelectResponseDataPath != nil && astjson.ValueIsNull(responseData) {
// First check if this is actually an entity null fetch, instead of a data null fetch.
// In this case we return early to avoid adding subgraph errors or merging this into items.
if isNullEntityFetch(res.postProcessing.SelectResponseDataPath, response) {
Comment thread
devsergiy marked this conversation as resolved.
Outdated
return nil
}

// When:
// - No errors or data are present
// - Status code is not within the 2XX range
Expand Down Expand Up @@ -637,6 +643,27 @@ func (l *Loader) mergeResult(fetchItem *FetchItem, res *result, items []*astjson
return nil
}

// isNullEntityFetch returns true, if the fetch at dataPath is null
// but it's part of an actual array response, i.e.
// { "data": { "_entities": [null] } }
// instead of
// { "data": null }
// The latter will return false.
func isNullEntityFetch(dataPath []string, response *astjson.Value) bool {
if len(dataPath) > 1 && dataPath[len(dataPath)-2] == "_entities" {
// data path is an entity array
// check that the entity fetch's parent array exists
parentPath := dataPath[:len(dataPath)-1]
parentData := response.Get(parentPath...)

if astjson.ValueIsNonNull(parentData) && parentData.Type() == astjson.TypeArray {
return true
}
}

return false
}

var (
errorsInvalidInputHeader = []byte(`{"errors":[{"message":"Failed to render Fetch Input","path":[`)
errorsInvalidInputFooter = []byte(`]}]}`)
Expand Down
72 changes: 72 additions & 0 deletions v2/pkg/engine/resolve/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2141,6 +2141,78 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) {
},
}, *NewContext(context.Background()), `{"errors":[{"message":"Failed to fetch from Subgraph 'Users' at Path 'query'."}],"data":null}`
}))
t.Run("fetch with null entity and non-nullable root field", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) {
mockDataSource := NewMockDataSource(ctrl)
mockDataSource.EXPECT().
Load(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, headers http.Header, input []byte) ([]byte, error) {
// Subgraph returns null for the entity
return []byte(`{"data":{"_entities":[null]}}`), nil
})
return &GraphQLResponse{
Fetches: SingleWithPath(&SingleFetch{
FetchConfiguration: FetchConfiguration{
DataSource: mockDataSource,
PostProcessing: PostProcessingConfiguration{
SelectResponseDataPath: []string{"data", "_entities", "0"},
SelectResponseErrorsPath: []string{"errors"},
},
},
Info: &FetchInfo{
DataSourceID: "Users",
DataSourceName: "Users",
},
}, "query"),
Data: &Object{
Nullable: false,
Fields: []*Field{
{
Name: []byte("name"),
Value: &String{
Path: []string{"name"},
Nullable: false,
},
},
},
},
}, *NewContext(context.Background()), `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.name'.","path":["name"]}],"data":null}`
}))
t.Run("fetch with null entity and nullable root field", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) {
Comment thread
devsergiy marked this conversation as resolved.
mockDataSource := NewMockDataSource(ctrl)
mockDataSource.EXPECT().
Load(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, headers http.Header, input []byte) ([]byte, error) {
// Subgraph returns null for the entity
return []byte(`{"data":{"_entities":[null]}}`), nil
})
return &GraphQLResponse{
Fetches: SingleWithPath(&SingleFetch{
FetchConfiguration: FetchConfiguration{
DataSource: mockDataSource,
PostProcessing: PostProcessingConfiguration{
SelectResponseDataPath: []string{"data", "_entities", "0"},
SelectResponseErrorsPath: []string{"errors"},
},
},
Info: &FetchInfo{
DataSourceID: "Users",
DataSourceName: "Users",
},
}, "query"),
Data: &Object{
Nullable: false,
Fields: []*Field{
{
Name: []byte("name"),
Value: &String{
Path: []string{"name"},
Nullable: true,
},
},
},
},
}, *NewContext(context.Background()), `{"data":{"name":null}}`
}))
t.Run("root field with nested non-nullable fields returns null", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) {
return &GraphQLResponse{
Fetches: Single(&SingleFetch{
Expand Down