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
-
#pointed_to ⇒ Object
Returns the value of attribute pointed_to.
-
#points_to ⇒ Object
Returns the value of attribute points_to.
-
#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
-
#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.
25 26 27 28 29 30 31 32 33 34 35 36 37 |
# 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.points_to = {} self.pointed_to = {} 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
#pointed_to ⇒ Object
Returns the value of attribute pointed_to.
15 16 17 |
# File 'lib/view_model/active_record/update_operation.rb', line 15 def pointed_to @pointed_to end |
#points_to ⇒ Object
Returns the value of attribute points_to.
15 16 17 |
# File 'lib/view_model/active_record/update_operation.rb', line 15 def points_to @points_to 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
256 257 258 259 260 261 262 263 264 |
# File 'lib/view_model/active_record/update_operation.rb', line 256 def add_update(association_data, update) target = case association_data.pointer_location when :remote then pointed_to when :local then points_to end target[association_data] = update end |
#build!(update_context) ⇒ Object
Recursively builds UpdateOperations for the associations in our UpdateData
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/view_model/active_record/update_operation.rb', line 221 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
45 46 47 |
# File 'lib/view_model/active_record/update_operation.rb', line 45 def built? @built end |
#propagate_tree_changes(association_data, child_changes) ⇒ Object
211 212 213 214 215 216 217 218 |
# File 'lib/view_model/active_record/update_operation.rb', line 211 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 |
#run!(deserialize_context:) ⇒ Object
Evaluate a built update tree, applying and saving changes to the models.
50 51 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 |
# File 'lib/view_model/active_record/update_operation.rb', line 50 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 # update user-specified attributes valid_members = viewmodel.class._members.keys.map(&:to_s).to_set bad_keys = attributes.keys.reject { |k| valid_members.include?(k) } if bad_keys.present? causes = bad_keys.map { |k| ViewModel::DeserializationError::UnknownAttribute.new(k, blame_reference) } raise ViewModel::DeserializationError::Collection.for_errors(causes) end attributes.each do |attr_name, serialized_value| # 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 # Update points-to associations before save points_to.each do |association_data, child_operation| reflection = association_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(association_data.association_name, context: deserialize_context) child_viewmodel = child_operation.run!(deserialize_context: child_ctx) propagate_tree_changes(association_data, child_viewmodel.previous_changes) child_viewmodel.model end association.writer(new_target) debug "<- #{debug_name}: Updated points-to association '#{reflection.name}'" end # validate deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, viewmodel) viewmodel.validate! # 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. pointed_to.each do |association_data, child_operation| 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
39 40 41 42 43 |
# File 'lib/view_model/active_record/update_operation.rb', line 39 def viewmodel_reference unless viewmodel.model.new_record? viewmodel.to_reference end end |