module ReactiveRecord module Setters def set_attr_value(attr, raw_value) set_common(attr, raw_value) { |value| update_simple_attribute(attr, value) } end def set_ar_aggregate(aggr, raw_value) set_common(aggr.attribute, raw_value) do |value, attr| @attributes[attr] ||= aggr.klass.new if new? abr = @attributes[attr].backing_record abr.virgin = false map = value.attributes if value aggr.mapped_attributes.each do |mapped_attr| abr.update_aggregate_attribute mapped_attr, map && map[mapped_attr] end return @attributes[attr] end end def set_non_ar_aggregate(aggregation, raw_value) set_common(aggregation.attribute, raw_value) do |value, attr| if data_loading? @synced_attributes[attr] = aggregation.deserialize(aggregation.serialize(value)) else changed = !@synced_attributes.key?(attr) || @synced_attributes[attr] != value end set_attribute_change_status_and_notify attr, changed, value end end def set_has_many(assoc, raw_value) set_common(assoc.attribute, raw_value) do |value, attr| # create a new collection to hold value, shove it in, and return the new collection # the replace method will take care of updating the inverse belongs_to links as # the collection is overwritten collection = Collection.new(assoc.klass, @ar_instance, assoc) collection.replace(value || []) @synced_attributes[attr] = value if data_loading? set_attribute_change_status_and_notify attr, value != @synced_attributes[attr], collection return collection end end def set_belongs_to(assoc, raw_value) set_common(assoc.attribute, raw_value) do |value, attr| current_value = @attributes[assoc.attribute] update_has_many_through_associations assoc, nil, current_value, :remove_member update_has_many_through_associations assoc, nil, value, :add_member remove_current_inverse_attribute assoc, nil, current_value add_new_inverse_attribute assoc, nil, value update_belongs_to attr, value.itself end end def set_belongs_to_via_has_many(orig, value) assoc = orig.inverse attr = assoc.attribute current_value = @attributes[attr] update_has_many_through_associations assoc, orig, current_value, :remove_member update_has_many_through_associations assoc, orig, value, :add_member remove_current_inverse_attribute assoc, orig, current_value add_new_inverse_attribute assoc, orig, value update_belongs_to attr, value.itself end def sync_has_many(attr) set_change_status_and_notify_only attr, @attributes[attr] != @synced_attributes[attr] end def update_simple_attribute(attr, value) if data_loading? @synced_attributes[attr] = value else changed = !@synced_attributes.key?(attr) || @synced_attributes[attr] != value end set_attribute_change_status_and_notify attr, changed, value end alias update_belongs_to update_simple_attribute alias update_aggregate_attribute update_simple_attribute private def set_common(attr, value) value = convert(attr, value) @virgin = false unless data_loading? if !@destroyed && ( !@attributes.key?(attr) || @attributes[attr].is_a?(Base::DummyValue) || @attributes[attr] != value) yield value, attr end value end def set_attribute_change_status_and_notify(attr, changed, new_value) if @virgin || @being_destroyed @attributes[attr] = new_value else change_status_and_notify_helper(attr, changed) do |had_key, current_value| @attributes[attr] = new_value if !data_loading? || (on_opal_client? && had_key && current_value.loaded? && current_value != new_value) Hyperstack::Internal::State::Variable.set(self, attr, new_value, data_loading?) end end end end def set_change_status_and_notify_only(attr, changed) return if @virgin || @being_destroyed change_status_and_notify_helper(attr, changed) do Hyperstack::Internal::State::Variable.set(self, attr, nil) unless data_loading? end end def change_status_and_notify_helper(attr, changed) empty_before = changed_attributes.empty? if !changed || data_loading? changed_attributes.delete(attr) elsif !changed_attributes.include?(attr) changed_attributes << attr end yield @attributes.key?(attr), @attributes[attr] return unless empty_before != changed_attributes.empty? if on_opal_client? && !data_loading? Hyperstack::Internal::State::Variable.set(self, '!CHANGED!', !changed_attributes.empty?, true) end return unless aggregate_owner aggregate_owner.set_change_status_and_notify_only( attr, !@attributes[attr].backing_record.changed_attributes.empty? ) end # when updating the inverse attribute of a belongs_to that is itself a belongs_to # (i.e. 1-1 relationship) we clear the existing inverse value and then # write the current record to the new value # when updating an inverse attribute of a belongs_to that is a has_many (i.e. a collection) # we need to first remove the current associated value (if non-nil), then add the new # value to the collection. If the inverse collection is not yet initialized we do it here. # the above is split into three methods, because the inverse of apolymorphic belongs to may # change from has_one to has_many. So we first deal with the current value, then # update the new value which uses the push_onto_collection helper def remove_current_inverse_attribute(association, orig, model) return if model.nil? inverse_association = association.inverse(model) return if inverse_association == orig if inverse_association.collection? # note we don't have to check if the collection exists, since it must # exist as at this ar_instance is already part of it. model.attributes[inverse_association.attribute].delete(@ar_instance) else model.attributes[inverse_association.attribute] = nil end end def add_new_inverse_attribute(association, orig, model) return if model.nil? inverse_association = association.inverse(model) return if inverse_association == orig if inverse_association.collection? model.backing_record.push_onto_collection(@model, inverse_association, @ar_instance) else inverse_attr = inverse_association.attribute model.attributes[inverse_attr] = @ar_instance return if data_loading? Hyperstack::Internal::State::Variable.set(model.backing_record, inverse_attr, @ar_instance) end end def push_onto_collection(model, association, ar_instance) @attributes[association.attribute] ||= Collection.new(model, @ar_instance, association) @attributes[association.attribute]._internal_push ar_instance end # class Membership < ActiveRecord::Base # belongs_to :uzer # belongs_to :memerable, polymorphic: true # end # # class Project < ActiveRecord::Base # has_many :memberships, as: :memerable, dependent: :destroy # has_many :uzers, through: :memberships # end # # class Group < ActiveRecord::Base # has_many :memberships, as: :memerable, dependent: :destroy # has_many :uzers, through: :memberships # end # # class Uzer < ActiveRecord::Base # has_many :memberships # has_many :groups, through: :memberships, source: :memerable, source_type: 'Group' # has_many :projects, through: :memberships, source: :memerable, source_type: 'Project' # end # membership.uzer = some_new_uzer (i.e. through association is changing) # means membership.some_new_uzer.(groups OR projects) << uzer.memberable (depending on type of memberable) # and we have to remove the current value of the source association (memerable) from the current uzer group or project # and we have to then find any inverse has_many_through association (i.e. group or projects.uzers) and delete the # current value from those collections and push the new value on def update_has_many_through_associations(assoc, orig, value, method) return if value.nil? assoc.through_associations(value).each do |ta| next if orig == ta source_value = @attributes[ta.source] # skip if source value is nil or if type of the association does not match type of source next unless source_value.class.to_s == ta.source_type ta.send method, source_value, value ta.source_associations(source_value).each do |sa| sa.send method, value, source_value end end end # def remove_src_assoc(sa, source_value, current_value) # source_inverse_collection = source_value.attributes[sa.attribute] # source_inverse_collection.delete(current_value) if source_inverse_collection # end # # def add_src_assoc(sa, source_value, new_value) # source_value.attributes[sa.attribute] ||= Collection.new(sa.owner_class, source_value, sa) # source_value.attributes[sa.attribute] << new_value # end end end