├── .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 | --------------------------------------------------------------------------------