diff --git a/v2/pkg/astjson/astjson.go b/v2/pkg/astjson/astjson.go index a999ac0c4..6ef1f6666 100644 --- a/v2/pkg/astjson/astjson.go +++ b/v2/pkg/astjson/astjson.go @@ -161,6 +161,10 @@ func (j *JSON) ObjectFieldKey(objectFieldRef int) []byte { return j.storage[j.Nodes[objectFieldRef].keyStart:j.Nodes[objectFieldRef].keyEnd] } +func (j *JSON) ObjectFieldValue(objectFieldRef int) int { + return j.Nodes[objectFieldRef].ObjectFieldValue +} + type Node struct { Kind NodeKind ObjectFieldValue int diff --git a/v2/pkg/astprinter/astprinter.go b/v2/pkg/astprinter/astprinter.go index 5a161268a..dc6618589 100644 --- a/v2/pkg/astprinter/astprinter.go +++ b/v2/pkg/astprinter/astprinter.go @@ -1024,7 +1024,9 @@ func (p *printVisitor) LeaveSchemaExtension(ref int) { if p.indent != nil { p.write(literal.LINETERMINATOR) } - p.write(literal.RBRACE) + if len(p.document.SchemaExtensions[ref].SchemaDefinition.RootOperationTypeDefinitions.Refs) > 0 { + p.write(literal.RBRACE) + } if !p.document.NodeIsLastRootNode(ast.Node{Kind: ast.NodeKindSchemaExtension, Ref: ref}) { if p.indent != nil { p.write(literal.LINETERMINATOR) diff --git a/v2/pkg/astprinter/astprinter_test.go b/v2/pkg/astprinter/astprinter_test.go index 0502f328a..10415f56d 100644 --- a/v2/pkg/astprinter/astprinter_test.go +++ b/v2/pkg/astprinter/astprinter_test.go @@ -209,6 +209,11 @@ vary: [String]! = []) on QUERY directive @include(if: Boolean!) repeatable on FI subscription: Subscription }`, `extend schema @foo {query: Query mutation: Mutation subscription: Subscription}`) }) + + t.Run("schema extension only directives", func(t *testing.T) { + run(t, `extend schema @foo `, `extend schema @foo `) + }) + t.Run("object type definition", func(t *testing.T) { run(t, ` type Foo { diff --git a/v2/pkg/engine/plan/configuration_visitor.go b/v2/pkg/engine/plan/configuration_visitor.go index 9e99866ad..6c28afd20 100644 --- a/v2/pkg/engine/plan/configuration_visitor.go +++ b/v2/pkg/engine/plan/configuration_visitor.go @@ -671,7 +671,7 @@ func (c *configurationVisitor) handleMissingPath(typeName string, fieldName stri } } - c.walker.StopWithInternalErr(errors.Wrap(fmt.Errorf("could not plan a field %s.%s on a path %s", typeName, fieldName, currentPath), "configurationVisitor.handleMissingPath")) + c.walker.StopWithInternalErr(errors.Wrap(fmt.Errorf("could not plan field %s.%s on path %s", typeName, fieldName, currentPath), "configurationVisitor.handleMissingPath")) } func (c *configurationVisitor) LeaveField(ref int) { diff --git a/v2/pkg/engine/plan/planner_closer_test.go b/v2/pkg/engine/plan/planner_closer_test.go deleted file mode 100644 index 7df8aee48..000000000 --- a/v2/pkg/engine/plan/planner_closer_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package plan - -import ( - "context" - "io" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/wundergraph/graphql-go-tools/v2/internal/pkg/unsafeparser" - "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" - "github.com/wundergraph/graphql-go-tools/v2/pkg/astnormalization" - "github.com/wundergraph/graphql-go-tools/v2/pkg/asttransform" - "github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation" - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" - "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" -) - -func TestCloser(t *testing.T) { - - definition := `schema {query:Query} type Query { me: String! }` - operation := `{me}` - - def := unsafeparser.ParseGraphqlDocumentString(definition) - op := unsafeparser.ParseGraphqlDocumentString(operation) - err := asttransform.MergeDefinitionWithBaseSchema(&def) - if err != nil { - t.Fatal(err) - } - norm := astnormalization.NewNormalizer(true, true) - report := &operationreport.Report{} - norm.NormalizeOperation(&op, &def, report) - valid := astvalidation.DefaultOperationValidator() - valid.Validate(&op, &def, report) - - ctx, cancel := context.WithCancel(context.Background()) - closedSignal := make(chan struct{}) - - factory := &FakeFactory{ - signalClosed: closedSignal, - } - - cfg := Configuration{ - DefaultFlushIntervalMillis: 500, - DataSources: []DataSourceConfiguration{ - { - RootNodes: []TypeField{ - { - TypeName: "Query", - FieldNames: []string{"me"}, - }, - }, - ChildNodes: nil, - Factory: factory, - Custom: nil, - }, - }, - Fields: nil, - } - - p := NewPlanner(ctx, cfg) - plan := p.Plan(&op, &def, "", report) - assert.NotNil(t, plan) - - cancel() // terminate all stateful sources - <-ctx.Done() // stateful source closed from closer - <-closedSignal - // test terminates only if stateful source closed -} - -type StatefulSource struct { - signalClosed chan struct{} -} - -func (s *StatefulSource) Start(ctx context.Context) { - <-ctx.Done() - close(s.signalClosed) -} - -type FakeFactory struct { - signalClosed chan struct{} -} - -func (f *FakeFactory) Planner(ctx context.Context) DataSourcePlanner { - source := &StatefulSource{ - signalClosed: f.signalClosed, - } - go source.Start(ctx) - return &FakePlanner{ - source: source, - } -} - -type FakePlanner struct { - source *StatefulSource -} - -func (f *FakePlanner) UpstreamSchema(dataSourceConfig DataSourceConfiguration) *ast.Document { - return nil -} - -func (f *FakePlanner) EnterDocument(operation, definition *ast.Document) { - -} - -func (f *FakePlanner) Register(visitor *Visitor, _ DataSourceConfiguration, _ DataSourcePlannerConfiguration) error { - visitor.Walker.RegisterEnterDocumentVisitor(f) - return nil -} - -func (f *FakePlanner) ConfigureFetch() resolve.FetchConfiguration { - return resolve.FetchConfiguration{ - DataSource: &FakeDataSource{ - source: f.source, - }, - } -} - -func (f *FakePlanner) ConfigureSubscription() SubscriptionConfiguration { - return SubscriptionConfiguration{} -} - -func (f *FakePlanner) DataSourcePlanningBehavior() DataSourcePlanningBehavior { - return DataSourcePlanningBehavior{ - MergeAliasedRootNodes: false, - OverrideFieldPathFromAlias: false, - } -} - -func (f *FakePlanner) DownstreamResponseFieldAlias(downstreamFieldRef int) (alias string, exists bool) { - return -} - -type FakeDataSource struct { - source *StatefulSource -} - -func (f *FakeDataSource) Load(ctx context.Context, input []byte, w io.Writer) (err error) { - return -} diff --git a/v2/pkg/engine/plan/schemausageinfo.go b/v2/pkg/engine/plan/schemausageinfo.go index 9833b7ae1..5807082ec 100644 --- a/v2/pkg/engine/plan/schemausageinfo.go +++ b/v2/pkg/engine/plan/schemausageinfo.go @@ -1,21 +1,128 @@ package plan import ( + "fmt" + + "github.com/pkg/errors" "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astjson" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" ) type SchemaUsageInfo struct { + // OperationType is the type of the operation that was executed, e.g. query, mutation, subscription OperationType ast.OperationType - TypeFields []TypeFieldUsageInfo + // TypeFields is a list of all fields that were used to define the response shape + TypeFields []TypeFieldUsageInfo + // Arguments is a list of all arguments that were used on response fields + Arguments []ArgumentUsageInfo + // InputTypeFields is a list of all fields that were used to define the input shape + InputTypeFields []InputTypeFieldUsageInfo } type TypeFieldUsageInfo struct { + // FieldName is the name of the field, e.g. "id" for this selection set: { id } + FieldName string + // FieldTypeName is the name of the field type, e.g. "ID" for this selection set: { id } + FieldTypeName string + // EnclosingTypeNames is a list of all possible enclosing types, e.g. ["User"] for the "id" field: { user { id } } + EnclosingTypeNames []string + // Path is a list of field names that lead to the field, e.g. ["user", "id"] for this selection set: { user { id } } + Path []string + // Source is a list of data source IDs that can be used to resolve the field + Source TypeFieldSource +} + +func (t *TypeFieldUsageInfo) Equals(other TypeFieldUsageInfo) bool { + if t.FieldName != other.FieldName { + return false + } + if t.FieldTypeName != other.FieldTypeName { + return false + } + if len(t.EnclosingTypeNames) != len(other.EnclosingTypeNames) { + return false + } + for i := range t.EnclosingTypeNames { + if t.EnclosingTypeNames[i] != other.EnclosingTypeNames[i] { + return false + } + } + if len(t.Path) != len(other.Path) { + return false + } + for i := range t.Path { + if t.Path[i] != other.Path[i] { + return false + } + } + if len(t.Source.IDs) != len(other.Source.IDs) { + return false + } + for i := range t.Source.IDs { + if t.Source.IDs[i] != other.Source.IDs[i] { + return false + } + } + return true +} + +type InputTypeFieldUsageInfo struct { + // IsRootVariable is true if the field is a root variable, e.g. $id + IsRootVariable bool + // Count is the number of times this field usage was captured, it's usually 1 but can be higher if the field is used multiple times + Count int + // FieldName is the name of the field, e.g. "id" for this selection set: { id } FieldName string - NamedType string - TypeNames []string - Path []string - Source TypeFieldSource + // FieldTypeName is the name of the field type, e.g. "ID" for this selection set: { id } + FieldTypeName string + // EnclosingTypeNames is a list of all possible enclosing types, e.g. ["User"] for the "id" field: { user { id } } + EnclosingTypeNames []string + // IsEnumField is true if the field is an enum + IsEnumField bool + // EnumValues is a list of all enum values that were used for this field + EnumValues []string +} + +func (t *InputTypeFieldUsageInfo) Equals(other InputTypeFieldUsageInfo) bool { + if t.IsRootVariable != other.IsRootVariable { + return false + } + if t.FieldName != other.FieldName { + return false + } + if t.FieldTypeName != other.FieldTypeName { + return false + } + if len(t.EnclosingTypeNames) != len(other.EnclosingTypeNames) { + return false + } + for i := range t.EnclosingTypeNames { + if t.EnclosingTypeNames[i] != other.EnclosingTypeNames[i] { + return false + } + } + if t.IsEnumField != other.IsEnumField { + return false + } + if len(t.EnumValues) != len(other.EnumValues) { + return false + } + for i := range t.EnumValues { + if t.EnumValues[i] != other.EnumValues[i] { + return false + } + } + return true +} + +type ArgumentUsageInfo struct { + FieldName string + EnclosingTypeName string + ArgumentName string + ArgumentTypeName string } type TypeFieldSource struct { @@ -23,7 +130,13 @@ type TypeFieldSource struct { IDs []string } -func GetSchemaUsageInfo(plan Plan) SchemaUsageInfo { +func GetSchemaUsageInfo(plan Plan, operation, definition *ast.Document, variables []byte) (*SchemaUsageInfo, error) { + js := astjson.Pool.Get() + defer astjson.Pool.Put(js) + err := js.ParseObject(variables) + if err != nil { + return nil, errors.WithStack(err) + } visitor := planVisitor{} switch p := plan.(type) { case *SynchronousResponsePlan: @@ -37,7 +150,24 @@ func GetSchemaUsageInfo(plan Plan) SchemaUsageInfo { } visitor.visitNode(p.Response.Response.Data, nil) } - return visitor.usage + walker := astvisitor.NewWalker(48) + vis := &schemaUsageInfoVisitor{ + usage: &visitor.usage, + walker: &walker, + operation: operation, + definition: definition, + variables: js, + } + walker.RegisterInputValueDefinitionVisitor(vis) + walker.RegisterArgumentVisitor(vis) + walker.RegisterFieldVisitor(vis) + walker.RegisterVariableDefinitionVisitor(vis) + report := &operationreport.Report{} + walker.Walk(operation, definition, report) + if report.HasErrors() { + return nil, errors.WithStack(fmt.Errorf("unable to generate schema usage info due to ast errors")) + } + return &visitor.usage, nil } type planVisitor struct { @@ -53,10 +183,10 @@ func (p *planVisitor) visitNode(node resolve.Node, path []string) { } newPath := append([]string{}, append(path, field.Info.Name)...) p.usage.TypeFields = append(p.usage.TypeFields, TypeFieldUsageInfo{ - FieldName: field.Info.Name, - TypeNames: field.Info.ParentTypeNames, - NamedType: field.Info.NamedType, - Path: newPath, + FieldName: field.Info.Name, + EnclosingTypeNames: field.Info.ParentTypeNames, + FieldTypeName: field.Info.NamedType, + Path: newPath, Source: TypeFieldSource{ IDs: field.Info.Source.IDs, }, @@ -67,3 +197,131 @@ func (p *planVisitor) visitNode(node resolve.Node, path []string) { p.visitNode(t.Item, path) } } + +type schemaUsageInfoVisitor struct { + usage *SchemaUsageInfo + walker *astvisitor.Walker + operation *ast.Document + definition *ast.Document + fieldEnclosingNode ast.Node + variables *astjson.JSON +} + +func (s *schemaUsageInfoVisitor) EnterVariableDefinition(ref int) { + varTypeRef := s.operation.VariableDefinitions[ref].Type + varName := s.operation.VariableValueNameString(s.operation.VariableDefinitions[ref].VariableValue.Ref) + varTypeName := s.operation.ResolveTypeNameString(varTypeRef) + jsonField := s.variables.GetObjectField(s.variables.RootNode, varName) + if jsonField == -1 { + return + } + s.traverseVariable(jsonField, varName, varTypeName, "") +} + +func (s *schemaUsageInfoVisitor) addUniqueInputFieldUsageInfoOrIncrementCount(info InputTypeFieldUsageInfo) { + for i := range s.usage.InputTypeFields { + if s.usage.InputTypeFields[i].Equals(info) { + s.usage.InputTypeFields[i].Count++ + return + } + } + info.Count = 1 + s.usage.InputTypeFields = append(s.usage.InputTypeFields, info) +} + +func (s *schemaUsageInfoVisitor) traverseVariable(jsonNodeRef int, fieldName, typeName, parentTypeName string) { + defNode, ok := s.definition.NodeByNameStr(typeName) + if !ok { + return + } + usageInfo := InputTypeFieldUsageInfo{ + FieldName: fieldName, + FieldTypeName: typeName, + } + switch defNode.Kind { + case ast.NodeKindInputObjectTypeDefinition: + for _, arrayValue := range s.variables.Nodes[jsonNodeRef].ArrayValues { + s.traverseVariable(arrayValue, fieldName, typeName, parentTypeName) + } + for _, field := range s.variables.Nodes[jsonNodeRef].ObjectFields { + key := s.variables.ObjectFieldKey(field) + value := s.variables.ObjectFieldValue(field) + fieldRef := s.definition.InputObjectTypeDefinitionInputValueDefinitionByName(defNode.Ref, key) + if fieldRef == -1 { + continue + } + fieldTypeName := s.definition.ResolveTypeNameString(s.definition.InputValueDefinitions[fieldRef].Type) + if s.definition.TypeIsList(s.definition.InputValueDefinitions[fieldRef].Type) { + for _, arrayValue := range s.variables.Nodes[value].ArrayValues { + s.traverseVariable(arrayValue, string(key), fieldTypeName, typeName) + } + } else { + s.traverseVariable(value, string(key), fieldTypeName, typeName) + } + } + case ast.NodeKindEnumTypeDefinition: + usageInfo.IsEnumField = true + switch s.variables.Nodes[jsonNodeRef].Kind { + case astjson.NodeKindString: + usageInfo.EnumValues = []string{string(s.variables.Nodes[jsonNodeRef].ValueBytes(s.variables))} + case astjson.NodeKindArray: + usageInfo.EnumValues = make([]string, len(s.variables.Nodes[jsonNodeRef].ArrayValues)) + for i, arrayValue := range s.variables.Nodes[jsonNodeRef].ArrayValues { + usageInfo.EnumValues[i] = string(s.variables.Nodes[arrayValue].ValueBytes(s.variables)) + } + } + } + if parentTypeName != "" { + usageInfo.EnclosingTypeNames = []string{parentTypeName} + } else { + usageInfo.FieldName = "" + usageInfo.IsRootVariable = true + } + s.addUniqueInputFieldUsageInfoOrIncrementCount(usageInfo) +} + +func (s *schemaUsageInfoVisitor) LeaveVariableDefinition(ref int) { + +} + +func (s *schemaUsageInfoVisitor) EnterField(ref int) { + s.fieldEnclosingNode = s.walker.EnclosingTypeDefinition +} + +func (s *schemaUsageInfoVisitor) LeaveField(ref int) { + +} + +func (s *schemaUsageInfoVisitor) EnterArgument(ref int) { + argName := s.operation.ArgumentNameBytes(ref) + anc := s.walker.Ancestors[len(s.walker.Ancestors)-1] + if anc.Kind != ast.NodeKindField { + return + } + fieldName := s.operation.FieldNameBytes(anc.Ref) + enclosingTypeName := s.definition.NodeNameBytes(s.fieldEnclosingNode) + argDef := s.definition.NodeFieldDefinitionArgumentDefinitionByName(s.fieldEnclosingNode, fieldName, argName) + if argDef == -1 { + return + } + argType := s.definition.InputValueDefinitionType(argDef) + typeName := s.definition.ResolveTypeNameBytes(argType) + s.usage.Arguments = append(s.usage.Arguments, ArgumentUsageInfo{ + FieldName: string(fieldName), + EnclosingTypeName: string(enclosingTypeName), + ArgumentName: string(argName), + ArgumentTypeName: string(typeName), + }) +} + +func (s *schemaUsageInfoVisitor) LeaveArgument(ref int) { + +} + +func (s *schemaUsageInfoVisitor) EnterInputValueDefinition(ref int) { + +} + +func (s *schemaUsageInfoVisitor) LeaveInputValueDefinition(ref int) { + +} diff --git a/v2/pkg/engine/plan/schemausageinfo_test.go b/v2/pkg/engine/plan/schemausageinfo_test.go index 4b34e1594..578ff6a9e 100644 --- a/v2/pkg/engine/plan/schemausageinfo_test.go +++ b/v2/pkg/engine/plan/schemausageinfo_test.go @@ -1,250 +1,480 @@ package plan import ( + "bytes" + "context" + "fmt" + "io" "testing" "github.com/stretchr/testify/assert" + "github.com/wundergraph/graphql-go-tools/v2/internal/pkg/unsafeparser" "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astjson" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astnormalization" + "github.com/wundergraph/graphql-go-tools/v2/pkg/asttransform" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" ) +const schemaUsageInfoTestSchema = ` + +directive @defer on FIELD + +directive @flushInterval(milliSeconds: Int!) on QUERY | SUBSCRIPTION + +directive @stream(initialBatchSize: Int) on FIELD + +union SearchResult = Human | Droid | Starship + +schema { + query: Query + mutation: Mutation + subscription: Subscription +} + +type Query { + hero: Character + droid(id: ID!): Droid + search(name: String!): SearchResult + searchResults(name: String!, filter: SearchFilter, filter2: SearchFilter, enumValue: Episode enumList: [Episode] enumList2: [Episode] filterList: [SearchFilter]): [SearchResult] +} + +input SearchFilter { + excludeName: String + enumField: Episode +} + +type Mutation { + createReview(episode: Episode!, review: ReviewInput!): Review +} + +type Subscription { + remainingJedis: Int! + newReviews: Review +} + +input ReviewInput { + stars: Int! + commentary: String +} + +type Review { + id: ID! + stars: Int! + commentary: String +} + +enum Episode { + NEWHOPE + EMPIRE + JEDI +} + +interface Character { + name: String! + friends: [Character] +} + +type Human implements Character { + name: String! + height: String! + friends: [Character] + inlineName(name: String!): String! +} + +type Droid implements Character { + name: String! + primaryFunction: String! + friends: [Character] + favoriteEpisode: Episode +} + +interface Vehicle { + length: Float! +} + +type Starship implements Vehicle { + name: String! + length: Float! +} +` + func TestGetSchemaUsageInfo(t *testing.T) { - source := resolve.TypeFieldSource{ - IDs: []string{"https://swapi.dev/api"}, + operation := ` + query Search($name: String!, $filter2: SearchFilter $enumValue: Episode $enumList: [Episode] $filterList: [SearchFilter]) { + searchResults(name: $name, filter: {excludeName: "Jannik"} filter2: $filter2, enumValue: $enumValue enumList: $enumList, enumList2: [JEDI, EMPIRE] filterList: $filterList ) { + __typename + ... on Human { + name + inlineName(name: "Jannik") + } + ... on Droid { + name + } + ... on Starship { + length + } + } + hero { + name + } + } +` + + variables := `{"name":"Jannik","filter2":{"enumField":"NEWHOPE"},"enumValue":"EMPIRE","enumList":["JEDI","EMPIRE","NEWHOPE"],"filterList":[{"excludeName":"Jannik"},{"enumField":"JEDI","excludeName":"Jannik"}]}` + + def := unsafeparser.ParseGraphqlDocumentString(schemaUsageInfoTestSchema) + op := unsafeparser.ParseGraphqlDocumentString(operation) + err := asttransform.MergeDefinitionWithBaseSchema(&def) + if err != nil { + t.Fatal(err) } - res := &resolve.GraphQLResponse{ - Info: &resolve.GraphQLResponseInfo{ - OperationType: ast.OperationTypeQuery, - }, - Data: &resolve.Object{ - Nullable: false, - Fields: []*resolve.Field{ - { - Name: []byte("searchResults"), - Info: &resolve.FieldInfo{ - Name: "searchResults", - NamedType: "SearchResults", - ParentTypeNames: []string{"Query"}, - Source: source, + report := &operationreport.Report{} + norm := astnormalization.NewNormalizer(true, true) + norm.NormalizeOperation(&op, &def, report) + valid := astvalidation.DefaultOperationValidator() + valid.Validate(&op, &def, report) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + p := NewPlanner(ctx, Configuration{ + DisableResolveFieldPositions: true, + IncludeInfo: true, + DataSources: []DataSourceConfiguration{ + { + RootNodes: []TypeField{ + { + TypeName: "Query", + FieldNames: []string{"searchResults", "hero"}, + }, + }, + ChildNodes: []TypeField{ + { + TypeName: "Human", + FieldNames: []string{"name", "inlineName"}, }, - Value: &resolve.Array{ - Path: []string{"searchResults"}, - Nullable: true, - ResolveAsynchronous: false, - Item: &resolve.Object{ - Nullable: true, - Fields: []*resolve.Field{ - { - Name: []byte("__typename"), - Value: &resolve.String{ - Path: []string{"__typename"}, - Nullable: false, - }, - Info: &resolve.FieldInfo{ - Name: "__typename", - NamedType: "String", - ParentTypeNames: []string{"Human", "Droid"}, - Source: source, - }, - }, - { - Name: []byte("name"), - Value: &resolve.String{ - Path: []string{"name"}, - Nullable: false, - }, - OnTypeNames: [][]byte{[]byte("Human"), []byte("Droid")}, - Info: &resolve.FieldInfo{ - Name: "name", - NamedType: "String", - ParentTypeNames: []string{"Human", "Droid"}, - Source: source, - }, - }, - { - Name: []byte("length"), - Value: &resolve.Float{ - Path: []string{"length"}, - Nullable: false, - }, - OnTypeNames: [][]byte{[]byte("Starship")}, - Info: &resolve.FieldInfo{ - Name: "length", - NamedType: "String", - ParentTypeNames: []string{"Starship"}, - Source: source, - }, - }, - { - Name: []byte("user"), - Info: &resolve.FieldInfo{ - Name: "user", - NamedType: "User", - ParentTypeNames: []string{"SearchResults"}, - Source: source, - }, - Value: &resolve.Object{ - Path: []string{"user"}, - Nullable: true, - Fields: []*resolve.Field{ - { - Name: []byte("account"), - Info: &resolve.FieldInfo{ - Name: "account", - NamedType: "Account", - ParentTypeNames: []string{"User"}, - Source: source, - }, - Value: &resolve.Object{ - Path: []string{"account"}, - Nullable: true, - Fields: []*resolve.Field{ - { - Name: []byte("name"), - Info: &resolve.FieldInfo{ - Name: "name", - NamedType: "String", - ParentTypeNames: []string{"Account"}, - Source: source, - }, - Value: &resolve.String{ - Path: []string{"name"}, - }, - }, - { - Name: []byte("shippingInfo"), - Info: &resolve.FieldInfo{ - Name: "shippingInfo", - NamedType: "ShippingInfo", - ParentTypeNames: []string{"Account"}, - Source: source, - }, - Value: &resolve.Object{ - Path: []string{"ShippingInfo"}, - Nullable: true, - Fields: []*resolve.Field{ - { - Name: []byte("zip"), - Info: &resolve.FieldInfo{ - Name: "zip", - NamedType: "String", - ParentTypeNames: []string{"ShippingInfo"}, - Source: source, - }, - Value: &resolve.String{ - Path: []string{"zip"}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, + { + TypeName: "Droid", + FieldNames: []string{"name"}, }, + { + TypeName: "Starship", + FieldNames: []string{"length"}, + }, + { + TypeName: "SearchResult", + FieldNames: []string{"__typename"}, + }, + { + TypeName: "Character", + FieldNames: []string{"name", "friends"}, + }, + }, + ID: "https://swapi.dev/api", + Factory: &FakeFactory{ + upstreamSchema: &def, }, + Custom: []byte(fmt.Sprintf(`{"UpstreamSchema":"%s"}`, schemaUsageInfoTestSchema)), }, }, - } - syncUsage := GetSchemaUsageInfo(&SynchronousResponsePlan{ - Response: res, }) - subscriptionUsage := GetSchemaUsageInfo(&SubscriptionResponsePlan{ + generatedPlan := p.Plan(&op, &def, "Search", report) + if report.HasErrors() { + t.Fatal(report.Error()) + } + vars := &astjson.JSON{} + err = vars.ParseObject([]byte(variables)) + assert.NoError(t, err) + extracted, err := vars.AppendObject(op.Input.Variables) + assert.NoError(t, err) + vars.MergeNodes(vars.RootNode, extracted) + mergedVariables := &bytes.Buffer{} + err = vars.PrintRoot(mergedVariables) + assert.NoError(t, err) + syncUsage, err := GetSchemaUsageInfo(generatedPlan, &op, &def, mergedVariables.Bytes()) + assert.NoError(t, err) + subscriptionUsage, err := GetSchemaUsageInfo(&SubscriptionResponsePlan{ Response: &resolve.GraphQLSubscription{ - Response: res, + Response: generatedPlan.(*SynchronousResponsePlan).Response, }, - }) - expected := SchemaUsageInfo{ + }, &op, &def, mergedVariables.Bytes()) + assert.NoError(t, err) + expected := &SchemaUsageInfo{ OperationType: ast.OperationTypeQuery, TypeFields: []TypeFieldUsageInfo{ { - FieldName: "searchResults", - TypeNames: []string{"Query"}, - Path: []string{"searchResults"}, - NamedType: "SearchResults", + FieldName: "searchResults", + EnclosingTypeNames: []string{"Query"}, + Path: []string{"searchResults"}, + FieldTypeName: "SearchResult", Source: TypeFieldSource{ IDs: []string{"https://swapi.dev/api"}, }, }, { - Path: []string{"searchResults", "__typename"}, - TypeNames: []string{"Human", "Droid"}, - FieldName: "__typename", - NamedType: "String", + Path: []string{"searchResults", "__typename"}, + EnclosingTypeNames: []string{"SearchResult"}, + FieldName: "__typename", + FieldTypeName: "String", Source: TypeFieldSource{ IDs: []string{"https://swapi.dev/api"}, }, }, { - Path: []string{"searchResults", "name"}, - TypeNames: []string{"Human", "Droid"}, - FieldName: "name", - NamedType: "String", + Path: []string{"searchResults", "name"}, + EnclosingTypeNames: []string{"Human"}, + FieldName: "name", + FieldTypeName: "String", Source: TypeFieldSource{ IDs: []string{"https://swapi.dev/api"}, }, }, { - Path: []string{"searchResults", "length"}, - TypeNames: []string{"Starship"}, - NamedType: "String", - FieldName: "length", + Path: []string{"searchResults", "inlineName"}, + EnclosingTypeNames: []string{"Human"}, + FieldName: "inlineName", + FieldTypeName: "String", Source: TypeFieldSource{ IDs: []string{"https://swapi.dev/api"}, }, }, { - Path: []string{"searchResults", "user"}, - NamedType: "User", - TypeNames: []string{"SearchResults"}, - FieldName: "user", + Path: []string{"searchResults", "name"}, + EnclosingTypeNames: []string{"Droid"}, + FieldName: "name", + FieldTypeName: "String", Source: TypeFieldSource{ IDs: []string{"https://swapi.dev/api"}, }, }, { - Path: []string{"searchResults", "user", "account"}, - TypeNames: []string{"User"}, - NamedType: "Account", - FieldName: "account", + Path: []string{"searchResults", "length"}, + EnclosingTypeNames: []string{"Starship"}, + FieldTypeName: "Float", + FieldName: "length", Source: TypeFieldSource{ IDs: []string{"https://swapi.dev/api"}, }, }, { - Path: []string{"searchResults", "user", "account", "name"}, - TypeNames: []string{"Account"}, - NamedType: "String", - FieldName: "name", + FieldName: "hero", + EnclosingTypeNames: []string{"Query"}, + Path: []string{"hero"}, + FieldTypeName: "Character", Source: TypeFieldSource{ IDs: []string{"https://swapi.dev/api"}, }, }, { - Path: []string{"searchResults", "user", "account", "shippingInfo"}, - NamedType: "ShippingInfo", - TypeNames: []string{"Account"}, - FieldName: "shippingInfo", + FieldName: "name", + EnclosingTypeNames: []string{"Character"}, + Path: []string{"hero", "name"}, + FieldTypeName: "String", Source: TypeFieldSource{ IDs: []string{"https://swapi.dev/api"}, }, }, + }, + Arguments: []ArgumentUsageInfo{ { - Path: []string{"searchResults", "user", "account", "shippingInfo", "zip"}, - TypeNames: []string{"ShippingInfo"}, - NamedType: "String", - FieldName: "zip", - Source: TypeFieldSource{ - IDs: []string{"https://swapi.dev/api"}, - }, + EnclosingTypeName: "Query", + FieldName: "searchResults", + ArgumentName: "name", + ArgumentTypeName: "String", + }, + { + EnclosingTypeName: "Query", + FieldName: "searchResults", + ArgumentName: "filter", + ArgumentTypeName: "SearchFilter", + }, + { + EnclosingTypeName: "Query", + FieldName: "searchResults", + ArgumentName: "filter2", + ArgumentTypeName: "SearchFilter", + }, + { + EnclosingTypeName: "Query", + FieldName: "searchResults", + ArgumentName: "enumValue", + ArgumentTypeName: "Episode", + }, + { + EnclosingTypeName: "Query", + FieldName: "searchResults", + ArgumentName: "enumList", + ArgumentTypeName: "Episode", + }, + { + EnclosingTypeName: "Query", + FieldName: "searchResults", + ArgumentName: "enumList2", + ArgumentTypeName: "Episode", + }, + { + EnclosingTypeName: "Query", + FieldName: "searchResults", + ArgumentName: "filterList", + ArgumentTypeName: "SearchFilter", + }, + { + EnclosingTypeName: "Human", + FieldName: "inlineName", + ArgumentName: "name", + ArgumentTypeName: "String", }, }, + InputTypeFields: []InputTypeFieldUsageInfo{ + { + Count: 2, + FieldTypeName: "String", + IsRootVariable: true, + }, + { + Count: 1, + FieldName: "enumField", + FieldTypeName: "Episode", + EnclosingTypeNames: []string{"SearchFilter"}, + EnumValues: []string{"NEWHOPE"}, + IsEnumField: true, + }, + { + Count: 5, + FieldTypeName: "SearchFilter", + IsRootVariable: true, + }, + { + Count: 1, + FieldTypeName: "Episode", + EnumValues: []string{"EMPIRE"}, + IsEnumField: true, + IsRootVariable: true, + }, + { + Count: 1, + FieldTypeName: "Episode", + EnumValues: []string{"JEDI", "EMPIRE", "NEWHOPE"}, + IsEnumField: true, + IsRootVariable: true, + }, + { + Count: 3, + FieldName: "excludeName", + FieldTypeName: "String", + EnclosingTypeNames: []string{"SearchFilter"}, + }, + { + Count: 1, + FieldName: "enumField", + FieldTypeName: "Episode", + EnclosingTypeNames: []string{"SearchFilter"}, + EnumValues: []string{"JEDI"}, + IsEnumField: true, + }, + { + Count: 1, + FieldTypeName: "Episode", + EnumValues: []string{"JEDI", "EMPIRE"}, + IsEnumField: true, + IsRootVariable: true, + }, + }, + } + assert.Equal(t, expected.OperationType, syncUsage.OperationType) + assert.Equal(t, len(expected.TypeFields), len(syncUsage.TypeFields)) + for i := range expected.TypeFields { + assert.Equal(t, expected.TypeFields[i].FieldName, syncUsage.TypeFields[i].FieldName, "Field %d", i) + assert.Equal(t, expected.TypeFields[i].EnclosingTypeNames, syncUsage.TypeFields[i].EnclosingTypeNames, "Field %d", i) + assert.Equal(t, expected.TypeFields[i].Path, syncUsage.TypeFields[i].Path, "Field %d", i) + assert.Equal(t, expected.TypeFields[i].FieldTypeName, syncUsage.TypeFields[i].FieldTypeName, "Field %d", i) + assert.Equal(t, expected.TypeFields[i].Source.IDs, syncUsage.TypeFields[i].Source.IDs, "Field %d", i) + } + assert.Equal(t, len(expected.Arguments), len(syncUsage.Arguments)) + for i := range expected.Arguments { + assert.Equal(t, expected.Arguments[i].FieldName, syncUsage.Arguments[i].FieldName, "Argument %d", i) + assert.Equal(t, expected.Arguments[i].EnclosingTypeName, syncUsage.Arguments[i].EnclosingTypeName, "Argument %d", i) + assert.Equal(t, expected.Arguments[i].ArgumentName, syncUsage.Arguments[i].ArgumentName, "Argument %d", i) + assert.Equal(t, expected.Arguments[i].ArgumentTypeName, syncUsage.Arguments[i].ArgumentTypeName, "Argument %d", i) + } + assert.Equal(t, len(expected.InputTypeFields), len(syncUsage.InputTypeFields)) + for i := range expected.InputTypeFields { + assert.Equal(t, expected.InputTypeFields[i].Count, syncUsage.InputTypeFields[i].Count, "InputTypeField %d", i) + assert.Equal(t, expected.InputTypeFields[i].FieldName, syncUsage.InputTypeFields[i].FieldName, "InputTypeField %d", i) + assert.Equal(t, expected.InputTypeFields[i].FieldTypeName, syncUsage.InputTypeFields[i].FieldTypeName, "InputTypeField %d", i) + assert.Equal(t, expected.InputTypeFields[i].EnclosingTypeNames, syncUsage.InputTypeFields[i].EnclosingTypeNames, "InputTypeField %d", i) } assert.Equal(t, expected, syncUsage) assert.Equal(t, expected, subscriptionUsage) } + +type StatefulSource struct { +} + +func (s *StatefulSource) Start(ctx context.Context) { + +} + +type FakeFactory struct { + upstreamSchema *ast.Document +} + +func (f *FakeFactory) Planner(ctx context.Context) DataSourcePlanner { + source := &StatefulSource{} + go source.Start(ctx) + return &FakePlanner{ + source: source, + upstreamSchema: f.upstreamSchema, + } +} + +type FakePlanner struct { + source *StatefulSource + upstreamSchema *ast.Document +} + +func (f *FakePlanner) UpstreamSchema(dataSourceConfig DataSourceConfiguration) *ast.Document { + return f.upstreamSchema +} + +func (f *FakePlanner) EnterDocument(operation, definition *ast.Document) { + +} + +func (f *FakePlanner) Register(visitor *Visitor, _ DataSourceConfiguration, _ DataSourcePlannerConfiguration) error { + visitor.Walker.RegisterEnterDocumentVisitor(f) + return nil +} + +func (f *FakePlanner) ConfigureFetch() resolve.FetchConfiguration { + return resolve.FetchConfiguration{ + DataSource: &FakeDataSource{ + source: f.source, + }, + } +} + +func (f *FakePlanner) ConfigureSubscription() SubscriptionConfiguration { + return SubscriptionConfiguration{} +} + +func (f *FakePlanner) DataSourcePlanningBehavior() DataSourcePlanningBehavior { + return DataSourcePlanningBehavior{ + MergeAliasedRootNodes: false, + OverrideFieldPathFromAlias: false, + } +} + +func (f *FakePlanner) DownstreamResponseFieldAlias(downstreamFieldRef int) (alias string, exists bool) { + return +} + +type FakeDataSource struct { + source *StatefulSource +} + +func (f *FakeDataSource) Load(ctx context.Context, input []byte, w io.Writer) (err error) { + return +} diff --git a/v2/pkg/federation/federationdata/local_type_field_extractor.go b/v2/pkg/federation/federationdata/local_type_field_extractor.go index 7b0b397a4..3b28b0d45 100644 --- a/v2/pkg/federation/federationdata/local_type_field_extractor.go +++ b/v2/pkg/federation/federationdata/local_type_field_extractor.go @@ -316,9 +316,6 @@ func (e *LocalTypeFieldExtractor) createChildNodes() { typeName := e.childrenToProcess[len(e.childrenToProcess)-1] e.childrenToProcess = e.childrenToProcess[:len(e.childrenToProcess)-1] nodeInfo, ok := e.nodeInfoMap[typeName] - if typeName == "Store" { - println("here") - } if !ok { continue