iknow_view_models 2.10.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +119 -0
- data/.travis.yml +31 -0
- data/Appraisals +6 -16
- data/gemfiles/{rails_7_0.gemfile → rails_6_0_beta.gemfile} +2 -2
- data/iknow_view_models.gemspec +3 -5
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model/active_record/association_data.rb +206 -92
- data/lib/view_model/active_record/association_manipulation.rb +22 -12
- data/lib/view_model/active_record/cache/cacheable_view.rb +3 -13
- data/lib/view_model/active_record/cache.rb +2 -2
- data/lib/view_model/active_record/cloner.rb +11 -11
- data/lib/view_model/active_record/controller.rb +0 -2
- data/lib/view_model/active_record/update_context.rb +21 -3
- data/lib/view_model/active_record/update_data.rb +43 -45
- data/lib/view_model/active_record/update_operation.rb +265 -153
- data/lib/view_model/active_record/visitor.rb +9 -6
- data/lib/view_model/active_record.rb +94 -74
- data/lib/view_model/after_transaction_runner.rb +3 -18
- data/lib/view_model/callbacks.rb +2 -2
- data/lib/view_model/changes.rb +24 -16
- data/lib/view_model/config.rb +6 -2
- data/lib/view_model/deserialization_error.rb +31 -0
- data/lib/view_model/deserialize_context.rb +2 -6
- data/lib/view_model/error_view.rb +6 -5
- data/lib/view_model/record/attribute_data.rb +11 -6
- data/lib/view_model/record.rb +44 -24
- data/lib/view_model/serialize_context.rb +2 -63
- data/lib/view_model/test_helpers/arvm_builder.rb +2 -4
- data/lib/view_model/traversal_context.rb +2 -2
- data/lib/view_model.rb +21 -13
- data/shell.nix +1 -1
- data/test/helpers/arvm_test_models.rb +4 -12
- data/test/helpers/arvm_test_utilities.rb +6 -0
- data/test/helpers/controller_test_helpers.rb +6 -6
- data/test/helpers/viewmodel_spec_helpers.rb +63 -52
- data/test/unit/view_model/access_control_test.rb +88 -37
- data/test/unit/view_model/active_record/belongs_to_test.rb +110 -178
- data/test/unit/view_model/active_record/cache_test.rb +11 -5
- data/test/unit/view_model/active_record/cloner_test.rb +1 -1
- data/test/unit/view_model/active_record/controller_test.rb +12 -20
- data/test/unit/view_model/active_record/has_many_test.rb +540 -316
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +12 -15
- data/test/unit/view_model/active_record/has_many_through_test.rb +15 -58
- data/test/unit/view_model/active_record/has_one_test.rb +288 -135
- data/test/unit/view_model/active_record/poly_test.rb +0 -1
- data/test/unit/view_model/active_record/shared_test.rb +21 -39
- data/test/unit/view_model/active_record/version_test.rb +3 -2
- data/test/unit/view_model/active_record_test.rb +5 -63
- data/test/unit/view_model/callbacks_test.rb +1 -0
- data/test/unit/view_model/record_test.rb +0 -32
- data/test/unit/view_model/traversal_context_test.rb +13 -12
- metadata +15 -25
- data/.github/workflows/gem-push.yml +0 -31
- data/.github/workflows/test.yml +0 -65
- data/gemfiles/rails_6_0.gemfile +0 -9
- data/gemfiles/rails_6_1.gemfile +0 -9
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +0 -58
@@ -40,10 +40,6 @@ class ViewModel::ActiveRecord
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
def deferred?
|
44
|
-
viewmodel.nil?
|
45
|
-
end
|
46
|
-
|
47
43
|
def built?
|
48
44
|
@built
|
49
45
|
end
|
@@ -112,9 +108,8 @@ class ViewModel::ActiveRecord
|
|
112
108
|
if child_operation
|
113
109
|
child_ctx = viewmodel.context_for_child(association_data.association_name, context: deserialize_context)
|
114
110
|
child_viewmodel = child_operation.run!(deserialize_context: child_ctx)
|
115
|
-
|
116
|
-
|
117
|
-
end
|
111
|
+
propagate_tree_changes(association_data, child_viewmodel.previous_changes)
|
112
|
+
|
118
113
|
child_viewmodel.model
|
119
114
|
end
|
120
115
|
association.writer(new_target)
|
@@ -153,9 +148,8 @@ class ViewModel::ActiveRecord
|
|
153
148
|
if child_operation
|
154
149
|
ViewModel::Utils.map_one_or_many(child_operation) do |op|
|
155
150
|
child_viewmodel = op.run!(deserialize_context: child_ctx)
|
156
|
-
|
157
|
-
|
158
|
-
end
|
151
|
+
propagate_tree_changes(association_data, child_viewmodel.previous_changes)
|
152
|
+
|
159
153
|
child_viewmodel.model
|
160
154
|
end
|
161
155
|
end
|
@@ -183,7 +177,11 @@ class ViewModel::ActiveRecord
|
|
183
177
|
child_hook_control.record_changes(changes)
|
184
178
|
end
|
185
179
|
|
186
|
-
|
180
|
+
if child_association_data.nested?
|
181
|
+
viewmodel.nested_children_changed!
|
182
|
+
elsif child_association_data.owned?
|
183
|
+
viewmodel.referenced_children_changed!
|
184
|
+
end
|
187
185
|
end
|
188
186
|
debug "<- #{debug_name}: Finished checking released children permissions"
|
189
187
|
end
|
@@ -208,9 +206,18 @@ class ViewModel::ActiveRecord
|
|
208
206
|
raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(ex, blame_reference)
|
209
207
|
end
|
210
208
|
|
209
|
+
def propagate_tree_changes(association_data, child_changes)
|
210
|
+
if association_data.nested?
|
211
|
+
viewmodel.nested_children_changed! if child_changes.changed_nested_tree?
|
212
|
+
viewmodel.referenced_children_changed! if child_changes.changed_referenced_children?
|
213
|
+
elsif association_data.owned?
|
214
|
+
viewmodel.referenced_children_changed! if child_changes.changed_owned_tree?
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
211
218
|
# Recursively builds UpdateOperations for the associations in our UpdateData
|
212
219
|
def build!(update_context)
|
213
|
-
raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation cannot build a deferred update") if
|
220
|
+
raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation cannot build a deferred update") if viewmodel.nil?
|
214
221
|
return self if built?
|
215
222
|
|
216
223
|
update_data.associations.each do |association_name, association_update_data|
|
@@ -231,8 +238,10 @@ class ViewModel::ActiveRecord
|
|
231
238
|
update =
|
232
239
|
if association_data.through?
|
233
240
|
build_updates_for_collection_referenced_association(association_data, reference_string, update_context)
|
241
|
+
elsif association_data.collection?
|
242
|
+
build_updates_for_collection_association(association_data, reference_string, update_context)
|
234
243
|
else
|
235
|
-
|
244
|
+
build_update_for_single_association(association_data, reference_string, update_context)
|
236
245
|
end
|
237
246
|
|
238
247
|
add_update(association_data, update)
|
@@ -254,38 +263,6 @@ class ViewModel::ActiveRecord
|
|
254
263
|
|
255
264
|
private
|
256
265
|
|
257
|
-
def build_update_for_single_referenced_association(association_data, reference_string, update_context)
|
258
|
-
# TODO intern loads for shared items so we only load them once
|
259
|
-
model = self.viewmodel.model
|
260
|
-
previous_child_viewmodel = model.public_send(association_data.direct_reflection.name).try do |previous_child_model|
|
261
|
-
vm_class = association_data.viewmodel_class_for_model!(previous_child_model.class)
|
262
|
-
vm_class.new(previous_child_model)
|
263
|
-
end
|
264
|
-
|
265
|
-
if reference_string.nil?
|
266
|
-
referred_update = nil
|
267
|
-
referred_viewmodel = nil
|
268
|
-
else
|
269
|
-
referred_update = update_context.resolve_reference(reference_string, blame_reference)
|
270
|
-
referred_viewmodel = referred_update.viewmodel
|
271
|
-
|
272
|
-
unless association_data.accepts?(referred_viewmodel.class)
|
273
|
-
raise ViewModel::DeserializationError::InvalidAssociationType.new(
|
274
|
-
association_data.association_name.to_s,
|
275
|
-
referred_viewmodel.class.view_name,
|
276
|
-
blame_reference)
|
277
|
-
end
|
278
|
-
|
279
|
-
referred_update.build!(update_context)
|
280
|
-
end
|
281
|
-
|
282
|
-
if previous_child_viewmodel != referred_viewmodel
|
283
|
-
viewmodel.association_changed!(association_data.association_name)
|
284
|
-
end
|
285
|
-
|
286
|
-
referred_update
|
287
|
-
end
|
288
|
-
|
289
266
|
# Resolve or construct viewmodels for incoming update data. Where a child
|
290
267
|
# hash references an existing model not currently attached to this parent,
|
291
268
|
# it must be found before recursing into that child. If the model is
|
@@ -321,6 +298,60 @@ class ViewModel::ActiveRecord
|
|
321
298
|
end
|
322
299
|
end
|
323
300
|
|
301
|
+
def resolve_referenced_viewmodels(association_data, update_datas, previous_child_viewmodels, update_context)
|
302
|
+
previous_child_viewmodels = Array.wrap(previous_child_viewmodels).index_by(&:to_reference)
|
303
|
+
|
304
|
+
ViewModel::Utils.map_one_or_many(update_datas) do |update_data|
|
305
|
+
if update_data.is_a?(UpdateData)
|
306
|
+
# Dummy child update data for an unmodified previous child of a
|
307
|
+
# functional update; create it an empty update operation.
|
308
|
+
viewmodel = previous_child_viewmodels.fetch(update_data.viewmodel_reference)
|
309
|
+
update = update_context.new_update(viewmodel, update_data)
|
310
|
+
next [update, viewmodel]
|
311
|
+
end
|
312
|
+
|
313
|
+
reference_string = update_data
|
314
|
+
child_update = update_context.resolve_reference(reference_string, blame_reference)
|
315
|
+
child_viewmodel = child_update.viewmodel
|
316
|
+
|
317
|
+
unless association_data.accepts?(child_viewmodel.class)
|
318
|
+
raise ViewModel::DeserializationError::InvalidAssociationType.new(
|
319
|
+
association_data.association_name.to_s,
|
320
|
+
child_viewmodel.class.view_name,
|
321
|
+
blame_reference)
|
322
|
+
end
|
323
|
+
|
324
|
+
child_ref = child_viewmodel.to_reference
|
325
|
+
|
326
|
+
# The case of two potential owners trying to claim a new referenced
|
327
|
+
# child is covered by set_reference_update_parent.
|
328
|
+
claimed = !association_data.owned? ||
|
329
|
+
child_update.update_data.new? ||
|
330
|
+
previous_child_viewmodels.has_key?(child_ref) ||
|
331
|
+
update_context.try_take_released_viewmodel(child_ref).present?
|
332
|
+
|
333
|
+
if claimed
|
334
|
+
[child_update, child_viewmodel]
|
335
|
+
else
|
336
|
+
# Return the reference to indicate a deferred update
|
337
|
+
[child_update, child_ref]
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def set_reference_update_parent(association_data, update, parent_data)
|
343
|
+
if update.reparent_to
|
344
|
+
# Another parent has already tried to take this (probably new)
|
345
|
+
# owned referenced view. It can only be claimed by one of them.
|
346
|
+
other_parent = update.reparent_to.viewmodel.to_reference
|
347
|
+
raise ViewModel::DeserializationError::DuplicateOwner.new(
|
348
|
+
association_data.association_name,
|
349
|
+
[blame_reference, other_parent])
|
350
|
+
end
|
351
|
+
|
352
|
+
update.reparent_to = parent_data
|
353
|
+
end
|
354
|
+
|
324
355
|
def build_update_for_single_association(association_data, association_update_data, update_context)
|
325
356
|
model = self.viewmodel.model
|
326
357
|
|
@@ -329,25 +360,63 @@ class ViewModel::ActiveRecord
|
|
329
360
|
vm_class.new(previous_child_model)
|
330
361
|
end
|
331
362
|
|
332
|
-
if
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
363
|
+
if association_data.pointer_location == :remote
|
364
|
+
if previous_child_viewmodel.present?
|
365
|
+
# Clear the cached association so that AR's save behaviour doesn't
|
366
|
+
# conflict with our explicit parent updates. If we still have a child
|
367
|
+
# after the update, we'll either call `Association#writer` or manually
|
368
|
+
# fix the target cache after recursing in run!(). If we don't, we promise
|
369
|
+
# that the child will no longer be attached in the database, so the new
|
370
|
+
# cached data of nil will be correct.
|
371
|
+
clear_association_cache(model, association_data.direct_reflection)
|
372
|
+
end
|
373
|
+
|
374
|
+
reparent_data =
|
375
|
+
ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
|
340
376
|
end
|
341
377
|
|
342
|
-
|
343
|
-
if
|
344
|
-
|
378
|
+
if association_update_data.present?
|
379
|
+
if association_data.referenced?
|
380
|
+
# resolve reference string
|
381
|
+
reference_string = association_update_data
|
382
|
+
child_update, child_viewmodel = resolve_referenced_viewmodels(association_data, reference_string,
|
383
|
+
previous_child_viewmodel, update_context)
|
384
|
+
|
385
|
+
if reparent_data
|
386
|
+
set_reference_update_parent(association_data, child_update, reparent_data)
|
387
|
+
end
|
388
|
+
|
389
|
+
if child_viewmodel.is_a?(ViewModel::Reference)
|
390
|
+
update_context.defer_update(child_viewmodel, child_update)
|
391
|
+
end
|
392
|
+
else
|
393
|
+
# Resolve direct children
|
394
|
+
child_viewmodel =
|
395
|
+
resolve_child_viewmodels(association_data, association_update_data, previous_child_viewmodel, update_context)
|
396
|
+
|
397
|
+
child_update =
|
398
|
+
if child_viewmodel.is_a?(ViewModel::Reference)
|
399
|
+
update_context.new_deferred_update(child_viewmodel, association_update_data, reparent_to: reparent_data)
|
400
|
+
else
|
401
|
+
update_context.new_update(child_viewmodel, association_update_data, reparent_to: reparent_data)
|
402
|
+
end
|
345
403
|
end
|
346
404
|
|
405
|
+
# Build the update if we've claimed it
|
406
|
+
unless child_viewmodel.is_a?(ViewModel::Reference)
|
407
|
+
child_update.build!(update_context)
|
408
|
+
end
|
409
|
+
else
|
410
|
+
child_update = nil
|
411
|
+
child_viewmodel = nil
|
412
|
+
end
|
413
|
+
|
414
|
+
# Handle changes
|
347
415
|
if previous_child_viewmodel != child_viewmodel
|
348
416
|
viewmodel.association_changed!(association_data.association_name)
|
349
|
-
|
350
|
-
if
|
417
|
+
|
418
|
+
# free previous child if present and owned
|
419
|
+
if previous_child_viewmodel.present? && association_data.owned?
|
351
420
|
if association_data.pointer_location == :local
|
352
421
|
# When we free a child that's pointed to from its old parent, we need to
|
353
422
|
# clear the cached association to that old parent. If we don't do this,
|
@@ -358,12 +427,7 @@ class ViewModel::ActiveRecord
|
|
358
427
|
# model.association(...).inverse_reflection_for(previous_child_model), but
|
359
428
|
# that's private.
|
360
429
|
|
361
|
-
inverse_reflection =
|
362
|
-
if association_data.direct_reflection.polymorphic?
|
363
|
-
association_data.direct_reflection.polymorphic_inverse_of(previous_child_viewmodel.model.class)
|
364
|
-
else
|
365
|
-
association_data.direct_reflection.inverse_of
|
366
|
-
end
|
430
|
+
inverse_reflection = association_data.direct_reflection_inverse(previous_child_viewmodel.model.class)
|
367
431
|
|
368
432
|
if inverse_reflection.present?
|
369
433
|
clear_association_cache(previous_child_viewmodel.model, inverse_reflection)
|
@@ -374,25 +438,7 @@ class ViewModel::ActiveRecord
|
|
374
438
|
end
|
375
439
|
end
|
376
440
|
|
377
|
-
|
378
|
-
if child_viewmodel.present?
|
379
|
-
# If the association's pointer is in the child, need to provide it with a
|
380
|
-
# ParentData to update
|
381
|
-
parent_data =
|
382
|
-
if association_data.pointer_location == :remote
|
383
|
-
ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
|
384
|
-
else
|
385
|
-
nil
|
386
|
-
end
|
387
|
-
|
388
|
-
case child_viewmodel
|
389
|
-
when ViewModel::Reference # deferred
|
390
|
-
vm_ref = child_viewmodel
|
391
|
-
update_context.new_deferred_update(vm_ref, association_update_data, reparent_to: parent_data)
|
392
|
-
else
|
393
|
-
update_context.new_update(child_viewmodel, association_update_data, reparent_to: parent_data).build!(update_context)
|
394
|
-
end
|
395
|
-
end
|
441
|
+
child_update
|
396
442
|
end
|
397
443
|
|
398
444
|
def build_updates_for_collection_association(association_data, association_update, update_context)
|
@@ -402,11 +448,13 @@ class ViewModel::ActiveRecord
|
|
402
448
|
parent_data = ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
|
403
449
|
|
404
450
|
# load children already attached to this model
|
405
|
-
child_viewmodel_class
|
451
|
+
child_viewmodel_class = association_data.viewmodel_class
|
452
|
+
|
406
453
|
previous_child_viewmodels =
|
407
454
|
model.public_send(association_data.direct_reflection.name).map do |child_model|
|
408
455
|
child_viewmodel_class.new(child_model)
|
409
456
|
end
|
457
|
+
|
410
458
|
if child_viewmodel_class._list_member?
|
411
459
|
previous_child_viewmodels.sort_by!(&:_list_attribute)
|
412
460
|
end
|
@@ -421,114 +469,178 @@ class ViewModel::ActiveRecord
|
|
421
469
|
clear_association_cache(model, association_data.direct_reflection)
|
422
470
|
end
|
423
471
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
472
|
+
# Update contents are either UpdateData in the case of a nested
|
473
|
+
# association, or reference strings in the case of a reference association.
|
474
|
+
# The former are resolved with resolve_child_viewmodels, the latter with
|
475
|
+
# resolve_referenced_viewmodels.
|
476
|
+
resolve_child_data_reference = ->(child_data) do
|
477
|
+
case child_data
|
478
|
+
when UpdateData
|
479
|
+
child_data.viewmodel_reference if child_data.id
|
480
|
+
when String
|
481
|
+
update_context.resolve_reference(child_data, nil).viewmodel_reference
|
482
|
+
else
|
483
|
+
raise ViewModel::DeserializationError::Internal.new(
|
484
|
+
"Unexpected child data type in collection update: #{child_data.class.name}")
|
485
|
+
end
|
486
|
+
end
|
428
487
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
UpdateData.empty_update_for(previous_child_viewmodel)
|
433
|
-
end
|
488
|
+
case association_update
|
489
|
+
when AbstractCollectionUpdate::Replace
|
490
|
+
child_datas = association_update.contents
|
434
491
|
|
435
|
-
|
492
|
+
when AbstractCollectionUpdate::Functional
|
493
|
+
# A fupdate isn't permitted to edit the same model twice.
|
494
|
+
association_update.check_for_duplicates!(update_context, blame_reference)
|
436
495
|
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
child_datas = child_datas.reject { |child| moved_refs.include?(child.viewmodel_reference) }
|
496
|
+
# Construct empty updates for previous children
|
497
|
+
child_datas =
|
498
|
+
previous_child_viewmodels.map do |previous_child_viewmodel|
|
499
|
+
UpdateData.empty_update_for(previous_child_viewmodel)
|
500
|
+
end
|
443
501
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
502
|
+
# Insert or replace with either real UpdateData or reference strings
|
503
|
+
association_update.actions.each do |fupdate|
|
504
|
+
case fupdate
|
505
|
+
when FunctionalUpdate::Append
|
506
|
+
# If we're referring to existing members, ensure that they're removed before we append/insert
|
507
|
+
existing_refs = fupdate.contents
|
508
|
+
.map(&resolve_child_data_reference)
|
509
|
+
.to_set
|
510
|
+
|
511
|
+
child_datas.reject! do |child_data|
|
512
|
+
child_ref = resolve_child_data_reference.(child_data)
|
513
|
+
child_ref && existing_refs.include?(child_ref)
|
514
|
+
end
|
450
515
|
|
451
|
-
|
452
|
-
|
516
|
+
if fupdate.before || fupdate.after
|
517
|
+
rel_ref = fupdate.before || fupdate.after
|
453
518
|
|
454
|
-
|
455
|
-
|
519
|
+
# Find the relative insert location. This might be an empty
|
520
|
+
# UpdateData from a previous child or an already-fupdated
|
521
|
+
# reference string.
|
522
|
+
index = child_datas.find_index do |child_data|
|
523
|
+
rel_ref == resolve_child_data_reference.(child_data)
|
524
|
+
end
|
456
525
|
|
526
|
+
unless index
|
527
|
+
raise ViewModel::DeserializationError::AssociatedNotFound.new(
|
528
|
+
association_data.association_name.to_s, rel_ref, blame_reference)
|
457
529
|
end
|
458
530
|
|
459
|
-
|
460
|
-
|
461
|
-
|
531
|
+
index += 1 if fupdate.after
|
532
|
+
child_datas.insert(index, *fupdate.contents)
|
533
|
+
|
534
|
+
else
|
535
|
+
child_datas.concat(fupdate.contents)
|
536
|
+
end
|
462
537
|
|
463
|
-
|
464
|
-
|
465
|
-
|
538
|
+
when FunctionalUpdate::Remove
|
539
|
+
removed_refs = fupdate.removed_vm_refs.to_set
|
540
|
+
child_datas.reject! do |child_data|
|
541
|
+
child_ref = resolve_child_data_reference.(child_data)
|
542
|
+
removed_refs.include?(child_ref)
|
543
|
+
end
|
466
544
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
end
|
545
|
+
when FunctionalUpdate::Update
|
546
|
+
# Already guaranteed that each ref has a single existing child attached
|
547
|
+
new_child_datas = fupdate.contents.index_by(&resolve_child_data_reference)
|
471
548
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
end
|
477
|
-
else
|
478
|
-
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
479
|
-
"Unknown functional update type: '#{fupdate.type}'",
|
480
|
-
blame_reference)
|
549
|
+
# Replace matched child_datas with the update contents.
|
550
|
+
child_datas.map! do |child_data|
|
551
|
+
child_ref = resolve_child_data_reference.(child_data)
|
552
|
+
new_child_datas.delete(child_ref) { child_data }
|
481
553
|
end
|
554
|
+
|
555
|
+
# Assertion that all values in the update were found in child_datas
|
556
|
+
unless new_child_datas.empty?
|
557
|
+
raise ViewModel::DeserializationError::AssociatedNotFound.new(
|
558
|
+
association_data.association_name.to_s, new_child_datas.keys, blame_reference)
|
559
|
+
end
|
560
|
+
else
|
561
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
562
|
+
"Unknown functional update type: '#{fupdate.type}'",
|
563
|
+
blame_reference)
|
482
564
|
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
if association_data.referenced?
|
569
|
+
# child_datas are either pre-resolved UpdateData (for non-fupdated
|
570
|
+
# existing members) or reference strings. Resolve into pairs of
|
571
|
+
# [UpdateOperation, ViewModel] if we can create or claim the
|
572
|
+
# UpdateOperation or [UpdateOperation, ViewModelReference] otherwise.
|
573
|
+
resolved_children =
|
574
|
+
resolve_referenced_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)
|
483
575
|
|
484
|
-
|
576
|
+
resolved_children.each do |child_update, child_viewmodel|
|
577
|
+
set_reference_update_parent(association_data, child_update, parent_data)
|
578
|
+
|
579
|
+
if child_viewmodel.is_a?(ViewModel::Reference)
|
580
|
+
update_context.defer_update(child_viewmodel, child_update)
|
581
|
+
end
|
485
582
|
end
|
486
583
|
|
487
|
-
|
584
|
+
else
|
585
|
+
# child datas are all UpdateData
|
586
|
+
child_viewmodels = resolve_child_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)
|
488
587
|
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
588
|
+
resolved_children = child_datas.zip(child_viewmodels).map do |child_data, child_viewmodel|
|
589
|
+
child_update =
|
590
|
+
if child_viewmodel.is_a?(ViewModel::Reference)
|
591
|
+
update_context.new_deferred_update(child_viewmodel, child_data, reparent_to: parent_data)
|
592
|
+
else
|
593
|
+
update_context.new_update(child_viewmodel, child_data, reparent_to: parent_data)
|
594
|
+
end
|
595
|
+
|
596
|
+
[child_update, child_viewmodel]
|
496
597
|
end
|
497
598
|
end
|
498
599
|
|
499
600
|
# Calculate new positions for children if in a list. Ignore previous
|
500
|
-
# positions for unresolved references: they'll always
|
501
|
-
# anyway since their parent pointer will change.
|
502
|
-
new_positions = Array.new(
|
601
|
+
# positions (i.e. return nil) for unresolved references: they'll always
|
602
|
+
# need to be updated anyway since their parent pointer will change.
|
603
|
+
new_positions = Array.new(resolved_children.length)
|
503
604
|
|
504
605
|
if association_data.viewmodel_class._list_member?
|
505
606
|
set_position = ->(index, pos) { new_positions[index] = pos }
|
607
|
+
|
506
608
|
get_previous_position = ->(index) do
|
507
|
-
vm =
|
609
|
+
vm = resolved_children[index][1]
|
508
610
|
vm._list_attribute unless vm.is_a?(ViewModel::Reference)
|
509
611
|
end
|
510
612
|
|
511
613
|
ActsAsManualList.update_positions(
|
512
|
-
(0...
|
614
|
+
(0...resolved_children.size).to_a, # indexes
|
513
615
|
position_getter: get_previous_position,
|
514
616
|
position_setter: set_position)
|
515
617
|
end
|
516
618
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
619
|
+
resolved_children.zip(new_positions).each do |(child_update, child_viewmodel), new_position|
|
620
|
+
child_update.reposition_to = new_position
|
621
|
+
|
622
|
+
# Recurse into building child updates that we've claimed
|
623
|
+
unless child_viewmodel.is_a?(ViewModel::Reference)
|
624
|
+
child_update.build!(update_context)
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
628
|
+
child_updates, child_viewmodels = resolved_children.transpose.presence || [[], []]
|
629
|
+
|
630
|
+
# if the new children differ, including in order, mark that this
|
631
|
+
# association has changed and release any no-longer-attached children
|
632
|
+
if child_viewmodels != previous_child_viewmodels
|
633
|
+
viewmodel.association_changed!(association_data.association_name)
|
634
|
+
|
635
|
+
released_child_viewmodels = previous_child_viewmodels - child_viewmodels
|
636
|
+
released_child_viewmodels.each do |vm|
|
637
|
+
release_viewmodel(vm, association_data, update_context)
|
525
638
|
end
|
526
639
|
end
|
527
640
|
|
528
641
|
child_updates
|
529
642
|
end
|
530
643
|
|
531
|
-
|
532
644
|
class ReferencedCollectionMember
|
533
645
|
attr_reader :indirect_viewmodel_reference, :direct_viewmodel
|
534
646
|
attr_accessor :ref_string, :position
|
@@ -694,7 +806,7 @@ class ViewModel::ActiveRecord
|
|
694
806
|
target_collection = MutableReferencedCollection.new(
|
695
807
|
association_data, update_context, previous_members, blame_reference)
|
696
808
|
|
697
|
-
# All updates to
|
809
|
+
# All updates to referenced collections produce a complete target list of
|
698
810
|
# ReferencedCollectionMembers including a ViewModel::Reference to the
|
699
811
|
# indirect child, and an existing (from previous) or new ViewModel for the
|
700
812
|
# direct child.
|
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class ViewModel::ActiveRecord::Visitor
|
4
|
-
attr_reader :visit_shared, :for_edit
|
4
|
+
attr_reader :visit_referenced, :visit_shared, :for_edit
|
5
5
|
|
6
|
-
def initialize(visit_shared: true, for_edit: false)
|
7
|
-
@
|
8
|
-
@
|
6
|
+
def initialize(visit_referenced: true, visit_shared: true, for_edit: false)
|
7
|
+
@visit_referenced = visit_referenced
|
8
|
+
@visit_shared = visit_shared
|
9
|
+
@for_edit = for_edit
|
9
10
|
end
|
10
11
|
|
11
12
|
def visit(view, context: nil)
|
@@ -29,8 +30,10 @@ class ViewModel::ActiveRecord::Visitor
|
|
29
30
|
# visit the underlying viewmodel for each association, ignoring any
|
30
31
|
# customization
|
31
32
|
view.class._members.each do |name, member_data|
|
32
|
-
next unless member_data.
|
33
|
-
next if member_data.
|
33
|
+
next unless member_data.association?
|
34
|
+
next if member_data.referenced? && !visit_referenced
|
35
|
+
next if !member_data.owned? && !visit_shared
|
36
|
+
|
34
37
|
children = Array.wrap(view._read_association(name))
|
35
38
|
children.each do |child|
|
36
39
|
if context
|