AbsintheでContextを組み立てたりMiddlewareを挟んだり
最近AbsintheをつかってGraphQLサーバを実装しはじめているのですが、これどう書くんだろと思って調べたことがあったのでメモ。 調べたと言ってもドキュメントに全部書いてあったので、ドキュメント写してるみたいになってる。まあいいよね。
Context
GraphQLの文脈におけるコンテキスト。GraphQLのドキュメントだとexecutionにこう書いてある。
context
A value which is provided to every resolver and holds important contextual information like the currently logged in user, or access to a database.
そう、"the currently logged in user"。具体的にはAuthorization HTTPヘッダにあるJWTからユーザIDを読んで、ユーザをDBから引っ張ってきて、それをresolve/1
の中で使いたい。結論としてはここに書いてあった。
The Context and Authentication – absinthe v1.4.7
普通にPhoenixでやるのと同様、plugをつくる。ただし、connにassign
するのではなくput_private
を使う。初期値の設定などしたかったので構造体を定義してるけど、普通のmapでもOK。
defmodule MyApp.ContextPlug do @behaviour Plug import Plug.Conn defmodule Context do defstruct [current_user: nil] end def init(opts), do: opts def call(conn, _opts) do context = build_context(conn) put_private(conn, :absinthe, %{context: context}) end defp build_context(conn) do %Context{} |> build_auth_context(conn) |> Map.from_struct() end defp build_auth_context(context, conn) do with ["Bearer " <> token | _] <- get_req_header(conn, "authorization"), {:ok, user} <- MyApp.User.from_token(token) do %{context | current_user: user} else _ -> context end end end
routerに仕込む。
defmodule MyApp.Router do use MyApp.Web, :router pipeline :graphql do plug MyApp.ContextPlug end scope "/api" do pipe_through :graphql forward "/graphql", Absinthe.Plug, schema: MyApp.Schema end end
あとはresolve
するときに、最後の引数に渡ってくるresolution
からcontext
が取れる。
object :user do field :is_me, :boolean do resolve fn user, _args, resolution -> %{current_user: current_user} = resolution.context {:ok, user.id == current_user.id} end end end
Authorization
認証されてないリクエストに対してはエラーを返したい、というケース。GraphQLの前にplugを置いてしまうと、このfieldだけは誰でもquery投げてOKなんですよというニーズを満たせないので、細かくコントロールできるようにしたい。これにはMiddlewareという仕組みを使います。
Absinthe.Middleware – absinthe v1.4.7
まずはAbsinthe.Middleware
behaviourを実装したモジュールを書きます。call/2
はresolution
を受けてresolution
を返す関数。Absinthe.Resolution.put_result/2
はresolution
の状態を:resolved
にして結果を確定させる模様。
defmodule MyApp.RequireCurrentUserMiddleware do @behaviour Absinthe.Middleware def call(resolution, _config) do case resolution.context do %{current_user: nil} -> resolution |> Absinthe.Resolution.put_result({:error, "unauthorized"}) _ -> resolution end end end
これをcurrent_user
が無いときにエラーを返してほしいところへ書きます。
query do field :posts, list_of(:post) do middleware MyApp.RequireCurrentUserMiddleware ... end end
MiddlewareもPlugと同様、単体テストが書きやすそうでいいすね。
ちなみに
middleware MyApp.RequireCurrentUserMiddleware, some: "config"
と書くと、call/2
の第2引数に[some: "config"]
が渡ってきます。使う場所によって微妙に動きを変えたいときに使う感じですかね。