agent_patch
runs/2026-02-28__13-38-10__gpt-5-3-codex/flux-pr-1297/flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/agent-logs/agent.patch
25410 bytes
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go b/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
index c5a7f3f..ae56a98 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
@@ -130,6 +130,8 @@ type FetchConfiguration struct {
type FederationConfiguration struct {
Enabled bool
ServiceSDL string
+ // ValidateRequiresDependencies enables optional validation of nullable @requires dependencies.
+ ValidateRequiresDependencies bool
}
type SchemaConfiguration struct {
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
index 3acdd07..5a2a4cd 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
@@ -338,6 +338,9 @@ func (p *Planner[T]) ConfigureFetch() resolve.FetchConfiguration {
postProcessing := DefaultPostProcessingConfiguration
requiresEntityFetch := p.requiresEntityFetch()
requiresEntityBatchFetch := p.requiresEntityBatchFetch()
+ validateRequiresDependencies := p.config.IsFederationEnabled() &&
+ p.config.FederationConfiguration().ValidateRequiresDependencies &&
+ (requiresEntityFetch || requiresEntityBatchFetch)
switch {
case requiresEntityFetch:
@@ -384,6 +387,7 @@ func (p *Planner[T]) ConfigureFetch() resolve.FetchConfiguration {
RequiresEntityBatchFetch: requiresEntityBatchFetch,
PostProcessing: postProcessing,
SetTemplateOutputToNullOnVariableNull: requiresEntityFetch || requiresEntityBatchFetch,
+ ValidateRequiresDependencies: validateRequiresDependencies,
QueryPlan: p.queryPlan,
OperationName: p.propagatedOperationName,
}
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go b/app/v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go
index f5d0b2a..81aef08 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go
+++ b/app/v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go
@@ -1,6 +1,8 @@
package postprocess
import (
+ "strings"
+
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve"
)
@@ -73,6 +75,7 @@ func (d *createConcreteSingleFetchTypes) createEntityBatchFetch(fetch *resolve.S
break
}
}
+ nullableRepresentationVariablePaths := d.nullableRepresentationVariablePaths(fetch, representationsVariableIndex)
return &resolve.BatchEntityFetch{
FetchDependencies: fetch.FetchDependencies,
@@ -88,9 +91,11 @@ func (d *createConcreteSingleFetchTypes) createEntityBatchFetch(fetch *resolve.S
SetTemplateOutputToNullOnVariableNull: fetch.InputTemplate.SetTemplateOutputToNullOnVariableNull,
},
},
- SkipNullItems: true,
- SkipEmptyObjectItems: true,
- SkipErrItems: true,
+ SkipNullItems: true,
+ SkipEmptyObjectItems: true,
+ SkipErrItems: true,
+ ValidateRequiresDependencies: fetch.ValidateRequiresDependencies,
+ NullableRepresentationVariablePaths: nullableRepresentationVariablePaths,
Separator: resolve.InputTemplate{
Segments: []resolve.TemplateSegment{
{
@@ -118,6 +123,7 @@ func (d *createConcreteSingleFetchTypes) createEntityFetch(fetch *resolve.Single
break
}
}
+ nullableRepresentationVariablePaths := d.nullableRepresentationVariablePaths(fetch, representationsVariableIndex)
return &resolve.EntityFetch{
FetchDependencies: fetch.FetchDependencies,
@@ -131,7 +137,9 @@ func (d *createConcreteSingleFetchTypes) createEntityFetch(fetch *resolve.Single
Segments: []resolve.TemplateSegment{fetch.InputTemplate.Segments[representationsVariableIndex]},
SetTemplateOutputToNullOnVariableNull: fetch.InputTemplate.SetTemplateOutputToNullOnVariableNull,
},
- SkipErrItem: true,
+ SkipErrItem: true,
+ ValidateRequiresDependencies: fetch.ValidateRequiresDependencies,
+ NullableRepresentationVariablePaths: nullableRepresentationVariablePaths,
Footer: resolve.InputTemplate{
Segments: fetch.InputTemplate.Segments[representationsVariableIndex+1:],
SetTemplateOutputToNullOnVariableNull: fetch.InputTemplate.SetTemplateOutputToNullOnVariableNull,
@@ -141,3 +149,64 @@ func (d *createConcreteSingleFetchTypes) createEntityFetch(fetch *resolve.Single
PostProcessing: fetch.PostProcessing,
}
}
+
+func (d *createConcreteSingleFetchTypes) nullableRepresentationVariablePaths(fetch *resolve.SingleFetch, representationsVariableIndex int) []string {
+ if representationsVariableIndex < 0 || representationsVariableIndex >= len(fetch.InputTemplate.Segments) {
+ return nil
+ }
+
+ renderer, ok := fetch.InputTemplate.Segments[representationsVariableIndex].Renderer.(*resolve.GraphQLVariableResolveRenderer)
+ if !ok {
+ return nil
+ }
+ root, ok := renderer.Node.(*resolve.Object)
+ if !ok || root == nil {
+ return nil
+ }
+
+ paths := make([]string, 0, len(root.Fields))
+ collectNullableRepresentationPaths(root, nil, &paths)
+
+ return paths
+}
+
+func collectNullableRepresentationPaths(node resolve.Node, currentPath []string, out *[]string) {
+ switch n := node.(type) {
+ case *resolve.Object:
+ if n.Nullable && len(currentPath) > 0 {
+ *out = append(*out, strings.Join(currentPath, "."))
+ }
+ for _, field := range n.Fields {
+ collectNullableRepresentationPaths(field.Value, append(currentPath, string(field.Name)), out)
+ }
+ case *resolve.Array:
+ if n.Nullable && len(currentPath) > 0 {
+ *out = append(*out, strings.Join(currentPath, "."))
+ }
+ collectNullableRepresentationPaths(n.Item, currentPath, out)
+ case *resolve.String:
+ if n.Nullable && len(currentPath) > 0 {
+ *out = append(*out, strings.Join(currentPath, "."))
+ }
+ case *resolve.Boolean:
+ if n.Nullable && len(currentPath) > 0 {
+ *out = append(*out, strings.Join(currentPath, "."))
+ }
+ case *resolve.Integer:
+ if n.Nullable && len(currentPath) > 0 {
+ *out = append(*out, strings.Join(currentPath, "."))
+ }
+ case *resolve.Float:
+ if n.Nullable && len(currentPath) > 0 {
+ *out = append(*out, strings.Join(currentPath, "."))
+ }
+ case *resolve.BigInt:
+ if n.Nullable && len(currentPath) > 0 {
+ *out = append(*out, strings.Join(currentPath, "."))
+ }
+ case *resolve.Scalar:
+ if n.Nullable && len(currentPath) > 0 {
+ *out = append(*out, strings.Join(currentPath, "."))
+ }
+ }
+}
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/fetch.go b/app/v2/pkg/engine/resolve/fetch.go
index 26e20c7..9f1c361 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/fetch.go
+++ b/app/v2/pkg/engine/resolve/fetch.go
@@ -187,8 +187,12 @@ type BatchInput struct {
// In this case, the error will be swallowed
// E.g. if a field is not nullable and the value is null, the item will be skipped
SkipErrItems bool
- Separator InputTemplate
- Footer InputTemplate
+ // ValidateRequiresDependencies enables filtering entities that have tainted nullable dependencies.
+ ValidateRequiresDependencies bool
+ // NullableRepresentationVariablePaths are the representation field paths that are nullable.
+ NullableRepresentationVariablePaths []string
+ Separator InputTemplate
+ Footer InputTemplate
}
func (*BatchEntityFetch) FetchKind() FetchKind {
@@ -220,7 +224,11 @@ type EntityInput struct {
Header InputTemplate
Item InputTemplate
SkipErrItem bool
- Footer InputTemplate
+ // ValidateRequiresDependencies enables filtering entities that have tainted nullable dependencies.
+ ValidateRequiresDependencies bool
+ // NullableRepresentationVariablePaths are the representation field paths that are nullable.
+ NullableRepresentationVariablePaths []string
+ Footer InputTemplate
}
func (*EntityFetch) FetchKind() FetchKind {
@@ -294,6 +302,8 @@ type FetchConfiguration struct {
// This is the case, e.g. when using batching and one sibling is null, resulting in a null value for one batch item
// Returning null in this case tells the batch implementation to skip this item
SetTemplateOutputToNullOnVariableNull bool
+ // ValidateRequiresDependencies enables optional validation of nullable @requires dependencies.
+ ValidateRequiresDependencies bool
QueryPlan *QueryPlan
@@ -328,6 +338,9 @@ func (fc *FetchConfiguration) Equals(other *FetchConfiguration) bool {
if fc.SetTemplateOutputToNullOnVariableNull != other.SetTemplateOutputToNullOnVariableNull {
return false
}
+ if fc.ValidateRequiresDependencies != other.ValidateRequiresDependencies {
+ return false
+ }
return true
}
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/loader.go b/app/v2/pkg/engine/resolve/loader.go
index 1e9f993..4fd7212 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/loader.go
+++ b/app/v2/pkg/engine/resolve/loader.go
@@ -159,6 +159,8 @@ type Loader struct {
resolvable *Resolvable
ctx *Context
info *GraphQLResponseInfo
+ // taintedRepresentationVariablePaths stores field paths on items that were returned as null with a downstream error.
+ taintedRepresentationVariablePaths map[*astjson.Value]map[string]struct{}
propagateSubgraphErrors bool
propagateSubgraphStatusCodes bool
@@ -189,6 +191,7 @@ func (l *Loader) LoadGraphQLResponseData(ctx *Context, response *GraphQLResponse
l.resolvable = resolvable
l.ctx = ctx
l.info = response.Info
+ l.taintedRepresentationVariablePaths = make(map[*astjson.Value]map[string]struct{})
return l.resolveFetchNode(response.Fetches)
}
@@ -557,6 +560,7 @@ func (l *Loader) mergeResult(fetchItem *FetchItem, res *result, items []*astjson
}
hasErrors := false
+ parsedResponse := value
// We check if the subgraph response has errors
if res.postProcessing.SelectResponseErrorsPath != nil {
@@ -572,6 +576,7 @@ func (l *Loader) mergeResult(fetchItem *FetchItem, res *result, items []*astjson
if err != nil {
return errors.WithStack(err)
}
+ l.trackTaintedRepresentationVariablePaths(parsedResponse, res, errorObjects, items)
}
}
}
@@ -991,6 +996,7 @@ const (
invalidGraphQLResponse = "invalid JSON"
invalidGraphQLResponseShape = "no data or errors in response"
invalidBatchItemCount = "returned entities count does not match the count of representation variables in the entities request. Expected %d, got %d"
+ missingRequiredDependencies = "required federation dependencies are missing from the entity representation"
)
func (l *Loader) renderAtPathErrorPart(path string) string {
@@ -1038,6 +1044,14 @@ func (l *Loader) renderErrorsFailedToFetch(fetchItem *FetchItem, res *result, re
return nil
}
+func (l *Loader) renderErrorsMissingRequiresDependencies(fetchItem *FetchItem, res *result) {
+ errorObject, err := astjson.ParseWithoutCache(l.renderSubgraphBaseError(res.ds, fetchItem.ResponsePath, missingRequiredDependencies))
+ if err != nil {
+ return
+ }
+ astjson.AppendToArray(l.resolvable.errors, errorObject)
+}
+
func (l *Loader) renderErrorsStatusFallback(fetchItem *FetchItem, res *result, statusCode int) error {
reason := fmt.Sprintf("%d", statusCode)
if statusText := http.StatusText(statusCode); statusText != "" {
@@ -1339,6 +1353,17 @@ func (l *Loader) loadEntityFetch(ctx context.Context, fetchItem *FetchItem, fetc
return nil
}
}
+ if fetch.Input.ValidateRequiresDependencies &&
+ len(items) == 1 &&
+ l.itemHasMissingNullableRequiresDependencies(items[0], fetch.Input.NullableRepresentationVariablePaths) {
+ // skip fetch if nullable @requires dependencies are tainted
+ res.fetchSkipped = true
+ if l.ctx.TracingOptions.Enable {
+ fetch.Trace.LoadSkipped = true
+ }
+ l.renderErrorsMissingRequiresDependencies(fetchItem, res)
+ return nil
+ }
_, _ = buf.item.WriteTo(buf.preparedInput)
err = fetch.Input.Footer.RenderAndCollectUndefinedVariables(l.ctx, nil, buf.preparedInput, &undefinedVariables)
if err != nil {
@@ -1425,6 +1450,12 @@ func (l *Loader) loadBatchEntityFetch(ctx context.Context, fetchItem *FetchItem,
WithNextItem:
for i, item := range items {
+ if fetch.Input.ValidateRequiresDependencies &&
+ l.itemHasMissingNullableRequiresDependencies(item, fetch.Input.NullableRepresentationVariablePaths) {
+ res.batchStats[i] = append(res.batchStats[i], -1)
+ l.renderErrorsMissingRequiresDependencies(fetchItem, res)
+ continue
+ }
for j := range fetch.Input.Items {
buf.itemInput.Reset()
err = fetch.Input.Items[j].Render(l.ctx, item, buf.itemInput)
@@ -1508,6 +1539,143 @@ WithNextItem:
return nil
}
+func (l *Loader) itemHasMissingNullableRequiresDependencies(item *astjson.Value, nullablePaths []string) bool {
+ if item == nil {
+ return false
+ }
+
+ taintedPaths, ok := l.taintedRepresentationVariablePaths[item]
+ if !ok || len(taintedPaths) == 0 || len(nullablePaths) == 0 {
+ return false
+ }
+
+ for _, nullablePath := range nullablePaths {
+ if _, ok := taintedPaths[nullablePath]; ok {
+ return true
+ }
+ for taintedPath := range taintedPaths {
+ if strings.HasPrefix(nullablePath, taintedPath+".") || strings.HasPrefix(taintedPath, nullablePath+".") {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func (l *Loader) trackTaintedRepresentationVariablePaths(responseValue *astjson.Value, res *result, errorObjects, items []*astjson.Value) {
+ if responseValue == nil || len(errorObjects) == 0 || len(items) == 0 {
+ return
+ }
+ if !isEntityResponsePath(res.postProcessing.SelectResponseDataPath) {
+ return
+ }
+
+ for _, errorObject := range errorObjects {
+ pathValue := errorObject.Get("path")
+ entityIndex, fieldPath, ok := entityErrorPath(pathValue)
+ if !ok || len(fieldPath) == 0 {
+ continue
+ }
+
+ fullPath := make([]string, 0, len(fieldPath)+3)
+ fullPath = append(fullPath, "data", "_entities", strconv.Itoa(entityIndex))
+ fullPath = append(fullPath, fieldPath...)
+ if !astjson.ValueIsNull(responseValue.Get(fullPath...)) {
+ continue
+ }
+
+ if res.batchStats != nil {
+ for itemIndex, stats := range res.batchStats {
+ for _, batchIndex := range stats {
+ if batchIndex == entityIndex && itemIndex < len(items) {
+ l.markItemAsTainted(items[itemIndex], fieldPath)
+ }
+ }
+ }
+ continue
+ }
+
+ if entityIndex >= 0 && entityIndex < len(items) {
+ l.markItemAsTainted(items[entityIndex], fieldPath)
+ }
+ }
+}
+
+func (l *Loader) markItemAsTainted(item *astjson.Value, fieldPath []string) {
+ if item == nil {
+ return
+ }
+ pathKey := strings.Join(fieldPath, ".")
+ if _, ok := l.taintedRepresentationVariablePaths[item]; !ok {
+ l.taintedRepresentationVariablePaths[item] = make(map[string]struct{})
+ }
+ l.taintedRepresentationVariablePaths[item][pathKey] = struct{}{}
+}
+
+func isEntityResponsePath(path []string) bool {
+ for _, item := range path {
+ if item == "_entities" {
+ return true
+ }
+ }
+ return false
+}
+
+func entityErrorPath(pathValue *astjson.Value) (entityIndex int, fieldPath []string, ok bool) {
+ if pathValue == nil || pathValue.Type() != astjson.TypeArray {
+ return 0, nil, false
+ }
+
+ pathItems := pathValue.GetArray()
+ if len(pathItems) < 2 {
+ return 0, nil, false
+ }
+
+ entitiesIndex := -1
+ for i, item := range pathItems {
+ if item.Type() == astjson.TypeString && unsafebytes.BytesToString(item.GetStringBytes()) == "_entities" {
+ entitiesIndex = i
+ break
+ }
+ }
+ if entitiesIndex == -1 || entitiesIndex+1 >= len(pathItems) {
+ return 0, nil, false
+ }
+
+ indexValue := pathItems[entitiesIndex+1]
+ switch indexValue.Type() {
+ case astjson.TypeNumber:
+ index, err := strconv.Atoi(unsafebytes.BytesToString(indexValue.MarshalTo(nil)))
+ if err != nil {
+ return 0, nil, false
+ }
+ entityIndex = index
+ case astjson.TypeString:
+ index, err := strconv.Atoi(unsafebytes.BytesToString(indexValue.GetStringBytes()))
+ if err != nil {
+ return 0, nil, false
+ }
+ entityIndex = index
+ default:
+ return 0, nil, false
+ }
+
+ fieldPath = make([]string, 0, len(pathItems)-(entitiesIndex+2))
+ for i := entitiesIndex + 2; i < len(pathItems); i++ {
+ switch pathItems[i].Type() {
+ case astjson.TypeString:
+ fieldPath = append(fieldPath, unsafebytes.BytesToString(pathItems[i].GetStringBytes()))
+ case astjson.TypeNumber:
+ fieldPath = append(fieldPath, unsafebytes.BytesToString(pathItems[i].MarshalTo(nil)))
+ default:
+ return 0, nil, false
+ }
+ }
+
+ return entityIndex, fieldPath, true
+}
+
func redactHeaders(rawJSON json.RawMessage) (json.RawMessage, error) {
var obj map[string]interface{}
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/loader_test.go b/app/v2/pkg/engine/resolve/loader_test.go
index 10c6168..d943e07 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/loader_test.go
+++ b/app/v2/pkg/engine/resolve/loader_test.go
@@ -1,8 +1,10 @@
package resolve
import (
+ "bytes"
"context"
"encoding/json"
+ "io"
"net/http"
"testing"
@@ -298,6 +300,223 @@ func TestLoader_LoadGraphQLResponseData(t *testing.T) {
assert.Equal(t, expected, out)
}
+func TestLoader_ValidateRequiresDependencies(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ usersService := mockedDS(t, ctrl,
+ `{"method":"POST","url":"http://users","body":{"query":"query{users{__typename id}}"}}`,
+ `{"users":[{"__typename":"User","id":"1"},{"__typename":"User","id":"2"}]}`)
+
+ namesService := NewMockDataSource(ctrl)
+ namesService.EXPECT().
+ Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})).
+ DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) {
+ assert.Equal(t, `{"method":"POST","url":"http://names","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {id name}}}","variables":{"representations":[{"__typename":"User","id":"1"},{"__typename":"User","id":"2"}]}}}`, string(input))
+ pair := NewBufPair()
+ pair.Data.WriteString(`{"_entities":[{"__typename":"User","id":"1","name":"Alice"},{"__typename":"User","id":"2","name":null}]}`)
+ pair.Errors.WriteString(`{"message":"failed to resolve name","path":["_entities",1,"name"]}`)
+ return writeGraphqlResponse(pair, w, false)
+ })
+
+ profilesService := mockedDS(t, ctrl,
+ `{"method":"POST","url":"http://profiles","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {profile}}}","variables":{"representations":[{"__typename":"User","id":"1","name":"Alice"}]}}}`,
+ `{"_entities":[{"profile":"profile-1"}]}`)
+
+ response := &GraphQLResponse{
+ Fetches: Sequence(
+ Single(&SingleFetch{
+ InputTemplate: InputTemplate{
+ Segments: []TemplateSegment{
+ {
+ Data: []byte(`{"method":"POST","url":"http://users","body":{"query":"query{users{__typename id}}"}}`),
+ SegmentType: StaticSegmentType,
+ },
+ },
+ },
+ FetchConfiguration: FetchConfiguration{
+ DataSource: usersService,
+ PostProcessing: PostProcessingConfiguration{
+ SelectResponseDataPath: []string{"data"},
+ },
+ },
+ }),
+ Single(&BatchEntityFetch{
+ Input: BatchInput{
+ Header: InputTemplate{
+ Segments: []TemplateSegment{
+ {
+ Data: []byte(`{"method":"POST","url":"http://names","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {id name}}}","variables":{"representations":[`),
+ SegmentType: StaticSegmentType,
+ },
+ },
+ },
+ Items: []InputTemplate{
+ {
+ Segments: []TemplateSegment{
+ {
+ SegmentType: VariableSegmentType,
+ VariableKind: ResolvableObjectVariableKind,
+ Renderer: NewGraphQLVariableResolveRenderer(&Object{
+ Fields: []*Field{
+ {
+ Name: []byte("__typename"),
+ Value: &String{
+ Path: []string{"__typename"},
+ },
+ },
+ {
+ Name: []byte("id"),
+ Value: &String{
+ Path: []string{"id"},
+ },
+ },
+ },
+ }),
+ },
+ },
+ },
+ },
+ Separator: InputTemplate{
+ Segments: []TemplateSegment{
+ {
+ Data: []byte(`,`),
+ SegmentType: StaticSegmentType,
+ },
+ },
+ },
+ Footer: InputTemplate{
+ Segments: []TemplateSegment{
+ {
+ Data: []byte(`]}}}`),
+ SegmentType: StaticSegmentType,
+ },
+ },
+ },
+ },
+ DataSource: namesService,
+ PostProcessing: PostProcessingConfiguration{
+ SelectResponseDataPath: []string{"data", "_entities"},
+ SelectResponseErrorsPath: []string{"errors"},
+ },
+ }, ArrayPath("users")),
+ Single(&BatchEntityFetch{
+ Input: BatchInput{
+ Header: InputTemplate{
+ Segments: []TemplateSegment{
+ {
+ Data: []byte(`{"method":"POST","url":"http://profiles","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {profile}}}","variables":{"representations":[`),
+ SegmentType: StaticSegmentType,
+ },
+ },
+ },
+ Items: []InputTemplate{
+ {
+ Segments: []TemplateSegment{
+ {
+ SegmentType: VariableSegmentType,
+ VariableKind: ResolvableObjectVariableKind,
+ Renderer: NewGraphQLVariableResolveRenderer(&Object{
+ Fields: []*Field{
+ {
+ Name: []byte("__typename"),
+ Value: &String{
+ Path: []string{"__typename"},
+ },
+ },
+ {
+ Name: []byte("id"),
+ Value: &String{
+ Path: []string{"id"},
+ },
+ },
+ {
+ Name: []byte("name"),
+ Value: &String{
+ Path: []string{"name"},
+ Nullable: true,
+ },
+ },
+ },
+ }),
+ },
+ },
+ },
+ },
+ ValidateRequiresDependencies: true,
+ NullableRepresentationVariablePaths: []string{"name"},
+ Separator: InputTemplate{
+ Segments: []TemplateSegment{
+ {
+ Data: []byte(`,`),
+ SegmentType: StaticSegmentType,
+ },
+ },
+ },
+ Footer: InputTemplate{
+ Segments: []TemplateSegment{
+ {
+ Data: []byte(`]}}}`),
+ SegmentType: StaticSegmentType,
+ },
+ },
+ },
+ },
+ DataSource: profilesService,
+ PostProcessing: PostProcessingConfiguration{
+ SelectResponseDataPath: []string{"data", "_entities"},
+ },
+ }, ArrayPath("users")),
+ ),
+ Data: &Object{
+ Fields: []*Field{
+ {
+ Name: []byte("users"),
+ Value: &Array{
+ Path: []string{"users"},
+ Item: &Object{
+ Fields: []*Field{
+ {
+ Name: []byte("id"),
+ Value: &String{
+ Path: []string{"id"},
+ },
+ },
+ {
+ Name: []byte("name"),
+ Value: &String{
+ Path: []string{"name"},
+ Nullable: true,
+ },
+ },
+ {
+ Name: []byte("profile"),
+ Value: &String{
+ Path: []string{"profile"},
+ Nullable: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ ctx := &Context{ctx: context.Background()}
+ resolvable := NewResolvable(ResolvableOptions{})
+ loader := &Loader{}
+ err := resolvable.Init(ctx, nil, ast.OperationTypeQuery)
+ assert.NoError(t, err)
+ err = loader.LoadGraphQLResponseData(ctx, response, resolvable)
+ assert.NoError(t, err)
+
+ out := fastjsonext.PrintGraphQLResponse(resolvable.data, resolvable.errors)
+ expected := `{"errors":[{"message":"Failed to fetch from Subgraph."},{"message":"Failed to fetch from Subgraph, Reason: required federation dependencies are missing from the entity representation."}],"data":{"users":[{"__typename":"User","id":"1","name":"Alice","profile":"profile-1"},{"__typename":"User","id":"2","name":null}]}}`
+ assert.Equal(t, expected, out)
+}
+
func TestLoader_MergeErrorDifferingTypes(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()