agent_patch
runs/2026-02-28__13-38-10__gpt-5-4/flux-pr-1297/flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/agent-logs/agent.patch
27148 bytes
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go b/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
index c5a7f3f..824d5a2 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
@@ -128,8 +128,9 @@ type FetchConfiguration struct {
}
type FederationConfiguration struct {
- Enabled bool
- ServiceSDL string
+ Enabled bool
+ ServiceSDL string
+ ValidateNullableRequiresDependencies bool
}
type SchemaConfiguration struct {
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
index 3acdd07..b5c8ac1 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
@@ -844,7 +844,12 @@ func (p *Planner[T]) addRepresentationsVariable() {
func (p *Planner[T]) buildRepresentationsVariable() resolve.Variable {
objects := make([]*resolve.Object, 0, len(p.dataSourcePlannerConfig.RequiredFields))
for _, cfg := range p.dataSourcePlannerConfig.RequiredFields {
- node, err := buildRepresentationVariableNode(p.visitor.Definition, cfg, p.dataSourceConfig.FederationConfiguration())
+ node, err := buildRepresentationVariableNode(
+ p.visitor.Definition,
+ cfg,
+ p.dataSourceConfig.FederationConfiguration(),
+ p.config.FederationConfiguration() != nil && p.config.FederationConfiguration().ValidateNullableRequiresDependencies,
+ )
if err != nil {
p.stopWithError(errors.WithStack(fmt.Errorf("buildRepresentationsVariable: failed to build representation variable node: %w", err)))
return nil
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/representation_variable.go b/app/v2/pkg/engine/datasource/graphql_datasource/representation_variable.go
index 18cb3ad..441a3c5 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/representation_variable.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/representation_variable.go
@@ -18,7 +18,7 @@ type objectFields struct {
// TODO: add support for remapping path
-func buildRepresentationVariableNode(definition *ast.Document, cfg plan.FederationFieldConfiguration, federationCfg plan.FederationMetaData) (*resolve.Object, error) {
+func buildRepresentationVariableNode(definition *ast.Document, cfg plan.FederationFieldConfiguration, federationCfg plan.FederationMetaData, validateRequiresDependencies bool) (*resolve.Object, error) {
key, report := plan.RequiredFieldsFragment(cfg.TypeName, cfg.SelectionSet, false)
if report.HasErrors() {
return nil, report
@@ -43,13 +43,14 @@ func buildRepresentationVariableNode(definition *ast.Document, cfg plan.Federati
}
visitor := &representationVariableVisitor{
- typeName: cfg.TypeName,
- interfaceObjectTypeName: interfaceObjectTypeName,
- entityInterfaceTypeName: entityInterfaceTypeName,
- addOnType: true,
- addTypeName: true,
- remapPaths: cfg.RemappedPaths,
- Walker: walker,
+ typeName: cfg.TypeName,
+ interfaceObjectTypeName: interfaceObjectTypeName,
+ entityInterfaceTypeName: entityInterfaceTypeName,
+ addOnType: true,
+ addTypeName: true,
+ remapPaths: cfg.RemappedPaths,
+ validateRequiresDependencies: validateRequiresDependencies && cfg.FieldName != "",
+ Walker: walker,
}
walker.RegisterEnterDocumentVisitor(visitor)
walker.RegisterFieldVisitor(visitor)
@@ -159,6 +160,8 @@ type representationVariableVisitor struct {
addOnType bool
addTypeName bool
remapPaths map[string]string
+
+ validateRequiresDependencies bool
}
func (v *representationVariableVisitor) EnterDocument(key, definition *ast.Document) {
@@ -219,9 +222,10 @@ func (v *representationVariableVisitor) EnterField(ref int) {
}
currentField := &resolve.Field{
- Name: fieldName,
- Value: v.resolveFieldValue(ref, fieldDefinitionType, true, []string{fieldPath}),
- OnTypeNames: v.resolveOnTypeNames(ref),
+ Name: fieldName,
+ Value: v.resolveFieldValue(ref, fieldDefinitionType, true, []string{fieldPath}),
+ OnTypeNames: v.resolveOnTypeNames(ref),
+ ValidatePresence: v.validateRequiresDependencies && !v.definition.TypeIsNonNull(fieldDefinitionType),
}
if v.addOnType && v.currentFields[len(v.currentFields)-1].isRoot {
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/representation_variable_test.go b/app/v2/pkg/engine/datasource/graphql_datasource/representation_variable_test.go
index e26d1a3..c1703f6 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/representation_variable_test.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/representation_variable_test.go
@@ -11,14 +11,17 @@ import (
)
func TestBuildRepresentationVariableNode(t *testing.T) {
- runTest := func(t *testing.T, definitionStr, keyStr string, federationMeta plan.FederationMetaData, expectedNode resolve.Node) {
+ runTest := func(t *testing.T, definitionStr, keyStr string, federationMeta plan.FederationMetaData, validateRequiresDependencies bool, expectedNode resolve.Node) {
definition, _ := astparser.ParseGraphqlDocumentString(definitionStr)
cfg := plan.FederationFieldConfiguration{
TypeName: "User",
SelectionSet: keyStr,
}
+ if validateRequiresDependencies {
+ cfg.FieldName = "name"
+ }
- node, err := buildRepresentationVariableNode(&definition, cfg, federationMeta)
+ node, err := buildRepresentationVariableNode(&definition, cfg, federationMeta, validateRequiresDependencies)
require.NoError(t, err)
require.Equal(t, expectedNode, node)
@@ -35,6 +38,7 @@ func TestBuildRepresentationVariableNode(t *testing.T) {
`,
`id name`,
plan.FederationMetaData{},
+ false,
&resolve.Object{
Nullable: true,
Fields: []*resolve.Field{
@@ -81,6 +85,7 @@ func TestBuildRepresentationVariableNode(t *testing.T) {
},
},
},
+ false,
&resolve.Object{
Nullable: true,
Fields: []*resolve.Field{
@@ -131,9 +136,10 @@ func TestBuildRepresentationVariableNode(t *testing.T) {
zip: Float!
}
- `,
+ `,
`id name account { accoundID address(home: true) { zip } }`,
plan.FederationMetaData{},
+ false,
&resolve.Object{
Nullable: true,
Fields: []*resolve.Field{
@@ -217,9 +223,10 @@ func TestBuildRepresentationVariableNode(t *testing.T) {
}
union ab = A | B
- `,
+ `,
`u { ... on A { a } } i { ... on B { title } }`,
plan.FederationMetaData{},
+ false,
&resolve.Object{
Nullable: true,
Fields: []*resolve.Field{
@@ -265,6 +272,62 @@ func TestBuildRepresentationVariableNode(t *testing.T) {
},
})
})
+
+ t.Run("nullable requires fields are marked for validation", func(t *testing.T) {
+ runTest(t, `
+ scalar String
+
+ type User {
+ id: String!
+ details: Details
+ }
+
+ type Details {
+ middlename: String
+ }
+ `,
+ `id details { middlename }`,
+ plan.FederationMetaData{},
+ true,
+ &resolve.Object{
+ Nullable: true,
+ Fields: []*resolve.Field{
+ {
+ Name: []byte("__typename"),
+ Value: &resolve.String{
+ Path: []string{"__typename"},
+ },
+ OnTypeNames: [][]byte{[]byte("User")},
+ },
+ {
+ Name: []byte("id"),
+ Value: &resolve.String{
+ Path: []string{"id"},
+ },
+ OnTypeNames: [][]byte{[]byte("User")},
+ },
+ {
+ Name: []byte("details"),
+ ValidatePresence: true,
+ Value: &resolve.Object{
+ Path: []string{"details"},
+ Nullable: true,
+ Fields: []*resolve.Field{
+ {
+ Name: []byte("middlename"),
+ ValidatePresence: true,
+ Value: &resolve.String{
+ Path: []string{"middlename"},
+ Nullable: true,
+ },
+ },
+ },
+ },
+ OnTypeNames: [][]byte{[]byte("User")},
+ },
+ },
+ })
+ })
}
func TestMergeRepresentationVariableNodes(t *testing.T) {
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/loader.go b/app/v2/pkg/engine/resolve/loader.go
index 1e9f993..21bf134 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/loader.go
+++ b/app/v2/pkg/engine/resolve/loader.go
@@ -27,6 +27,7 @@ import (
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient"
"github.com/wundergraph/graphql-go-tools/v2/pkg/errorcodes"
+ "github.com/wundergraph/graphql-go-tools/v2/pkg/fastjsonext"
"github.com/wundergraph/graphql-go-tools/v2/pkg/internal/unsafebytes"
)
@@ -139,6 +140,8 @@ type result struct {
loaderHookContext context.Context
httpResponseContext *httpclient.ResponseContext
+
+ missingRequiredDependencyErrorPaths [][]fastjsonext.PathElement
}
func (r *result) init(postProcessing PostProcessingConfiguration, info *FetchInfo) {
@@ -540,6 +543,7 @@ func (l *Loader) mergeResult(fetchItem *FetchItem, res *result, items []*astjson
}
return nil
}
+ l.renderMissingRequiredDependencyErrors(res)
if res.fetchSkipped {
return nil
}
@@ -567,6 +571,7 @@ func (l *Loader) mergeResult(fetchItem *FetchItem, res *result, items []*astjson
// If errors field are present in response, but the errors array is empty, we don't consider it as an error
// Note: it is not compliant to graphql spec
if hasErrors {
+ l.markMissingRequiredDependencies(fetchItem, res, items, errorObjects)
// Look for errors in the response and merge them into the errors array
err = l.mergeErrors(res, fetchItem, errorsValue, errorObjects)
if err != nil {
@@ -669,6 +674,112 @@ func (l *Loader) mergeResult(fetchItem *FetchItem, res *result, items []*astjson
return nil
}
+func (l *Loader) renderMissingRequiredDependencyErrors(res *result) {
+ for i := range res.missingRequiredDependencyErrorPaths {
+ fastjsonext.AppendErrorToArray(l.resolvable.astjsonArena, l.resolvable.errors, missingRequiredFederationDependencyMessage, res.missingRequiredDependencyErrorPaths[i])
+ }
+}
+
+func (l *Loader) markMissingRequiredDependencies(fetchItem *FetchItem, res *result, items []*astjson.Value, errorObjects []*astjson.Value) {
+ switch fetchItem.Fetch.(type) {
+ case *EntityFetch, *BatchEntityFetch:
+ default:
+ return
+ }
+
+ trueValue := astjson.MustParse(`true`)
+ for _, errorObject := range errorObjects {
+ errorPath := errorObject.Get("path")
+ if errorPath == nil || errorPath.Type() != astjson.TypeArray {
+ continue
+ }
+ pathItems := errorPath.GetArray()
+ if len(pathItems) < 3 {
+ continue
+ }
+ if string(pathItems[0].GetStringBytes()) != "_entities" {
+ continue
+ }
+
+ entityIndex, ok := l.pathItemIndex(pathItems[1])
+ if !ok {
+ continue
+ }
+
+ markerSuffix := make([]string, 0, len(pathItems)-2)
+ for i := 2; i < len(pathItems); i++ {
+ switch pathItems[i].Type() {
+ case astjson.TypeString:
+ markerSuffix = append(markerSuffix, string(pathItems[i].GetStringBytes()))
+ case astjson.TypeNumber:
+ markerSuffix = append(markerSuffix, strconv.Itoa(pathItems[i].GetInt()))
+ }
+ }
+ if len(markerSuffix) == 0 {
+ continue
+ }
+
+ for _, itemIndex := range l.matchItemsForBatchIndex(items, res, entityIndex) {
+ if itemIndex < 0 || itemIndex >= len(items) || items[itemIndex] == nil {
+ continue
+ }
+ astjson.SetValue(items[itemIndex], trueValue, append([]string{requiresDependencyTaintKey}, markerSuffix...)...)
+ }
+ }
+}
+
+func (l *Loader) matchItemsForBatchIndex(items []*astjson.Value, res *result, entityIndex int) []int {
+ if res.batchStats == nil {
+ if entityIndex >= 0 && entityIndex < len(items) {
+ return []int{entityIndex}
+ }
+ if entityIndex == 0 && len(items) == 1 {
+ return []int{0}
+ }
+ return nil
+ }
+
+ matches := make([]int, 0, len(items))
+ for i := range res.batchStats {
+ for _, batchIndex := range res.batchStats[i] {
+ if batchIndex == entityIndex {
+ matches = append(matches, i)
+ break
+ }
+ }
+ }
+ return matches
+}
+
+func (l *Loader) pathItemIndex(value *astjson.Value) (int, bool) {
+ switch value.Type() {
+ case astjson.TypeNumber:
+ return value.GetInt(), true
+ case astjson.TypeString:
+ index, err := strconv.Atoi(string(value.GetStringBytes()))
+ if err != nil {
+ return 0, false
+ }
+ return index, true
+ default:
+ return 0, false
+ }
+}
+
+func (l *Loader) missingDependencyErrorPath(fetchItem *FetchItem, itemIndex, totalItems int) []fastjsonext.PathElement {
+ path := make([]fastjsonext.PathElement, 0, len(fetchItem.ResponsePathElements)+1)
+ for i := range fetchItem.ResponsePathElements {
+ if totalItems > 1 && i == len(fetchItem.ResponsePathElements)-1 {
+ path = append(path, fastjsonext.PathElement{Idx: itemIndex})
+ }
+ path = append(path, fastjsonext.PathElement{Name: fetchItem.ResponsePathElements[i]})
+ }
+ if totalItems > 1 && len(fetchItem.ResponsePathElements) == 0 {
+ path = append(path, fastjsonext.PathElement{Idx: itemIndex})
+ }
+ return path
+}
+
var (
errorsInvalidInputHeader = []byte(`{"errors":[{"message":"Failed to render Fetch Input","path":[`)
errorsInvalidInputFooter = []byte(`]}]}`)
@@ -1311,6 +1422,9 @@ func (l *Loader) loadEntityFetch(ctx context.Context, fetchItem *FetchItem, fetc
err = fetch.Input.Item.Render(l.ctx, input, buf.item)
if err != nil {
if fetch.Input.SkipErrItem {
+ if goerrors.Is(err, errMissingRequiredFederationDependency) {
+ res.missingRequiredDependencyErrorPaths = append(res.missingRequiredDependencyErrorPaths, l.missingDependencyErrorPath(fetchItem, 0, len(items)))
+ }
// skip fetch on render item error
if l.ctx.TracingOptions.Enable {
fetch.Trace.LoadSkipped = true
@@ -1430,6 +1544,9 @@ WithNextItem:
err = fetch.Input.Items[j].Render(l.ctx, item, buf.itemInput)
if err != nil {
if fetch.Input.SkipErrItems {
+ if goerrors.Is(err, errMissingRequiredFederationDependency) {
+ res.missingRequiredDependencyErrorPaths = append(res.missingRequiredDependencyErrorPaths, l.missingDependencyErrorPath(fetchItem, i, len(items)))
+ }
err = nil // nolint:ineffassign
res.batchStats[i] = append(res.batchStats[i], -1)
continue
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/node_object.go b/app/v2/pkg/engine/resolve/node_object.go
index 7f5e94a..f835c6a 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/node_object.go
+++ b/app/v2/pkg/engine/resolve/node_object.go
@@ -95,6 +95,7 @@ type Field struct {
OnTypeNames [][]byte
ParentOnTypeNames []ParentOnTypeNames
Info *FieldInfo
+ ValidatePresence bool
}
type ParentOnTypeNames struct {
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolvable.go b/app/v2/pkg/engine/resolve/resolvable.go
index 5219c91..c3de3bd 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolvable.go
+++ b/app/v2/pkg/engine/resolve/resolvable.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
+ stdErrors "errors"
"fmt"
"io"
"strconv"
@@ -23,6 +24,13 @@ import (
const invalidPath = "invalid path"
+const (
+ missingRequiredFederationDependencyMessage = "Missing required federation dependency."
+ requiresDependencyTaintKey = "__wg__fed__requires"
+)
+
+var errMissingRequiredFederationDependency = stdErrors.New("missing required federation dependency")
+
type Resolvable struct {
options ResolvableOptions
@@ -58,6 +66,9 @@ type Resolvable struct {
enclosingTypeNames []string
currentFieldInfo *FieldInfo
+
+ variableRoot *astjson.Value
+ nodeResolutionError error
}
type ResolvableOptions struct {
@@ -97,6 +108,8 @@ func (r *Resolvable) Reset() {
r.authorizationError = nil
r.astjsonArena.Reset()
r.xxh.Reset()
+ r.variableRoot = nil
+ r.nodeResolutionError = nil
for k := range r.authorizationAllow {
delete(r.authorizationAllow, k)
}
@@ -169,9 +182,14 @@ func (r *Resolvable) ResolveNode(node Node, data *astjson.Value, out io.Writer)
r.printErr = nil
r.authorizationError = nil
r.errors = r.astjsonArena.NewArray()
+ r.variableRoot = data
+ r.nodeResolutionError = nil
hasErrors := r.walkNode(node, data)
if hasErrors {
+ if r.nodeResolutionError != nil {
+ return r.nodeResolutionError
+ }
return fmt.Errorf("error resolving node")
}
@@ -668,9 +686,16 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
r.printBytes(quote)
r.printBytes(colon)
}
+ if !r.print && obj.Fields[i].ValidatePresence && r.hasMissingRequiredFederationDependency(value, obj.Fields[i].Value.NodePath()) {
+ r.nodeResolutionError = errMissingRequiredFederationDependency
+ return true
+ }
r.currentFieldInfo = obj.Fields[i].Info
err := r.walkNode(obj.Fields[i].Value, value)
if err {
+ if r.nodeResolutionError != nil {
+ return true
+ }
if obj.Nullable {
if len(obj.Path) > 0 {
astjson.SetNull(parent, obj.Path...)
@@ -852,6 +877,9 @@ func (r *Resolvable) walkArray(arr *Array, value *astjson.Value) bool {
err := r.walkNode(arr.Item, arrayValue)
r.popArrayPathElement()
if err {
+ if r.nodeResolutionError != nil {
+ return true
+ }
if arr.Item.NodeKind() == NodeKindObject && arr.Item.NodeNullable() {
value.SetArrayItem(i, astjson.NullValue)
continue
@@ -869,6 +897,29 @@ func (r *Resolvable) walkArray(arr *Array, value *astjson.Value) bool {
return false
}
+func (r *Resolvable) hasMissingRequiredFederationDependency(value *astjson.Value, path []string) bool {
+ if r.variableRoot == nil || len(path) == 0 {
+ return false
+ }
+ fieldValue := value.Get(path...)
+ if !astjson.ValueIsNull(fieldValue) {
+ return false
+ }
+
+ markerPath := make([]string, 0, len(r.path)+len(path)+1)
+ markerPath = append(markerPath, requiresDependencyTaintKey)
+ for i := range r.path {
+ if r.path[i].Name != "" {
+ markerPath = append(markerPath, r.path[i].Name)
+ } else {
+ markerPath = append(markerPath, strconv.Itoa(r.path[i].Idx))
+ }
+ }
+ markerPath = append(markerPath, path...)
+
+ return astjson.ValueIsNonNull(r.variableRoot.Get(markerPath...))
+}
+
func (r *Resolvable) walkNull() bool {
if r.print {
r.printBytes(null)
diff --git a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolve_federation_test.go b/app/v2/pkg/engine/resolve/resolve_federation_test.go
index 2547c6d..5a73665 100644
--- a/tmp/agent-patch-flux-pr-1297.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolve_federation_test.go
+++ b/app/v2/pkg/engine/resolve/resolve_federation_test.go
@@ -9,6 +9,8 @@ import (
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
)
type TestingTB interface {
@@ -2148,6 +2150,210 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) {
}, Context{ctx: context.Background()}, `{"data":{"user":{"account":{"address":{"fullAddress":"line1 line2 line3-1 city-1 country-1 zip-1"}}}}}`
}))
+ t.Run("skips tainted nullable requires dependencies", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ rCtx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ secondService := NewMockDataSource(ctrl)
+ secondService.EXPECT().
+ Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})).
+ DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) error {
+ require.Equal(t,
+ `{"method":"POST","url":"http://second.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {details {middlename}}}}","variables":{"representations":[{"__typename":"User","id":"1"}]}}}`,
+ string(input),
+ )
+
+ pair := NewBufPair()
+ pair.Data.WriteString(`{"_entities":[{"__typename":"User","details":{"middlename":null}}]}`)
+ pair.Errors.WriteString(`{"message":"boom","path":["_entities",0,"details","middlename"]}`)
+ return writeGraphqlResponse(pair, w, false)
+ })
+
+ node := &GraphQLResponse{
+ Info: &GraphQLResponseInfo{
+ OperationType: ast.OperationTypeQuery,
+ },
+ Fetches: Sequence(
+ Single(&SingleFetch{
+ InputTemplate: InputTemplate{
+ Segments: []TemplateSegment{
+ {
+ Data: []byte(`{"method":"POST","url":"http://first.service","body":{"query":"{me {__typename id}}"}}`),
+ SegmentType: StaticSegmentType,
+ },
+ },
+ },
+ FetchConfiguration: FetchConfiguration{
+ DataSource: FakeDataSource(`{"data":{"me":{"__typename":"User","id":"1"}}}`),
+ PostProcessing: PostProcessingConfiguration{
+ SelectResponseDataPath: []string{"data"},
+ },
+ },
+ }),
+ SingleWithPath(&EntityFetch{
+ FetchDependencies: FetchDependencies{
+ FetchID: 1,
+ DependsOnFetchIDs: []int{0},
+ },
+ Input: EntityInput{
+ Header: InputTemplate{
+ Segments: []TemplateSegment{{
+ Data: []byte(`{"method":"POST","url":"http://second.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {details {middlename}}}}","variables":{"representations":[`),
+ SegmentType: StaticSegmentType,
+ }},
+ },
+ Item: InputTemplate{
+ Segments: []TemplateSegment{{
+ SegmentType: VariableSegmentType,
+ VariableKind: ResolvableObjectVariableKind,
+ Renderer: NewGraphQLVariableResolveRenderer(&Object{
+ Nullable: true,
+ Fields: []*Field{
+ {
+ Name: []byte("__typename"),
+ Value: &String{
+ Path: []string{"__typename"},
+ },
+ },
+ {
+ Name: []byte("id"),
+ Value: &String{
+ Path: []string{"id"},
+ },
+ },
+ },
+ }),
+ }},
+ },
+ Footer: InputTemplate{
+ Segments: []TemplateSegment{{
+ Data: []byte(`]}}}`),
+ SegmentType: StaticSegmentType,
+ }},
+ },
+ SkipErrItem: true,
+ },
+ DataSource: secondService,
+ PostProcessing: PostProcessingConfiguration{
+ SelectResponseDataPath: []string{"data", "_entities", "0"},
+ SelectResponseErrorsPath: []string{"errors"},
+ },
+ }, "me", ObjectPath("me")),
+ SingleWithPath(&EntityFetch{
+ FetchDependencies: FetchDependencies{
+ FetchID: 2,
+ DependsOnFetchIDs: []int{1},
+ },
+ Input: EntityInput{
+ Header: InputTemplate{
+ Segments: []TemplateSegment{{
+ Data: []byte(`{"method":"POST","url":"http://third.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {details {age}}}}","variables":{"representations":[`),
+ SegmentType: StaticSegmentType,
+ }},
+ },
+ Item: InputTemplate{
+ Segments: []TemplateSegment{{
+ SegmentType: VariableSegmentType,
+ VariableKind: ResolvableObjectVariableKind,
+ Renderer: NewGraphQLVariableResolveRenderer(&Object{
+ Nullable: true,
+ Fields: []*Field{
+ {
+ Name: []byte("__typename"),
+ Value: &String{
+ Path: []string{"__typename"},
+ },
+ },
+ {
+ Name: []byte("id"),
+ Value: &String{
+ Path: []string{"id"},
+ },
+ },
+ {
+ Name: []byte("details"),
+ ValidatePresence: true,
+ Value: &Object{
+ Path: []string{"details"},
+ Nullable: true,
+ Fields: []*Field{
+ {
+ Name: []byte("middlename"),
+ ValidatePresence: true,
+ Value: &String{
+ Path: []string{"middlename"},
+ Nullable: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ }),
+ }},
+ },
+ Footer: InputTemplate{
+ Segments: []TemplateSegment{{
+ Data: []byte(`]}}}`),
+ SegmentType: StaticSegmentType,
+ }},
+ },
+ SkipErrItem: true,
+ },
+ DataSource: FakeDataSource(`{"data":{"_entities":[{"__typename":"User","details":{"age":42}}]}}`),
+ PostProcessing: PostProcessingConfiguration{
+ SelectResponseDataPath: []string{"data", "_entities", "0"},
+ },
+ }, "me.details.age", ObjectPath("me")),
+ ),
+ Data: &Object{
+ Fields: []*Field{
+ {
+ Name: []byte("me"),
+ Value: &Object{
+ Path: []string{"me"},
+ Nullable: true,
+ Fields: []*Field{
+ {
+ Name: []byte("details"),
+ Value: &Object{
+ Path: []string{"details"},
+ Nullable: true,
+ Fields: []*Field{
+ {
+ Name: []byte("middlename"),
+ Value: &String{
+ Path: []string{"middlename"},
+ Nullable: true,
+ },
+ },
+ {
+ Name: []byte("age"),
+ Value: &Integer{
+ Path: []string{"age"},
+ Nullable: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ buf := &bytes.Buffer{}
+ _, err := newResolver(rCtx).ResolveGraphQLResponse(&Context{ctx: context.Background()}, node, nil, buf)
+ require.NoError(t, err)
+ assert.Contains(t, buf.String(), `"message":"Missing required federation dependency.","path":["me","details","age"]`)
+ assert.Contains(t, buf.String(), `"middlename":null`)
+ assert.Contains(t, buf.String(), `"age":null`)
+ ctrl.Finish()
+ })
+
t.Run("nested batching", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) {
productsService := mockedDS(t, ctrl,