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) +}