Resolver→Connection の流れ
Cursorに作ってもらったざっくりとした流れ
クエリ実行開始
GraphQL::Execution::Interpreter#executeメソッドが呼び出される
クエリの実行を開始し、各フィールドの解決を開始する
フィールド評価
GraphQL::Execution::Interpreter::Runtime#evaluate_selectionメソッドが呼び出される
Resolver実行
GraphQL::Schema::Resolverクラスのresolve_with_supportメソッドが呼び出される
lib/graphql/schema/resolver.rb (66-114行目)
Field実行
GraphQL::Schema::Fieldクラスのresolveメソッドが呼び出される
lib/graphql/schema/field.rb (637-846行目)
Runtime処理
GraphQL::Execution::Interpreter::Runtimeクラスのcontinue_fieldメソッドが実行される
lib/graphql/execution/interpreter/runtime.rb (557-670行目)
Connection検出と処理
GraphQL::Schema::Field::ConnectionExtensionクラスのafter_resolveメソッドが呼び出される
lib/graphql/schema/field/connection_extension.rb (24-62行目)
Connection Wrapper適用
GraphQL::Pagination::Connectionsクラスのwrapメソッドが呼び出される
lib/graphql/pagination/connections.rb (60-64行目)
Connectionオブジェクト処理
GraphQL::Pagination::Connectionクラスのメソッド(nodes, edges, page_info)が必要に応じて呼び出される
lib/graphql/pagination/connection.rb
遅延値(Lazy)の解決
GraphQL::Execution::Interpreter::Resolve.resolve_allメソッドが呼び出される
結果の構築
GraphQL::Execution::Interpreterクラスが最終的な結果を構築する
cursorに作ってもらったシーケンス図
code:mermaid
sequenceDiagram
participant Client
participant Interpreter
participant Runtime
participant Resolver
participant Field
participant ConnectionExtension
participant Connections
participant Connection
participant Resolve
Client->>Interpreter: executeクエリ
Interpreter->>Runtime: run_eager
Runtime->>Resolver: resolve_with_support
Resolver->>Field: resolve
Field->>Runtime: continue_field
Runtime->>ConnectionExtension: after_resolve
ConnectionExtension->>Connections: wrap
Connections->>Connection: new
Connection->>Runtime: 結果を返す
Runtime->>Resolve: resolve_all
Resolve->>Interpreter: 結果を構築
Interpreter->>Client: 最終結果を返す
---
Connectionじゃない場合
GraphQL::Schema::FieldExtension が呼ばれる
Connectionの場合
ConnectionExtension < GraphQL::Schema::FieldExtension が呼ばれる
code:ruby
def self.connection_extension(new_extension_class = nil)
if new_extension_class
@connection_extension = new_extension_class
else
@connection_extension ||= find_inherited_value(:connection_extension, ConnectionExtension)
end
end
after_resolve で Connectionにcontextやargumentsがsetされる
code:ruby
def after_resolve(value:, object:, arguments:, context:, memo:)
original_arguments = memo
# rename some inputs to avoid conflicts inside the block
maybe_lazy = value
value = nil
context.query.after_lazy(maybe_lazy) do |resolved_value|
value = resolved_value
if value.is_a? GraphQL::ExecutionError
# This isn't even going to work because context doesn't have ast_node anymore
context.add_error(value)
nil
elsif value.nil?
nil
elsif value.is_a?(GraphQL::Pagination::Connection)
# update the connection with some things that may not have been provided
value.context ||= context
value.parent ||= object.object
value.first_value ||= original_arguments:first value.after_value ||= original_arguments:after value.last_value ||= original_arguments:last value.before_value ||= original_arguments:before value.arguments ||= original_arguments # rubocop:disable Development/ContextIsPassedCop -- unrelated .arguments method
value.field ||= field
if field.has_max_page_size? && !value.has_max_page_size_override?
value.max_page_size = field.max_page_size
end
if field.has_default_page_size? && !value.has_default_page_size_override?
value.default_page_size = field.default_page_size
end
if context.schema.new_connections? && (custom_t = context.schema.connections.edge_class_for_field(@field))
value.edge_class = custom_t
end
value
else
context.namespace(:connections):all_wrappers ||= context.schema.connections.all_wrappers context.schema.connections.wrap(field, object.object, value, original_arguments, context)
end
end
end
code:ruby
# @return Array<Edge> {nodes}, but wrapped with Edge instances def edges
@edges ||= nodes.map { |n| @edge_class.new(n, self) }
end
ActiveRecordの場合
load_nodes で
nodes
has_previous_page
has_next_page
cursor_for
の値が決まる
code:ruby
def nodes
load_nodes
@nodes
end
def has_previous_page
load_nodes
@has_previous_page
end
def has_next_page
load_nodes
@has_next_page
end
def cursor_for(item)
idx = items.find_index(item) + 1
encode(idx.to_s)
end
private
def index_from_cursor(cursor)
decode(cursor).to_i
end
# Populate all the pagination info _once_,
# It doesn't do anything on subsequent calls.
def load_nodes
@nodes ||= begin
sliced_nodes = if before && after
end_idx = index_from_cursor(before) - 2
elsif before
end_idx = index_from_cursor(before) - 2
elsif after
else
items
end
@has_previous_page = if last
# There are items preceding the ones in this result
sliced_nodes.count > last
elsif after
# We've paginated into the Array a bit, there are some behind us
index_from_cursor(after) > 0
else
false
end
@has_next_page = if before
# The original array is longer than the before index
index_from_cursor(before) < items.length + 1
elsif first
# There are more items after these items
sliced_nodes.count > first
else
false
end
limited_nodes = sliced_nodes
limited_nodes = limited_nodes.first(first) if first
limited_nodes = limited_nodes.last(last) if last
limited_nodes
end