graphql-rubyでransackの検索を利用する

# TL;DR

#### 検索ロジック
```rb
module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      description "Find a houses ransack"
      argument :q, BaseScalar, required: false # これが`any`タイプ
      argument :page, Integer, required: false
      argument :per, Integer, required: false
    end
    def houses(q: nil, page: nil, per: nil)
      if q.present?
        @q = House.joins(:city).ransack(q)
        @q.sorts = 'id asc' if @q.sorts.blank? # デフォルト並び替えの追加
        @q.result(distinct: true).page(page).per(per)
      else
        House.all
      end
    end
  end
end
```

#### GraphQL
```graphql
{
   houses(q: {city_name_cont: "London", s: "name desc"}, page: 2, per: 4) {
    name
  }
}
```
#### 結果

```json
{
  "data": {
    "houses": [
      {
        "name": "Thomas"
      },
      {
        "name": "Ruby"
      },
      {
        "name": "Kelly"
      },
      {
        "name": "Jeffrey"
      }
    ]
  }
}
```

# 解説
## QueryType
まず、GraphQLはSchemaの定義が要求されているが、Ransackの`述語`という概念と相性があまり良いとは言えません。
そこで出された案は[anyタイプ](https://stackoverflow.com/a/47880926/11263491)を利用することでした。GraphQLでは`BaseScalar`というタイプは`any`タイプと近い使い方ができます。
下記のコードのように、`q`というパラメータに任意のqueryを送信できます。

```query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      argument :q, BaseScalar, required: false
    end
    def houses(q: nil, page: nil, per: nil)
    end
  end
end
```

```graphql
{
   houses(q: {city_name_cont: "London"}) {
    name
  }
}
```

## Ransackの検索
Ransackについて詳しいの解説はこちらでは割愛いたします。[こちらの記事](http://nekorails.hatenablog.com/entry/2017/05/31/173925)を参考して、`q`パラメータを組み立て、`House.joins(:city).ransack(q)`のようにQueryを投げ込めばいいです。良いのも悪いのも、述語が間違っても、Ransackはエラーが発生しないことでした。

## 検索結果の並び替え
`q: { s: 'id DESC' }`のように、`q`パラメータの中に、`s`フィールドを利用すれば、簡単に並び替えられます。また、`q: { s: ['id DESC', 'name asc'] }`のように、配列を与えば、複数条件での並び替えも可能です。
デフォルトの並び替えについてですが、[こちらの記事](http://319ring.net/blog/archives/2628/)を参考して、以下のロジックを実装すれば簡単にできます。

```query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      argument :q, BaseScalar, required: false
    end
    def houses(q: nil, page: nil, per: nil)
      @q = House.joins(:city).ransack(q)
      @q.sorts = 'id asc' if @q.sorts.blank? # デフォルト並び替えの追加
      @q.result(distinct: true)
    end
  end
end
```


## Kaminariでページネーション
普通、フォームで検索をする際、[Kaminari](https://github.com/kaminari/kaminari)は`params[:page]`を利用してページ切り替えを行います。ここでは似たような書き方で、`page`Queryを定義します。また、フォームで検索をする場合、windowのサイズを固定するのが一般ですが、APIで検索する際、windowのサイズを可変にする方が使い勝手がいいので、`per`Queryも追加します。

```query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      argument :q, BaseScalar, required: false
      argument :page, Integer, required: false
      argument :per, Integer, required: false
    end
    def houses(q: nil, page: nil, per: nil)
      @q = House.joins(:city).ransack(q)
      @q.sorts = 'id asc' if @q.sorts.blank? # デフォルト並び替えの追加
      @q.result(distinct: true).page(page).per(per)
    end
  end
end
```

## おまけ
### ユーザー認証機能
デフォルトの状態では、GraphQLは誰でも検索可能な状態で非常によくありません、JWTなどでログイン機能を追加することができます。

```app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  skip_before_action :verify_authenticity_token # GraphQLはPOSTなので、ないと怒られる
  before_action :authenticate_user!, unless: :from_graphiql?

  # 省略...

  private

  # 開発環境でgraphiqlが開けなくなる恐れがあるので、パイパスを作る
  def from_graphiql?
    referrer = request.referer
    if Rails.env.development? && referrer =~ /\/graphiql/
      return true
    end
  end
end
```

また、`execute`Actionに`context`を設置すれば、`query_type.rb`では`current_user`などが使用可能になります。

```app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  # 省略...

  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
      # Query context goes here, for example:
      current_user: current_user,
    }
    result = CaesarisSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  rescue => e
    raise e unless Rails.env.development?
    handle_error_in_development e
  end

  # 省略...
end
```

### `camelCase`での検索
GraphQLの作法は`camelCase`ですが、RansackなどRailsのGemは`snake_case`であるため、`houses(q: {city_name_cont: "London"})`のようなQueryは非常に気分が悪いです。

下記のメソッドを実装すれば、`camelCase`での検索も可能になります。


```query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :houses, [HouseType], null: true do
      argument :q, BaseScalar, required: false
      argument :page, Integer, required: false
      argument :per, Integer, required: false
    end
    def houses(q: nil, page: nil, per: nil)
      @q = House.joins(:city).ransack(form_ransack_params(q))
      @q.sorts = 'id asc' if @q.sorts.blank? # デフォルト並び替えの追加
      @q.result(distinct: true).page(page).per(per)
    end
  end

  def form_ransack_params(params)
    param_res = {}
    params.each do |key, val|
      key = key.to_s.underscore
      if (key == 's' || key == 'sorts') && val.instance_of?(String)
        param_res[key] = val.split(/\s+/).map(&:underscore).join(' ').strip
      elsif (key == 's' || key == 'sorts') && val.instance_of?(Array)
        param_res[key] = val.map{|v| v.split(/\s+/).map(&:underscore).join(' ').strip }
      elsif (key == 'g' || key == 'groupings') && (val.instance_of?(Hash) || val.instance_of?(Array))
        if val.instance_of?(Hash)
          param_res[key] = val.values.map{|v| Util.form_ransack_params(v) }
        else
          param_res[key] = val.map{|v| Util.form_ransack_params(v) }
        end
      else
        param_res[key] = val
      end
    end
    param_res
  end
end
```

```graphql
{
   houses(q: {cityNameCont: "London", s: "cityName DESC"}) {
    name
  }
}
```


# 参考
https://stackoverflow.com/questions/45598812/graphql-blackbox-any-type
https://qiita.com/jerrywdlee/items/a08589ea160c470e67f6
https://qiita.com/toshiotm@github/items/c7909b9bb97f845d831a
https://github.com/activerecord-hackery/ransack
https://github.com/rmosolgo/graphql-ruby/blob/master/guides/type_definitions/lists.md
https://github.com/rmosolgo/graphql-ruby/blob/master/guides/fields/arguments.md