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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
address: Address
deliveryAddress: Address
secretAddress: Address
providedAddress: Address
shippingInfo: ShippingInfo
}
type Address {
Expand Down Expand Up @@ -1149,6 +1150,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
info: Info
shippingInfo: ShippingInfo
secretAddress: Address
providedAddress: Address @provides(fields: "line1 line2 line3(test:\"BOOM\") zip")
}

type Info {
Expand Down Expand Up @@ -1182,7 +1184,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
},
{
TypeName: "Account",
FieldNames: []string{"id", "name", "info", "shippingInfo", "secretAddress"},
FieldNames: []string{"id", "name", "info", "shippingInfo", "secretAddress", "providedAddress"},
},
{
TypeName: "Address",
Expand Down Expand Up @@ -1224,6 +1226,13 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
SelectionSet: "zip",
},
},
Provides: plan.FederationFieldConfigurations{
{
TypeName: "Account",
FieldName: "providedAddress",
SelectionSet: "zip line1 line2 line3(test:\"BOOM\")",
},
},
},
},
mustCustomConfiguration(t,
Expand Down Expand Up @@ -1320,7 +1329,6 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
Keys: plan.FederationFieldConfigurations{
{
TypeName: "Address",
FieldName: "",
SelectionSet: "id",
},
},
Expand Down Expand Up @@ -4303,7 +4311,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
},
DataSourceIdentifier: []byte("graphql_datasource.Source"),
FetchConfiguration: resolve.FetchConfiguration{
Input: `{"method":"POST","url":"http://user.service","body":{"query":"{user {oldAccount {deliveryAddress {line1} shippingInfo {zip} __typename id info {a b}}}}"}}`,
Input: `{"method":"POST","url":"http://user.service","body":{"query":"{user {oldAccount {deliveryAddress {line1}}}}"}}`,
DataSource: &Source{},
PostProcessing: DefaultPostProcessingConfiguration,
},
Expand Down Expand Up @@ -4394,6 +4402,187 @@ func TestGraphQLDataSourceFederation(t *testing.T) {
)
})

t.Run("nested selection set - but requirements are provided with entity query", func(t *testing.T) {
operation := `
query Requires {
user {
account {
providedAddress {
secretLine
fullAddress
}
}
}
}`

operationName := "Requires"

expectedPlan := func() *plan.SynchronousResponsePlan {
return &plan.SynchronousResponsePlan{
Response: &resolve.GraphQLResponse{
Fetches: resolve.Sequence(
resolve.Single(&resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 0,
},
DataSourceIdentifier: []byte("graphql_datasource.Source"),
FetchConfiguration: resolve.FetchConfiguration{
Input: `{"method":"POST","url":"http://user.service","body":{"query":"{user {account {__typename id info {a b}}}}"}}`,
DataSource: &Source{},
PostProcessing: DefaultPostProcessingConfiguration,
},
}),
resolve.SingleWithPath(&resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 1,
DependsOnFetchIDs: []int{0},
},
DataSourceIdentifier: []byte("graphql_datasource.Source"),
FetchConfiguration: resolve.FetchConfiguration{
Input: `{"method":"POST","url":"http://account.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on Account {__typename providedAddress {secretLine fullAddress}}}}","variables":{"representations":[$$0$$]}}}`,
DataSource: &Source{},
PostProcessing: SingleEntityPostProcessingConfiguration,
RequiresEntityFetch: true,
Variables: []resolve.Variable{
&resolve.ResolvableObjectVariable{
Renderer: resolve.NewGraphQLVariableResolveRenderer(&resolve.Object{
Nullable: true,
Fields: []*resolve.Field{
{
Name: []byte("__typename"),
Value: &resolve.String{
Path: []string{"__typename"},
},
OnTypeNames: [][]byte{[]byte("Account")},
},
{
Name: []byte("id"),
Value: &resolve.Scalar{
Path: []string{"id"},
},
OnTypeNames: [][]byte{[]byte("Account")},
},
{
Name: []byte("info"),
Value: &resolve.Object{
Path: []string{"info"},
Nullable: true,
Fields: []*resolve.Field{
{
Name: []byte("a"),
Value: &resolve.Scalar{
Path: []string{"a"},
},
},
{
Name: []byte("b"),
Value: &resolve.Scalar{
Path: []string{"b"},
},
},
},
},
OnTypeNames: [][]byte{[]byte("Account")},
},
},
}),
},
},
SetTemplateOutputToNullOnVariableNull: true,
},
}, "user.account", resolve.ObjectPath("user"), resolve.ObjectPath("account")),
),
Data: &resolve.Object{
Fields: []*resolve.Field{
{
Name: []byte("user"),
Value: &resolve.Object{
Path: []string{"user"},
Nullable: true,
PossibleTypes: map[string]struct{}{
"User": {},
},
TypeName: "User",
Fields: []*resolve.Field{
{
Name: []byte("account"),
Value: &resolve.Object{
Path: []string{"account"},
Nullable: true,
PossibleTypes: map[string]struct{}{
"Account": {},
},
TypeName: "Account",
Fields: []*resolve.Field{
{
Name: []byte("providedAddress"),
Value: &resolve.Object{
Path: []string{"providedAddress"},
Nullable: true,
PossibleTypes: map[string]struct{}{
"Address": {},
},
TypeName: "Address",
Fields: []*resolve.Field{
{
Name: []byte("secretLine"),
Value: &resolve.String{
Path: []string{"secretLine"},
},
},
{
Name: []byte("fullAddress"),
Value: &resolve.String{
Path: []string{"fullAddress"},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
}
}

RunWithPermutations(
t,
definition,
operation,
operationName,
expectedPlan(),
plan.Configuration{
Debug: plan.DebugConfiguration{},
DataSources: []plan.DataSource{
usersDatasourceConfiguration,
accountsDatasourceConfiguration,
addressesDatasourceConfiguration,
addressesEnricherDatasourceConfiguration,
},
DisableResolveFieldPositions: true,
Fields: plan.FieldConfigurations{
{
TypeName: "Address",
FieldName: "line3",
Arguments: plan.ArgumentsConfigurations{
{
Name: "test",
SourceType: plan.FieldArgumentSource,
},
},
},
},
},
WithDefaultPostProcessor(),
)
})

t.Run("requires fields from the root query subgraph", func(t *testing.T) {
definition := `
type User {
Expand Down
63 changes: 39 additions & 24 deletions v2/pkg/engine/plan/datasource_filter_collect_nodes_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ func (c *nodesCollector) initVisitors() {
keys: make([]DSKeyInfo, 0, 2),
localSeenKeys: make(map[SeenKeyPath]struct{}),
localSuggestionLookup: make(map[int]struct{}),
providesEntries: make(map[string]struct{}),
globalSeenKeys: c.seenKeys,
dataSource: dataSource,
notExternalKeyPaths: make(map[string]struct{}),
}
visitor.dataSource = dataSource
visitor.notExternalKeyPaths = make(map[string]struct{})
c.dsVisitors = append(c.dsVisitors, visitor)
c.dsVisitorsReports = append(c.dsVisitorsReports, operationreport.NewReport())
}
Expand Down Expand Up @@ -252,22 +253,37 @@ type collectNodesDSVisitor struct {
definition *ast.Document
dataSource DataSource

// local suggestions stores suggestions for the current run of collecting fields
// they are reset after each run, because each time we collect suggestion only for new field refs
localSuggestions []*NodeSuggestion
localSuggestionLookup map[int]struct{}

providesEntries []*NodeSuggestion
// local provides entries, they should survive reset
// because unique fields refs are collected only once
providesEntries map[string]struct{}

// global node suggestion, we append to them after each run
nodes *NodeSuggestions

// notExternalKeyPaths - stores paths of fields used in keys, which marked external
// but semantically are not true external
notExternalKeyPaths map[string]struct{}
info map[int]fieldInfo

// reference to a global cache of field info shared between all collector instances
info map[int]fieldInfo

// information about keys available for a given path collected during the current run
keys []DSKeyInfo

// globalSeenKeys - stores key information which is shared globally between collectors, needed to avoid collecting keys on the same path
// between different runs, as evaluating keys are very expensive
// as it is shared between goroutines of collector we read it during the run, and write after the run finished
globalSeenKeys map[SeenKeyPath]struct{}
localSeenKeys map[SeenKeyPath]struct{}
// seen keys local to the current run
localSeenKeys map[SeenKeyPath]struct{}
}

// reset - cleanups only data which should not be persisted between runs
func (f *collectNodesDSVisitor) reset() {
f.localSuggestions = f.localSuggestions[:0]
f.keys = f.keys[:0]
Expand Down Expand Up @@ -346,31 +362,26 @@ func (f *collectNodesDSVisitor) handleProvidesSuggestions(fieldRef int, typeName
}
fieldTypeName := f.definition.FieldDefinitionTypeNameString(fieldDefRef)

providesFieldSet, report := providesFragment(fieldTypeName, providesSelectionSet, f.definition)
if report.HasErrors() {
return fmt.Errorf("failed to parse provides fields for %s.%s at path %s: %v", typeName, fieldName, currentPath, report)
}

selectionSetRef, ok := f.operation.FieldSelectionSet(fieldRef)
_, ok = f.operation.FieldSelectionSet(fieldRef)
if !ok {
return fmt.Errorf("failed to get selection set ref for %s.%s at path %s. Field with provides directive should have a selections", typeName, fieldName, currentPath)
}

input := &providesInput{
providesFieldSet: providesFieldSet,
operation: f.operation,
definition: f.definition,
operationSelectionSet: selectionSetRef,
report: report,
parentPath: currentPath,
dataSource: f.dataSource,
}
providesSuggestions := providesSuggestions(input)
parentTypeName: fieldTypeName,
providesSelectionSet: providesSelectionSet,
definition: f.definition,
parentPath: currentPath,
}
providesSuggestions, report := providesSuggestions(input)
if report.HasErrors() {
return fmt.Errorf("failed to get provides suggestions for %s.%s at path %s: %v", typeName, fieldName, currentPath, report)
}

f.providesEntries = append(f.providesEntries, providesSuggestions...)
for providedKey := range providesSuggestions {
f.providesEntries[providedKey] = struct{}{}
}

return nil
}

Expand Down Expand Up @@ -449,9 +460,7 @@ func (f *collectNodesDSVisitor) EnterField(fieldRef int, itemIds []int, treeNode
return nil
}

isProvided := slices.ContainsFunc(f.providesEntries, func(suggestion *NodeSuggestion) bool {
return suggestion.TypeName == info.typeName && suggestion.FieldName == info.fieldName && suggestion.Path == info.currentPath
})
_, isProvided := f.providesEntries[providedFieldKey(info.typeName, info.fieldName, info.currentPath)]

if info.isTypeName && f.isInterfaceObject(info.typeName) {
// we should not add a typename on the interface object
Expand Down Expand Up @@ -522,6 +531,7 @@ func (f *collectNodesDSVisitor) EnterField(fieldRef int, itemIds []int, treeNode
}

func (f *collectNodesDSVisitor) applySuggestions() {
// copy local suggestions to the global nodes suggestions
for _, suggestion := range f.localSuggestions {
f.nodes.addSuggestion(suggestion)
itemId := len(f.nodes.items) - 1
Expand All @@ -531,6 +541,11 @@ func (f *collectNodesDSVisitor) applySuggestions() {
itemIds = append(itemIds, itemId)
treeNode.SetData(itemIds)
}

// apply provides entries
for entry := range f.providesEntries {
f.nodes.addProvidedField(entry, f.dataSource.Hash())
}
}

func TreeNodeID(fieldRef int) uint {
Expand Down
Loading