Skip to content

Commit

Permalink
Support exposing the selected fields in a resolver
Browse files Browse the repository at this point in the history
Copied from graph-gophers#169 and
used package 'query' instead of 'selected'.
  • Loading branch information
dvic committed Mar 27, 2018
1 parent 910c80f commit 5472fce
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ The method has up to two arguments:

- Optional `context.Context` argument.
- Mandatory `*struct { ... }` argument if the corresponding GraphQL field has arguments. The names of the struct fields have to be [exported](https://golang.org/ref/spec#Exported_identifiers) and have to match the names of the GraphQL arguments in a non-case-sensitive way.
- Optional `[]query.SelectedField` argument to receive the tree of selected subfields in the GraphQL query (useful for preloading of database relations)

The method has up to two results:

Expand Down
123 changes: 123 additions & 0 deletions graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package graphql_test

import (
"context"
"reflect"
"testing"
"time"

"github.com/qdentity/graphql-go"
"github.com/qdentity/graphql-go/example/starwars"
"github.com/qdentity/graphql-go/gqltesting"
"github.com/qdentity/graphql-go/query"
)

type helloWorldResolver1 struct{}
Expand All @@ -16,6 +18,26 @@ func (r *helloWorldResolver1) Hello() string {
return "Hello world!"
}

type selectedFieldsResolver struct {
assert func(fields []query.SelectedField)
}

func (r *selectedFieldsResolver) Do1(fields []query.SelectedField) *helloWorldResolver1 {
r.assert(fields)
return &helloWorldResolver1{}
}

func (r *selectedFieldsResolver) Do2(_ context.Context, fields []query.SelectedField) *helloWorldResolver1 {
r.assert(fields)
return &helloWorldResolver1{}
}

func (r *selectedFieldsResolver) Do3(_ context.Context, args struct{ Name string },
fields []query.SelectedField) *helloWorldResolver1 {
r.assert(fields)
return &helloWorldResolver1{}
}

type helloWorldResolver2 struct{}

func (r *helloWorldResolver2) Hello(ctx context.Context) (string, error) {
Expand Down Expand Up @@ -111,6 +133,107 @@ func TestHelloWorld(t *testing.T) {
})
}

func TestSelectedFields(t *testing.T) {
gqltesting.RunTests(t, []*gqltesting.Test{
{
Schema: graphql.MustParseSchema(`
schema {
query: Query
}
type Hello {
hello: String!
}
type Query {
do1: Hello!
}
`, &selectedFieldsResolver{
assert: func(got []query.SelectedField) {
want := []query.SelectedField{
{Name: "hello"},
}
if !reflect.DeepEqual(want, got) {
t.Errorf("want %#v, got %#v", want, got)
}
},
}),
Query: `
{
do1 { hello }
}
`,
ExpectedResult: `
{
"do1": { "hello": "Hello world!" }
}
`,
},
{
Schema: graphql.MustParseSchema(`
schema {
query: Query
}
type Hello {
hello: String!
}
type Query {
do2: Hello!
}
`, &selectedFieldsResolver{
assert: func(got []query.SelectedField) {
want := []query.SelectedField{
{Name: "hello"},
}
if !reflect.DeepEqual(want, got) {
t.Errorf("want %#v, got %#v", want, got)
}
},
}),
Query: `
{
do2 { hello }
}
`,
ExpectedResult: `
{
"do2": { "hello": "Hello world!" }
}
`,
},
{
Schema: graphql.MustParseSchema(`
schema {
query: Query
}
type Hello {
hello: String!
}
type Query {
do3(name: String!): Hello!
}
`, &selectedFieldsResolver{
assert: func(got []query.SelectedField) {
want := []query.SelectedField{
{Name: "hello"},
}
if !reflect.DeepEqual(want, got) {
t.Errorf("want %#v, got %#v", want, got)
}
},
}),
Query: `
{
do3(name: "") { hello }
}
`,
ExpectedResult: `
{
"do3": { "hello": "Hello world!" }
}
`,
},
})
}

func TestHelloSnake(t *testing.T) {
gqltesting.RunTests(t, []*gqltesting.Test{
{
Expand Down
22 changes: 22 additions & 0 deletions internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/qdentity/graphql-go/internal/query"
"github.com/qdentity/graphql-go/internal/schema"
"github.com/qdentity/graphql-go/log"
pubquery "github.com/qdentity/graphql-go/query"
"github.com/qdentity/graphql-go/trace"
)

Expand Down Expand Up @@ -142,6 +143,24 @@ func typeOf(tf *selected.TypenameField, resolver reflect.Value) string {
return ""
}

func selectionToSelectedFields(sels []selected.Selection) []pubquery.SelectedField {
n := len(sels)
if n == 0 {
return nil
}
selectedFields := make([]pubquery.SelectedField, 0, n)
for _, sel := range sels {
selField, ok := sel.(*selected.SchemaField)
if ok {
selectedFields = append(selectedFields, pubquery.SelectedField{
Name: selField.Field.Name,
Selected: selectionToSelectedFields(selField.Sels),
})
}
}
return selectedFields
}

func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *pathSegment, applyLimiter bool) {
if applyLimiter {
r.Limiter <- struct{}{}
Expand Down Expand Up @@ -180,6 +199,9 @@ func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *p
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
}
if f.field.HasSelected {
in = append(in, reflect.ValueOf(selectionToSelectedFields(f.sels)))
}
callOut := f.resolver.Method(f.field.MethodIndex).Call(in)
result = callOut[0]
if f.field.HasError && !callOut[1].IsNil() {
Expand Down
9 changes: 9 additions & 0 deletions internal/exec/resolvable/resolvable.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/qdentity/graphql-go/internal/common"
"github.com/qdentity/graphql-go/internal/exec/packer"
"github.com/qdentity/graphql-go/internal/schema"
pubquery "github.com/qdentity/graphql-go/query"
)

type Schema struct {
Expand All @@ -34,6 +35,7 @@ type Field struct {
MethodIndex int
HasContext bool
HasError bool
HasSelected bool
ArgsPacker *packer.StructPacker
ValueExec Resolvable
TraceLabel string
Expand Down Expand Up @@ -251,6 +253,7 @@ func (b *execBuilder) makeObjectExec(typeName string, fields schema.FieldList, p
}

var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
var selectedType = reflect.TypeOf([]pubquery.SelectedField(nil))
var errorType = reflect.TypeOf((*error)(nil)).Elem()

func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect.Method, methodIndex int, methodHasReceiver bool) (*Field, error) {
Expand Down Expand Up @@ -280,6 +283,11 @@ func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect.
in = in[1:]
}

hasSelected := len(in) > 0 && in[0] == selectedType
if hasSelected {
in = in[1:]
}

if len(in) > 0 {
return nil, fmt.Errorf("too many parameters")
}
Expand All @@ -300,6 +308,7 @@ func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect.
TypeName: typeName,
MethodIndex: methodIndex,
HasContext: hasContext,
HasSelected: hasSelected,
ArgsPacker: argsPacker,
HasError: hasError,
TraceLabel: fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name),
Expand Down
6 changes: 6 additions & 0 deletions query/selected_field.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package query

type SelectedField struct {
Name string
Selected []SelectedField
}

0 comments on commit 5472fce

Please sign in to comment.