Skip to content

Commit

Permalink
feat: add args and enums to schema usage info (#664)
Browse files Browse the repository at this point in the history
  • Loading branch information
jensneuse authored Nov 10, 2023
1 parent dc412cb commit df37764
Show file tree
Hide file tree
Showing 5 changed files with 530 additions and 190 deletions.
4 changes: 4 additions & 0 deletions v2/pkg/astjson/astjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion v2/pkg/engine/plan/configuration_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 7 additions & 5 deletions v2/pkg/engine/plan/planner_closer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
)

func TestCloser(t *testing.T) {

definition := `schema {query:Query} type Query { me: String! }`
operation := `{me}`

Expand Down Expand Up @@ -78,7 +77,8 @@ func (s *StatefulSource) Start(ctx context.Context) {
}

type FakeFactory struct {
signalClosed chan struct{}
signalClosed chan struct{}
upstreamSchema *ast.Document
}

func (f *FakeFactory) Planner(ctx context.Context) DataSourcePlanner {
Expand All @@ -87,16 +87,18 @@ func (f *FakeFactory) Planner(ctx context.Context) DataSourcePlanner {
}
go source.Start(ctx)
return &FakePlanner{
source: source,
source: source,
upstreamSchema: f.upstreamSchema,
}
}

type FakePlanner struct {
source *StatefulSource
source *StatefulSource
upstreamSchema *ast.Document
}

func (f *FakePlanner) UpstreamSchema(dataSourceConfig DataSourceConfiguration) *ast.Document {
return nil
return f.upstreamSchema
}

func (f *FakePlanner) EnterDocument(operation, definition *ast.Document) {
Expand Down
226 changes: 217 additions & 9 deletions v2/pkg/engine/plan/schemausageinfo.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,94 @@
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 ast.OperationType
TypeFields []TypeFieldUsageInfo
OperationType ast.OperationType
TypeFields []TypeFieldUsageInfo
Arguments []ArgumentUsageInfo
InputTypeFields []TypeFieldUsageInfo
}

type TypeFieldUsageInfo struct {
FieldName string
NamedType string
TypeNames []string
Path []string
Source TypeFieldSource
Count int
FieldName string
NamedType string
TypeNames []string
Path []string
Source TypeFieldSource
EnumValues []string
}

func (t *TypeFieldUsageInfo) Equals(other TypeFieldUsageInfo) bool {
if t.FieldName != other.FieldName {
return false
}
if t.NamedType != other.NamedType {
return false
}
if len(t.TypeNames) != len(other.TypeNames) {
return false
}
for i := range t.TypeNames {
if t.TypeNames[i] != other.TypeNames[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
}
}
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
NamedType string
ArgumentName string
ArgumentTypeName string
}

type TypeFieldSource struct {
// IDs is a list of data source IDs that can be used to resolve the field
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:
Expand All @@ -37,7 +102,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 {
Expand Down Expand Up @@ -67,3 +149,129 @@ 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 TypeFieldUsageInfo) {
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 := TypeFieldUsageInfo{
FieldName: fieldName,
NamedType: 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:
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.TypeNames = []string{parentTypeName}
} else {
usageInfo.FieldName = ""
}
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),
NamedType: 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) {

}
Loading

0 comments on commit df37764

Please sign in to comment.