Skip to content

Commit

Permalink
fix accepts_nested_attributes_for triggering db connection in rails…
Browse files Browse the repository at this point in the history
… 7.2 (#194)

---------

Co-authored-by: Mikhail Varabyou <[email protected]>
  • Loading branch information
balbesina and Mikhail Varabyou authored Nov 23, 2024
1 parent c39579b commit bec6015
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 44 deletions.
36 changes: 24 additions & 12 deletions lib/store_model/nested_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/store_model/types.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
25 changes: 25 additions & 0 deletions lib/store_model/types/base.rb
Original file line number Diff line number Diff line change
@@ -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
18 changes: 2 additions & 16 deletions lib/store_model/types/many_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 2 additions & 15 deletions lib/store_model/types/one_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion spec/store_model/nested_attributes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit bec6015

Please sign in to comment.