Class: ViewModel::ActiveRecord::UpdateOperation
- Inherits:
-
Object
- Object
- ViewModel::ActiveRecord::UpdateOperation
- Includes:
- ErrorWrapping
- Defined in:
- lib/view_model/active_record/update_operation.rb
Defined Under Namespace
Classes: MutableReferencedCollection, ParentData, ReferencedCollectionMember
Instance Attribute Summary collapse
-
#association_updates ⇒ Object
Returns the value of attribute association_updates.
-
#released_children ⇒ Object
Returns the value of attribute released_children.
-
#reparent_to ⇒ Object
Returns the value of attribute reparent_to.
-
#reposition_to ⇒ Object
Returns the value of attribute reposition_to.
-
#update_data ⇒ Object
Returns the value of attribute update_data.
-
#viewmodel ⇒ Object
Returns the value of attribute viewmodel.
Instance Method Summary collapse
- #add_update(association_data, update) ⇒ Object
-
#build!(update_context) ⇒ Object
Recursively builds UpdateOperations for the associations in our UpdateData.
- #built? ⇒ Boolean
-
#initialize(viewmodel, update_data, reparent_to: nil, reposition_to: nil) ⇒ UpdateOperation
constructor
A new instance of UpdateOperation.
- #propagate_tree_changes(association_data, child_changes) ⇒ Object
- #reference_only? ⇒ Boolean
-
#run!(deserialize_context:) ⇒ Object
Evaluate a built update tree, applying and saving changes to the models.
- #viewmodel_reference ⇒ Object
Methods included from ErrorWrapping
Constructor Details
#initialize(viewmodel, update_data, reparent_to: nil, reposition_to: nil) ⇒ UpdateOperation
Returns a new instance of UpdateOperation.
25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/view_model/active_record/update_operation.rb', line 25 def initialize(viewmodel, update_data, reparent_to: nil, reposition_to: nil) self.viewmodel = viewmodel self.update_data = update_data self.association_updates = {} self.reparent_to = reparent_to self.reposition_to = reposition_to self.released_children = [] @run_state = RunState::Pending @changed_associations = [] @built = false end |
Instance Attribute Details
#association_updates ⇒ Object
Returns the value of attribute association_updates.
16 17 18 |
# File 'lib/view_model/active_record/update_operation.rb', line 16 def association_updates @association_updates end |
#released_children ⇒ Object
Returns the value of attribute released_children.
16 17 18 |
# File 'lib/view_model/active_record/update_operation.rb', line 16 def released_children @released_children end |
#reparent_to ⇒ Object
Returns the value of attribute reparent_to.
16 17 18 |
# File 'lib/view_model/active_record/update_operation.rb', line 16 def reparent_to @reparent_to end |
#reposition_to ⇒ Object
Returns the value of attribute reposition_to.
16 17 18 |
# File 'lib/view_model/active_record/update_operation.rb', line 16 def reposition_to @reposition_to end |
#update_data ⇒ Object
Returns the value of attribute update_data.
16 17 18 |
# File 'lib/view_model/active_record/update_operation.rb', line 16 def update_data @update_data end |
#viewmodel ⇒ Object
Returns the value of attribute viewmodel.
16 17 18 |
# File 'lib/view_model/active_record/update_operation.rb', line 16 def viewmodel @viewmodel end |
Instance Method Details
#add_update(association_data, update) ⇒ Object
268 269 270 |
# File 'lib/view_model/active_record/update_operation.rb', line 268 def add_update(association_data, update) self.association_updates[association_data] = update end |
#build!(update_context) ⇒ Object
Recursively builds UpdateOperations for the associations in our UpdateData
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/view_model/active_record/update_operation.rb', line 233 def build!(update_context) raise ViewModel::DeserializationError::Internal.new('Internal error: UpdateOperation cannot build a deferred update') if viewmodel.nil? return self if built? update_data.associations.each do |association_name, association_update_data| association_data = self.viewmodel.class._association_data(association_name) update = if association_data.collection? build_updates_for_collection_association(association_data, association_update_data, update_context) else build_update_for_single_association(association_data, association_update_data, update_context) end add_update(association_data, update) end update_data.referenced_associations.each do |association_name, reference_string| association_data = self.viewmodel.class._association_data(association_name) update = if association_data.through? build_updates_for_collection_referenced_association(association_data, reference_string, update_context) elsif association_data.collection? build_updates_for_collection_association(association_data, reference_string, update_context) else build_update_for_single_association(association_data, reference_string, update_context) end add_update(association_data, update) end @built = true self end |
#built? ⇒ Boolean
44 45 46 |
# File 'lib/view_model/active_record/update_operation.rb', line 44 def built? @built end |
#propagate_tree_changes(association_data, child_changes) ⇒ Object
223 224 225 226 227 228 229 230 |
# File 'lib/view_model/active_record/update_operation.rb', line 223 def propagate_tree_changes(association_data, child_changes) if association_data.nested? viewmodel.nested_children_changed! if child_changes.changed_nested_tree? viewmodel.referenced_children_changed! if child_changes.changed_referenced_children? elsif association_data.owned? viewmodel.referenced_children_changed! if child_changes.changed_owned_tree? end end |
#reference_only? ⇒ Boolean
48 49 50 |
# File 'lib/view_model/active_record/update_operation.rb', line 48 def reference_only? update_data.reference_only? && reparent_to.nil? && reposition_to.nil? end |
#run!(deserialize_context:) ⇒ Object
Evaluate a built update tree, applying and saving changes to the models.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/view_model/active_record/update_operation.rb', line 53 def run!(deserialize_context:) raise ViewModel::DeserializationError::Internal.new('Internal error: UpdateOperation run before build') unless built? case @run_state when RunState::Running raise ViewModel::DeserializationError::Internal.new('Internal error: Cycle found in running UpdateOperation') when RunState::Run return viewmodel end @run_state = RunState::Running model = viewmodel.model debug_name = "#{model.class.name}:#{model.id || '<new>'}" debug "-> #{debug_name}: Entering" model.class.transaction do # Run context and viewmodel hooks wrap_active_record_errors(self.blame_reference) do ViewModel::Callbacks.wrap_deserialize(viewmodel, deserialize_context: deserialize_context) do |hook_control| # update parent association if reparent_to.present? debug "-> #{debug_name}: Updating parent pointer to '#{reparent_to.viewmodel.class.view_name}:#{reparent_to.viewmodel.id}'" association = model.association(reparent_to.association_reflection.name) association.writer(reparent_to.viewmodel.model) debug "<- #{debug_name}: Updated parent pointer" end # update position if reposition_to.present? debug "-> #{debug_name}: Updating position to #{reposition_to}" viewmodel._list_attribute = reposition_to end # Visit attributes and associations as much as possible in the order # that they're declared in the view. We can visit attributes and # points-to associations before save, but points-from associations # must be visited after save. pre_save_members, post_save_members = viewmodel.class._members.values.partition do |member_data| !member_data.association? || member_data.pointer_location == :local end pre_save_members.each do |member_data| if member_data.association? next unless association_updates.include?(member_data) child_operation = association_updates[member_data] reflection = member_data.direct_reflection debug "-> #{debug_name}: Updating points-to association '#{reflection.name}'" association = model.association(reflection.name) new_target = if child_operation child_ctx = viewmodel.context_for_child(member_data.association_name, context: deserialize_context) child_viewmodel = child_operation.run!(deserialize_context: child_ctx) propagate_tree_changes(member_data, child_viewmodel.previous_changes) child_viewmodel.model end association.writer(new_target) debug "<- #{debug_name}: Updated points-to association '#{reflection.name}'" else attr_name = member_data.name next unless attributes.include?(attr_name) serialized_value = attributes[attr_name] # Note that the VM::AR deserialization tree asserts ownership over any # references it's provided, and so they're intentionally not passed on # to attribute deserialization for use by their `using:` viewmodels. A # (better?) alternative would be to provide them as reference-only # hashes, to indicate that no modification can be permitted. viewmodel.public_send("deserialize_#{attr_name}", serialized_value, references: {}, deserialize_context: deserialize_context) end end # If a request makes no assertions about the model, we don't demand # that the current state of the model is valid. This permits making # edits to other models that refer to this model when this model is # invalid. unless reference_only? && !viewmodel.new_model? deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, viewmodel) viewmodel.validate! end # Save if the model has been altered. Covers not only models with # view changes but also lock version assertions. if viewmodel.model.changed? || viewmodel.model.new_record? debug "-> #{debug_name}: Saving" model.save! debug "<- #{debug_name}: Saved" end # Update association cache of pointed-from associations after save: the # child update will have saved the pointer. post_save_members.each do |association_data| next unless association_updates.include?(association_data) child_operation = association_updates[association_data] reflection = association_data.direct_reflection debug "-> #{debug_name}: Updating pointed-to association '#{reflection.name}'" association = model.association(reflection.name) child_ctx = viewmodel.context_for_child(association_data.association_name, context: deserialize_context) new_target = if child_operation ViewModel::Utils.map_one_or_many(child_operation) do |op| child_viewmodel = op.run!(deserialize_context: child_ctx) propagate_tree_changes(association_data, child_viewmodel.previous_changes) child_viewmodel.model end end association.target = new_target debug "<- #{debug_name}: Updated pointed-to association '#{reflection.name}'" end if self.released_children.present? # Released children that were not reclaimed by other parents during the # build phase will be deleted: check access control. debug "-> #{debug_name}: Checking released children permissions" self.released_children.reject(&:claimed?).each do |released_child| debug "-> #{debug_name}: Checking #{released_child.viewmodel.to_reference}" child_vm = released_child.viewmodel child_association_data = released_child.association_data child_ctx = viewmodel.context_for_child(child_association_data.association_name, context: deserialize_context) ViewModel::Callbacks.wrap_deserialize(child_vm, deserialize_context: child_ctx) do |child_hook_control| changes = ViewModel::Changes.new(deleted: true) child_ctx.run_callback(ViewModel::Callbacks::Hook::OnChange, child_vm, changes: changes) child_hook_control.record_changes(changes) end if child_association_data.nested? viewmodel.nested_children_changed! elsif child_association_data.owned? viewmodel.referenced_children_changed! end end debug "<- #{debug_name}: Finished checking released children permissions" end final_changes = viewmodel.clear_changes! if final_changes.changed? # Now that the change has been fully attempted, call the OnChange # hook if local changes were made deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, viewmodel, changes: final_changes) end hook_control.record_changes(final_changes) end end end debug "<- #{debug_name}: Leaving" @run_state = RunState::Run viewmodel end |
#viewmodel_reference ⇒ Object
38 39 40 41 42 |
# File 'lib/view_model/active_record/update_operation.rb', line 38 def viewmodel_reference unless viewmodel.model.new_record? viewmodel.to_reference end end |