Elixir PhoenixのGraphQLライブラリ - AbsintheでConnectionを使うところまで実装する

# はじめに
ElixirのWebフレームワークPhoenixでは、Absintheというライブラリを用いることでGraphQLのSchemaやTypeなどを簡単に定義、実装することができます。
しかしGraphQLでPaginationを実装する上で必要となるConnectionの実装方法については日本語のドキュメントがあまりなかったのでまとめておこうと思います。
せっかくなのでAbsintheの導入からまとめていきます。（Mutationの実装は行いません）

ちなみにこの記事ではGraphQLに対しての説明はしないので、GraphQLがなんなのかについて知りたい場合は以下のサイトなどを参考にしてください。
[Introduction GraphQL](https://graphql.org/learn/)


# 環境
`Elixir 1.7.3`
`Phoenix 1.4.0`

前提として以下のようなContextに対して実装を行なっていきます

```exs:lib/blog_app/blogs/blogs.ex
defmodule BlogApp.Blogs do
  @moduledoc """
  The Blogs context.
  """

  import Ecto.Query, warn: false
  alias BlogApp.Repo

  alias BlogApp.Blogs.Post

  def list_posts_query do
    Post
    |> order_by(desc: :inserted_at)
  end

  def get_post(id), do: Repo.get(Post, id)
end
```

```exs:lib/blog_app/blogs/post.ex
defmodule BlogApp.Blogs.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field :body, :string
    field :image, :string
    field :title, :string

    timestamps()
  end

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title, :body, :image])
    |> validate_required([:title, :body])
  end
end

```

# ライブラリのインストール
`mix.exs`の`deps`に以下のライブラリを追加します。

```exs
{:absinthe, "~> 1.4.0"},
{:absinthe_plug, "~> 1.4.0"},
{:poison, "~> 3.1.0"},
{:absinthe_relay, "~> 1.4"},
```
`absinthe`と`absinthe_plug`, `poison`は基本的な実装に必要なもので,`absinthe_relay`はGraphQL Connectionを定義するのに必要なライブラリです。

追加を行なったら`mix deps.get`を実行します。
これでライブラリの準備は完了です。

# Typesの実装
ここから実際にGraphQLの実装を行なっていきます。
GraphQLの実装は`/lib/blog_app_web/blogs/`以下に行なっていきます。

はじめにTypesの実装をします。実装はPostと PostのConnectionの二つに対して行います。
TypesにはObject TypesやScalar Typesなど複数の種類があるので、それごとにディレクトリを分割していき、
`types.ex`でそれらをまとめ上げる形にします。

今回実装を行うPostはObject Typeで定義できるので、以下のように実装を行います。

```exs:lib/blog_app_web/graphql/blogs/types/objects/post.ex
defmodule BlogAppWeb.Blogs.Objects.Post do
  defmacro __using__(_) do
    quote do
      object :post do
        field :id, non_null(:id)
        field :title, non_null(:string)
        field :body, non_null(:string)
        field :image, :string
        field :inserted_at, non_null(:naive_datetime)
      end

      connection(node_type: :post)
    end
  end
end
```

```exs:lib/blog_app_web/graphql/blogs/types/objects/objects.ex
defmodule BlogAppWeb.Blogs.Objects do
  alias BlogAppWeb.Blogs.Objects

  defmacro __using__(_) do
    quote do
      use Objects.Post
    end
  end
end
```
`objects.ex`は実装したObject Typeをまとめ上げる役割をしています。

具体的にPostとPost Connectionの定義を行なっているのは`post.ex`です。
`quote`内のはじめのブロックで`post`の定義を行なっています。
また、`connection(node_type: :post)`はConnectionの定義です。
これでPost、Post Connectionの実装が完了しました。

最後に実装したObject Typeを以下のようにまとめあげます

```exs:lib/blog_app_web/graphql/blogs/types/types.ex
defmodule BlogAppWeb.Blogs.Types do
  use Absinthe.Schema.Notation
  use Absinthe.Relay.Schema.Notation, :modern
  use BlogAppWeb.Blogs.Objects
end
```

これでTypesの実装は完了です。

# Schemaの実装
次に実際のクエリのfieldをSchemaで実装します。
今回定義したいのは

1. Post
2. Post Connection

の二つです。これらの定義は以下のように実装できます。

```exs:lib/blog_app_web/graphql/blogs/schema.ex
defmodule BlogAppWeb.Blogs.Schema do
  use Absinthe.Schema
  use Absinthe.Relay.Schema, :modern

  import_types(BlogAppWeb.Blogs.Types)
  import_types(Absinthe.Type.Custom)

  alias BlogAppWeb.Blogs.Resolvers
  alias Absinthe.Relay.Connection

  query do
    @desc "Get posts connection"
    connection field :posts, node_type: :post do
      resolve(&Resolvers.Post.posts_connection/2)
    end

    @desc "Get a post of the blog"
    field :post, :post do
      arg(:id, non_null(:id))
      resolve(&Resolvers.Post.find_post/3)
    end
  end
end
```
`Connection.from_query`によってSQLクエリが実行されます。このメソッドはGraphQLクエリの引数にある`first`や`before`などのカーソルの指定から自動的にSQLクエリに`LIMIT`や`OFFSET`を追加してくれ、返り値をConnectionのデータフォーマットに整形してくれます。

これでSchemaの実装は完了です。

# Resolversの実装
次にResolversの実装を行います。
今回実装するのは`schema.ex`内で呼び出している
`posts_connection`と`find_post`の二つです。
これらを実装すると以下のようになります。

```exs:lib/blog_app_web/graphql/blogs/resolvers/post.ex
defmodule BlogAppWeb.Blogs.Resolvers.Post do
  alias Absinthe.Relay.Connection
  alias BlogApp.Repo
  alias BlogApp.Blogs

  def posts_connection(pagination_args, _scope) do
    Blogs.list_posts_query()
    |> Connection.from_query(&Repo.all/1, pagination_args)
  end

  def find_post(_parent, %{id: id}, _resolution) do
    case Blogs.get_post(id) do
      nil ->
        {:error, "Post ID #{id} not found"}

      post ->
        {:ok, post}
    end
  end
end
```

これでResolversの実装は完了です。
なお、具体的なロジックはContextにあるので、それらを呼び出す形で実装を行なっています。

# APIとして呼び出せるようにする
前の項まででGraphQLの実装自体は完了です。最後にAPIとして呼び出せるようにします。
リクエストへのつなぎ合わせは簡単で,`router.ex`に以下のように実装を行うだけです。

```exs:lib/blog_app_web/router.ex
…
  scope "/api" do
    pipe_through :api
    forward "/graph", Absinthe.Plug, schema: BlogAppWeb.Blogs.Schema
  end
…
```

これで`/api/graph`をエンドポイントとしてGraphQLのリクエストを送ることができます。

# おまけ: Connectionのクエリでoffsetを指定できるようにする
最後におまけでConnectionクエリでoffsetを指定できるようにする方法を書いておきます。

GraphQLのConnectionのクエリでは、以下のような引数を渡して取得するデータの指定を行います。

1. first
2. last
3. before
4. after

これらはカーソルという概念で用いられるものなのですが、これが単純なページネーションをするだけだと少しまどろっこしいので、offsetという引数も使えるようにします。
[参考：カーソルとoffsetの説明](https://graphql.org/learn/pagination/#pagination-and-edges)

`Absinthe.Relay.Connection`には`offset_to_cursor`というメソッドがあります。これは引数に渡した値をオフセットとして`End Cursor`に変換してくれるメソッドです。
これを用いて`schema.ex`を以下のように修正します。

```exs
defmodule BlogAppWeb.Blogs.Schema do
  use Absinthe.Schema
  use Absinthe.Relay.Schema, :modern

  import_types(BlogAppWeb.Blogs.Types)
  import_types(Absinthe.Type.Custom)

  alias BlogAppWeb.Blogs.Resolvers
  alias Absinthe.Relay.Connection

  query do
    @desc "Get all posts"
    connection field :posts, node_type: :post do
      arg(:offset, :integer)
      resolve(&Resolvers.Post.posts_connection(convert_offset_to_before(&1), &2))
    end

    @desc "Get a post of the blog"
    field :post, :post do
      arg(:id, non_null(:uuid))
      resolve(&Resolvers.Post.find_post/3)
    end
  end

  defp convert_offset_to_before(pagination_args) do
    case pagination_args do
      %{offset: offset} ->
        Map.merge(pagination_args, %{before: Connection.offset_to_cursor(offset)})

      _ ->
        pagination_args
    end
  end
end
```

これで`offset`を引数に指定することができるようになります。

# 最後に
一度骨組みを実装してしまえば柔軟に開発を行うことができるのがGraphQLの強みだと思います。
ただGraphQLは言語ごとにライブラリがあるので、言語によってはドキュメントが少なかったり、未熟だったりするので困ることがあります。
この記事がPhoenixでGraphQLを実装をする際に少しでも参考になれば幸いです。

ちなみに今回サンプルに用いた実装は
https://github.com/getty104/blog_app_ex
から取ってきています。
ここにはアプリケーションのコード全てがあるのでよかったら見てみてください。

# 参考文献

1. https://hexdocs.pm/absinthe_relay/Absinthe.Relay.html
2. https://hexdocs.pm/absinthe/plug-phoenix.html
3. https://hexdocs.pm/absinthe_relay/Absinthe.Relay.Connection.html#cursor_to_offset/1