GoでGraphQLを話すサーバを作ってみた

GoでGraphQLのサーバーサイド実装をしてみたので備忘録がてら記事に残す。

# 準備

今回は厳密に型付けされたgraphqlサーバーを素早く作成するためのライブラリ *vektah/gqlgen* を使ってみる

```
$ go get -u github.com/vektah/gqlgen
```

# 実装

まずはプロジェクト配下に./schema.graphqlを作成する。
それぞれUser型、Query/Mutationの定義、入力パラメータの定義になる。

```:schema.graphql
type User {
    id: ID!
    name: String!
}

type Query {
    user(id: String!): User
    users: [User!]!
}

input NewUser {
    name: String!
}

type Mutation {
    createUser(input: NewUser!): User!
}
```

次にgraphディレクトリを作成して以下2ファイルを作成する

```:graph/graph.go
package graph

type User struct {
	ID   string
	Name string
}
```

```:graph/type.json
{
  "User": "github.com/s-ichikawa/gql-todo/graph.User"
}
```

これでスキーマ定義は出来たので以下のコマンドを実行

```
$ cd path/to/project/graph
$ gqlgen -typemap type.json -schema ../schema.graphql
```

するとgraphディレクトリ内にgenerated.go, model_get.goが生成される
中身はスキーマ定義に従ってGraphQLサーバとして動作するためのコードで長いので割愛するが、大事なのはgenerated.goのResolverインターフェース

```:graph/generated.go
type Resolvers interface {
	Mutation_createUser(ctx context.Context, input NewUser) (User, error)
	Query_user(ctx context.Context, id string) (*User, error)
	Query_users(ctx context.Context) ([]User, error)
}
```

次にこれを実装していく

```:graph/graph.go
package graph

import (
	"context"
	"fmt"
	"math/rand"
	"github.com/pkg/errors"
)

type User struct {
	ID   string
	Name string
}

type Resolver struct {
	users []User
}

func (r *Resolver) Mutation_createUser(ctx context.Context, input NewUser) (User, error) {
	user := User{
		ID:   fmt.Sprintf("%d", rand.Int()),
		Name: input.Name,
	}
	r.users = append(r.users, user)
	return user, nil
}

func (r *Resolver) Query_user(ctx context.Context, id string) (*User, error) {
	for _, user := range r.users {
		if (user.ID == id) {
			return &user, nil
		}
	}
	return &User{}, errors.New("Sorry, Not Found.")
}

func (r *Resolver) Query_users(ctx context.Context) ([]User, error) {
	return r.users, nil
}
```

次にmain.goを作成
`/`でPlaygroundというクエリ実行が出来るページが開けるエンドポイント
`/query`がGraphQLを受け取るエンドポイント

```:main.go
package main

import (
	"github.com/s-ichikawa/gql-todo/graph"
	"net/http"
	"github.com/vektah/gqlgen/handler"
	"fmt"
	"log"
)

func main() {
	resolvers := &graph.Resolver{}
	http.Handle("/", handler.Playground("Todo", "/query"))
	http.Handle("/query", handler.GraphQL(graph.MakeExecutableSchema(resolvers)))

	fmt.Println("Listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

で実行してみる

```
$ go run main.go 
Listening on :8080
```

localhost:8080にアクセス
<img width="600" alt="スクリーンショット 2018-06-30 19.27.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/4d9c853b-5c62-422f-08a4-d943bad1f7ae.png">

右側のSCHEMAタブを開くとスキーマ定義が確認出来て非常に便利
<img width="600" alt="スクリーンショット 2018-06-30 19.27.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/d8ccd3be-d3dc-c622-e812-e3f47497e11f.png">

`mutation.createUser`をリクエストしてみる
mutationのクエリを書いたらページ下の方の`QUERY VARIABLES`タブを開いてパラメータを入力して実行
<img width="600" alt="スクリーンショット 2018-06-30 19.26.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/cb9ef392-615e-7d19-12aa-f16ed5de5a1e.png">

何度かユーザーを追加して次は`query.users`
<img width="600" alt="スクリーンショット 2018-06-30 19.36.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/6292c043-23a1-a5d7-a046-bd5e8c269f32.png">

最後に`query.user`
<img width="600" alt="スクリーンショット 2018-06-30 19.37.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/f683a610-6199-f9da-4dca-334440238c0b.png">

という感じで非常に簡単にGraphQLを話せるサーバを実装出来た。
Userの保存・取得をDBなどからするようにしたりすると、graph.goの`r.users = append(r.users, user)`の部分や`for _, user := range r.users`の辺りがSQL発行に置き換わる事になるのだろう

DB等を扱うのは今回はやめて、もう少しスキーマ定義で遊んでみる
Userに紐づくTodoを登録出来るようにしてみる

Todo用の型やQuery/Mutationを定義

```:schema.graphql
+ type Todo {
+     id: ID!
+     text: String!
+     done: Boolean!
+     user: User!
+ }

type Query {
    user(id: String!): User
    users: [User!]!
+     todo(id: String!): Todo!
+     todos: [Todo!]!
}

+ input NewTodo {
+     text: String!
+     userId: String!
+ }

type Mutation {
    createUser(input: NewUser!): User!
+    createTodo(input: NewTodo!): Todo!
}
```

type.jsonにTodoの定義を追加

```:graph/type.json
{
  "User": "github.com/s-ichikawa/gql-todo/graph.User",
  "Todo": "github.com/s-ichikawa/gql-todo/graph.Todo"
}
```

graph.goにTodo型を追加

```:graph/graph.go
type Todo struct {
	ID string
	Text string
	UserId string
}
```

再度gqlgenコマンドを実行

```
$ cd path/to/project/graph
$ gqlgen -typemap type.json -schema ../schema.graphql
```

追加されたResolverを実装する（長いので追加した部分だけ記載）
Userの時とほぼ変わらないが、スキーマ定義の `try Todo {..., user: User!}`を解決するための`func Todo_user`を追加している

```:graph/graph.go
func (r *Resolver) Mutation_createTodo(ctx context.Context, input NewTodo) (Todo, error) {
	todo := Todo{
		ID:     fmt.Sprintf("T%d", rand.Int()),
		UserId: fmt.Sprintf("U%d", input.UserId),
		Text:   input.Text,
	}
	r.todos = append(r.todos, todo)
	return todo, nil
}

func (r *Resolver) Query_todo(ctx context.Context, id string) (Todo, error)  {
	for _, todo := range r.todos {
		if todo.ID == id {
			return todo, nil
		}
	}
	return Todo{}, errors.New("Not Found")
}

func (r *Resolver) Query_todos(ctx context.Context) ([]Todo, error) {
	return r.todos, nil
}

func (r *Resolver) Todo_user(ctx context.Context, obj *Todo) (User, error) {
	for _, user := range r.users {
		if (user.ID == obj.UserId) {
			return user, nil
		}
	}
	return User{}, errors.New("Sorry, Not Found.")
}
```

main.goを再起動して再度クエリを発行してみる

データの永続化はしていないので再度createUserを行いUser.Idを控える
そして`QUERY VARIABLES`の`NewTodo.userId`に設定してクエリを発行
<img width="600" alt="スクリーンショット 2018-06-30 20.23.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/1495232b-d5cb-3c36-9618-c63ffe808f90.png">

成功してTodoとそれに紐づくUserの情報も取得出来ている。

一応`query.todos`
<img width="600" alt="スクリーンショット 2018-06-30 20.24.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/685b4f56-c453-3fd6-7092-efe672171d71.png">

ちなみに存在しないIDの場合はerrorsが返される
<img width="600" alt="スクリーンショット 2018-06-30 20.33.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/14172/530d7bf0-2392-d7fc-73b5-b228c227806c.png">


ということで、ある型に関係する別の型の取得まで成功したので一旦ここまで。

### 追記
そういえば`go run main.go`した時に以下のようなエラー出て　
`$ go get github.com/gorilla/websocket` したので一応それもメモ

```
$ go run main.go 
../../vektah/gqlgen/handler/graphql.go:10:2: cannot find package "github.com/gorilla/websocket" in any of:
...
```