はじめに
graphql-rubyを試していて created_ad が 2019-01-01 ~ 2019-05-01 のデータが欲しいとなった時にどう実装するか悩んだのでメモ。
graphql-ruby の使い方自体は特に説明しません。
尚、version 1.8 以降の Class-based API を使用しているので 1.7 以前とは互換性がありません。
テーブル定義
Novel table
column name | type |
---|---|
id | Integer |
title | String |
release_date | DateTime |
型定義
module Types
class NovelType < Types::BaseObject
field :id, Integer, null: true
field :title, String, null: true
field :release_date, Types::DateTimeType, null: true
end
end
やりたいこと
release_date が 2019-01-01 ~ 2019-05-01 のデータが欲しいケースでこんな感じの query を投げたい。
{
novels(releaseDate: { from: "2019-01-01", to: "2019-05-01" }) {
id
title
releaseDate
}
}
実装
内部的には以下のような処理にしたかったので range オブジェクトを作成できる field を定義しました。
Model.where(release_date: (Time.zone.parse('2019-01-01')..Time.zone.parse('2019-05-01')))
Range を表現する InputObject を作成します。
GraphQL::Schema::InputObject はインスタンス変数として@arguments と@context を持っているので
それを使って range を作成するメソッドも追加しておきます。
module Types
class TimeRange < GraphQL::Schema::InputObject
argument :from, Types::DateTimeType, required: true
argument :to, Types::DateTimeType, required: true
def range
(@arguments['from']..@arguments['to'])
end
end
end
デフォルトでは DateTime 型が存在しないので自分で型を定義する必要がありますが、 公式のリポジトリにDateTime の定義があるのでそれを使います。
module Types
class DateTimeType < GraphQL::Schema::Scalar
def self.coerce_input(value, _context)
Time.zone.parse(value)
end
def self.coerce_result(value, _context)
value.utc.iso8601
end
end
end
使う側の定義は以下のようになります。
module Types
class QueryType < GraphQL::Schema::Object
field :novels, [Types::NovelType], null: true do
argument :release_date,
Types::TimeRange,
required: false,
prepare: -> (arg, _ctx) {
arg.range
}
end
def novels(release_date:)
Novel.where(release_date: release_date)
end
end
end
prepare 内で TimeRange クラスの range をコールして from、to を range オブジェクトに変換してます。
変換処理を prepare で実行する必要があるのが微妙なポイントです。
ドキュメントをもっと読めばいい方法があるかもしれないです…
とはいえ、これで当初目的だったクエリが実行できるようになりました。
まとめ
from のデフォルト値を極端な値(1950-01-01 とか)にしておけば使う側では(..to)みたいな range を表現できるんですがユースケースにもよるので今回は from、to を必須にしてます。
サンプルでは簡略化してますが、検索用の InputObject を定義して filter field でパラメーターを纏めていたり、pagination のためにCursor Connections を採用してるので最終的な query は以下のようになってます。
{
novels(filter: { releaseDate: { from: "2019-01-01", to: "2019-05-01" } }) {
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
edges {
node {
id
title
releaseDate
}
}
totalCount
}
}
InputObject
module Types
module InputObject
class NovelFilter < Types::InputObject::BaseInputObject
argument :OR, [self], required: false
argument :id, Integer, required: false
argument :title, String, required: false
argument :release_date,
Types::InputObject::TimeRange,
required: false,
prepare: -> (arg, _ctx) {
arg.range
}
end
end
end
すごく余談だけど markdown で tabel 表記したらカラム数が少ないと微妙な見た目になってしまう…
調整する気力は無い。