GraphQL-Ruby Tips

GraphQL、GraphQL-Rubyを使う上での困り事や便利な拡張などをご紹介します。

## ActiveRecord::Relation以外のデータソースでConnectionを使う
GraphQL-Rubyでは`ActiveRecord::Relation`をConnection型にresolveすると`first`, `last`, `after`, `betore`からよしなに`limit`, `offset`をセットしてくれます
ただし他のデータソースから取得する場合、自前でConnectionを実装する必要があります。

### 例: Elasticsearchから取得する場合
```elasticsearch_connection.rb
class ElasticsearchConnection < GraphQL::Relay::BaseConnection
  def cursor_from_node(item)
    item_index = paged_nodes_array.index(item)
    if item_index.nil?
      raise("Can't generate cursor, item not found in connection: #{item}")
    else
      offset = paged_nodes.from + item_index + 1
      encode(offset.to_s)
    end
  end

  def has_next_page
    if (es_total = sliced_nodes.search_result.dig("hits", "total")) &&
       sliced_nodes.from.present? && sliced_nodes.size.present?
      return (sliced_nodes.from + sliced_nodes.size) < es_total
    end

    false
  end

  def has_previous_page
    if (es_total = sliced_nodes.search_result.dig("hits", "total")) &&
       sliced_nodes.from.present? && sliced_nodes.size.present?
      from_cursor = sliced_nodes.from - sliced_nodes.size
      return from_cursor >= 0 && from_cursor <= es_total
    end

    false
  end

  private

  def first
    return @first if defined? @first

    @first = get_limited_arg(:first)
    @first = max_page_size if @first && max_page_size && @first > max_page_size
    @first
  end

  def last
    return @last if defined? @last

    @last = get_limited_arg(:last)
    @last = max_page_size if @last && max_page_size && @last > max_page_size
    @last
  end

  def paged_nodes
    return @paged_nodes if defined? @paged_nodes
    items = sliced_nodes

    if first
      items.size = first
    end

    if last
      if !items.size
        items.from = calc_offset(items.from, items.count, last)
      elsif last <= items.size
        items.from = calc_offset(items.from, items.size, last)
      end
      items.size = last
    end

    @paged_nodes = items
  end

  def calc_offset(from, size, last)
    (from || 0) + size - last
  end

  def sliced_nodes
    @sliced_nodes = nodes
    if first
      @sliced_nodes.size = first
    end
    if after
      after_i = offset_from_cursor(after)
      @sliced_nodes.from = after_i
    end
    if before && after
      after_i = offset_from_cursor(after)
      before_i = offset_from_cursor(before)
      @sliced_nodes.size = before_i - after_i - 1
    elsif before
      before_i = offset_from_cursor(before)
      @sliced_nodes.size = before_i - 1
    end
    @sliced_nodes
  end

  def offset_from_cursor(cursor)
    decode(cursor).to_i
  end

  def paged_nodes_array
    return @paged_nodes_array if defined?(@paged_nodes_array)
    @paged_nodes_array = paged_nodes.fetch
  end
end
```

このConnectionを特定のクラスのインスタンスをresolveした場合に作動させるよう登録します

```config/initializers/graphql_connection.rb
GraphQL::Relay::BaseConnection.register_connection_implementation(ElasticsearchRepository, ElasticsearchConnection)
```

そしてElasticsearchRepositoryを実装します
簡易的に書きますが、以下のようになります

```elasticsearch_repository.rb
class ElasticsearchRepository
  # ポイント1
  extend Forwardable
  def_delegators :fetch, *Enumerable.instance_methods
  # ポイント2
  attr_accessor :size, :from

  def fetch
    # ここに検索処理を実装します
  end

  def search_result
    @es_result
  end

  def hits
    @es_result.dig("hits", "total")
  end
end
```

#### ポイント１
GraphQL-rubyの内部で`ElasticsearchRepository`のインスタンスに対し`map`などを経由してデータを取得するメソッドコールが発生するためfetchに流します
※ 1.8がでた当初に確認した事象なので現在は変更となっている場合があります

#### ポイント２
`elasticsearch_connection.rb`からsize, fromをセットできるようにしておきます
この２つを`fetch`内での検索クエリの構築に利用します

あとは`ElasticsearchRepository`のインスタンスをresolveすることでConnectionに対応ができます。

## エラーハンドリング
私のチームの場合、[graphql-errors](https://github.com/exAspArk/graphql-errors)を活用してmutation、queryのエラーハンドリングを一括して行っています。

```.rb
GraphQL::Errors.configure(schema) do
  rescue_from SomeError do |_exception|
    raise ApiError.new(exception.message, "SomeError")
  end

  rescue_from ActiveRecord::RecordInvalid do |exception|
    raise ApiError.new(exception.message, "InvalidInput")
  end
end
```

同時にエラーに`type`フィールドを加えることでクライアントサイドでの特定を容易にしています。

```
class ApiError < GraphQL::ExecutionError
  attr_reader :type
  def initialize(message, type = nil)
    @type = type
    super(message)
  end

  def to_h
    super.merge("type" => type)
  end
end
```

## graphql-voyagerの活用
GraphQLのIDE、ドキュメントとして[graphiql](https://github.com/graphql/graphiql)がよく買うつようされているように思いますが、型同士のつながり、特定の型をどういう経路で参照しづらい場合があります。
[graphql-voyager](https://github.com/APIs-guru/graphql-voyager)も併用することで視覚的に探索しやすくなります。
<img width="800" src="https://qiita-image-store.s3.amazonaws.com/0/6046/40bbfed4-52d5-6961-bf54-51ad5e5054dc.png">

## ActiveRecordのEnumをGraphQLのEnumにきれいにドキュメント化する
```rb
class SomeEnum < Types::BaseEnum
  SomeAr.statuses.each do |key, _val|
    value(key, SomeAr.human_attribute_name("status.#{key}"))
  end
end
```
`each`,`human_attribute_name`を使うことで誰でも読める形式でEnumを定義することができます。

## まとめ
いかがでしたでしょうか。
GraphQL-Rubyは更新も盛んで自身で手を入れることでより使いやすくすることができるようになってきました。
また別の機会にアクセス制御やテストなど今回触れなかったGraphQLの実装例についてご紹介できればと思います。

## 最後に
DeNA TechCon2019に登壇します！
KubernetesやGraphQLの活用事例を紹介致しますのでぜひご参加ください！
https://techcon.dena.com/2019/