From 7aa1bee85a63746ebf38479788b333ccc4420ad1 Mon Sep 17 00:00:00 2001 From: Anton Katunin Date: Mon, 23 Sep 2024 16:28:01 +1000 Subject: [PATCH] Require explicit attribute list for nested attribute --- .../extras/nested_attributes.rb | 50 ++++++++++++++++++- .../extras/strong_params.rb | 5 +- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/active_interaction/extras/nested_attributes.rb b/lib/active_interaction/extras/nested_attributes.rb index bca9e84..493f158 100644 --- a/lib/active_interaction/extras/nested_attributes.rb +++ b/lib/active_interaction/extras/nested_attributes.rb @@ -31,6 +31,14 @@ def accepts_nested_attributes_for(*attributes) options = attributes.extract_options! options.reverse_merge!(allow_destroy: false, update_only: false) + if options.key?(:permit) + if options[:permit].is_a?(Array) + options[:permit] |= [:id, :_destroy] + end + else + raise ArgumentError, "missing :permit option" + end + attributes.each do |attribute| nested_attribute_options[attribute] = options @@ -38,7 +46,10 @@ def accepts_nested_attributes_for(*attributes) when ActiveInteraction::ArrayFilter define_association_setter_for_many attribute, options else - raise "Nested attributes are not supported for single object" + define_association_setter_for_single(attribute, options) + define_method "#{attribute}_attributes=" do |*args, **kwargs| + send("#{attribute}=", *args, **kwargs) + end end end end @@ -50,9 +61,20 @@ def define_association_setter_for_many(association, options) end end + def define_association_setter_for_single(association, options) + return unless options&.dig(:allow_destroy) + + define_method "#{association}=" do |attributes| + if ActiveRecord::Type::Boolean.new.cast(attributes.stringify_keys.dig("_destroy")) + super(nil) + else + super(attributes) + end + end + end + def process_nested_collection(attributes, options = nil) attributes = attributes.values if attributes.is_a?(Hash) - if options&.dig(:allow_destroy) attributes.reject! do |attribute| ActiveRecord::Type::Boolean.new.cast(attribute.stringify_keys.dig("_destroy")) @@ -75,4 +97,28 @@ def call_reject_if(attributes, callback) end end end + + def assign_collection_association(model, association_name, attributes_collection, destroy_missing: false, **options) + + if attributes_collection.is_a?(ActiveRecord::Associations::CollectionProxy) + return attributes_collection + end + + model.instance_exec do + begin + old_options = nested_attributes_options[association_name] + nested_attributes_options[association_name] = options + + list = assign_nested_attributes_for_collection_association(association_name, attributes_collection) + + if destroy_missing + association(association_name).scope.excluding(list).each(&:destroy!) + end + + list + ensure + nested_attributes_options[association_name] = old_options + end + end + end end diff --git a/lib/active_interaction/extras/strong_params.rb b/lib/active_interaction/extras/strong_params.rb index 81ebea1..c367a69 100644 --- a/lib/active_interaction/extras/strong_params.rb +++ b/lib/active_interaction/extras/strong_params.rb @@ -37,8 +37,9 @@ def permitted_params end.flatten(1).compact if respond_to?(:nested_attribute_options) - nested_attribute_options.each do |attribute, _| - permissions << {"#{attribute}_attributes": {}} + nested_attribute_options.each do |attribute, opts| + permitted_list = opts.fetch(:permit) + permissions << {"#{attribute}_attributes": permitted_list} end end