iknow_view_models 3.4.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/iknow_view_models.gemspec +2 -2
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model.rb +12 -0
- data/lib/view_model/active_record.rb +46 -5
- data/lib/view_model/active_record/association_data.rb +3 -1
- data/lib/view_model/active_record/association_manipulation.rb +186 -49
- data/lib/view_model/active_record/cloner.rb +13 -12
- data/lib/view_model/active_record/collection_nested_controller.rb +5 -2
- data/lib/view_model/active_record/controller_base.rb +45 -6
- data/lib/view_model/active_record/nested_controller_base.rb +126 -14
- data/lib/view_model/active_record/singular_nested_controller.rb +5 -2
- data/lib/view_model/callbacks.rb +1 -1
- data/lib/view_model/controller.rb +24 -2
- data/lib/view_model/record.rb +1 -1
- data/lib/view_model/schemas.rb +44 -0
- data/test/helpers/arvm_test_utilities.rb +65 -0
- data/test/helpers/controller_test_helpers.rb +65 -34
- data/test/unit/view_model/active_record/controller_nested_test.rb +599 -0
- data/test/unit/view_model/active_record/controller_test.rb +6 -362
- data/test/unit/view_model/active_record/has_many_test.rb +24 -7
- data/test/unit/view_model/active_record/has_many_through_test.rb +28 -12
- data/test/unit/view_model/traversal_context_test.rb +15 -1
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cb094f674e990ef9a67bbcb7b0ee260065356cdb6a56ee6e1b19582c320e96c
|
4
|
+
data.tar.gz: 5bb5c175e7fd30442794c2ff87687eb5dec132fa7da0d0128a1b012bd50131d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f196d4af404bcb91d88519e04ed2afcd7d73c590af4838e16b32b4f73171d20500a4a092cb76d693d7a683bf4ca6ad337d5e79a10a5233870f9c244acfffe969
|
7
|
+
data.tar.gz: bfdd726357dc50f72732498d112274301089ab82719e46d7c5d7bfc5414a6bcf3dc12459bb9dee6ffa9c174dea1c81e2a0499649d9bbe1d1618702bfad18ef04
|
data/iknow_view_models.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.name = 'iknow_view_models'
|
9
9
|
spec.version = IknowViewModels::VERSION
|
10
10
|
spec.authors = ['iKnow Team']
|
11
|
-
spec.email = ['
|
11
|
+
spec.email = ['systems@iknow.jp']
|
12
12
|
spec.summary = 'ViewModels provide a means of encapsulating a collection of related data and specifying its JSON serialization.'
|
13
13
|
spec.description = ''
|
14
14
|
spec.homepage = 'https://github.com/iknow/cerego_view_models'
|
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_dependency 'activesupport', '>= 5.0'
|
26
26
|
|
27
27
|
spec.add_dependency 'acts_as_manual_list'
|
28
|
-
spec.add_dependency 'deep_preloader', '>= 1.0.
|
28
|
+
spec.add_dependency 'deep_preloader', '>= 1.0.2'
|
29
29
|
spec.add_dependency 'iknow_cache'
|
30
30
|
spec.add_dependency 'iknow_params', '~> 2.2.0'
|
31
31
|
spec.add_dependency 'keyword_builder'
|
data/lib/view_model.rb
CHANGED
@@ -12,6 +12,10 @@ class ViewModel
|
|
12
12
|
VERSION_ATTRIBUTE = '_version'
|
13
13
|
NEW_ATTRIBUTE = '_new'
|
14
14
|
|
15
|
+
BULK_UPDATE_TYPE = '_bulk_update'
|
16
|
+
BULK_UPDATES_ATTRIBUTE = 'updates'
|
17
|
+
BULK_UPDATE_ATTRIBUTE = 'update'
|
18
|
+
|
15
19
|
# Migrations leave a metadata attribute _migrated on any views that they
|
16
20
|
# alter. This attribute is accessible as metadata when deserializing migrated
|
17
21
|
# input, and is included in the output serialization sent to clients.
|
@@ -27,6 +31,14 @@ class ViewModel
|
|
27
31
|
attr_reader :view_aliases
|
28
32
|
attr_writer :view_name
|
29
33
|
|
34
|
+
# Boolean to indicate if the viewmodel is synthetic. Synthetic
|
35
|
+
# viewmodels are nearly-invisible glue. They're full viewmodels,
|
36
|
+
# but do not participate in hooks or registration. For example, a
|
37
|
+
# join table connecting A and B through T has a synthetic
|
38
|
+
# viewmodel T to represent the join model, but the external
|
39
|
+
# interface is a relationship of A to a list of Bs.
|
40
|
+
attr_accessor :synthetic
|
41
|
+
|
30
42
|
def inherited(subclass)
|
31
43
|
super
|
32
44
|
subclass.initialize_as_viewmodel
|
@@ -35,14 +35,9 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
35
35
|
|
36
36
|
class << self
|
37
37
|
attr_reader :_list_attribute_name
|
38
|
-
attr_accessor :synthetic
|
39
38
|
|
40
39
|
delegate :transaction, to: :model_class
|
41
40
|
|
42
|
-
def should_register?
|
43
|
-
super && !synthetic
|
44
|
-
end
|
45
|
-
|
46
41
|
# Specifies that the model backing this viewmodel is a member of an
|
47
42
|
# `acts_as_manual_list` collection.
|
48
43
|
def acts_as_list(attr = :position)
|
@@ -368,6 +363,52 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
368
363
|
end
|
369
364
|
end
|
370
365
|
|
366
|
+
# Rails 6.1 introduced "previously_new_record?", but this library still
|
367
|
+
# supports activerecord >= 5.0. This is an approximation.
|
368
|
+
def self.model_previously_new?(model)
|
369
|
+
if (id_changes = model.saved_change_to_id)
|
370
|
+
old_id, _new_id = id_changes
|
371
|
+
return true if old_id.nil?
|
372
|
+
end
|
373
|
+
false
|
374
|
+
end
|
375
|
+
|
376
|
+
# Helper to return entities that were part of the last deserialization. The
|
377
|
+
# interface is complex due to the data requirements, and the implementation is
|
378
|
+
# inefficient.
|
379
|
+
#
|
380
|
+
# Intended to be used by replace_associated style methods which may touch very
|
381
|
+
# large collections that must not be returned fully. Since the collection is
|
382
|
+
# not being returned, order is also ignored.
|
383
|
+
def _read_association_touched(association_name, touched_ids:)
|
384
|
+
association_data = self.class._association_data(association_name)
|
385
|
+
|
386
|
+
associated = model.public_send(association_data.direct_reflection.name)
|
387
|
+
return nil if associated.nil?
|
388
|
+
|
389
|
+
case
|
390
|
+
when association_data.through?
|
391
|
+
# associated here are join-table models; we need to get the far side out
|
392
|
+
associated.map do |through_model|
|
393
|
+
model = through_model.public_send(association_data.indirect_reflection.name)
|
394
|
+
|
395
|
+
next unless self.class.model_previously_new?(through_model) || touched_ids.include?(model.id)
|
396
|
+
|
397
|
+
association_data.viewmodel_class_for_model!(model.class).new(model)
|
398
|
+
end.reject(&:nil?)
|
399
|
+
when association_data.collection?
|
400
|
+
associated.map do |model|
|
401
|
+
next unless self.class.model_previously_new?(model) || touched_ids.include?(model.id)
|
402
|
+
|
403
|
+
association_data.viewmodel_class_for_model!(model.class).new(model)
|
404
|
+
end.reject(&:nil?)
|
405
|
+
else
|
406
|
+
# singleton always touched by definition
|
407
|
+
model = associated
|
408
|
+
association_data.viewmodel_class_for_model!(model.class).new(model)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
371
412
|
def _serialize_association(association_name, json, serialize_context:)
|
372
413
|
associated = self.public_send(association_name)
|
373
414
|
association_data = self.class._association_data(association_name)
|
@@ -127,7 +127,9 @@ class ViewModel::ActiveRecord::AssociationData
|
|
127
127
|
end
|
128
128
|
|
129
129
|
def polymorphic?
|
130
|
-
|
130
|
+
# STI polymorphism isn't shown on the association reflection, so in that
|
131
|
+
# case we have to infer it by having multiple target viewmodel types.
|
132
|
+
target_reflection.polymorphic? || viewmodel_classes.size > 1
|
131
133
|
end
|
132
134
|
|
133
135
|
# The side of the immediate association that holds the pointer.
|
@@ -3,6 +3,8 @@
|
|
3
3
|
# Mix-in for VM::ActiveRecord providing direct manipulation of
|
4
4
|
# directly-associated entities. Avoids loading entire collections.
|
5
5
|
module ViewModel::ActiveRecord::AssociationManipulation
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
6
8
|
def load_associated(association_name, scope: nil, eager_include: true, serialize_context: self.class.new_serialize_context)
|
7
9
|
association_data = self.class._association_data(association_name)
|
8
10
|
direct_reflection = association_data.direct_reflection
|
@@ -16,7 +18,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
16
18
|
associated_viewmodel = association_data.viewmodel_class
|
17
19
|
direct_viewmodel = association_data.direct_viewmodel
|
18
20
|
else
|
19
|
-
raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.
|
21
|
+
raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic?
|
20
22
|
|
21
23
|
associated_viewmodel = association.klass.try { |k| association_data.viewmodel_class_for_model!(k) }
|
22
24
|
direct_viewmodel = associated_viewmodel
|
@@ -49,49 +51,63 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
52
|
-
# Replace the current member(s) of an association with the provided
|
54
|
+
# Replace the current member(s) of an association with the provided
|
55
|
+
# hash(es). Only mentioned member(s) will be returned.
|
56
|
+
#
|
57
|
+
# This interface deals with associations directly where reasonable,
|
58
|
+
# with the notable exception of referenced+shared associations. That
|
59
|
+
# is to say, that owned associations should be presented in the form
|
60
|
+
# of direct update hashes, regardless of their
|
61
|
+
# referencing. Reference and shared associations are excluded to
|
62
|
+
# ensure that the update hash for a shared entity is unique, and
|
63
|
+
# that edits may only be specified once.
|
53
64
|
def replace_associated(association_name, update_hash, references: {}, deserialize_context: self.class.new_deserialize_context)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
action_type_name = action[ViewModel::ActiveRecord::TYPE_ATTRIBUTE]
|
65
|
-
if action_type_name == ViewModel::ActiveRecord::FunctionalUpdate::Remove::NAME
|
66
|
-
# Remove actions are always type/id refs; others need to be translated to proper refs
|
67
|
-
next
|
68
|
-
end
|
65
|
+
_updated_parent, changed_children =
|
66
|
+
self.class.replace_associated_bulk(
|
67
|
+
association_name,
|
68
|
+
{ self.id => update_hash },
|
69
|
+
references: references,
|
70
|
+
deserialize_context: deserialize_context
|
71
|
+
).first
|
72
|
+
|
73
|
+
changed_children
|
74
|
+
end
|
69
75
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
76
|
+
class_methods do
|
77
|
+
# Replace the current member(s) of an association with the provided
|
78
|
+
# hash(es) for many viewmodels. Only mentioned members will be returned.
|
79
|
+
#
|
80
|
+
# This is an interim implementation that requires loading the contents of
|
81
|
+
# all collections into memory and filtering for the mentioned entities,
|
82
|
+
# even for functional updates. This is in contrast to append_associated,
|
83
|
+
# which only operates on the new entities.
|
84
|
+
def replace_associated_bulk(association_name, updates_by_parent_id, references:, deserialize_context: self.class.new_deserialize_context)
|
85
|
+
association_data = _association_data(association_name)
|
86
|
+
|
87
|
+
touched_ids = updates_by_parent_id.each_with_object({}) do |(parent_id, update_hash), acc|
|
88
|
+
acc[parent_id] =
|
89
|
+
mentioned_children(
|
90
|
+
update_hash,
|
91
|
+
references: references,
|
92
|
+
association_data: association_data,
|
93
|
+
).to_set
|
83
94
|
end
|
84
|
-
end
|
85
95
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
96
|
+
root_update_hashes = updates_by_parent_id.map do |parent_id, update_hash|
|
97
|
+
{
|
98
|
+
ViewModel::ID_ATTRIBUTE => parent_id,
|
99
|
+
ViewModel::TYPE_ATTRIBUTE => view_name,
|
100
|
+
association_name.to_s => update_hash,
|
101
|
+
}
|
102
|
+
end
|
91
103
|
|
92
|
-
|
104
|
+
root_update_viewmodels = deserialize_from_view(
|
105
|
+
root_update_hashes, references: references, deserialize_context: deserialize_context)
|
93
106
|
|
94
|
-
|
107
|
+
root_update_viewmodels.each_with_object({}) do |updated, acc|
|
108
|
+
acc[updated] = updated._read_association_touched(association_name, touched_ids: touched_ids.fetch(updated.id))
|
109
|
+
end
|
110
|
+
end
|
95
111
|
end
|
96
112
|
|
97
113
|
# Create or update members of a associated collection. For an ordered
|
@@ -118,7 +134,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
118
134
|
direct_viewmodel_class = association_data.direct_viewmodel
|
119
135
|
root_update_data, referenced_update_data = construct_indirect_append_updates(association_data, subtree_hashes, references)
|
120
136
|
else
|
121
|
-
raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.
|
137
|
+
raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic?
|
122
138
|
|
123
139
|
direct_viewmodel_class = association_data.viewmodel_class
|
124
140
|
root_update_data, referenced_update_data = construct_direct_append_updates(association_data, subtree_hashes, references)
|
@@ -242,7 +258,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
242
258
|
direct_viewmodel = association_data.direct_viewmodel
|
243
259
|
association_scope = association_scope.where(association_data.indirect_reflection.foreign_key => associated_id)
|
244
260
|
else
|
245
|
-
raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.
|
261
|
+
raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic?
|
246
262
|
|
247
263
|
# viewmodel type for current association: nil in case of empty polymorphic association
|
248
264
|
direct_viewmodel = association.klass.try { |k| association_data.viewmodel_class_for_model!(k) }
|
@@ -313,7 +329,10 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
313
329
|
indirect_update_data, referenced_update_data = ViewModel::ActiveRecord::UpdateData.parse_hashes(subtree_hashes, references)
|
314
330
|
|
315
331
|
# Convert associated update data to references
|
316
|
-
indirect_references =
|
332
|
+
indirect_references =
|
333
|
+
self.class.convert_updates_to_references(
|
334
|
+
indirect_update_data, key: 'indirect_append')
|
335
|
+
|
317
336
|
referenced_update_data.merge!(indirect_references)
|
318
337
|
|
319
338
|
# Find any existing models for the direct association: need to re-use any
|
@@ -352,12 +371,6 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
352
371
|
return direct_update_data, referenced_update_data
|
353
372
|
end
|
354
373
|
|
355
|
-
def convert_updates_to_references(indirect_update_data, key:)
|
356
|
-
indirect_update_data.each.with_index.with_object({}) do |(update, i), indirect_references|
|
357
|
-
indirect_references["__#{key}_ref_#{i}"] = update
|
358
|
-
end
|
359
|
-
end
|
360
|
-
|
361
374
|
# TODO: this functionality could reasonably be extracted into `acts_as_manual_list`.
|
362
375
|
def select_append_positions(association_data, position_attr, append_count, before:, after:)
|
363
376
|
direct_reflection = association_data.direct_reflection
|
@@ -395,10 +408,134 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
395
408
|
def check_association_type!(association_data, type)
|
396
409
|
if type && !association_data.accepts?(type)
|
397
410
|
raise ViewModel::SerializationError.new(
|
398
|
-
"Type error: association '#{
|
411
|
+
"Type error: association '#{association_data.association_name}' can't refer to viewmodel #{type.view_name}")
|
399
412
|
elsif association_data.polymorphic? && !type
|
400
413
|
raise ViewModel::SerializationError.new(
|
401
|
-
"Need to specify target viewmodel type for polymorphic association '#{
|
414
|
+
"Need to specify target viewmodel type for polymorphic association '#{association_data.association_name}'")
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
class_methods do
|
419
|
+
def convert_updates_to_references(indirect_update_data, key:)
|
420
|
+
indirect_update_data.each.with_index.with_object({}) do |(update, i), indirect_references|
|
421
|
+
indirect_references["__#{key}_ref_#{i}"] = update
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def add_reference_indirection(update_hash, association_data:, references:, key:)
|
426
|
+
raise ArgumentError.new('Not a referenced association') unless association_data.referenced?
|
427
|
+
|
428
|
+
is_fupdate =
|
429
|
+
association_data.collection? &&
|
430
|
+
update_hash.is_a?(Hash) &&
|
431
|
+
update_hash[ViewModel::ActiveRecord::TYPE_ATTRIBUTE] == ViewModel::ActiveRecord::FUNCTIONAL_UPDATE_TYPE
|
432
|
+
|
433
|
+
if is_fupdate
|
434
|
+
update_hash[ViewModel::ActiveRecord::ACTIONS_ATTRIBUTE].each_with_index do |action, i|
|
435
|
+
action_type_name = action[ViewModel::ActiveRecord::TYPE_ATTRIBUTE]
|
436
|
+
if action_type_name == ViewModel::ActiveRecord::FunctionalUpdate::Remove::NAME
|
437
|
+
# Remove actions are always type/id refs; others need to be translated to proper refs
|
438
|
+
next
|
439
|
+
end
|
440
|
+
|
441
|
+
association_references = convert_updates_to_references(
|
442
|
+
action[ViewModel::ActiveRecord::VALUES_ATTRIBUTE],
|
443
|
+
key: "#{key}_#{action_type_name}_#{i}")
|
444
|
+
references.merge!(association_references)
|
445
|
+
action[ViewModel::ActiveRecord::VALUES_ATTRIBUTE] =
|
446
|
+
association_references.each_key.map { |ref| { ViewModel::REFERENCE_ATTRIBUTE => ref } }
|
447
|
+
end
|
448
|
+
|
449
|
+
update_hash
|
450
|
+
else
|
451
|
+
ViewModel::Utils.wrap_one_or_many(update_hash) do |sh|
|
452
|
+
association_references = convert_updates_to_references(sh, key: "#{key}_replace")
|
453
|
+
references.merge!(association_references)
|
454
|
+
association_references.each_key.map { |ref| { ViewModel::REFERENCE_ATTRIBUTE => ref } }
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
# Traverses literals and fupdates to return referenced children.
|
460
|
+
#
|
461
|
+
# Runs before the main parser, so must be defensive
|
462
|
+
def each_child_hash(assoc_update, association_data:)
|
463
|
+
return enum_for(__method__, assoc_update, association_data: association_data) unless block_given?
|
464
|
+
|
465
|
+
is_fupdate =
|
466
|
+
association_data.collection? &&
|
467
|
+
assoc_update.is_a?(Hash) &&
|
468
|
+
assoc_update[ViewModel::ActiveRecord::TYPE_ATTRIBUTE] == ViewModel::ActiveRecord::FUNCTIONAL_UPDATE_TYPE
|
469
|
+
|
470
|
+
if is_fupdate
|
471
|
+
assoc_update.fetch(ViewModel::ActiveRecord::ACTIONS_ATTRIBUTE).each do |action|
|
472
|
+
action_type_name = action[ViewModel::ActiveRecord::TYPE_ATTRIBUTE]
|
473
|
+
if action_type_name.nil?
|
474
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
475
|
+
"Functional update missing '#{ViewModel::ActiveRecord::TYPE_ATTRIBUTE}'"
|
476
|
+
)
|
477
|
+
end
|
478
|
+
|
479
|
+
if action_type_name == ViewModel::ActiveRecord::FunctionalUpdate::Remove::NAME
|
480
|
+
# Remove actions are not considered children of the action.
|
481
|
+
next
|
482
|
+
end
|
483
|
+
|
484
|
+
values = action.fetch(ViewModel::ActiveRecord::VALUES_ATTRIBUTE) {
|
485
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
486
|
+
"Functional update missing '#{ViewModel::ActiveRecord::VALUES_ATTRIBUTE}'"
|
487
|
+
)
|
488
|
+
}
|
489
|
+
values.each { |x| yield x }
|
490
|
+
end
|
491
|
+
else
|
492
|
+
ViewModel::Utils.wrap_one_or_many(assoc_update) do |assoc_updates|
|
493
|
+
assoc_updates.each { |u| yield u }
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# Collects the ids of children that are mentioned in the update data.
|
499
|
+
#
|
500
|
+
# Runs before the main parser, so must be defensive.
|
501
|
+
def mentioned_children(assoc_update, references:, association_data:)
|
502
|
+
return enum_for(__method__, assoc_update, references: references, association_data: association_data) unless block_given?
|
503
|
+
|
504
|
+
each_child_hash(assoc_update, association_data: association_data).each do |child_hash|
|
505
|
+
unless child_hash.is_a?(Hash)
|
506
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
507
|
+
"Expected update hash, received: #{child_hash}"
|
508
|
+
)
|
509
|
+
end
|
510
|
+
|
511
|
+
if association_data.referenced?
|
512
|
+
ref_handle = child_hash.fetch(ViewModel::REFERENCE_ATTRIBUTE) {
|
513
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
514
|
+
"Reference hash missing '#{ViewModel::REFERENCE_ATTRIBUTE}'"
|
515
|
+
)
|
516
|
+
}
|
517
|
+
|
518
|
+
ref_update_hash = references.fetch(ref_handle) {
|
519
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
520
|
+
"Reference '#{ref_handle}' does not exist in references"
|
521
|
+
)
|
522
|
+
}
|
523
|
+
|
524
|
+
unless ref_update_hash.is_a?(Hash)
|
525
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
526
|
+
"Expected update hash, received: #{child_hash}"
|
527
|
+
)
|
528
|
+
end
|
529
|
+
|
530
|
+
if (id = ref_update_hash[ViewModel::ID_ATTRIBUTE])
|
531
|
+
yield id
|
532
|
+
end
|
533
|
+
else
|
534
|
+
if (id = child_hash[ViewModel::ID_ATTRIBUTE])
|
535
|
+
yield id
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
402
539
|
end
|
403
540
|
end
|
404
541
|
end
|
@@ -52,22 +52,23 @@ class ViewModel::ActiveRecord::Cloner
|
|
52
52
|
new_associated = associated
|
53
53
|
else
|
54
54
|
# Otherwise descend into the child, and attach the result
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
55
|
+
build_vm = ->(model) do
|
56
|
+
vm_class =
|
57
|
+
if association_data.through?
|
58
|
+
# descend into the synthetic join table viewmodel
|
59
|
+
association_data.direct_viewmodel
|
60
|
+
else
|
61
|
+
association_data.viewmodel_class_for_model!(model.class)
|
62
|
+
end
|
63
|
+
|
64
|
+
vm_class.new(model)
|
65
|
+
end
|
65
66
|
|
66
67
|
new_associated =
|
67
68
|
if ViewModel::Utils.array_like?(associated)
|
68
|
-
associated.map { |m| clone(
|
69
|
+
associated.map { |m| clone(build_vm.(m)) }.compact
|
69
70
|
else
|
70
|
-
clone(
|
71
|
+
clone(build_vm.(associated))
|
71
72
|
end
|
72
73
|
end
|
73
74
|
end
|