diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go index 316e2fcdad..f70b37ac21 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go @@ -269,14 +269,6 @@ func (p *Planner[T]) DownstreamResponseFieldAlias(downstreamFieldRef int) (alias return "", false } -func (p *Planner[T]) DataSourcePlanningBehavior() plan.DataSourcePlanningBehavior { - return plan.DataSourcePlanningBehavior{ - MergeAliasedRootNodes: true, - OverrideFieldPathFromAlias: true, - IncludeTypeNameFields: true, - } -} - func (p *Planner[T]) Register(visitor *plan.Visitor, configuration plan.DataSourceConfiguration[T], dataSourcePlannerConfiguration plan.DataSourcePlannerConfiguration) error { p.visitor = visitor @@ -1825,6 +1817,16 @@ func (f *Factory[T]) UpstreamSchema(dataSourceConfig plan.DataSourceConfiguratio return schema, true } +func (f *Factory[T]) PlanningBehavior() plan.DataSourcePlanningBehavior { + b := plan.DataSourcePlanningBehavior{ + MergeAliasedRootNodes: true, + OverrideFieldPathFromAlias: true, + AllowPlanningTypeName: true, + AlwaysFlattenFragments: f.grpcClient != nil || f.grpcClientProvider != nil, + } + return b +} + type Source struct { httpClient *http.Client } diff --git a/v2/pkg/engine/datasource/introspection_datasource/factory.go b/v2/pkg/engine/datasource/introspection_datasource/factory.go index a251521fdb..9d49a066ab 100644 --- a/v2/pkg/engine/datasource/introspection_datasource/factory.go +++ b/v2/pkg/engine/datasource/introspection_datasource/factory.go @@ -26,6 +26,14 @@ func (f *Factory[T]) Context() context.Context { return context.TODO() } -func (f *Factory[T]) UpstreamSchema(dataSourceConfig plan.DataSourceConfiguration[T]) (*ast.Document, bool) { +func (f *Factory[T]) UpstreamSchema(_ plan.DataSourceConfiguration[T]) (*ast.Document, bool) { return nil, false } + +func (f *Factory[T]) PlanningBehavior() plan.DataSourcePlanningBehavior { + return plan.DataSourcePlanningBehavior{ + MergeAliasedRootNodes: false, + OverrideFieldPathFromAlias: true, + AllowPlanningTypeName: true, + } +} diff --git a/v2/pkg/engine/datasource/introspection_datasource/planner.go b/v2/pkg/engine/datasource/introspection_datasource/planner.go index 2b05800cb7..481dc3c458 100644 --- a/v2/pkg/engine/datasource/introspection_datasource/planner.go +++ b/v2/pkg/engine/datasource/introspection_datasource/planner.go @@ -45,14 +45,6 @@ func (p *Planner[T]) DownstreamResponseFieldAlias(_ int) (alias string, exists b return } -func (p *Planner[T]) DataSourcePlanningBehavior() plan.DataSourcePlanningBehavior { - return plan.DataSourcePlanningBehavior{ - MergeAliasedRootNodes: false, - OverrideFieldPathFromAlias: true, - IncludeTypeNameFields: true, - } -} - func (p *Planner[T]) EnterField(ref int) { fieldName := p.v.Operation.FieldNameString(ref) fieldAliasOrName := p.v.Operation.FieldAliasOrNameString(ref) diff --git a/v2/pkg/engine/datasource/pubsub_datasource/pubsub_datasource.go b/v2/pkg/engine/datasource/pubsub_datasource/pubsub_datasource.go index 60faa27883..bdf12304dd 100644 --- a/v2/pkg/engine/datasource/pubsub_datasource/pubsub_datasource.go +++ b/v2/pkg/engine/datasource/pubsub_datasource/pubsub_datasource.go @@ -275,14 +275,6 @@ func (p *Planner[T]) ConfigureSubscription() plan.SubscriptionConfiguration { return plan.SubscriptionConfiguration{} } -func (p *Planner[T]) DataSourcePlanningBehavior() plan.DataSourcePlanningBehavior { - return plan.DataSourcePlanningBehavior{ - MergeAliasedRootNodes: false, - OverrideFieldPathFromAlias: false, - IncludeTypeNameFields: true, - } -} - func (p *Planner[T]) DownstreamResponseFieldAlias(_ int) (alias string, exists bool) { return "", false } @@ -312,10 +304,18 @@ func (f *Factory[T]) Context() context.Context { return f.executionContext } -func (f *Factory[T]) UpstreamSchema(dataSourceConfig plan.DataSourceConfiguration[T]) (*ast.Document, bool) { +func (f *Factory[T]) UpstreamSchema(_ plan.DataSourceConfiguration[T]) (*ast.Document, bool) { return nil, false } +func (f *Factory[T]) PlanningBehavior() plan.DataSourcePlanningBehavior { + return plan.DataSourcePlanningBehavior{ + MergeAliasedRootNodes: false, + OverrideFieldPathFromAlias: false, + AllowPlanningTypeName: true, + } +} + func buildEventDataBytes(ref int, visitor *plan.Visitor, variables *resolve.Variables) ([]byte, error) { // Collect the field arguments for fetch based operations fieldArgs := visitor.Operation.FieldArguments(ref) diff --git a/v2/pkg/engine/datasource/staticdatasource/static_datasource.go b/v2/pkg/engine/datasource/staticdatasource/static_datasource.go index 3449afb86f..e9074635cc 100644 --- a/v2/pkg/engine/datasource/staticdatasource/static_datasource.go +++ b/v2/pkg/engine/datasource/staticdatasource/static_datasource.go @@ -12,13 +12,9 @@ import ( "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) -type Configuration struct { - Data string `json:"data"` -} - type Factory[T Configuration] struct{} -func (f *Factory[T]) Planner(logger abstractlogger.Logger) plan.DataSourcePlanner[T] { +func (f *Factory[T]) Planner(_ abstractlogger.Logger) plan.DataSourcePlanner[T] { return &Planner[T]{} } @@ -26,10 +22,18 @@ func (f *Factory[T]) Context() context.Context { return context.TODO() } -func (f *Factory[T]) UpstreamSchema(dataSourceConfig plan.DataSourceConfiguration[T]) (*ast.Document, bool) { +func (f *Factory[T]) UpstreamSchema(_ plan.DataSourceConfiguration[T]) (*ast.Document, bool) { return nil, false } +func (f *Factory[T]) PlanningBehavior() plan.DataSourcePlanningBehavior { + return plan.DataSourcePlanningBehavior{} +} + +type Configuration struct { + Data string `json:"data"` +} + type Planner[T Configuration] struct { id int config Configuration @@ -47,14 +51,6 @@ func (p *Planner[T]) DownstreamResponseFieldAlias(downstreamFieldRef int) (alias // skip, not required return } - -func (p *Planner[T]) DataSourcePlanningBehavior() plan.DataSourcePlanningBehavior { - return plan.DataSourcePlanningBehavior{ - MergeAliasedRootNodes: false, - OverrideFieldPathFromAlias: false, - } -} - func (p *Planner[T]) Register(_ *plan.Visitor, configuration plan.DataSourceConfiguration[T], _ plan.DataSourcePlannerConfiguration) error { p.config = Configuration(configuration.CustomConfiguration()) return nil diff --git a/v2/pkg/engine/plan/abstract_selection_rewriter.go b/v2/pkg/engine/plan/abstract_selection_rewriter.go index 284ad421d5..1332a6ef55 100644 --- a/v2/pkg/engine/plan/abstract_selection_rewriter.go +++ b/v2/pkg/engine/plan/abstract_selection_rewriter.go @@ -13,9 +13,11 @@ import ( ) var ( - FieldDoesntHaveSelectionSetErr = errors.New("unexpected error: field does not have a selection set") - InlineFragmentDoesntHaveSelectionSetErr = errors.New("unexpected error: inline fragment does not have a selection set") - InlineFragmentTypeIsNotExistsErr = errors.New("unexpected error: inline fragment type condition does not exists") + ErrFieldHasNoSelectionSet = errors.New("unexpected error: field does not have a selection set") + ErrInlineFragmentHasNoSelectionSet = errors.New("unexpected error: inline fragment does not have a selection set") + ErrInlineFragmentHasNoCondition = errors.New("unexpected error: inline fragment type condition does not exist") + + ErrNoUpstreamSchema = errors.New("unexpected error: upstream schema is not defined in DataSource") ) /* @@ -65,6 +67,7 @@ type fieldSelectionRewriter struct { dsConfiguration DataSource skipFieldRefs []int + alwaysRewrite bool } type RewriteResult struct { @@ -74,19 +77,18 @@ type RewriteResult struct { var resultNotRewritten = RewriteResult{} -func newFieldSelectionRewriter(operation *ast.Document, definition *ast.Document) *fieldSelectionRewriter { - return &fieldSelectionRewriter{ - operation: operation, - definition: definition, +func newFieldSelectionRewriter(operation *ast.Document, definition *ast.Document, dsConfiguration DataSource) (*fieldSelectionRewriter, error) { + upstreamDefinition, ok := dsConfiguration.UpstreamSchema() + if !ok { + return nil, ErrNoUpstreamSchema } -} - -func (r *fieldSelectionRewriter) SetUpstreamDefinition(upstreamDefinition *ast.Document) { - r.upstreamDefinition = upstreamDefinition -} - -func (r *fieldSelectionRewriter) SetDatasourceConfiguration(dsConfiguration DataSource) { - r.dsConfiguration = dsConfiguration + return &fieldSelectionRewriter{ + operation: operation, + definition: definition, + upstreamDefinition: upstreamDefinition, + dsConfiguration: dsConfiguration, + alwaysRewrite: dsConfiguration.PlanningBehavior().AlwaysFlattenFragments, + }, nil } func (r *fieldSelectionRewriter) RewriteFieldSelection(fieldRef int, enclosingNode ast.Node) (res RewriteResult, err error) { @@ -168,7 +170,17 @@ func (r *fieldSelectionRewriter) processUnionSelection(fieldRef int, unionDefRef }, nil } +func (r *fieldSelectionRewriter) mustRewrite(s selectionSetInfo) bool { + return r.alwaysRewrite && + (s.hasInlineFragmentsOnInterfaces || + s.hasInlineFragmentsOnUnions || + s.hasInlineFragmentsOnObjects) +} + func (r *fieldSelectionRewriter) unionFieldSelectionNeedsRewrite(selectionSetInfo selectionSetInfo, unionTypeNames, entityNames []string) (needRewrite bool) { + if r.mustRewrite(selectionSetInfo) { + return true + } if selectionSetInfo.hasInlineFragmentsOnObjects { // when we have types not exists in the current datasource - we need to rewrite if r.objectFragmentsRequiresCleanup(selectionSetInfo.inlineFragmentsOnObjects, unionTypeNames) { @@ -263,7 +275,7 @@ func (r *fieldSelectionRewriter) replaceFieldSelections(fieldRef int, newSelecti func (r *fieldSelectionRewriter) processObjectSelection(fieldRef int, objectDefRef int) (res RewriteResult, err error) { selectionSetRef, ok := r.operation.FieldSelectionSet(fieldRef) if !ok { - return resultNotRewritten, FieldDoesntHaveSelectionSetErr + return resultNotRewritten, ErrFieldHasNoSelectionSet } fieldTypeName := r.definition.ObjectTypeDefinitionNameBytes(objectDefRef) @@ -346,6 +358,9 @@ func (r *fieldSelectionRewriter) rewriteObjectSelection(fieldRef int, fieldInfo } func (r *fieldSelectionRewriter) objectFieldSelectionNeedsRewrite(selectionSetInfo selectionSetInfo, objectTypeName string) (needRewrite bool) { + if r.mustRewrite(selectionSetInfo) { + return true + } if selectionSetInfo.hasInlineFragmentsOnObjects { if r.objectFragmentsRequiresCleanup(selectionSetInfo.inlineFragmentsOnObjects, []string{objectTypeName}) { return true @@ -412,6 +427,10 @@ func (r *fieldSelectionRewriter) processInterfaceSelection(fieldRef int, interfa } func (r *fieldSelectionRewriter) interfaceFieldSelectionNeedsRewrite(selectionSetInfo selectionSetInfo, interfaceTypeNames []string, entityNames []string) (needRewrite bool) { + if r.mustRewrite(selectionSetInfo) { + return true + } + // when we do not have fragments if !selectionSetInfo.hasInlineFragmentsOnInterfaces && !selectionSetInfo.hasInlineFragmentsOnUnions && @@ -436,9 +455,10 @@ func (r *fieldSelectionRewriter) interfaceFieldSelectionNeedsRewrite(selectionSe return true } - // in case it is an interface object, and we have fragments on concrete types - we have to add shared __typename selection - // it will mean that we will rewrite a query to separate concrete type fragments, but due to nature of the interface object - // they eventually will be flattened by datasource into a single fragment or just a flatten query. + // In case it is an interface object, and we have fragments on concrete types - we have to add shared __typename selection. + // It will mean that we will rewrite a query to separate concrete type fragments, + // but due to the nature of the interface object, they eventually will be flattened by datasource + // into a single fragment or just a flattened query. // So it should be safe to rewrite a field. if selectionSetInfo.isInterfaceObject { return !selectionSetInfo.hasTypeNameSelection @@ -618,7 +638,7 @@ func (r *fieldSelectionRewriter) collectChangedRefs(fieldRef int, fieldRefsPaths for fieldRef, path := range fieldRefsPaths { newRefs, ok := pathsToRefs[path] if !ok { - // TODO: some path actually could dissapear due to rewrite + // TODO: some paths could actually disappear due to rewrite continue } diff --git a/v2/pkg/engine/plan/abstract_selection_rewriter_info.go b/v2/pkg/engine/plan/abstract_selection_rewriter_info.go index 950ccb06a1..4a2389535d 100644 --- a/v2/pkg/engine/plan/abstract_selection_rewriter_info.go +++ b/v2/pkg/engine/plan/abstract_selection_rewriter_info.go @@ -83,7 +83,7 @@ func (r *fieldSelectionRewriter) selectionSetFieldSelections(selectionSetRef int func (r *fieldSelectionRewriter) collectFieldInformation(fieldRef int) (selectionSetInfo, error) { fieldSelectionSetRef, ok := r.operation.FieldSelectionSet(fieldRef) if !ok { - return selectionSetInfo{}, FieldDoesntHaveSelectionSetErr + return selectionSetInfo{}, ErrFieldHasNoSelectionSet } return r.collectSelectionSetInformation(fieldSelectionSetRef) @@ -101,7 +101,7 @@ func (r *fieldSelectionRewriter) collectInlineFragmentInformation( typeCondition := r.operation.InlineFragmentTypeConditionNameString(inlineFragmentRef) inlineFragmentSelectionSetRef, ok := r.operation.InlineFragmentSelectionSet(inlineFragmentRef) if !ok { - return InlineFragmentDoesntHaveSelectionSetErr + return ErrInlineFragmentHasNoSelectionSet } hasDirectives := r.operation.InlineFragmentHasDirectives(inlineFragmentRef) @@ -110,7 +110,7 @@ func (r *fieldSelectionRewriter) collectInlineFragmentInformation( // because it could be absent in the current SUBGRAPH document definitionNode, hasNode := r.definition.NodeByNameStr(typeCondition) if !hasNode { - return InlineFragmentTypeIsNotExistsErr + return ErrInlineFragmentHasNoCondition } selectionSetInfo, err := r.collectSelectionSetInformation(inlineFragmentSelectionSetRef) @@ -140,7 +140,7 @@ func (r *fieldSelectionRewriter) collectInlineFragmentInformation( typeNamesImplementingInterface: typeNamesImplementingInterface, } - // NOTE: We are getting type names implementing interface from the current SUBGRAPH definion + // NOTE: We are getting type names implementing interface from the current SUBGRAPH definition // NOTE: at this point we ignore case when upstreamNode is not exists in the upstream schema upstreamNode, hasUpstreamNode := r.upstreamDefinition.NodeByNameStr(typeCondition) if hasUpstreamNode { @@ -163,7 +163,7 @@ func (r *fieldSelectionRewriter) collectInlineFragmentInformation( unionMemberTypeNames: unionMemberTypeNames, } - // NOTE: We are getting type names of union members from the current SUBGRAPH definion + // NOTE: We are getting type names of union members from the current SUBGRAPH definition // NOTE: at this point we ignore case when upstreamNode is not exists in the upstream schema upstreamNode, hasUpstreamNode := r.upstreamDefinition.NodeByNameStr(typeCondition) if hasUpstreamNode { diff --git a/v2/pkg/engine/plan/abstract_selection_rewriter_test.go b/v2/pkg/engine/plan/abstract_selection_rewriter_test.go index 11b5fc87b6..30ab42dbd6 100644 --- a/v2/pkg/engine/plan/abstract_selection_rewriter_test.go +++ b/v2/pkg/engine/plan/abstract_selection_rewriter_test.go @@ -15,8 +15,8 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type testCase struct { name string definition string - upstreamDefinition string - dsConfiguration DataSource + upstreamDefinition string // will be used by dsBuilder for dataSourceConfiguration.factory + dsBuilder *dsBuilder operation string expectedOperation string enclosingTypeName string // default is "Query" @@ -30,8 +30,6 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { op := unsafeparser.ParseGraphqlDocumentString(testCase.operation) def := unsafeparser.ParseGraphqlDocumentStringWithBaseSchema(testCase.definition) - upstreamDef := unsafeparser.ParseGraphqlDocumentStringWithBaseSchema(testCase.upstreamDefinition) - if testCase.fieldName == "" { testCase.fieldName = "iface" } @@ -46,12 +44,16 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { break } } + require.NotEqual(t, ast.InvalidRef, fieldRef) + + ds := testCase.dsBuilder. + SchemaMergedWithBase(testCase.upstreamDefinition). + DS() node, _ := def.Index.FirstNodeByNameStr(testCase.enclosingTypeName) - rewriter := newFieldSelectionRewriter(&op, &def) - rewriter.SetUpstreamDefinition(&upstreamDef) - rewriter.SetDatasourceConfiguration(testCase.dsConfiguration) + rewriter, err := newFieldSelectionRewriter(&op, &def, ds) + require.NoError(t, err) result, err := rewriter.RewriteFieldSelection(fieldRef, node) require.NoError(t, err) @@ -161,25 +163,197 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { id: ID! }` - dsConfigurationA := dsb(). - RootNode("Query", "u1"). - RootNode("A", "id"). - RootNode("B", "id"). - ChildNode("Inter1", "id"). - ChildNode("Inter2", "id"). - KeysMetadata(FederationFieldConfigurations{ - { - TypeName: "A", - SelectionSet: "id", - }, - { - TypeName: "B", - SelectionSet: "id", - }, - }). - DS() + // dsBuilderA is a function to make sure that each test starts from a clean state. + dsBuilderA := func() *dsBuilder { + return dsb(). + RootNode("Query", "u1"). + RootNode("A", "id"). + RootNode("B", "id"). + ChildNode("Inter1", "id"). + ChildNode("Inter2", "id"). + KeysMetadata(FederationFieldConfigurations{ + { + TypeName: "A", + SelectionSet: "id", + }, + { + TypeName: "B", + SelectionSet: "id", + }, + }) + } + + definitionB := ` + type Query { + named: Named + union: U + } + + union U = User + + interface Named { + name: String + } + + interface Numbered { + number: Int + } + + type User implements Named & Numbered { + id: ID + name: String + number: Int + }` + + dsBuilderB := func() *dsBuilder { + return dsb(). + RootNode("Query", "named", "union"). + RootNode("User", "id", "name", "number"). + ChildNode("Named", "name"). + ChildNode("Numbered", "number"). + KeysMetadata(FederationFieldConfigurations{ + { + TypeName: "User", + SelectionSet: "id", + }, + }) + } testCases := []testCase{ + { + name: "should flatten interfaces for gRPC", + fieldName: "named", + definition: definitionB, + upstreamDefinition: definitionB, + dsBuilder: dsBuilderB(). + WithBehavior(DataSourcePlanningBehavior{ + AlwaysFlattenFragments: true, + }), + operation: ` + query { + named { + ... on Numbered { + ... on User { + name + } + } + ... on User { + id + } + } + }`, + expectedOperation: ` + query { + named { + ... on User { + id + name + } + } + }`, + shouldRewrite: true, + }, + { + name: "should flatten union for gRPC", + fieldName: "union", + definition: definitionB, + upstreamDefinition: definitionB, + dsBuilder: dsBuilderB(). + WithBehavior(DataSourcePlanningBehavior{ + AlwaysFlattenFragments: true, + }), + operation: ` + query { + union { + ... on Numbered { + ... on User { + name + } + } + ... on User { + id + } + } + }`, + expectedOperation: ` + query { + union { + ... on User { + id + name + } + } + }`, + shouldRewrite: true, + }, + { + name: "should not flatten interfaces for non-gRPC", + fieldName: "named", + definition: definitionB, + upstreamDefinition: definitionB, + dsBuilder: dsBuilderB(), + operation: ` + query { + named { + ... on Numbered { + ... on User { + name + } + } + ... on User { + id + } + } + }`, + expectedOperation: ` + query { + named { + ... on Numbered { + ... on User { + name + } + } + ... on User { + id + } + } + }`, + shouldRewrite: false, + }, + { + name: "should not flatten union for non-gRPC", + fieldName: "union", + definition: definitionB, + upstreamDefinition: definitionB, + dsBuilder: dsBuilderB(), + operation: ` + query { + union { + ... on Numbered { + ... on User { + name + } + } + ... on User { + id + } + } + }`, + expectedOperation: ` + query { + union { + ... on Numbered { + ... on User { + name + } + } + ... on User { + id + } + } + }`, + shouldRewrite: false, + }, { name: "one field is external. query without fragments", definition: definition, @@ -204,7 +378,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { iface: Node! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -217,8 +391,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -287,7 +460,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { iface: Node! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -300,8 +473,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -371,7 +543,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { iface: Node! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -384,8 +556,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -459,7 +630,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { iface: Node! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -472,8 +643,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -547,7 +717,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { iface: Node! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -565,8 +735,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Moderator", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -621,7 +790,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -634,8 +803,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -664,7 +832,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "no shared fields. query has user fragment", definition: definition, upstreamDefinition: definition, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -677,8 +845,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -702,7 +869,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "only __typename as a shared field. query has user fragment", definition: definition, upstreamDefinition: definition, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -715,8 +882,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -761,7 +927,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { iface: Node! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "isUser"). RootNode("Admin", "id"). @@ -774,8 +940,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -827,7 +992,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { iface: Node! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "isUser"). RootNode("Admin", "id"). @@ -840,8 +1005,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -894,7 +1058,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -907,8 +1071,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { accounts { @@ -955,7 +1118,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -968,8 +1131,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { accounts { @@ -1020,7 +1182,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { iface: Node! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "isUser"). RootNode("Admin", "id"). @@ -1033,8 +1195,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -1067,7 +1228,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "one field is external. query has admin and user fragment, all fragments has shared field", definition: definition, upstreamDefinition: definition, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "isUser"). RootNode("Admin", "id"). @@ -1080,8 +1241,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -1116,7 +1276,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "all fields local. query without fragments", definition: definition, upstreamDefinition: definition, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name"). RootNode("Admin", "id", "name"). @@ -1129,8 +1289,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -1150,7 +1309,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "all fields local but one of the fields has requires directive. query without fragments", definition: definition, upstreamDefinition: definition, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name"). RootNode("Admin", "id", "name"). @@ -1172,8 +1331,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { SelectionSet: "any", }, } - }). - DS(), + }), operation: ` query { iface { @@ -1204,7 +1362,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "all fields local. query has user fragment", definition: definition, upstreamDefinition: definition, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id", "name"). @@ -1217,8 +1375,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -1244,11 +1401,10 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "all fields local. query with user fragment. types are not entities", definition: definition, upstreamDefinition: definition, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). ChildNode("User", "id", "name", "isUser"). - ChildNode("Admin", "id", "name"). - DS(), + ChildNode("Admin", "id", "name"), operation: ` query { iface { @@ -1306,7 +1462,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { iface: Node! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id", "name"). @@ -1319,8 +1475,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -1357,7 +1512,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { iface: User! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). ChildNode("User", "id", "name", "isUser"). KeysMetadata(FederationFieldConfigurations{ @@ -1365,8 +1520,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "User", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -1441,7 +1595,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { container: ContainerInterface! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "container"). ChildNode("Container", "iface"). ChildNode("ContainerInterface", "iface"). @@ -1456,8 +1610,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { container { @@ -1536,7 +1689,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { container: ContainerInterface! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "container"). ChildNode("Container", "node"). ChildNode("ContainerInterface", "node"). @@ -1551,8 +1704,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { container { @@ -1609,7 +1761,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface", "accounts"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id", "name"). @@ -1628,8 +1780,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Moderator", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "accounts", operation: ` query { @@ -1683,7 +1834,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface", "accounts"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id", "name"). @@ -1711,8 +1862,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { SelectionSet: "importantNote", }, } - }). - DS(), + }), fieldName: "accounts", operation: ` query { @@ -1774,7 +1924,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface", "accounts"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -1793,8 +1943,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Moderator", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "accounts", operation: ` query { @@ -1851,7 +2000,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface", "accounts"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -1865,8 +2014,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "accounts", operation: ` query { @@ -1920,7 +2068,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface", "accounts"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -1934,8 +2082,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "accounts", operation: ` query { @@ -1993,7 +2140,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface", "accounts"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -2007,8 +2154,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "accounts", operation: ` query { @@ -2072,7 +2218,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface", "accounts"). RootNode("User", "id", "name", "isUser"). RootNode("Moderator", "id", "name", "isModerator"). @@ -2087,8 +2233,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "accounts", operation: ` query { @@ -2145,7 +2290,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { accounts: [Account!]! } `, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface", "accounts"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -2159,8 +2304,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "accounts", operation: ` query { @@ -2218,7 +2362,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { iface: HasName! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -2232,8 +2376,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -2311,7 +2454,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { returnsUnion: Account! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -2325,8 +2468,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { returnsUnion { @@ -2398,7 +2540,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { returnsUnion: Account! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "returnsUnion"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -2411,8 +2553,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { returnsUnion { @@ -2480,7 +2621,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { returnsUnion: Account! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "returnsUnion"). RootNode("User", "id", "name", "isUser"). RootNode("Admin", "id"). @@ -2494,8 +2635,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { returnsUnion { @@ -2567,7 +2707,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { iface: HasName! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "title"). RootNode("Admin", "id", "name", "title"). @@ -2582,8 +2722,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -2613,7 +2752,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { shouldRewrite: false, }, { - name: "everything is local, nested interface selections with typename, first interface is differs from field return type", + name: "everything is local, nested interface selections with typename, first interface differs from field return type", definition: ` interface HasName { name: String! @@ -2662,7 +2801,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { iface: HasName! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name", "title"). RootNode("Admin", "id", "name", "title"). @@ -2677,8 +2816,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -2736,7 +2874,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { returnsUser: User! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "returnsUser"). RootNode("User", "id", "name"). KeysMetadata(FederationFieldConfigurations{ @@ -2744,8 +2882,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "User", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { returnsUser { @@ -2801,7 +2938,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { iface: User! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name"). ChildNode("Account", "id"). @@ -2810,8 +2947,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "User", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -2871,7 +3007,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type QueryType { iface: User! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "iface"). RootNode("User", "id", "name"). ChildNode("Account", "id"). @@ -2880,8 +3016,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "User", SelectionSet: "id", }, - }). - DS(), + }), operation: ` query { iface { @@ -2907,7 +3042,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is union - union fragment wrapped into concrete type fragment with different from wrapping type fragments", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "u1", operation: ` query { @@ -2940,7 +3075,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is union - union fragment wrapped into concrete type fragment with matching to wrapping type fragment", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "u1", operation: ` query { @@ -2970,7 +3105,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is union - select not existing in the current subgraph type", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "u1", operation: ` query { @@ -2999,7 +3134,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is interface - select not existing in the current subgraph type", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "i1", operation: ` query { @@ -3028,7 +3163,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is an interface - interface fragment inside concrete type fragment with matching type", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "i1", operation: ` query { @@ -3056,7 +3191,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is an interface - interface fragment inside concrete type fragment with not matching type", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "i1", operation: ` query { @@ -3082,7 +3217,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is an interface - interface fragment inside concrete type fragment select shared field", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "i1", operation: ` query { @@ -3108,7 +3243,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is an interface - multiple level of nesting interface and union fragments with concrete types on different levels", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "i1", operation: ` query { @@ -3146,7 +3281,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is a interface - union fragment is not exists in the current subgraph", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "i1", operation: ` query { @@ -3178,7 +3313,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is a interface - interface fragment is not exists in the current subgraph", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "i1", operation: ` query { @@ -3210,7 +3345,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is a union - union fragment is not exists in the current subgraph", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "u1", operation: ` query { @@ -3245,7 +3380,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { name: "field is a union - interface fragment is not exists in the current subgraph", definition: definitionA, upstreamDefinition: upstreamDefinitionA, - dsConfiguration: dsConfigurationA, + dsBuilder: dsBuilderA(), fieldName: "u1", operation: ` query { @@ -3309,7 +3444,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { user: User! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "user"). RootNode("User", "id"). ChildNode("Account", "id"). @@ -3322,8 +3457,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "user", operation: ` query { @@ -3388,7 +3522,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { user: User! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "user"). RootNode("User", "id"). ChildNode("Account", "id"). @@ -3401,8 +3535,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "user", operation: ` query { @@ -3489,7 +3622,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { user: User! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "user"). RootNode("User", "id"). ChildNode("Account", "id"). @@ -3502,8 +3635,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { TypeName: "Admin", SelectionSet: "id", }, - }). - DS(), + }), fieldName: "user", operation: ` query { @@ -3598,12 +3730,11 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { user: User! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Query", "user"). ChildNode("User", "id", "name", "surname"). ChildNode("Admin", "id", "name", "login"). - ChildNode("Account", "id"). - DS(), + ChildNode("Account", "id"), fieldName: "user", operation: ` query { @@ -3695,7 +3826,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { id: ID! title: String! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Account", "id", "title"). WithMetadata(func(m *FederationMetaData) { m.InterfaceObjects = []EntityInterfaceConfiguration{ @@ -3704,8 +3835,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { ConcreteTypeNames: []string{"Admin", "User"}, }, } - }). - DS(), + }), fieldName: "name", operation: ` query { @@ -3774,7 +3904,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { type Query { user: Named! }`, - dsConfiguration: dsb(). + dsBuilder: dsb(). RootNode("Account", "id", "title"). RootNode("User", "id", "name", "surname"). WithMetadata(func(m *FederationMetaData) { @@ -3784,8 +3914,7 @@ func TestInterfaceSelectionRewriter_RewriteOperation(t *testing.T) { ConcreteTypeNames: []string{"Admin", "User"}, }, } - }). - DS(), + }), fieldName: "user", operation: ` query { diff --git a/v2/pkg/engine/plan/datasource_configuration.go b/v2/pkg/engine/plan/datasource_configuration.go index 07ec63e760..1a6b7fcebc 100644 --- a/v2/pkg/engine/plan/datasource_configuration.go +++ b/v2/pkg/engine/plan/datasource_configuration.go @@ -14,33 +14,41 @@ import ( type DSHash uint64 -// PlannerFactory is the factory for the creation of the concrete DataSourcePlanner +// PlannerFactory creates concrete DataSourcePlanner's. // For stateful datasources, the factory should contain execution context -// Once the context gets cancelled, all stateful DataSources must close their connections and cleanup themselves. +// Once the context gets canceled, all stateful DataSources must close their connections and cleanup themselves. type PlannerFactory[DataSourceSpecificConfiguration any] interface { + // Planner creates a new DataSourcePlanner Planner(logger abstractlogger.Logger) DataSourcePlanner[DataSourceSpecificConfiguration] + // Context returns the execution context of the factory // For stateful datasources, the factory should contain cancellable global execution context // This method serves as a flag that factory should have a context Context() context.Context + UpstreamSchema(dataSourceConfig DataSourceConfiguration[DataSourceSpecificConfiguration]) (*ast.Document, bool) + PlanningBehavior() DataSourcePlanningBehavior } type DataSourceMetadata struct { - // FederationMetaData - describes the behavior of the DataSource in the context of the Federation + // FederationMetaData has federation-specific configuration for entity interfaces and + // the @key, @requires, @provides directives. FederationMetaData - // RootNodes - defines the nodes where the responsibility of the DataSource begins - // RootNode is a node from which you could start a query or a subquery - // Note: for federation root nodes are root query type fields, entity type fields, and entity object fields + // RootNodes defines the nodes where the responsibility of the DataSource begins. + // RootNode is a node from which we could start a query or a subquery. + // For a federation, RootNodes contain root query type fields, entity type fields, + // and entity object fields. RootNodes TypeFields - // ChildNodes - describes additional fields which will be requested along with fields which has a datasources - // They are always required for the Graphql datasources cause each field could have its own datasource - // For any flat datasource like HTTP/REST or GRPC we could not request fewer fields, as we always get a full response - // Note: for federation child nodes are non-entity type fields and interface type fields - // Note: Unions are not present in the child or root nodes + + // ChildNodes describes additional fields, which are requested along with fields that the datasource has. + // They're always required for Graphql datasources because each field could have its own datasource. + // For a flat datasource (HTTP/REST or GRPC) we cannot request fewer fields, as we always get a full response. + // For a federation, ChildNodes contain non-entity type fields and interface type fields. + // Unions shouldn't be present in the child or root nodes. ChildNodes TypeFields + Directives *DirectiveConfigurations rootNodesIndex map[string]fieldsIndex @@ -245,15 +253,14 @@ type DataSourceConfiguration[T any] interface { CustomConfiguration() T } -type DataSourceUpstreamSchema interface { - UpstreamSchema() (*ast.Document, bool) -} - type DataSource interface { FederationInfo NodesInfo DirectivesConfigurations - DataSourceUpstreamSchema + + UpstreamSchema() (*ast.Document, bool) + PlanningBehavior() DataSourcePlanningBehavior + Id() string Name() string Hash() DSHash @@ -287,6 +294,10 @@ func (d *dataSourceConfiguration[T]) UpstreamSchema() (*ast.Document, bool) { return d.factory.UpstreamSchema(d) } +func (d *dataSourceConfiguration[T]) PlanningBehavior() DataSourcePlanningBehavior { + return d.factory.PlanningBehavior() +} + func (d *dataSourceConfiguration[T]) Id() string { return d.id } @@ -363,18 +374,21 @@ func (d *DirectiveConfigurations) RenameTypeNameOnMatchBytes(directiveName []byt return directiveName } +// DataSourcePlanningBehavior contains DataSource-specific planning flags. type DataSourcePlanningBehavior struct { - // MergeAliasedRootNodes will reuse a data source for multiple root fields with aliases if true. + // MergeAliasedRootNodes set to true will reuse a data source for multiple root fields with aliases. // Example: // { // rootField // alias: rootField // } - // On dynamic data sources (e.g. GraphQL, SQL, ...) this should return true and for - // static data sources (e.g. REST, static, GRPC...) it should be false. + // On dynamic data sources (GraphQL, SQL) this should be set to true, + // and for static data sources (REST, static, gRPC) it should be false. MergeAliasedRootNodes bool - // OverrideFieldPathFromAlias will let the planner know if the response path should also be aliased (= true) - // or not (= false) + + // OverrideFieldPathFromAlias set to true will let the planner know + // if the response path should also be aliased. + // // Example: // { // rootField @@ -383,8 +397,13 @@ type DataSourcePlanningBehavior struct { // When true expected response will be { "rootField": ..., "alias": ... } // When false expected response will be { "rootField": ..., "original": ... } OverrideFieldPathFromAlias bool - // IncludeTypeNameFields should be set to true if the planner allows to plan __typename fields - IncludeTypeNameFields bool + + // AllowPlanningTypeName set to true will allow the planner to plan __typename fields. + AllowPlanningTypeName bool + + // If true then planner will rewrite the operation + // to flatten inline fragments to only the concrete types. + AlwaysFlattenFragments bool } type DataSourceFetchPlanner interface { @@ -393,13 +412,13 @@ type DataSourceFetchPlanner interface { } type DataSourceBehavior interface { - DataSourcePlanningBehavior() DataSourcePlanningBehavior - // DownstreamResponseFieldAlias allows the DataSourcePlanner to overwrite the response path with an alias - // It's required to set OverrideFieldPathFromAlias to true - // This function is useful in the following scenario - // 1. The downstream Query doesn't contain an alias - // 2. The path configuration rewrites the field to an existing field - // 3. The DataSourcePlanner is using an alias to the upstream + // DownstreamResponseFieldAlias allows the DataSourcePlanner to overwrite the response path with an alias. + // It requires DataSourcePlanningBehavior.OverrideFieldPathFromAlias to be set to true. + // This function is useful in the following scenarios: + // 1. The downstream Query doesn't contain an alias, + // 2. The path configuration rewrites the field to an existing field, + // 3. The DataSourcePlanner using an alias to the upstream. + // // Example: // // type Query { diff --git a/v2/pkg/engine/plan/datasource_filter_visitor_test.go b/v2/pkg/engine/plan/datasource_filter_visitor_test.go index acf02a971e..bbb3f451e4 100644 --- a/v2/pkg/engine/plan/datasource_filter_visitor_test.go +++ b/v2/pkg/engine/plan/datasource_filter_visitor_test.go @@ -8,7 +8,6 @@ import ( "github.com/kylelemons/godebug/pretty" "github.com/stretchr/testify/assert" - "github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation" "github.com/wundergraph/graphql-go-tools/v2/pkg/internal/unsafeparser" "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" @@ -21,7 +20,9 @@ type dsBuilder struct { } func dsb() *dsBuilder { - return &dsBuilder{ds: &dataSourceConfiguration[any]{DataSourceMetadata: &DataSourceMetadata{}}} + return &dsBuilder{ + ds: &dataSourceConfiguration[any]{DataSourceMetadata: &DataSourceMetadata{}}, + } } func (b *dsBuilder) RootNode(typeName string, fieldNames ...string) *dsBuilder { @@ -54,13 +55,29 @@ func (b *dsBuilder) AddChildNodeExternalFieldNames(typeName string, fieldNames . return b } +func (b *dsBuilder) WithBehavior(behavior DataSourcePlanningBehavior) *dsBuilder { + if b.ds.factory != nil { + panic("WithBehavior should be used before the Schema method") + } + b.behavior = &behavior + return b +} + func (b *dsBuilder) Schema(schema string) *dsBuilder { def := unsafeparser.ParseGraphqlDocumentString(schema) b.ds.factory = &FakeFactory[any]{ upstreamSchema: &def, behavior: b.behavior, } + return b +} +func (b *dsBuilder) SchemaMergedWithBase(schema string) *dsBuilder { + def := unsafeparser.ParseGraphqlDocumentStringWithBaseSchema(schema) + b.ds.factory = &FakeFactory[any]{ + upstreamSchema: &def, + behavior: b.behavior, + } return b } @@ -83,7 +100,6 @@ func (b *dsBuilder) Id(id string) *dsBuilder { b.ds.id = id return b } - func (b *dsBuilder) DS() DataSource { if err := b.ds.DataSourceMetadata.Init(); err != nil { panic(err) @@ -91,11 +107,6 @@ func (b *dsBuilder) DS() DataSource { return b.ds } -func (b *dsBuilder) WithBehavior(behavior DataSourcePlanningBehavior) *dsBuilder { - b.behavior = &behavior - return b -} - func strptr(s string) *string { return &s } func newNodeSuggestions(nodes []NodeSuggestion) *NodeSuggestions { diff --git a/v2/pkg/engine/plan/node_selection_visitor.go b/v2/pkg/engine/plan/node_selection_visitor.go index 95c4f01aea..41ff0d5c89 100644 --- a/v2/pkg/engine/plan/node_selection_visitor.go +++ b/v2/pkg/engine/plan/node_selection_visitor.go @@ -9,12 +9,12 @@ import ( "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" ) -// nodeSelectionVisitor - walks through the operation multiple times to rewrite operation +// nodeSelectionVisitor walks through the operation multiple times to rewrite it // to be able to resolve fields from different datasources. -// During walks, it is adding required fields and rewrites abstract selection if it is necessary. -// We are revisiting query when we have: -// - new required fields were added to operation -// - when we have rewritten abstract field selection set +// This visitor might add required fields and rewrite abstract selection if necessary. +// This visitor will walk the operation again if it has: +// - added new required fields to the operation, +// - rewritten an abstract field selection set. type nodeSelectionVisitor struct { debug DebugConfiguration @@ -22,10 +22,10 @@ type nodeSelectionVisitor struct { operation, definition *ast.Document // graphql operation and schema documents walker *astvisitor.Walker - dataSources []DataSource // data sources configurations, which used by the current operation + dataSources []DataSource // data sources configurations, used by the current operation nodeSuggestions *NodeSuggestions // nodeSuggestions holds information about suggested data sources for each field - selectionSetRefs []int // selectionSetRefs is a stack of selection set refs - used to add a required fields + selectionSetRefs []int // selectionSetRefs is a stack of selection set refs - used to add required fields skipFieldsRefs []int // skipFieldsRefs holds required field refs added by planner and should not be added to user response pendingKeyRequirements map[int]pendingKeyRequirements // pendingKeyRequirements is a map[selectionSetRef][]keyRequirements @@ -644,14 +644,16 @@ func (c *nodeSelectionVisitor) rewriteSelectionSetHavingAbstractFragments(fieldR } c.visitedFieldsAbstractChecks[fieldRef] = struct{}{} - upstreamSchema, ok := ds.UpstreamSchema() + _, ok := ds.UpstreamSchema() if !ok { return } - rewriter := newFieldSelectionRewriter(c.operation, c.definition) - rewriter.SetUpstreamDefinition(upstreamSchema) - rewriter.SetDatasourceConfiguration(ds) + rewriter, err := newFieldSelectionRewriter(c.operation, c.definition, ds) + if err != nil { + c.walker.StopWithInternalErr(err) + return + } result, err := rewriter.RewriteFieldSelection(fieldRef, c.walker.EnclosingTypeDefinition) if err != nil { diff --git a/v2/pkg/engine/plan/path_builder_visitor.go b/v2/pkg/engine/plan/path_builder_visitor.go index bd4d6b2aec..ce97bad9d9 100644 --- a/v2/pkg/engine/plan/path_builder_visitor.go +++ b/v2/pkg/engine/plan/path_builder_visitor.go @@ -719,8 +719,8 @@ func (c *pathBuilderVisitor) isAllFieldDependenciesOnSameDataSource(fieldRef int func (c *pathBuilderVisitor) planWithExistingPlanners(fieldRef int, typeName, fieldName, currentPath, parentPath, precedingParentPath string, suggestion *NodeSuggestion) (plannerIdx int, planned bool) { for plannerIdx, plannerConfig := range c.planners { - planningBehaviour := plannerConfig.DataSourcePlanningBehavior() dsConfiguration := plannerConfig.DataSourceConfiguration() + planningBehaviour := dsConfiguration.PlanningBehavior() currentPlannerDSHash := dsConfiguration.Hash() hasSuggestion := suggestion != nil @@ -1237,17 +1237,17 @@ func (c *pathBuilderVisitor) LeaveField(ref int) { c.removeArrayField(ref) } -// addPlannerPathForTypename adds a path for the __typename field -// adding __typename should be done only in case particular planner has parent path -// otherwise it will be added to all planners and will cause visiting of incorrect selection sets +// addPlannerPathForTypename adds a path for the __typename field. func (c *pathBuilderVisitor) addPlannerPathForTypename( plannerIndex int, currentPath string, parentPath string, fieldRef int, fieldName string, typeName string, planningBehaviour DataSourcePlanningBehavior, ) (pathAdded bool) { + // Adding __typename should happen only if particular planner has parent path, + // otherwise it will be added to all planners and will cause visiting of incorrect selection sets. if fieldName != typeNameField { return false } - if !planningBehaviour.IncludeTypeNameFields { + if !planningBehaviour.AllowPlanningTypeName { return false } diff --git a/v2/pkg/engine/plan/planner_configuration.go b/v2/pkg/engine/plan/planner_configuration.go index 69e23e8bbb..7bc5614d66 100644 --- a/v2/pkg/engine/plan/planner_configuration.go +++ b/v2/pkg/engine/plan/planner_configuration.go @@ -62,10 +62,6 @@ func (p *plannerConfiguration[T]) ObjectFetchConfiguration() *objectFetchConfigu return p.objectFetchConfiguration } -func (p *plannerConfiguration[T]) DataSourcePlanningBehavior() DataSourcePlanningBehavior { - return p.planner.DataSourcePlanningBehavior() -} - func (p *plannerConfiguration[T]) DownstreamResponseFieldAlias(downstreamFieldRef int) (alias string, exists bool) { return p.planner.DownstreamResponseFieldAlias(downstreamFieldRef) } diff --git a/v2/pkg/engine/plan/planner_test.go b/v2/pkg/engine/plan/planner_test.go index 0a498adff2..5710d6912f 100644 --- a/v2/pkg/engine/plan/planner_test.go +++ b/v2/pkg/engine/plan/planner_test.go @@ -996,11 +996,18 @@ type FakeFactory[T any] struct { behavior *DataSourcePlanningBehavior } -func (f *FakeFactory[T]) UpstreamSchema(dataSourceConfig DataSourceConfiguration[T]) (*ast.Document, bool) { +func (f *FakeFactory[T]) UpstreamSchema(_ DataSourceConfiguration[T]) (*ast.Document, bool) { return f.upstreamSchema, true } -func (f *FakeFactory[T]) Planner(logger abstractlogger.Logger) DataSourcePlanner[T] { +func (f *FakeFactory[T]) PlanningBehavior() DataSourcePlanningBehavior { + if f.behavior == nil { + f.behavior = &DataSourcePlanningBehavior{} + } + return *f.behavior +} + +func (f *FakeFactory[T]) Planner(_ abstractlogger.Logger) DataSourcePlanner[T] { source := &StatefulSource{} go source.Start() return &FakePlanner[T]{ diff --git a/v2/pkg/engine/plan/visitor.go b/v2/pkg/engine/plan/visitor.go index 848c5cc4a1..560cb97f0a 100644 --- a/v2/pkg/engine/plan/visitor.go +++ b/v2/pkg/engine/plan/visitor.go @@ -1021,7 +1021,8 @@ func (v *Visitor) resolveFieldPath(ref int) []string { aliasOverride := false if plannerConfig != nil && plannerConfig.Planner() != nil { - aliasOverride = plannerConfig.DataSourcePlanningBehavior().OverrideFieldPathFromAlias + behavior := plannerConfig.DataSourceConfiguration().PlanningBehavior() + aliasOverride = behavior.OverrideFieldPathFromAlias } for i := range v.Config.Fields {