Skip to content

Commit

Permalink
fix accepts_nested_attributes_for is triggering db connection in rail…
Browse files Browse the repository at this point in the history
…s 7.2. add single types/base.
  • Loading branch information
Mikhail Varabyou committed Nov 21, 2024
1 parent c39579b commit cbd2819
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,11 @@ 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

# 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 +43,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 +54,16 @@ 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.
# in order to avoid dirty hacks we say goodbye to rails attribute_types and rely on
# @store_model_attribute_types object, that we populate ourselves.
#
# @param attribute [String, Symbol]
# @return [StoreModel::Types::Base, nil]
def nested_attribute_type(attribute)
attribute_types[attribute.to_s]
store_model_attribute_types[attribute.to_s]
end

def define_store_model_attr_accessors(attribute, options) # rubocop:disable Metrics/MethodLength
Expand Down Expand Up @@ -107,6 +110,15 @@ def define_association_setter_for_single(association, options)
end
end
end

private

# marked in activemodel/attribute_registration as a method for modules to override
# add storemodel type of attribute if it is storemodel type
def hook_attribute_type(name, type)
store_model_attribute_types[name.to_s] = type if type.is_a?(Types::Base)
super
end
end

# Base
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 cbd2819

Please sign in to comment.