diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e8a23a..f0f3de4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 98606f2..884e03e 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/lib/graphql/query_resolver.rb b/lib/graphql/query_resolver.rb index 8031d20..eedd88e 100644 --- a/lib/graphql/query_resolver.rb +++ b/lib/graphql/query_resolver.rb @@ -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) + 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 diff --git a/spec/graphql/query_resolver_spec.rb b/spec/graphql/query_resolver_spec.rb index d91b0c1..b84a5b2 100644 --- a/spec/graphql/query_resolver_spec.rb +++ b/spec/graphql/query_resolver_spec.rb @@ -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 = %{ @@ -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