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
117 changes: 67 additions & 50 deletions lib/graphql/query_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,92 @@

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)
dig_deeper = selection
Copy link
Owner

Choose a reason for hiding this comment

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

dig_deeper sounds a little weird. But I can't quite think of an alternative name here...

Copy link
Owner

Choose a reason for hiding this comment

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

it is almost like: "we want to replace the current selection with this nested option".

elsif fragment_spread?(selection)
dig_deeper = fragment_definitions[selection.name]
elsif relay_connection_using_edges?(selection)
dig_deeper = selection.selections.find { |sel| sel.name == 'node' }
end

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

if using_nodes_pagination?(selection)
map_dependencies(class_name, selection, dependencies)
next
end
name = selection.name
next unless preloadable_reflection?(class_name, name)
Copy link
Owner

@nettofarah nettofarah Sep 10, 2017

Choose a reason for hiding this comment

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

I'm not a big fan of using unless in cases like this.
I would replace this with:

if !preloadable_reflection?(class_name, name)
  next
end

Mostly because I find this easier to parse visually


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