├── .gitignore
├── LICENSE
├── README.md
├── app.go
├── app.yaml
├── cover.jpg
├── index.yaml
├── models.go
├── resolvers.go
└── utilities.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.dll
4 | *.so
5 | *.dylib
6 |
7 | # Test binary, build with `go test -c`
8 | *.test
9 |
10 | # Output of the go coverage tool, specifically when used with LiteIDE
11 | *.out
12 |
13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
14 | .glide/
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Tin Rabzelj
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Using GraphQL with Google App Engine
2 |
3 |
4 |
5 |
6 |
7 | This is the underlying code for [Create a GraphQL Server with Go and Google App Engine](https://outcrawl.com/graphql-server-go-google-app-engine) article.
8 |
--------------------------------------------------------------------------------
/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http"
7 |
8 | "github.com/graphql-go/graphql"
9 | "google.golang.org/appengine"
10 | )
11 |
12 | func makeListField(listType graphql.Output, resolve graphql.FieldResolveFn) *graphql.Field {
13 | return &graphql.Field{
14 | Type: listType,
15 | Resolve: resolve,
16 | Args: graphql.FieldConfigArgument{
17 | "limit": &graphql.ArgumentConfig{Type: graphql.Int},
18 | "offset": &graphql.ArgumentConfig{Type: graphql.Int},
19 | },
20 | }
21 | }
22 |
23 | func makeNodeListType(name string, nodeType *graphql.Object) *graphql.Object {
24 | return graphql.NewObject(graphql.ObjectConfig{
25 | Name: name,
26 | Fields: graphql.Fields{
27 | "nodes": &graphql.Field{Type: graphql.NewList(nodeType)},
28 | "totalCount": &graphql.Field{Type: graphql.Int},
29 | },
30 | })
31 | }
32 |
33 | var schema graphql.Schema
34 | var userType = graphql.NewObject(graphql.ObjectConfig{
35 | Name: "User",
36 | Fields: graphql.Fields{
37 | "id": &graphql.Field{Type: graphql.String},
38 | "name": &graphql.Field{Type: graphql.String},
39 | "posts": makeListField(makeNodeListType("PostList", postType), queryPostsByUser),
40 | },
41 | })
42 | var postType = graphql.NewObject(graphql.ObjectConfig{
43 | Name: "Post",
44 | Fields: graphql.Fields{
45 | "id": &graphql.Field{Type: graphql.String},
46 | "userId": &graphql.Field{Type: graphql.String},
47 | "createdAt": &graphql.Field{Type: graphql.DateTime},
48 | "content": &graphql.Field{Type: graphql.String},
49 | },
50 | })
51 |
52 | var rootMutation = graphql.NewObject(graphql.ObjectConfig{
53 | Name: "RootMutation",
54 | Fields: graphql.Fields{
55 | "createUser": &graphql.Field{
56 | Type: userType,
57 | Args: graphql.FieldConfigArgument{
58 | "name": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
59 | },
60 | Resolve: createUser,
61 | },
62 | "createPost": &graphql.Field{
63 | Type: postType,
64 | Args: graphql.FieldConfigArgument{
65 | "userId": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
66 | "content": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
67 | },
68 | Resolve: createPost,
69 | },
70 | },
71 | })
72 |
73 | var rootQuery = graphql.NewObject(graphql.ObjectConfig{
74 | Name: "RootQuery",
75 | Fields: graphql.Fields{
76 | "user": &graphql.Field{
77 | Type: userType,
78 | Args: graphql.FieldConfigArgument{
79 | "id": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
80 | },
81 | Resolve: queryUser,
82 | },
83 | "posts": makeListField(makeNodeListType("PostList", postType), queryPosts),
84 | },
85 | })
86 |
87 | func init() {
88 | schema, _ = graphql.NewSchema(graphql.SchemaConfig{
89 | Query: rootQuery,
90 | Mutation: rootMutation,
91 | })
92 | http.HandleFunc("/", handler)
93 | }
94 |
95 | func handler(w http.ResponseWriter, r *http.Request) {
96 | ctx := appengine.NewContext(r)
97 | body, err := ioutil.ReadAll(r.Body)
98 | if err != nil {
99 | responseError(w, "Invalid request body", http.StatusBadRequest)
100 | return
101 | }
102 | resp := graphql.Do(graphql.Params{
103 | Schema: schema,
104 | RequestString: string(body),
105 | Context: ctx,
106 | })
107 | if len(resp.Errors) > 0 {
108 | responseError(w, fmt.Sprintf("%+v", resp.Errors), http.StatusBadRequest)
109 | return
110 | }
111 | responseJSON(w, resp)
112 | }
113 |
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: go
2 | api_version: go1.8
3 |
4 | handlers:
5 | - url: /.*
6 | script: _go_app
7 |
--------------------------------------------------------------------------------
/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tinrab/graphql-appengine/0f99094a6150a9f4cbad558708af8783ba21b229/cover.jpg
--------------------------------------------------------------------------------
/index.yaml:
--------------------------------------------------------------------------------
1 | indexes:
2 |
3 | # AUTOGENERATED
4 |
5 | # This index.yaml is automatically updated whenever the dev_appserver
6 | # detects that a new type of query is run. If you want to manage the
7 | # index.yaml file manually, remove the above marker line (the line
8 | # saying "# AUTOGENERATED"). If you want to manage some indexes
9 | # manually, move them above the marker line. The index.yaml file is
10 | # automatically uploaded to the admin console when you next deploy
11 | # your application using appcfg.py.
12 |
13 | - kind: Post
14 | properties:
15 | - name: UserID
16 | - name: CreatedAt
17 | direction: desc
18 |
--------------------------------------------------------------------------------
/models.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import "time"
4 |
5 | type User struct {
6 | ID string `json:"id" datastore:"-"`
7 | Name string `json:"name"`
8 | }
9 |
10 | type Post struct {
11 | ID string `json:"id" datastore:"-"`
12 | UserID string `json:"userId"`
13 | CreatedAt time.Time `json:"createdAt"`
14 | Content string `json:"content"`
15 | }
16 |
--------------------------------------------------------------------------------
/resolvers.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "strconv"
7 | "time"
8 |
9 | "github.com/graphql-go/graphql"
10 | "google.golang.org/appengine/datastore"
11 | )
12 |
13 | type PostListResult struct {
14 | Nodes []Post `json:"nodes"`
15 | TotalCount int `json:"totalCount"`
16 | }
17 |
18 | func createUser(params graphql.ResolveParams) (interface{}, error) {
19 | ctx := params.Context
20 | name, _ := params.Args["name"].(string)
21 | user := &User{Name: name}
22 |
23 | key := datastore.NewIncompleteKey(ctx, "User", nil)
24 | if generatedKey, err := datastore.Put(ctx, key, user); err != nil {
25 | return User{}, err
26 | } else {
27 | user.ID = strconv.FormatInt(generatedKey.IntID(), 10)
28 | }
29 | return user, nil
30 | }
31 |
32 | func queryUser(params graphql.ResolveParams) (interface{}, error) {
33 | ctx := params.Context
34 | if strID, ok := params.Args["id"].(string); ok {
35 | id, err := strconv.ParseInt(strID, 10, 64)
36 | if err != nil {
37 | return nil, errors.New("Invalid id")
38 | }
39 | user := &User{ID: strID}
40 | key := datastore.NewKey(ctx, "User", "", id, nil)
41 | if err := datastore.Get(ctx, key, user); err != nil {
42 | return nil, errors.New("User not found")
43 | }
44 | return user, nil
45 | }
46 | return User{}, nil
47 | }
48 |
49 | func createPost(params graphql.ResolveParams) (interface{}, error) {
50 | ctx := params.Context
51 | content, _ := params.Args["content"].(string)
52 | userID, _ := params.Args["userId"].(string)
53 | post := &Post{UserID: userID, Content: content, CreatedAt: time.Now().UTC()}
54 |
55 | key := datastore.NewIncompleteKey(ctx, "Post", nil)
56 | if generatedKey, err := datastore.Put(ctx, key, post); err != nil {
57 | return Post{}, err
58 | } else {
59 | post.ID = strconv.FormatInt(generatedKey.IntID(), 10)
60 | }
61 | return post, nil
62 | }
63 |
64 | func queryPosts(params graphql.ResolveParams) (interface{}, error) {
65 | ctx := params.Context
66 | query := datastore.NewQuery("Post")
67 | if limit, ok := params.Args["limit"].(int); ok {
68 | query = query.Limit(limit)
69 | }
70 | if offset, ok := params.Args["offset"].(int); ok {
71 | query = query.Offset(offset)
72 | }
73 | return queryPostList(ctx, query)
74 | }
75 |
76 | func queryPostsByUser(params graphql.ResolveParams) (interface{}, error) {
77 | ctx := params.Context
78 | query := datastore.NewQuery("Post")
79 | if limit, ok := params.Args["limit"].(int); ok {
80 | query = query.Limit(limit)
81 | }
82 | if offset, ok := params.Args["offset"].(int); ok {
83 | query = query.Offset(offset)
84 | }
85 | if user, ok := params.Source.(*User); ok {
86 | query = query.Filter("UserID =", user.ID)
87 | }
88 | return queryPostList(ctx, query)
89 | }
90 |
91 | func queryPostList(ctx context.Context, query *datastore.Query) (PostListResult, error) {
92 | query = query.Order("-CreatedAt")
93 | var result PostListResult
94 | if keys, err := query.GetAll(ctx, &result.Nodes); err != nil {
95 | return result, err
96 | } else {
97 | for i, key := range keys {
98 | result.Nodes[i].ID = strconv.FormatInt(key.IntID(), 10)
99 | }
100 | result.TotalCount = len(result.Nodes)
101 | }
102 | return result, nil
103 | }
104 |
--------------------------------------------------------------------------------
/utilities.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | )
7 |
8 | func responseError(w http.ResponseWriter, message string, code int) {
9 | w.Header().Set("Content-Type", "application/json")
10 | w.WriteHeader(code)
11 | json.NewEncoder(w).Encode(map[string]string{"error": message})
12 | }
13 |
14 | func responseJSON(w http.ResponseWriter, data interface{}) {
15 | w.Header().Set("Content-Type", "application/json")
16 | json.NewEncoder(w).Encode(data)
17 | }
18 |
--------------------------------------------------------------------------------