GraphQL、GraphQL-Rubyを使う上での困り事や便利な拡張などをご紹介します。
ActiveRecord::Relation以外のデータソースでConnectionを使う
GraphQL-RubyではActiveRecord::Relation
をConnection型にresolveするとfirst
, last
, after
, betore
からよしなにlimit
, offset
をセットしてくれます
ただし他のデータソースから取得する場合、自前でConnectionを実装する必要があります。
例: Elasticsearchから取得する場合
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した場合に作動させるよう登録します
GraphQL::Relay::BaseConnection.register_connection_implementation(ElasticsearchRepository, ElasticsearchConnection)
そしてElasticsearchRepositoryを実装します
簡易的に書きますが、以下のようになります
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
ポイント1
GraphQL-rubyの内部でElasticsearchRepository
のインスタンスに対しmap
などを経由してデータを取得するメソッドコールが発生するためfetchに流します
※ 1.8がでた当初に確認した事象なので現在は変更となっている場合があります
ポイント2
elasticsearch_connection.rb
からsize, fromをセットできるようにしておきます
この2つをfetch
内での検索クエリの構築に利用します
あとはElasticsearchRepository
のインスタンスをresolveすることでConnectionに対応ができます。
エラーハンドリング
私のチームの場合、graphql-errorsを活用してmutation、queryのエラーハンドリングを一括して行っています。
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がよく買うつようされているように思いますが、型同士のつながり、特定の型をどういう経路で参照しづらい場合があります。
graphql-voyagerも併用することで視覚的に探索しやすくなります。
ActiveRecordのEnumをGraphQLのEnumにきれいにドキュメント化する
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/