From 1576055873e79362309c36456a630328d8de5fd9 Mon Sep 17 00:00:00 2001 From: Matthieu Vachon Date: Thu, 12 Nov 2020 23:20:15 -0500 Subject: [PATCH] Added possibility to explore the actual field selection tree Each resolver's context will now be able to access through the graph of elements that were selected as part of the execution of this resolve via the standard go context's value. The returned field is an interface representing a subset of the functionality of the internal/exec/selected package, enough for the developer to know what was queried by the user. I'm opening the PR to start a discussion about offering this feature to end user of the library. We have started using it in our own fork of the library. ##### Design This introduces new API interfaces the would become part of the standard library. The interface is currently a simple wrapper around elements of `internal/exec/selected/Selection` interface needed to "walk" the selected field. Hopefully, I got the structure right. I've currently used `Identifier()` instead of `Name()` and `Aliased()` instead of `Alias` to not renamed the field's name since there would be conflict with struct fields in the `internal/exec/selected/Selection` package. If we agree, the internal struct could change so the interface has the best name. A future addition that I forsee could be: - A "path" retrieval syntax "a la" gjson to quickly extract some nodes of the selection, for example with the star wars element ".search[]" that would return all type assertion nodes below the `search` field. Exact semantic to be discussed. - Maybe a visitor pattern to ease the walking of the selection tree. --- context.go | 26 +++++++++ example/starwars/starwars.go | 14 ++++- internal/context/context.go | 32 ++++++++++++ internal/exec/exec.go | 3 +- internal/exec/selected/selected.go | 84 ++++++++++++++++++++++++++++++ internal/exec/subscribe.go | 3 +- selected/interface.go | 78 +++++++++++++++++++++++++++ 7 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 context.go create mode 100644 internal/context/context.go create mode 100644 selected/interface.go diff --git a/context.go b/context.go new file mode 100644 index 00000000..5bba944b --- /dev/null +++ b/context.go @@ -0,0 +1,26 @@ +package graphql + +import ( + "context" + + gcontext "github.com/graph-gophers/graphql-go/internal/context" + "github.com/graph-gophers/graphql-go/selected" +) + +type Context struct { + Field selected.Field +} + +// GraphQLContext is used to retrieved the graphql from the context. If no graphql +// is present in the context, the `fallbackGraphql` received in parameter +// is returned instead. +func GraphQLContext(ctx context.Context) *Context { + field, found := gcontext.GraphQL(ctx) + if !found { + return nil + } + + return &Context{ + Field: field.ToSelection().(selected.Field), + } +} diff --git a/example/starwars/starwars.go b/example/starwars/starwars.go index 07cbb9f4..237830f1 100644 --- a/example/starwars/starwars.go +++ b/example/starwars/starwars.go @@ -4,12 +4,14 @@ package starwars import ( + "context" "encoding/base64" "fmt" "strconv" "strings" graphql "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/selected" ) var Schema = ` @@ -93,6 +95,8 @@ var Schema = ` appearsIn: [Episode!]! # This droid's primary function primaryFunction: String + + } # A connection object for a character's friends type FriendsConnection { @@ -301,7 +305,10 @@ func (r *Resolver) Reviews(args struct{ Episode string }) []*reviewResolver { return l } -func (r *Resolver) Search(args struct{ Text string }) []*searchResultResolver { +func (r *Resolver) Search(ctx context.Context, args struct{ Text string }) []*searchResultResolver { + graphqlContext := graphql.GraphQLContext(ctx) + selected.Dump(graphqlContext.Field) + var l []*searchResultResolver for _, h := range humans { if strings.Contains(h.Name, args.Text) { @@ -338,7 +345,10 @@ func (r *Resolver) Human(args struct{ ID graphql.ID }) *humanResolver { return nil } -func (r *Resolver) Droid(args struct{ ID graphql.ID }) *droidResolver { +func (r *Resolver) Droid(ctx context.Context, args struct{ ID graphql.ID }) *droidResolver { + graphqlContext := graphql.GraphQLContext(ctx) + selected.Dump(graphqlContext.Field) + if d := droidData[args.ID]; d != nil { return &droidResolver{d} } diff --git a/internal/context/context.go b/internal/context/context.go new file mode 100644 index 00000000..45f73083 --- /dev/null +++ b/internal/context/context.go @@ -0,0 +1,32 @@ +package context + +import ( + "context" + + "github.com/graph-gophers/graphql-go/internal/exec/selected" +) + +type graphqlKeyType int + +const graphqlFieldKey graphqlKeyType = iota + +// WithGraphQLContext is used to create a new context with a graphql added to it +// so it can be later retrieved using `Graphql`. +func WithGraphQLContext(ctx context.Context, field *selected.SchemaField) context.Context { + return context.WithValue(ctx, graphqlFieldKey, field) +} + +// GraphQL is used to retrieved the graphql from the context. If no graphql +// is present in the context, the `fallbackGraphql` received in parameter +// is returned instead. +func GraphQL(ctx context.Context) (field *selected.SchemaField, found bool) { + if ctx == nil { + return + } + + if v, ok := ctx.Value(graphqlFieldKey).(*selected.SchemaField); ok { + return v, true + } + + return +} diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 1e409bb8..a43d6c72 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -11,6 +11,7 @@ import ( "github.com/graph-gophers/graphql-go/errors" "github.com/graph-gophers/graphql-go/internal/common" + gcontext "github.com/graph-gophers/graphql-go/internal/context" "github.com/graph-gophers/graphql-go/internal/exec/resolvable" "github.com/graph-gophers/graphql-go/internal/exec/selected" "github.com/graph-gophers/graphql-go/internal/query" @@ -197,7 +198,7 @@ func execFieldSelection(ctx context.Context, r *Request, s *resolvable.Schema, f if f.field.UseMethodResolver() { var in []reflect.Value if f.field.HasContext { - in = append(in, reflect.ValueOf(traceCtx)) + in = append(in, reflect.ValueOf(gcontext.WithGraphQLContext(traceCtx, f.field))) } if f.field.ArgsPacker != nil { in = append(in, f.field.PackedArgs) diff --git a/internal/exec/selected/selected.go b/internal/exec/selected/selected.go index 3075521e..e1a672b7 100644 --- a/internal/exec/selected/selected.go +++ b/internal/exec/selected/selected.go @@ -12,6 +12,7 @@ import ( "github.com/graph-gophers/graphql-go/internal/query" "github.com/graph-gophers/graphql-go/internal/schema" "github.com/graph-gophers/graphql-go/introspection" + "github.com/graph-gophers/graphql-go/selected" ) type Request struct { @@ -44,6 +45,19 @@ func ApplyOperation(r *Request, s *resolvable.Schema, op *query.Operation) []Sel type Selection interface { isSelection() + ToSelection() selected.Selection +} + +func toSelections(sels []Selection) (out []selected.Selection) { + if len(sels) == 0 { + return + } + + out = make([]selected.Selection, len(sels)) + for i, sel := range sels { + out[i] = sel.ToSelection() + } + return } type SchemaField struct { @@ -56,16 +70,86 @@ type SchemaField struct { FixedResult reflect.Value } +func (f *SchemaField) Kind() selected.Kind { + return selected.FieldKind +} + +func (f *SchemaField) Identifier() string { + return f.Name +} + +func (f *SchemaField) Aliased() string { + return f.Alias +} + +func (f *SchemaField) Children() (out []selected.Selection) { + return toSelections(f.Sels) +} + +func (f *SchemaField) ToSelection() selected.Selection { + return selected.Selection(f) +} + type TypeAssertion struct { resolvable.TypeAssertion Sels []Selection } +func (f *TypeAssertion) Kind() selected.Kind { + return selected.TypeAssertionKind +} + +func (f *TypeAssertion) Type() string { + var toType func(resolvable.Resolvable) string + toType = func(r resolvable.Resolvable) string { + if f.TypeExec == nil { + return "" + } + + switch v := f.TypeExec.(type) { + case *resolvable.Scalar: + return "scalar" + case *resolvable.List: + return toType(v.Elem) + case *resolvable.Object: + return v.Name + default: + return "" + } + } + + return toType(f.TypeExec) +} + +func (f *TypeAssertion) Children() (out []selected.Selection) { + return toSelections(f.Sels) +} + +func (f *TypeAssertion) ToSelection() selected.Selection { + return selected.Selection(f) +} + type TypenameField struct { resolvable.Object Alias string } +func (f *TypenameField) Kind() selected.Kind { + return selected.TypenameFieldKind +} + +func (f *TypenameField) Aliased() string { + return f.Alias +} + +func (f *TypenameField) Type() string { + return f.Name +} + +func (f *TypenameField) ToSelection() selected.Selection { + return selected.Selection(f) +} + func (*SchemaField) isSelection() {} func (*TypeAssertion) isSelection() {} func (*TypenameField) isSelection() {} diff --git a/internal/exec/subscribe.go b/internal/exec/subscribe.go index a42a8634..df2d459b 100644 --- a/internal/exec/subscribe.go +++ b/internal/exec/subscribe.go @@ -10,6 +10,7 @@ import ( "github.com/graph-gophers/graphql-go/errors" "github.com/graph-gophers/graphql-go/internal/common" + gcontext "github.com/graph-gophers/graphql-go/internal/context" "github.com/graph-gophers/graphql-go/internal/exec/resolvable" "github.com/graph-gophers/graphql-go/internal/exec/selected" "github.com/graph-gophers/graphql-go/internal/query" @@ -40,7 +41,7 @@ func (r *Request) Subscribe(ctx context.Context, s *resolvable.Schema, op *query var in []reflect.Value if f.field.HasContext { - in = append(in, reflect.ValueOf(ctx)) + in = append(in, reflect.ValueOf(gcontext.WithGraphQLContext(ctx, f.field))) } if f.field.ArgsPacker != nil { in = append(in, f.field.PackedArgs) diff --git a/selected/interface.go b/selected/interface.go new file mode 100644 index 00000000..a6b0d66e --- /dev/null +++ b/selected/interface.go @@ -0,0 +1,78 @@ +package selected + +import ( + "fmt" +) + +type Kind int + +func (k Kind) String() string { + switch k { + case FieldKind: + return "field" + case TypeAssertionKind: + return "type_assertion" + case TypenameFieldKind: + return "typename_field" + default: + panic(fmt.Errorf("invalid kind %d received", k)) + } +} + +const ( + FieldKind Kind = iota + TypeAssertionKind + TypenameFieldKind +) + +type Selection interface { + Kind() Kind +} + +type Field interface { + Selection + Identifier() string + Aliased() string + Children() []Selection +} + +type TypeAssertion interface { + Selection + Type() string + Children() []Selection +} + +type TypenameField interface { + Selection + Type() string + Aliased() string +} + +func Dump(selection Selection) { + if selection == nil { + fmt.Println("Selection ") + return + } + + var print func(string, Selection) + print = func(indent string, sel Selection) { + switch v := sel.(type) { + case Field: + fmt.Printf(indent+"Field %s (%s)\n", v.Identifier(), v.Aliased()) + for _, subSel := range v.Children() { + print(indent+" ", subSel) + } + case TypeAssertion: + fmt.Printf(indent+"TypeAssertion %s\n", v.Type()) + for _, subSel := range v.Children() { + print(indent+" ", subSel) + } + case TypenameField: + fmt.Printf(indent+"TypenameField %s (%s)\n", v.Type(), v.Aliased()) + default: + panic(fmt.Errorf("invalid selection %T received", v)) + } + } + + print("", selection) +}