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 #194

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/internal/validation/testdata/graphql-js
/internal/validation/testdata/node_modules
/vendor
.DS_Store
.idea/
.vscode/
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 ./example/field-resolvers/server/server.go`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean go run?


and go to localhost:9011 to interact
66 changes: 66 additions & 0 deletions example/social/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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"
)

var schema *graphql.Schema

func init() {
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers(), graphql.MaxParallelism(20)}
schema = graphql.MustParseSchema(social.Schema, &social.Resolver{}, opts...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(minor) Is there any reason for these to be in the init() func? They could easily be just live in a var( block or live in main().

}

func main() {
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>
`)
201 changes: 201 additions & 0 deletions example/social/social.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package social

import (
"context"
"fmt"
"strings"
"time"
)

const Schema = `
schema {
query: Query
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix indentation, please.

}

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 *int
Last *int
}

type admin interface {
IdResolver() string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, rename IdResolver to IDResolver.

NameResolver() string
RoleResolver() string
}

type searchResult struct {
result interface{}
}

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

type user struct {
Id string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, renameId -> ID.

Name string
Role string
Email string
Phone string
Address *[]string
Friends *[]*user
CreatedAt time.Time
}

func (u user) IdResolver() string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, rename IdResolver to IDResolver.

return u.Id
}

func (u user) NameResolver() string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the Resolver suffix here?

return u.Name
}

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

func (u user) FriendsResolver(args struct{ Page *page }) (*[]*user, error) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, remove empty line.

from := 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer var from int, instead of from := 0

numFriends := len(*u.Friends)
to := numFriends

if args.Page != nil {
if args.Page.First != nil {
from = *args.Page.First
}
if args.Page.Last != nil {
to = *args.Page.Last
if to > numFriends {
to = numFriends
}
}
}

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

return &friends, nil
}

var users = []*user{
{
Id: "0x01",
Name: "Albus Dumbledore",
Role: "ADMIN",
Email: "Albus@hogwarts.com",
Phone: "000-000-0000",
Address: &[]string{"Office @ Hogwarts", "where Horcruxes are"},
CreatedAt: time.Now(),
},
{
Id: "0x02",
Name: "Harry Potter",
Role: "USER",
Email: "harry@hogwarts.com",
Phone: "000-000-0001",
Address: &[]string{"123 dorm room @ Hogwarts", "456 random place"},
CreatedAt: time.Now(),
},
{
Id: "0x03",
Name: "Hermione Granger",
Role: "USER",
Email: "hermione@hogwarts.com",
Phone: "000-000-0011",
Address: &[]string{"233 dorm room @ Hogwarts", "786 @ random place"},
CreatedAt: time.Now(),
},
{
Id: "0x04",
Name: "Ronald Weasley",
Role: "USER",
Email: "ronald@hogwarts.com",
Phone: "000-000-0111",
Address: &[]string{"411 dorm room @ Hogwarts", "981 @ random place"},
CreatedAt: 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.Id] = 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.Role == 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.Name, 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 @@ -73,6 +72,13 @@ type Schema struct {
// SchemaOpt is an option to pass to ParseSchema or MustParseSchema.
type SchemaOpt func(*Schema)

// Specifies whether to use struct field resolvers
func UseFieldResolvers() SchemaOpt {
return func(s *Schema) {
s.schema.UseFieldResolvers = true
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that you use SchemaOpt here. This is awesome! 👍


// 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
42 changes: 26 additions & 16 deletions internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,22 +173,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
return err
if f.field.MethodIndex != -1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a comment on top of this if statement. Something like:

// check if this is a method or field resolver
if f.field.MethodIndex != -1 {

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
return err
}
} else {
// TODO extract out unwrapping ptr logic to a common place
res := f.resolver
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably it's a good idea to move res before the outer if in order to use it on line 185 as well.

if res.Kind() == reflect.Ptr {
res = res.Elem()
}
result = res.Field(f.field.FieldIndex)
}

return nil
}()

Expand All @@ -201,7 +212,6 @@ func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *p
f.out.WriteString("null") // TODO handle non-nil
return
}

r.execSelectionSet(traceCtx, f.sels, f.field.Type, path, result, f.out)
}

Expand Down
Loading