[GraphQL]gqlgenを使ってアレがしたい


概要

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

main.go
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した際にアプリケーションのプロセスを落とさないように制御することができる

main.go
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には予め用意された型が用意されているがアプリケーション独自に定義した型を使用したい場合がある。
その型を指定した場合、どのような振る舞いを行うかを表現するためのインターフェースがある。

それはUnmarshalGQLMarshalGQLである。
UnmarshalGQLはリクエストされた値をstructにバインドするときに呼び出される関数で、MarshalGQLはjsonレスポンスを返す手前の処理として呼び出される関数。

例えばUUIDという独自の型を定義し利用する場合は以下のようになる

schema.graphql
scalar UUID
gqlgen.yaml
# 一部抜粋
models:
  UUID:
    model: github.com/graphql-app/graph/model.UUID
uuid.go
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.graphql
schema {
    query: Query
    mutation: Mutation
}

type Query
type Mutation
user.graphql
type User {
    id: ID!
    name: String!
}

# QueryやMutationはextendキーワードを使用して宣言する
extend type Query {
    getUser: User!
}

extend type Mutation {
    addUser: User!
}
scalar.graphql
scalar UUID

気軽にQueryを叩いたり、GraphQLスキーマを見たりしたい

grpcのリフレクションと似たような機能で、Introspectionを有効にすると定義しているスキーマ一覧を参照できる。
gqlgenにはplayground.HandlerというWebUIを表示する機能が内包されているので、これと組み合わせるとスキーマ定義からQueryやMutionを入力補完を効かせてシュッと確認することができて最高である。

ただし、この機能は開発時のみONにするのがよい。

PlayGroundと組み合わせて使用する場合は次のような感じ

main.go
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とかとか使ってみたい)