Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for inline and named fragments #6

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 0.3.0
- PR #5 - Add support to Relay style pagination using `nodes` shortcut
- PR #6 - Add support to inline and named fragments

# 0.2.0
- Add support to Relay style pagination

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Every matched selection will be then passed on to
`ActiveRecord::Associations::Preloader.new` so your queries now only issue
one `SELECT` statement for every level of the GraphQL AST.

The resolver works transparently with Relay-style pagination (either when using
`edges.node` or the `nodes` shortcut as selections)

## Installation

Add this line to your application's Gemfile:
Expand Down
119 changes: 69 additions & 50 deletions lib/graphql/query_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,94 @@

module GraphQL
module QueryResolver
def self.run(model_class, context, _return_type, &block)
Resolver.new(model_class, context).call(&block)
end

def self.run(model_class, context, return_type)
to_load = yield
dependencies = {}

reflection_dependencies = map_dependencies(model_class, context.ast_node)
dependencies = reflection_dependencies.merge(dependencies)
class Resolver
attr_reader :context, :fragment_definitions

if dependencies.any? && to_load.present?
if ActiveRecord::VERSION::MAJOR < 4
ActiveRecord::Associations::Preloader.new(to_load, dependencies).run
else
ActiveRecord::Associations::Preloader.new.preload(to_load, dependencies)
end
def initialize(model_class, context)
@model_class = model_class
@context = context
@fragment_definitions = context.query.fragments
end

to_load
end
def call
to_load = yield
dependencies = map_dependencies(@model_class, context.ast_node)

def self.using_relay_pagination?(selection)
selection.name == 'edges'
end
if dependencies.any? && to_load.present?
preload_dependencies(to_load, dependencies)
end

def self.using_nodes_pagination?(selection)
selection.name == 'nodes'
end
to_load
end

def self.map_relay_pagination_depencies(class_name, selection, dependencies)
node_selection = selection.selections.find { |sel| sel.name == 'node' }
private

if node_selection.present?
map_dependencies(class_name, node_selection, dependencies)
else
dependencies
end
end
def preload_dependencies(to_load, dependencies)
if ActiveRecord::VERSION::MAJOR < 4
return ActiveRecord::Associations::Preloader.new(
to_load, dependencies
).run
end

def self.has_reflection_with_name?(class_name, selection_name)
class_name.reflections.with_indifferent_access[selection_name].present?
end
ActiveRecord::Associations::Preloader.new.preload(to_load, dependencies)
end

def self.map_dependencies(class_name, ast_node, dependencies={})
ast_node.selections.each do |selection|
name = selection.name
def map_dependencies(class_name, ast_node, dependencies = {})
ast_node.selections.each do |selection|
if inline_fragment?(selection) || relay_connection_using_nodes?(selection)
proceed_to = selection
elsif fragment_spread?(selection)
proceed_to = fragment_definitions[selection.name]
elsif relay_connection_using_edges?(selection)
proceed_to = selection.selections.find { |sel| sel.name == 'node' }
end

if using_relay_pagination?(selection)
map_relay_pagination_depencies(class_name, selection, dependencies)
next
end
if proceed_to.present?
map_dependencies(class_name, proceed_to, dependencies)
next
end

if using_nodes_pagination?(selection)
map_dependencies(class_name, selection, dependencies)
next
end
name = selection.name
if !preloadable_reflection?(class_name, name)
next
end

if has_reflection_with_name?(class_name, name)
begin
current_class_name = selection.name.singularize.classify.constantize
dependencies[name] = map_dependencies(current_class_name, selection)
next_model_class = name.singularize.classify.constantize
rescue NameError
selection_name = class_name.reflections.with_indifferent_access[selection.name].options[:class_name]
current_class_name = selection_name.singularize.classify.constantize
dependencies[selection.name.to_sym] = map_dependencies(current_class_name, selection)
next
selection_name = class_name.reflections[name].options[:class_name]
next_model_class = selection_name.singularize.classify.constantize
end

dependencies[name.to_sym] = map_dependencies(next_model_class, selection)
end

dependencies
end

dependencies
def preloadable_reflection?(class_name, selection_name)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole section is great. Love the abstraction :)

class_name.reflections.with_indifferent_access[selection_name].present?
end

def relay_connection_using_edges?(selection)
selection.name == 'edges'
end

def relay_connection_using_nodes?(selection)
selection.name == 'nodes'
end

def inline_fragment?(selection)
selection.is_a?(GraphQL::Language::Nodes::InlineFragment)
end

def fragment_spread?(selection)
selection.is_a?(GraphQL::Language::Nodes::FragmentSpread)
end
end
end
end
68 changes: 68 additions & 0 deletions spec/graphql/query_resolver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,44 @@
expect(queries[4]).to include('SELECT "vendors".* FROM "vendors" WHERE "vendors"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)')
end

it 'works with inline and spread fragments' do
data = nil

queries = track_queries do
data = GQL.query(%{
fragment ChefFragment on chef {
name
recipes {
title
ingredients {
name, quantity
vendor {
name
}
}
}
}

query {
restaurant(id: 1) {
... on restaurant {
name
owner {
... ChefFragment
}
}
}
}
})
end

expect(queries[0]).to include('SELECT "restaurants".* FROM "restaurants" WHERE "restaurants"."id" = ?')
expect(queries[1]).to include('SELECT "chefs".* FROM "chefs" WHERE "chefs"."id"') # AR 4 will use id IN (1), AR 5 will use id = 1
expect(queries[2]).to include('SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id"') # AR 4 will use id IN (1), AR 5 will use id = 1
expect(queries[3]).to include('SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."recipe_id" IN (1, 2, 3, 4)')
expect(queries[4]).to include('SELECT "vendors".* FROM "vendors" WHERE "vendors"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)')
end

it 'works with alias reflections' do
# Owner is an instance of Chef
query = %{
Expand Down Expand Up @@ -152,4 +190,34 @@

expect(queries.size).to eq(2)
end

it 'mixing "nodes" and fragments' do
query = %{
query {
vendors(first: 5) {
nodes {
... on vendor {
id
name
ingredients {
... on ingredient {
name
quantity
}
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
queries = track_queries do
GQL.query(query)
end

expect(queries.size).to eq(2)
end
end