Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Struct Field Resolver instead of Method #282

Merged
merged 3 commits into from
Feb 4, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,17 @@ $ curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query

### Resolvers

A resolver must have one method for each field of the GraphQL type it resolves. The method name has to be [exported](https://golang.org/ref/spec#Exported_identifiers) and match the field's name in a non-case-sensitive way.
A resolver must have one method or field for each field of the GraphQL type it resolves. The method or field name has to be [exported](https://golang.org/ref/spec#Exported_identifiers) and match the schema's field's name in a non-case-sensitive way.
You can use struct fields as resolvers by using `SchemaOpt: UseFieldResolvers()`. For example,
```
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers()}
schema := graphql.MustParseSchema(s, &query{}, opts...)
```

When using `UseFieldResolvers`, a field will be used *only* when:
0xSalman marked this conversation as resolved.
Show resolved Hide resolved
- there is no method
- it does not implement an interface
0xSalman marked this conversation as resolved.
Show resolved Hide resolved
- it does not have arguments
0xSalman marked this conversation as resolved.
Show resolved Hide resolved

The method has up to two arguments:

Expand Down
9 changes: 9 additions & 0 deletions example/social/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Social App

A simple example of how to use struct fields as resolvers instead of methods.

To run this server

`go run ./example/field-resolvers/server/server.go`

and go to localhost:9011 to interact
63 changes: 63 additions & 0 deletions example/social/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"log"
"net/http"

"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/example/social"
"github.com/graph-gophers/graphql-go/relay"
)

func main() {

0xSalman marked this conversation as resolved.
Show resolved Hide resolved
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers(), graphql.MaxParallelism(20)}
schema := graphql.MustParseSchema(social.Schema, &social.Resolver{}, opts...)

http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(page)
}))

http.Handle("/query", &relay.Handler{Schema: schema})

log.Fatal(http.ListenAndServe(":9011", nil))
}

var page = []byte(`
<!DOCTYPE html>
<html>
<head>
<link href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js"></script>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
<div id="graphiql" style="height: 100vh;">Loading...</div>
<script>
function graphQLFetcher(graphQLParams) {
return fetch("/query", {
method: "post",
body: JSON.stringify(graphQLParams),
credentials: "include",
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}

ReactDOM.render(
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
document.getElementById("graphiql")
);
</script>
</body>
</html>
`)
206 changes: 206 additions & 0 deletions example/social/social.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package social

import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/graph-gophers/graphql-go"
)

const Schema = `
schema {
query: Query
}

type Query {
admin(id: ID!, role: Role = ADMIN): Admin!
user(id: ID!): User!
search(text: String!): [SearchResult]!
}

interface Admin {
id: ID!
name: String!
role: Role!
}

scalar Time

type User implements Admin {
id: ID!
name: String!
email: String!
role: Role!
phone: String!
address: [String!]
friends(page: Pagination): [User]
createdAt: Time!
}

input Pagination {
first: Int
last: Int
}

enum Role {
ADMIN
USER
}

union SearchResult = User
`

type page struct {
First *float64
Last *float64
}

type admin interface {
ID() graphql.ID
Name() string
Role() string
}

type searchResult struct {
result interface{}
}

func (r *searchResult) ToUser() (*user, bool) {
res, ok := r.result.(*user)
return res, ok
}

type user struct {
IDField string
NameField string
RoleField string
Email string
Phone string
Address *[]string
Friends *[]*user
CreatedAt graphql.Time
}

func (u user) ID() graphql.ID {
return graphql.ID(u.IDField)
}

func (u user) Name() string {
return u.NameField
}

func (u user) Role() string {
return u.RoleField
}

func (u user) FriendsResolver(args struct{ Page *page }) (*[]*user, error) {
var from int
numFriends := len(*u.Friends)
to := numFriends

if args.Page != nil {
if args.Page.First != nil {
from = int(*args.Page.First)
if from > numFriends {
return nil, errors.New("not enough users")
}
}
if args.Page.Last != nil {
to = int(*args.Page.Last)
if to == 0 || to > numFriends {
to = numFriends
}
}
}

friends := (*u.Friends)[from:to]

return &friends, nil
}

var users = []*user{
{
IDField: "0x01",
NameField: "Albus Dumbledore",
RoleField: "ADMIN",
Email: "Albus@hogwarts.com",
Phone: "000-000-0000",
Address: &[]string{"Office @ Hogwarts", "where Horcruxes are"},
CreatedAt: graphql.Time{Time: time.Now()},
},
{
IDField: "0x02",
NameField: "Harry Potter",
RoleField: "USER",
Email: "harry@hogwarts.com",
Phone: "000-000-0001",
Address: &[]string{"123 dorm room @ Hogwarts", "456 random place"},
CreatedAt: graphql.Time{Time: time.Now()},
},
{
IDField: "0x03",
NameField: "Hermione Granger",
RoleField: "USER",
Email: "hermione@hogwarts.com",
Phone: "000-000-0011",
Address: &[]string{"233 dorm room @ Hogwarts", "786 @ random place"},
CreatedAt: graphql.Time{Time: time.Now()},
},
{
IDField: "0x04",
NameField: "Ronald Weasley",
RoleField: "USER",
Email: "ronald@hogwarts.com",
Phone: "000-000-0111",
Address: &[]string{"411 dorm room @ Hogwarts", "981 @ random place"},
CreatedAt: graphql.Time{Time: time.Now()},
},
}

var usersMap = make(map[string]*user)

func init() {
users[0].Friends = &[]*user{users[1]}
users[1].Friends = &[]*user{users[0], users[2], users[3]}
users[2].Friends = &[]*user{users[1], users[3]}
users[3].Friends = &[]*user{users[1], users[2]}
for _, usr := range users {
usersMap[usr.IDField] = usr
}
}

type Resolver struct{}

func (r *Resolver) Admin(ctx context.Context, args struct {
Id string
0xSalman marked this conversation as resolved.
Show resolved Hide resolved
Role string
}) (admin, error) {
if usr, ok := usersMap[args.Id]; ok {
if usr.RoleField == args.Role {
return *usr, nil
}
}
err := fmt.Errorf("user with id=%s and role=%s does not exist", args.Id, args.Role)
return user{}, err
}

func (r *Resolver) User(ctx context.Context, args struct{ Id string }) (user, error) {
if usr, ok := usersMap[args.Id]; ok {
return *usr, nil
}
err := fmt.Errorf("user with id=%s does not exist", args.Id)
return user{}, err
}

func (r *Resolver) Search(ctx context.Context, args struct{ Text string }) ([]*searchResult, error) {
var result []*searchResult
for _, usr := range users {
if strings.Contains(usr.NameField, args.Text) {
result = append(result, &searchResult{usr})
}
}
return result, nil
}
10 changes: 8 additions & 2 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package graphql

import (
"context"
"fmt"

"encoding/json"
"fmt"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
Expand Down Expand Up @@ -84,6 +83,13 @@ func UseStringDescriptions() SchemaOpt {
}
}

// Specifies whether to use struct field resolvers
0xSalman marked this conversation as resolved.
Show resolved Hide resolved
func UseFieldResolvers() SchemaOpt {
return func(s *Schema) {
s.schema.UseFieldResolvers = true
}
}

// MaxDepth specifies the maximum field nesting depth in a query. The default is 0 which disables max depth checking.
func MaxDepth(n int) SchemaOpt {
return func(s *Schema) {
Expand Down
43 changes: 26 additions & 17 deletions internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,24 +178,33 @@ func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *p
return errors.Errorf("%s", err) // don't execute any more resolvers if context got cancelled
}

var in []reflect.Value
if f.field.HasContext {
in = append(in, reflect.ValueOf(traceCtx))
}
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
}
callOut := f.resolver.Method(f.field.MethodIndex).Call(in)
result = callOut[0]
if f.field.HasError && !callOut[1].IsNil() {
resolverErr := callOut[1].Interface().(error)
err := errors.Errorf("%s", resolverErr)
err.Path = path.toSlice()
err.ResolverError = resolverErr
if ex, ok := callOut[1].Interface().(extensionser); ok {
err.Extensions = ex.Extensions()
res := f.resolver
if f.field.UseMethodResolver() {
var in []reflect.Value
if f.field.HasContext {
in = append(in, reflect.ValueOf(traceCtx))
}
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
}
callOut := res.Method(f.field.MethodIndex).Call(in)
result = callOut[0]
if f.field.HasError && !callOut[1].IsNil() {
resolverErr := callOut[1].Interface().(error)
err := errors.Errorf("%s", resolverErr)
err.Path = path.toSlice()
err.ResolverError = resolverErr
if ex, ok := callOut[1].Interface().(extensionser); ok {
err.Extensions = ex.Extensions()
}
return err
}
} else {
// TODO extract out unwrapping ptr logic to a common place
if res.Kind() == reflect.Ptr {
res = res.Elem()
}
return err
result = res.Field(f.field.FieldIndex)
}
return nil
}()
Expand Down
Loading