From bec6015618bdad7450bce289ac636d50ac684705 Mon Sep 17 00:00:00 2001 From: Mikhail Varabyou Date: Sat, 23 Nov 2024 11:58:54 +0100 Subject: [PATCH] fix `accepts_nested_attributes_for` triggering db connection in rails 7.2 (#194) --------- Co-authored-by: Mikhail Varabyou --- lib/store_model/nested_attributes.rb | 36 ++++++++++++++-------- lib/store_model/types.rb | 1 + lib/store_model/types/base.rb | 25 +++++++++++++++ lib/store_model/types/many_base.rb | 18 ++--------- lib/store_model/types/one_base.rb | 17 ++-------- spec/store_model/nested_attributes_spec.rb | 3 +- 6 files changed, 56 insertions(+), 44 deletions(-) create mode 100644 lib/store_model/types/base.rb diff --git a/lib/store_model/nested_attributes.rb b/lib/store_model/nested_attributes.rb index d40397d..0f05a92 100644 --- a/lib/store_model/nested_attributes.rb +++ b/lib/store_model/nested_attributes.rb @@ -8,6 +8,17 @@ def self.included(base) # :nodoc: end module ClassMethods # :nodoc: + # gather storemodel attribute types on the class-level + def store_model_attribute_types + @store_model_attribute_types ||= {} + end + + # add storemodel type of attribute if it is storemodel type + def attribute(name, type = nil, **) + store_model_attribute_types[name.to_s] = type if type.is_a?(Types::Base) + super + end + # Enables handling of nested StoreModel::Model attributes # # @param associations [Array] list of associations and options to define attributes, for example: @@ -38,8 +49,7 @@ def accepts_nested_attributes_for(*attributes) options = attributes.extract_options! attributes.each do |attribute| - case nested_attribute_type(attribute) - when Types::OneBase, Types::ManyBase + if nested_attribute_type(attribute).is_a?(Types::Base) options.reverse_merge!(allow_destroy: false, update_only: false) define_store_model_attr_accessors(attribute, options) else @@ -50,17 +60,19 @@ def accepts_nested_attributes_for(*attributes) private - # If attribute defined in ActiveRecord model but you dont yet have database created - # you cannot access attribute types. - # To handle this case, we can use ActiveRecord::Attributes 'attributes_to_define_after_schema_loads' - # which stores information about custom defined attributes. - # See ActiveRecord::Attributes#atribute - # If #accepts_nested_attributes_for is used inside active model instance - # schema is not required to determine attribute type so we can still use attribute_types - # If schema loaded the attribute_types already populated and we can safely use it - # See ActiveRecord::ModelSchema#load_schema! + # when db connection is not available, it becomes impossible to read attributes types from + # ActiveModel::AttributeRegistration::ClassMethods.attribute_types, because activerecord + # overrides _default_attributes and triggers db connection. + # for activerecord model only use attribute_types if it has db connected + # + # @param attribute [String, Symbol] + # @return [StoreModel::Types::Base, nil] def nested_attribute_type(attribute) - attribute_types[attribute.to_s] + if self < ActiveRecord::Base && !schema_loaded? + store_model_attribute_types[attribute.to_s] + else + attribute_types[attribute.to_s] + end end def define_store_model_attr_accessors(attribute, options) # rubocop:disable Metrics/MethodLength diff --git a/lib/store_model/types.rb b/lib/store_model/types.rb index 9123a68..ba04f1b 100644 --- a/lib/store_model/types.rb +++ b/lib/store_model/types.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "store_model/types/polymorphic_helper" +require "store_model/types/base" require "store_model/types/one_base" require "store_model/types/one" diff --git a/lib/store_model/types/base.rb b/lib/store_model/types/base.rb new file mode 100644 index 0000000..eb9bcce --- /dev/null +++ b/lib/store_model/types/base.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "active_model" + +module StoreModel + module Types + # Base type for StoreModel::Model + class Base < ActiveModel::Type::Value + attr_reader :model_klass + + # Returns type + # + # @return [Symbol] + def type + raise NotImplementedError + end + + protected + + def raise_cast_error(_value) + raise NotImplementedError + end + end + end +end diff --git a/lib/store_model/types/many_base.rb b/lib/store_model/types/many_base.rb index d902af6..8efb9f4 100644 --- a/lib/store_model/types/many_base.rb +++ b/lib/store_model/types/many_base.rb @@ -4,18 +4,8 @@ module StoreModel module Types - # Implements ActiveModel::Type::Value type for handling an array of - # StoreModel::Model - class ManyBase < ActiveModel::Type::Value - attr_reader :model_klass - - # Returns type - # - # @return [Symbol] - def type - raise NotImplementedError - end - + # Implements type for handling an array of StoreModel::Model + class ManyBase < Base # Casts +value+ from DB or user to StoreModel::Model instance # # @param value [Object] a value to cast @@ -70,10 +60,6 @@ def cast_model_type_value(_value) raise NotImplementedError end - def raise_cast_error(_value) - raise NotImplementedError - end - private # rubocop:disable Style/RescueModifier diff --git a/lib/store_model/types/one_base.rb b/lib/store_model/types/one_base.rb index f43e45f..90561ae 100644 --- a/lib/store_model/types/one_base.rb +++ b/lib/store_model/types/one_base.rb @@ -4,17 +4,8 @@ module StoreModel module Types - # Implements ActiveModel::Type::Value type for handling an instance of StoreModel::Model - class OneBase < ActiveModel::Type::Value - attr_reader :model_klass - - # Returns type - # - # @return [Symbol] - def type - raise NotImplementedError - end - + # Implements type for handling an instance of StoreModel::Model + class OneBase < Base # Casts +value+ from DB or user to StoreModel::Model instance # # @param value [Object] a value to cast @@ -36,10 +27,6 @@ def changed_in_place?(raw_old_value, new_value) protected - def raise_cast_error(_value) - raise NotImplementedError - end - def model_instance(_value) raise NotImplementedError end diff --git a/spec/store_model/nested_attributes_spec.rb b/spec/store_model/nested_attributes_spec.rb index b642993..2c4a92d 100644 --- a/spec/store_model/nested_attributes_spec.rb +++ b/spec/store_model/nested_attributes_spec.rb @@ -535,7 +535,8 @@ def self.name end end - it { expect { subject }.to raise_error(ActiveRecord::StatementInvalid) } + it { expect { subject }.not_to(raise_error) } + it { expect(subject.instance_methods).to(include(:suppliers_attributes=)) } end end end