Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions execution/engine/federation_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,16 @@ func TestFederationIntegrationTest(t *testing.T) {
assert.Equal(t, compact(expected), string(resp))
})

t.Run("recursive fragment", func(t *testing.T) {
setup := federationtesting.NewFederationSetup(addGateway(false))
t.Cleanup(setup.Close)
gqlClient := NewGraphqlClient(http.DefaultClient)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
resp := gqlClient.QueryStatusCode(ctx, setup.GatewayServer.URL, testQueryPath("queries/recursive_fragment.graphql"), nil, http.StatusInternalServerError, t)
assert.Len(t, resp, 0)
})

t.Run("Union response type with interface fragments", func(t *testing.T) {
setup := federationtesting.NewFederationSetup(addGateway(false))
t.Cleanup(setup.Close)
Expand Down
12 changes: 12 additions & 0 deletions execution/engine/graphql_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ func (g *GraphqlClient) Query(ctx context.Context, addr, queryFilePath string, v
return responseBodyBytes
}

func (g *GraphqlClient) QueryStatusCode(ctx context.Context, addr, queryFilePath string, variables queryVariables, expectedStatusCode int, t *testing.T) []byte {
reqBody := loadQuery(t, queryFilePath, variables)
req, err := http.NewRequest(http.MethodPost, addr, bytes.NewBuffer(reqBody))
require.NoError(t, err)
req = req.WithContext(ctx)
resp, err := g.httpClient.Do(req)
require.NoError(t, err)
responseBodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
return responseBodyBytes
}

func (g *GraphqlClient) Subscription(ctx context.Context, addr, queryFilePath string, variables queryVariables, t *testing.T) chan []byte {
messageCh := make(chan []byte)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
query RecursiveFragmentQuery {
...FragmentA
}

fragment FragmentA on Query {
...FragmentB
}

fragment FragmentB on Query {
...FragmentA
}

Original file line number Diff line number Diff line change
Expand Up @@ -3097,8 +3097,44 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
},
DataSourceIdentifier: []byte("graphql_datasource.Source"),
FetchConfiguration: resolve.FetchConfiguration{
Input: `{"method":"POST","url":"http://address-enricher.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Address {__typename country city}}}","variables":{"representations":[$$0$$]}}}`,
DataSource: &Source{},
Input: `{"method":"POST","url":"http://address-enricher.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Address {__typename country city}}}","variables":{"representations":[$$0$$]}}}`,
DataSource: &Source{},
CoordinateDependencies: []resolve.FetchDependency{
{
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "country",
},
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
Subgraph: "user.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "id",
},
IsKey: true,
},
},
},
{
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "city",
},
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
Subgraph: "user.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "id",
},
IsKey: true,
},
},
},
},
PostProcessing: SingleEntityPostProcessingConfiguration,
RequiresEntityFetch: true,
Variables: []resolve.Variable{
Expand Down Expand Up @@ -3138,6 +3174,60 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
DataSource: &Source{},
PostProcessing: SingleEntityPostProcessingConfiguration,
RequiresEntityFetch: true,
CoordinateDependencies: []resolve.FetchDependency{
{
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "line3",
},
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
Subgraph: "user.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "id",
},
IsKey: true,
},
},
},
{
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "zip",
},
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 1,
Subgraph: "address-enricher.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "country",
},
IsRequires: true,
},
{
FetchID: 1,
Subgraph: "address-enricher.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "city",
},
IsRequires: true,
},
{
FetchID: 0,
Subgraph: "user.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "id",
},
IsKey: true,
},
},
},
},
Variables: []resolve.Variable{
&resolve.ResolvableObjectVariable{
Renderer: resolve.NewGraphQLVariableResolveRenderer(&resolve.Object{
Expand Down Expand Up @@ -3185,9 +3275,65 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
},
DataSourceIdentifier: []byte("graphql_datasource.Source"),
FetchConfiguration: resolve.FetchConfiguration{
Input: `{"method":"POST","url":"http://account.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Address {__typename fullAddress}}}","variables":{"representations":[$$0$$]}}}`,
DataSource: &Source{},
PostProcessing: SingleEntityPostProcessingConfiguration,
Input: `{"method":"POST","url":"http://account.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Address {__typename fullAddress}}}","variables":{"representations":[$$0$$]}}}`,
DataSource: &Source{},
PostProcessing: SingleEntityPostProcessingConfiguration,
CoordinateDependencies: []resolve.FetchDependency{
{
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "fullAddress",
},
IsUserRequested: true,
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
Subgraph: "user.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "line1",
},
IsRequires: true,
},
{
FetchID: 0,
Subgraph: "user.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "line2",
},
IsRequires: true,
},
{
FetchID: 2,
Subgraph: "address.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "line3",
},
IsRequires: true,
},
{
FetchID: 2,
Subgraph: "address.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "zip",
},
IsRequires: true,
},
{
FetchID: 0,
Subgraph: "user.service",
Coordinate: resolve.GraphCoordinate{
TypeName: "Address",
FieldName: "id",
},
IsKey: true,
},
},
},
},
RequiresEntityFetch: true,
Variables: []resolve.Variable{
&resolve.ResolvableObjectVariable{
Expand Down Expand Up @@ -3328,6 +3474,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
},
},
WithDefaultPostProcessor(),
WithFieldDependencies(),
)
})

Expand Down
20 changes: 16 additions & 4 deletions v2/pkg/engine/datasourcetesting/datasourcetesting.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ import (
)

type testOptions struct {
postProcessors []*postprocess.Processor
skipReason string
withFieldInfo bool
withPrintPlan bool
postProcessors []*postprocess.Processor
skipReason string
withFieldInfo bool
withPrintPlan bool
withFieldDependencies bool
}

func WithPostProcessors(postProcessors ...*postprocess.Processor) func(*testOptions) {
Expand Down Expand Up @@ -67,6 +68,12 @@ func WithPrintPlan() func(*testOptions) {
}
}

func WithFieldDependencies() func(*testOptions) {
return func(o *testOptions) {
o.withFieldDependencies = true
}
}

func RunWithPermutations(t *testing.T, definition, operation, operationName string, expectedPlan plan.Plan, config plan.Configuration, options ...func(*testOptions)) {
t.Helper()

Expand Down Expand Up @@ -125,6 +132,7 @@ func RunTestWithVariables(definition, operation, operationName, variables string

// by default, we don't want to have field info in the tests because it's too verbose
config.DisableIncludeInfo = true
config.DisableIncludeFieldDependencies = true

opts := &testOptions{}
for _, o := range options {
Expand All @@ -135,6 +143,10 @@ func RunTestWithVariables(definition, operation, operationName, variables string
config.DisableIncludeInfo = false
}

if opts.withFieldDependencies {
config.DisableIncludeFieldDependencies = false
}

if opts.skipReason != "" {
t.Skip(opts.skipReason)
}
Expand Down
3 changes: 2 additions & 1 deletion v2/pkg/engine/plan/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ type Configuration struct {

MinifySubgraphOperations bool

DisableIncludeInfo bool
DisableIncludeInfo bool
DisableIncludeFieldDependencies bool
}

type DebugConfiguration struct {
Expand Down
11 changes: 11 additions & 0 deletions v2/pkg/engine/plan/node_selection_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,21 @@ type NodeSelectionBuilder struct {
nodeSelectionsVisitor *nodeSelectionVisitor
}

type fieldDependencyKind int

const (
fieldDependencyKindKey fieldDependencyKind = iota
fieldDependencyKindRequires
)

type NodeSelectionResult struct {
dataSources []DataSource // data sources configurations, which used by the current operation
nodeSuggestions *NodeSuggestions // nodeSuggestions holds information about suggested data sources for each field
fieldDependsOn map[fieldIndexKey][]int // fieldDependsOn is a map[fieldIndexKey][]fieldRef - holds list of field refs which are required by a field ref, e.g. field should be planned only after required fields were planned
fieldRequirementsConfigs map[fieldIndexKey][]FederationFieldConfiguration // fieldRequirementsConfigs is a map[fieldIndexKey]FederationFieldConfiguration - holds a list of required configuratuibs for a field ref to later built representation variables
skipFieldsRefs []int // skipFieldsRefs holds required field refs added by planner and should not be added to user response
fieldRefDependsOn map[int][]int
fieldDependencyKind map[fieldDependencyKey]fieldDependencyKind
}

func NewNodeSelectionBuilder(config *Configuration) *NodeSelectionBuilder {
Expand Down Expand Up @@ -170,6 +179,8 @@ func (p *NodeSelectionBuilder) SelectNodes(operation, definition *ast.Document,
fieldDependsOn: p.nodeSelectionsVisitor.fieldDependsOn,
fieldRequirementsConfigs: p.nodeSelectionsVisitor.fieldRequirementsConfigs,
skipFieldsRefs: p.nodeSelectionsVisitor.skipFieldsRefs,
fieldRefDependsOn: p.nodeSelectionsVisitor.fieldRefDependsOn,
fieldDependencyKind: p.nodeSelectionsVisitor.fieldDependencyKind,
}
}

Expand Down
15 changes: 15 additions & 0 deletions v2/pkg/engine/plan/node_selection_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type nodeSelectionVisitor struct {
fieldRefDependsOn map[int][]int // fieldRefDependsOn is a map[fieldRef][]fieldRef - holds list of field refs which are required by a field ref, it is a second index without datasource hash
fieldRequirementsConfigs map[fieldIndexKey][]FederationFieldConfiguration // fieldRequirementsConfigs is a map[fieldIndexKey]FederationFieldConfiguration - holds a list of required configuratuibs for a field ref to later built representation variables
fieldLandedTo map[int]DSHash // fieldLandedTo is a map[fieldRef]DSHash - holds a datasource hash where field was landed to
fieldDependencyKind map[fieldDependencyKey]fieldDependencyKind

secondaryRun bool // secondaryRun is a flag to indicate that we're running the nodeSelectionVisitor not the first time
hasNewFields bool // hasNewFields is used to determine if we need to run the planner again. It will be true in case required fields were added
Expand All @@ -47,6 +48,10 @@ type nodeSelectionVisitor struct {
rewrittenFieldRefs []int
}

type fieldDependencyKey struct {
field, dependsOn int
}

func (c *nodeSelectionVisitor) shouldRevisit() bool {
return c.hasNewFields || c.hasUnresolvedFields
}
Expand Down Expand Up @@ -142,6 +147,7 @@ func (c *nodeSelectionVisitor) EnterDocument(operation, definition *ast.Document
c.visitedFieldsKeyChecks = make(map[fieldIndexKey]struct{})
c.pendingKeyRequirements = make(map[int]pendingKeyRequirements)
c.pendingFieldRequirements = make(map[int]pendingFieldRequirements)
c.fieldDependencyKind = make(map[fieldDependencyKey]fieldDependencyKind)

c.fieldDependsOn = make(map[fieldIndexKey][]int)
c.fieldRefDependsOn = make(map[int][]int)
Expand Down Expand Up @@ -502,6 +508,9 @@ func (c *nodeSelectionVisitor) addFieldRequirementsToOperation(selectionSetRef i
RemappedPaths: addFieldsResult.remappedPaths,
}
c.fieldRequirementsConfigs[fieldKey] = append(c.fieldRequirementsConfigs[fieldKey], fieldConfiguration)
for _, requiredFieldRef := range addFieldsResult.requiredFieldRefs {
c.fieldDependencyKind[fieldDependencyKey{field: requestedByFieldRef, dependsOn: requiredFieldRef}] = fieldDependencyKindRequires
}
}

c.hasNewFields = true
Expand Down Expand Up @@ -590,6 +599,9 @@ func (c *nodeSelectionVisitor) addKeyRequirementsToOperation(selectionSetRef int
TypeName: previousJump.TypeName,
SelectionSet: previousJump.SelectionSet,
})
for _, requiredFieldRef := range currentFieldRefs {
c.fieldDependencyKind[fieldDependencyKey{field: requestedByFieldRef, dependsOn: requiredFieldRef}] = fieldDependencyKindKey
}
}
}
currentFieldRefs = addFieldsResult.requiredFieldRefs
Expand All @@ -610,6 +622,9 @@ func (c *nodeSelectionVisitor) addKeyRequirementsToOperation(selectionSetRef int
TypeName: jump.TypeName,
SelectionSet: jump.SelectionSet,
})
for _, requiredFieldRef := range addFieldsResult.requiredFieldRefs {
c.fieldDependencyKind[fieldDependencyKey{field: requestedByFieldRef, dependsOn: requiredFieldRef}] = fieldDependencyKindKey
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions v2/pkg/engine/plan/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ func (p *Planner) Plan(operation, definition *ast.Document, operationName string
p.planningVisitor.planners = plannersConfigurations
p.planningVisitor.Config = p.config
p.planningVisitor.skipFieldsRefs = selectionsConfig.skipFieldsRefs
p.planningVisitor.fieldRefDependsOnFieldRefs = selectionsConfig.fieldRefDependsOn
p.planningVisitor.fieldDependencyKind = selectionsConfig.fieldDependencyKind

p.planningWalker.ResetVisitors()
p.planningWalker.SetVisitorFilter(p.planningVisitor)
Expand Down
Loading
Loading