概要
gqlgenを使ってGraphQLサーバを実装している。
今は開発に慣れてきた部分があるが、gqlgenの導入時に調査が必要であったアレやるには?コレやるには?を少し書いてみる。
しかし、どれも大体公式ドキュメントに乗っている内容なので詳細はそちらを見るのがよい
公式doc
エラーをフックしてアプリ固有の処理をしたい
例えばResolverからエラーを返す際に必ず以下のことを行いたいケースがあるとする
- Internalエラーはログ出力したい
- エラーに応じて決められたjsonフォーマットでエラーメッセージを返したい
このようなニーズに応えるためにgqlgenのhandler.ServerにはSetErrorPresenter
という関数でresolverが返したエラーをHookする仕組みがある。
GraphQLでのエラーメッセージはextensions
というjsonキーにセットする。
他のjson階層には追加できない (将来的にGraphQL側の名前と被る可能性があるため)
https://github.com/graphql/graphql-spec/releases/tag/June2018
func main() {
// 一部抜粋
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{}))
srv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{
Message: err.Error(),
Extensions: map[string]interface{}{
// 実際には引数のerrorをerrors.As()なりで、動的にtype, codeなどをセットする
"type": "Internal",
"code": "1002567",
},
}
})
}
panicを制御したい
panic発生時にアプリケーションのプロセスが終了しないように制御したいことはただある。
エラーのHookと同じくgqlgenのhandler.ServerにはSetRecoverFunc
関数が用意されていて、これを使用してpanicした際にアプリケーションのプロセスを落とさないように制御することができる
func main() {
// 一部抜粋
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{}))
srv.SetRecoverFunc(func(ctx context.Context, err interface{}) error {
log.From(ctx).Error("recovered", zap.Error(fmt.Errorf("%v", err)))
return &gqlerror.Error{
Message: "server error",
Extensions: map[string]interface{}{
"type": "Internal",
"code": "Unknown",
},
}
})
}
カスタム型を作りたい
gqlgenには予め用意された型が用意されているがアプリケーション独自に定義した型を使用したい場合がある。
その型を指定した場合、どのような振る舞いを行うかを表現するためのインターフェースがある。
それはUnmarshalGQL
とMarshalGQL
である。
UnmarshalGQL
はリクエストされた値をstructにバインドするときに呼び出される関数で、MarshalGQL
はjsonレスポンスを返す手前の処理として呼び出される関数。
例えばUUID
という独自の型を定義し利用する場合は以下のようになる
scalar UUID
# 一部抜粋
models:
UUID:
model: github.com/graphql-app/graph/model.UUID
package model
type UUID struct {
string
}
// UnmarshalGQL implements the graphql.Unmarshaler interface
func (u *UUID) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("uuid must be string")
}
if _, err := uuid.Parse(str); err != nil {
return fmt.Errorf("not in uuid format: %w", err)
}
u.string = str
return nil
}
// MarshalGQL implements the graphql.Marshaler interface
func (u UUID) MarshalGQL(w io.Writer) {
_, _ = io.WriteString(w, strconv.Quote(u.string))
}
.graphqlスキーマファイルを分割したい
gqlgenのexampleではschema.graphqlの中にQuery, Mutation, scalarなど宣言されているケースが多い。1つのファイルに追記する形でももちろん良いが、役割ごとにファイル分割したい場合は以下のように分割ができる。
※gqlgen.yml
で設定した拡張子にだけ揃えればファイル名は任意で問題ない
schema {
query: Query
mutation: Mutation
}
type Query
type Mutation
type User {
id: ID!
name: String!
}
# QueryやMutationはextendキーワードを使用して宣言する
extend type Query {
getUser: User!
}
extend type Mutation {
addUser: User!
}
scalar UUID
気軽にQueryを叩いたり、GraphQLスキーマを見たりしたい
grpcのリフレクションと似たような機能で、Introspection
を有効にすると定義しているスキーマ一覧を参照できる。
gqlgenにはplayground.Handler
というWebUIを表示する機能が内包されているので、これと組み合わせるとスキーマ定義からQueryやMutionを入力補完を効かせてシュッと確認することができて最高である。
ただし、この機能は開発時のみONにするのがよい。
PlayGroundと組み合わせて使用する場合は次のような感じ
func main() {
// 一部抜粋
srv := handler.New(generated.NewExecutableSchema(generated.Config{}))
srv.AddTransport(transport.Options{})
srv.AddTransport(transport.GET{})
srv.AddTransport(transport.POST{})
// 環境変数なりでON/OFFするとよい
usePlayGround := true
if usePlayGround {
srv.Use(extension.Introspection{})
}
// ここではルーティングに"github.com/go-chi/chi"を使う前提。ルーティングは好きなものを使ってOK
router := chi.NewRouter()
router.Group(func(r chi.Router) {
r.Handle("/graphql", srv)
if usePlayGround {
r.Handle("/", playground.Handler("graphql playground", "/graphql"))
}
})
}
PlayGroundのWebUI
他にも
いろいろな機能がサポートされているが、使ったことがない機能が沢山あるので利用しだい追記する
(Dataloaders、Directives、Cachingとかとか使ってみたい)