Posted at

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



ポイント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/