Class: ViewModel::ActiveRecord::UpdateOperation
- Inherits:
-
Object
- Object
- ViewModel::ActiveRecord::UpdateOperation
- 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
Constructor Details
#initialize(viewmodel, update_data, reparent_to: nil, reposition_to: nil) ⇒ UpdateOperation
Returns a new instance of UpdateOperation.
24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/view_model/active_record/update_operation.rb', line 24 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.
15 16 17 |
# File 'lib/view_model/active_record/update_operation.rb', line 15 def association_updates @association_updates end |
#released_children ⇒ Object
Returns the value of attribute released_children.
15 16 17 |
# File 'lib/view_model/active_record/update_operation.rb', line 15 def released_children @released_children end |
#reparent_to ⇒ Object
Returns the value of attribute reparent_to.
15 16 17 |
# File 'lib/view_model/active_record/update_operation.rb', line 15 def reparent_to @reparent_to end |
#reposition_to ⇒ Object
Returns the value of attribute reposition_to.
15 16 17 |
# File 'lib/view_model/active_record/update_operation.rb', line 15 def reposition_to @reposition_to end |
#update_data ⇒ Object
Returns the value of attribute update_data.
15 16 17 |
# File 'lib/view_model/active_record/update_operation.rb', line 15 def update_data @update_data end |
#viewmodel ⇒ Object
Returns the value of attribute viewmodel.
15 16 17 |
# File 'lib/view_model/active_record/update_operation.rb', line 15 def viewmodel @viewmodel end |
Instance Method Details
#add_update(association_data, update) ⇒ Object
273 274 275 |
# File 'lib/view_model/active_record/update_operation.rb', line 273 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
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 267 268 269 270 271 |
# File 'lib/view_model/active_record/update_operation.rb', line 238 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
43 44 45 |
# File 'lib/view_model/active_record/update_operation.rb', line 43 def built? @built end |
#propagate_tree_changes(association_data, child_changes) ⇒ Object
228 229 230 231 232 233 234 235 |
# File 'lib/view_model/active_record/update_operation.rb', line 228 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
47 48 49 |
# File 'lib/view_model/active_record/update_operation.rb', line 47 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.
52 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 222 223 224 225 226 |
# File 'lib/view_model/active_record/update_operation.rb', line 52 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 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" begin model.save! rescue ::ActiveRecord::RecordInvalid => ex raise ViewModel::DeserializationError::Validation.from_active_model(ex.errors, blame_reference) rescue ::ActiveRecord::StaleObjectError => _ex raise ViewModel::DeserializationError::LockFailure.new(blame_reference) end 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 debug "<- #{debug_name}: Leaving" @run_state = RunState::Run viewmodel rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::InvalidForeignKey, ::ActiveRecord::RecordNotSaved => ex raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(ex, blame_reference) end |
#viewmodel_reference ⇒ Object
37 38 39 40 41 |
# File 'lib/view_model/active_record/update_operation.rb', line 37 def viewmodel_reference unless viewmodel.model.new_record? viewmodel.to_reference end end |