-
Notifications
You must be signed in to change notification settings - Fork 493
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
Changes from all commits
95f2b85
de4e1e1
2fcff20
4946780
c869a3d
3c3c15b
de6fb84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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/ |
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` | ||
|
||
and go to localhost:9011 to interact |
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...) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (minor) Is there any reason for these to be in the |
||
} | ||
|
||
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> | ||
`) |
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]! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, rename |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, rename |
||
Name string | ||
Role string | ||
Email string | ||
Phone string | ||
Address *[]string | ||
Friends *[]*user | ||
CreatedAt time.Time | ||
} | ||
|
||
func (u user) IdResolver() string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, rename |
||
return u.Id | ||
} | ||
|
||
func (u user) NameResolver() string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need the |
||
return u.Name | ||
} | ||
|
||
func (u user) RoleResolver() string { | ||
return u.Role | ||
} | ||
|
||
func (u user) FriendsResolver(args struct{ Page *page }) (*[]*user, error) { | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, remove empty line. |
||
from := 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer |
||
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: "[email protected]", | ||
Phone: "000-000-0000", | ||
Address: &[]string{"Office @ Hogwarts", "where Horcruxes are"}, | ||
CreatedAt: time.Now(), | ||
}, | ||
{ | ||
Id: "0x02", | ||
Name: "Harry Potter", | ||
Role: "USER", | ||
Email: "[email protected]", | ||
Phone: "000-000-0001", | ||
Address: &[]string{"123 dorm room @ Hogwarts", "456 random place"}, | ||
CreatedAt: time.Now(), | ||
}, | ||
{ | ||
Id: "0x03", | ||
Name: "Hermione Granger", | ||
Role: "USER", | ||
Email: "[email protected]", | ||
Phone: "000-000-0011", | ||
Address: &[]string{"233 dorm room @ Hogwarts", "786 @ random place"}, | ||
CreatedAt: time.Now(), | ||
}, | ||
{ | ||
Id: "0x04", | ||
Name: "Ronald Weasley", | ||
Role: "USER", | ||
Email: "[email protected]", | ||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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 | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that you use |
||
|
||
// 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) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add a comment on top of this // 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably it's a good idea to move |
||
if res.Kind() == reflect.Ptr { | ||
res = res.Elem() | ||
} | ||
result = res.Field(f.field.FieldIndex) | ||
} | ||
|
||
return nil | ||
}() | ||
|
||
|
@@ -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) | ||
} | ||
|
||
|
There was a problem hiding this comment.
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
?