diff --git a/Gemfile.lock b/Gemfile.lock index 1c3e9b6..46d2871 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,30 +1,30 @@ PATH remote: . specs: - gql_serializer (2.1.1) - activerecord (>= 5.2, < 6.1) + gql_serializer (2.2.0) + activerecord (>= 5.2, < 8.0) GEM remote: https://rubygems.org/ specs: - activemodel (6.0.3.5) - activesupport (= 6.0.3.5) - activerecord (6.0.3.5) - activemodel (= 6.0.3.5) - activesupport (= 6.0.3.5) - activesupport (6.0.3.5) + activemodel (6.1.5) + activesupport (= 6.1.5) + activerecord (6.1.5) + activemodel (= 6.1.5) + activesupport (= 6.1.5) + activesupport (6.1.5) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) coderay (1.1.3) - concurrent-ruby (1.1.8) + concurrent-ruby (1.1.9) diff-lcs (1.4.4) - i18n (1.8.9) + i18n (1.10.0) concurrent-ruby (~> 1.0) method_source (1.0.0) - minitest (5.14.4) + minitest (5.15.0) pry (0.13.1) coderay (~> 1.1) method_source (~> 1.0) @@ -43,10 +43,9 @@ GEM rspec-support (~> 3.10.0) rspec-support (3.10.0) sqlite3 (1.4.2) - thread_safe (0.3.6) - tzinfo (1.2.9) - thread_safe (~> 0.1) - zeitwerk (2.4.2) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + zeitwerk (2.5.4) PLATFORMS ruby diff --git a/README.md b/README.md index 76bc907..6b7433e 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,11 @@ In a Rails application, the configuration can be added to an initializer in `con ```ruby GqlSerializer.configure do |config| - config.case = GqlSerializer::Configuration::NONE_CASE # no case conversion + # no case conversion + config.case = GqlSerializer::Configuration::NONE_CASE + # set to true to avoid additional query in some cases. + # The default of false avoids a potential breaking change from version 2.1 to 2.2 + config.preload = false end ``` diff --git a/gql_serializer.gemspec b/gql_serializer.gemspec index 5aa4ea7..0293320 100644 --- a/gql_serializer.gemspec +++ b/gql_serializer.gemspec @@ -32,5 +32,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "pry", "~> 0.13.1" spec.add_development_dependency "sqlite3", "~> 1.4.2" - spec.add_runtime_dependency "activerecord", ">= 5.2", "< 7.0" + spec.add_runtime_dependency "activerecord", ">= 5.2", "< 8.0" end diff --git a/lib/gql_serializer.rb b/lib/gql_serializer.rb index 1e2392a..5d98d58 100644 --- a/lib/gql_serializer.rb +++ b/lib/gql_serializer.rb @@ -27,15 +27,15 @@ def self.parse_query(input) def self.query_include(model, hasharray) include_array = [] - relations = model.reflections.keys - hasharray.each do |e| - if e.is_a? String - key = e.split(':')[0] - include_array.push(key) if relations.include?(key) - elsif e.is_a? Hash - key = e.keys.first.split(':')[0] + relations = model.reflections + hasharray.each do |element| + if element.is_a? String + key = element.split(':')[0] + include_array.push(key) if relations[key] + elsif element.is_a? Hash + key = element.keys.first.split(':')[0] relation_model = model.reflections[key].klass - relation_hasharray = self.query_include(relation_model, e.values.first) + relation_hasharray = self.query_include(relation_model, element.values.first) if relation_hasharray.empty? include_array.push(key) else @@ -46,53 +46,71 @@ def self.query_include(model, hasharray) include_array end - def self.serialize(record, hasharray, options) + # example hasharray = ["id", "name", "tags", { "panels" => ["id", { "cards" => ["content"] }] }] + def self.serialize(records, hasharray, options, instructions = {}) - if record.nil? + if records.nil? return nil end - if record.respond_to? :map - return record.map do |r| - self.serialize(r, hasharray, options) + + if records.respond_to? :map + return records.map do |record| + self.serialize(record, hasharray, options, instructions) end end + record = records - hash = {} - model = record.class - all_relations = model.reflections.keys + id = "#{record.class}, #{hasharray}" + instruction = instructions[id] + if (!instruction) + instruction = {klass: record.class, hasharray: hasharray, relations: [], attributes: []} + instructions[id] = instruction - relations = hasharray.filter do |e| - next true if !e.is_a?(String) - key, alias_key = e.split(':') - all_relations.include?(key) - end + model = record.class + all_relations = model.reflections.keys - attributes = hasharray - relations - attributes = model.attribute_names if attributes.empty? + relations = hasharray.filter do |relation| + next true if !relation.is_a?(String) - attributes.each do |e| - key, alias_key = e.split(':') - alias_key = apply_case(alias_key || key, options[:case]) + key, alias_key = relation.split(':') - hash[alias_key] = coerce_value(record.public_send(key)) - end + all_relations.include?(key) + end - relations.each do |e| - if e.is_a? String - key, alias_key = e.split(':') - alias_key = apply_case(alias_key || key, options[:case]) + attributes = hasharray - relations + attributes = model.attribute_names if attributes.empty? - rel_records = record.public_send(key) - hash[alias_key] = self.serialize(rel_records, [], options) - else - key, alias_key = e.keys.first.split(':') + attributes.each do |attribute| + key, alias_key = attribute.split(':') alias_key = apply_case(alias_key || key, options[:case]) - rel_records = record.public_send(key) - hash[alias_key] = self.serialize(rel_records, e.values.first, options) + instruction[:attributes].push({key: key, alias_key: alias_key}) end + + relations.each do |relation| + if relation.is_a? String + key, alias_key = relation.split(':') + alias_key = apply_case(alias_key || key, options[:case]) + + instruction[:relations].push({key: key, alias_key: alias_key, hasharray: []}) + else + key, alias_key = relation.keys.first.split(':') + alias_key = apply_case(alias_key || key, options[:case]) + + instruction[:relations].push({key: key, alias_key: alias_key, hasharray: relation.values.first}) + end + end + end + + hash = {} + instruction[:attributes].each do |attribute| + hash[attribute[:alias_key]] = coerce_value(record.public_send(attribute[:key])) + end + instruction[:relations].each do |relation| + relation_records = record.public_send(relation[:key]) + hash[relation[:alias_key]] = self.serialize(relation_records, relation[:hasharray], options, instructions) end hash diff --git a/lib/gql_serializer/configuration.rb b/lib/gql_serializer/configuration.rb index 48bde79..32e5e62 100644 --- a/lib/gql_serializer/configuration.rb +++ b/lib/gql_serializer/configuration.rb @@ -11,7 +11,7 @@ def initialize reset end - attr_reader :case + attr_reader :case, :preload def case=(value) raise "Specified case '#{value}' is not supported" unless SUPPORTED_CASES.include?(value) @@ -20,6 +20,7 @@ def case=(value) def reset @case = NONE_CASE + @preload = false # Default will be true in version 3+ end def to_h diff --git a/lib/gql_serializer/extensions.rb b/lib/gql_serializer/extensions.rb index 7413656..b8ebd7d 100644 --- a/lib/gql_serializer/extensions.rb +++ b/lib/gql_serializer/extensions.rb @@ -9,10 +9,10 @@ def as_gql(query = nil) module Relation def as_gql(query = nil, options = {}) + options_with_defaults = GqlSerializer.configuration.to_h.merge(options) query_hasharray = query ? GqlSerializer.parse_query(query) : [] include_hasharray = GqlSerializer.query_include(self.model, query_hasharray) records = self.includes(include_hasharray).records - options_with_defaults = GqlSerializer.configuration.to_h.merge(options) GqlSerializer.serialize(records, query_hasharray, options_with_defaults) end end @@ -23,12 +23,26 @@ def self.as_gql(query = nil, options = {}) end def as_gql(query = nil, options = {}) - + options_with_defaults = GqlSerializer.configuration.to_h.merge(options) query_hasharray = query ? GqlSerializer.parse_query(query) : [] include_hasharray = GqlSerializer.query_include(self.class, query_hasharray) - record = include_hasharray.empty? ? self : self.class.where(id: self).includes(include_hasharray).first - options_with_defaults = GqlSerializer.configuration.to_h.merge(options) - GqlSerializer.serialize(record, query_hasharray, options_with_defaults) + if options_with_defaults[:preload] + GqlSerializer._preload([self], include_hasharray) + GqlSerializer.serialize(self, query_hasharray, options_with_defaults) + else + record = include_hasharray.empty? ? self : self.class.where(id: self).includes(include_hasharray).first + GqlSerializer.serialize(record, query_hasharray, options_with_defaults) + end + end + end + + def self._preload(records, include_hasharray) + if ::ActiveRecord::VERSION::MAJOR >= 7 + ::ActiveRecord::Associations::Preloader. + new(records: records, associations: include_hasharray).call + else + ::ActiveRecord::Associations::Preloader. + new.preload(records, include_hasharray) end end end diff --git a/lib/gql_serializer/version.rb b/lib/gql_serializer/version.rb index 8a30604..bf03595 100644 --- a/lib/gql_serializer/version.rb +++ b/lib/gql_serializer/version.rb @@ -1,3 +1,3 @@ module GqlSerializer - VERSION = "2.1.1" + VERSION = "2.2.0" end diff --git a/spec/gql_serializer_spec.rb b/spec/gql_serializer_spec.rb index 0b25e34..1340730 100644 --- a/spec/gql_serializer_spec.rb +++ b/spec/gql_serializer_spec.rb @@ -213,6 +213,27 @@ def camelCase GqlSerializer.configuration.reset end end - end + describe 'preload' do + it 'reloads records when disabled' do + original_user = TestUser.create(name: 'John', email: 'john@test.com') + TestOrder.create(total: 5.0, test_user: original_user) + same_user = TestUser.find(original_user.id) + same_user.update(name: 'David') + + expect(original_user.as_gql('name email test_orders {total}')) + .to eq({'name' => 'David', 'email' => 'john@test.com', 'test_orders' => [{'total' => 5.0}]}) + end + + it 'preloads records when enabled' do + original_user = TestUser.create(name: 'John', email: 'john@test.com') + TestOrder.create(total: 5.0, test_user: original_user) + same_user = TestUser.find(original_user.id) + same_user.update(name: 'David') + + expect(original_user.as_gql('name email test_orders {total}', {preload: true})) + .to eq({'name' => 'John', 'email' => 'john@test.com', 'test_orders' => [{'total' => 5.0}]}) + end + end + end end