Skip to content

Commit

Permalink
Merge pull request #282 from salman-ahmad/master
Browse files Browse the repository at this point in the history
Use Struct Field Resolver instead of Method
  • Loading branch information
pavelnikolov authored Feb 4, 2019
2 parents d5b7dc6 + e3e3046 commit e582242
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 81 deletions.
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` schema option, a struct field will be used *only* when:
- there is no method for a struct field
- a struct field does not implement an interface method
- a struct field does not have arguments

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
62 changes: 62 additions & 0 deletions example/social/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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() {
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: "[email protected]",
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: "[email protected]",
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: "[email protected]",
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: "[email protected]",
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
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 {
}
}

// UseFieldResolvers specifies whether to use struct field resolvers
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

0 comments on commit e582242

Please sign in to comment.