iknow_view_models 2.9.0 → 3.0.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 +1 -1
- 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/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.rb +17 -8
- data/shell.nix +1 -1
- data/test/helpers/arvm_test_utilities.rb +6 -0
- data/test/helpers/controller_test_helpers.rb +5 -3
- 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 +3 -2
- 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 +5 -8
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +0 -58
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a86c98f9ceda1cb4bf6980a84eb2051b5bc3904c08ab85b8546c4b16c78d26ff
|
4
|
+
data.tar.gz: 3b988a29d63e06d929c3fa0ca31b7b11f585e03263653c8f5bea64f2583c2fa3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f281b79eda3e83689d5f2aec0239f89d73a4c1ad0778ee017c20af65088c50dc7356d2149bce7b487766a68947c85645cddd0f6e92f0312a7c2cad1489ccb6f0
|
7
|
+
data.tar.gz: a3f4960911039bc0679c068cc67c86bee0cdad278ab7acfbea66e36cdf8e064e45eed8ea709ecd3b75ddc42a7934645f93ad9699be5c40a0595a3d2b5db10dc6
|
data/iknow_view_models.gemspec
CHANGED
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_dependency "activesupport", ">= 5.0"
|
25
25
|
|
26
26
|
spec.add_dependency "acts_as_manual_list"
|
27
|
-
spec.add_dependency "deep_preloader"
|
27
|
+
spec.add_dependency "deep_preloader", ">= 1.0.1"
|
28
28
|
spec.add_dependency "iknow_cache"
|
29
29
|
spec.add_dependency "iknow_params", "~> 2.2.0"
|
30
30
|
spec.add_dependency "safe_values"
|
@@ -1,95 +1,137 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# TODO consider rephrase scope for consistency
|
4
3
|
class ViewModel::ActiveRecord::AssociationData
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
4
|
+
class InvalidAssociation < RuntimeError; end
|
5
|
+
|
6
|
+
attr_reader :association_name, :direct_reflection
|
7
|
+
|
8
|
+
def initialize(owner:,
|
9
|
+
association_name:,
|
10
|
+
direct_association_name:,
|
11
|
+
indirect_association_name:,
|
12
|
+
target_viewmodels:,
|
13
|
+
external:,
|
14
|
+
through_order_attr:,
|
15
|
+
read_only:)
|
16
|
+
@association_name = association_name
|
17
|
+
|
18
|
+
@direct_reflection = owner.model_class.reflect_on_association(direct_association_name)
|
19
|
+
if @direct_reflection.nil?
|
20
|
+
raise InvalidAssociation.new("Association '#{direct_association_name}' not found in model '#{model_class.name}'")
|
21
|
+
end
|
22
|
+
|
23
|
+
@indirect_association_name = indirect_association_name
|
24
|
+
|
25
|
+
@read_only = read_only
|
26
|
+
@external = external
|
27
|
+
@through_order_attr = through_order_attr
|
28
|
+
@target_viewmodels = target_viewmodels
|
29
|
+
|
30
|
+
# Target models/reflections/viewmodels are lazily evaluated so that we can
|
31
|
+
# safely express cycles.
|
32
|
+
@initialized = false
|
33
|
+
@mutex = Mutex.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def lazy_initialize!
|
37
|
+
@mutex.synchronize do
|
38
|
+
return if @initialized
|
39
|
+
|
40
|
+
if through?
|
41
|
+
intermediate_model = @direct_reflection.klass
|
42
|
+
@indirect_reflection = load_indirect_reflection(intermediate_model, @indirect_association_name)
|
43
|
+
target_reflection = @indirect_reflection
|
44
|
+
else
|
45
|
+
target_reflection = @direct_reflection
|
46
|
+
end
|
47
|
+
|
48
|
+
@viewmodel_classes =
|
49
|
+
if @target_viewmodels.present?
|
50
|
+
# Explicitly named
|
51
|
+
@target_viewmodels.map { |v| resolve_viewmodel_class(v) }
|
22
52
|
else
|
23
|
-
|
53
|
+
# Infer name from name of model
|
54
|
+
if target_reflection.polymorphic?
|
55
|
+
raise InvalidAssociation.new(
|
56
|
+
'Cannot automatically infer target viewmodels from polymorphic association')
|
57
|
+
end
|
58
|
+
infer_viewmodel_class(target_reflection.klass)
|
24
59
|
end
|
60
|
+
|
61
|
+
@referenced = @viewmodel_classes.first.root?
|
62
|
+
|
63
|
+
# Non-referenced viewmodels must be owned. For referenced viewmodels, we
|
64
|
+
# own it if it points to us. Through associations aren't considered
|
65
|
+
# `owned?`: while we do own the implicit direct viewmodel, we don't own
|
66
|
+
# the target of the association.
|
67
|
+
@owned = !@referenced || (target_reflection.macro != :belongs_to)
|
68
|
+
|
69
|
+
unless @viewmodel_classes.all? { |v| v.root? == @referenced }
|
70
|
+
raise InvalidAssociation.new('Invalid association target: mixed root and non-root viewmodels')
|
25
71
|
end
|
26
|
-
end
|
27
72
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
# express the ownership of the indirect target, but this direct
|
32
|
-
# association to the intermediate is in fact owned. This ownership
|
33
|
-
# property isn't directly used anywhere: the synthetic intermediate
|
34
|
-
# viewmodel is only used in the deserialization update operations, which
|
35
|
-
# directly understands the semantics of through associations.
|
36
|
-
raise ArgumentError.new("Through associations must be to a shared target") unless @shared
|
37
|
-
raise ArgumentError.new("Through associations must be `has_many`") unless direct_reflection.macro == :has_many
|
38
|
-
end
|
39
|
-
end
|
73
|
+
if external? && !@referenced
|
74
|
+
raise InvalidAssociation.new('External associations must be to root viewmodels')
|
75
|
+
end
|
40
76
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
77
|
+
if through?
|
78
|
+
unless @referenced
|
79
|
+
raise InvalidAssociation.new('Through associations must be to root viewmodels')
|
80
|
+
end
|
81
|
+
|
82
|
+
@direct_viewmodel = build_direct_viewmodel(@direct_reflection, @indirect_reflection,
|
83
|
+
@viewmodel_classes, @through_order_attr)
|
84
|
+
end
|
85
|
+
|
86
|
+
@initialized = true
|
47
87
|
end
|
48
88
|
end
|
49
89
|
|
50
|
-
def
|
51
|
-
|
90
|
+
def association?
|
91
|
+
true
|
52
92
|
end
|
53
93
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
@viewmodel_classes ||=
|
58
|
-
begin
|
59
|
-
model_class = target_reflection.klass
|
60
|
-
if model_class.nil?
|
61
|
-
raise "Couldn't derive target class for association '#{target_reflection.name}"
|
62
|
-
end
|
63
|
-
inferred_view_name = ViewModel::Registry.default_view_name(model_class.name)
|
64
|
-
viewmodel_class = ViewModel::Registry.for_view_name(inferred_view_name) # TODO: improve error message to show it's looking for default name
|
65
|
-
[viewmodel_class]
|
66
|
-
end
|
94
|
+
def referenced?
|
95
|
+
lazy_initialize! unless @initialized
|
96
|
+
@referenced
|
67
97
|
end
|
68
98
|
|
69
|
-
|
70
|
-
|
71
|
-
h[vm.model_class] = vm
|
72
|
-
end
|
99
|
+
def nested?
|
100
|
+
!referenced?
|
73
101
|
end
|
74
102
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
vm.view_aliases.each do |view_alias|
|
79
|
-
h[view_alias] = vm
|
80
|
-
end
|
81
|
-
end
|
103
|
+
def owned?
|
104
|
+
lazy_initialize! unless @initialized
|
105
|
+
@owned
|
82
106
|
end
|
83
107
|
|
84
108
|
def shared?
|
85
|
-
|
109
|
+
!owned?
|
110
|
+
end
|
111
|
+
|
112
|
+
def external?
|
113
|
+
@external
|
114
|
+
end
|
115
|
+
|
116
|
+
def read_only?
|
117
|
+
@read_only
|
118
|
+
end
|
119
|
+
|
120
|
+
# reflection for the target of this association: indirect if through, direct otherwise
|
121
|
+
def target_reflection
|
122
|
+
if through?
|
123
|
+
indirect_reflection
|
124
|
+
else
|
125
|
+
direct_reflection
|
126
|
+
end
|
86
127
|
end
|
87
128
|
|
88
|
-
def
|
89
|
-
|
129
|
+
def polymorphic?
|
130
|
+
target_reflection.polymorphic?
|
90
131
|
end
|
91
132
|
|
92
|
-
|
133
|
+
# The side of the immediate association that holds the pointer.
|
134
|
+
def pointer_location
|
93
135
|
case direct_reflection.macro
|
94
136
|
when :belongs_to
|
95
137
|
:local
|
@@ -98,6 +140,24 @@ class ViewModel::ActiveRecord::AssociationData
|
|
98
140
|
end
|
99
141
|
end
|
100
142
|
|
143
|
+
def indirect_reflection
|
144
|
+
lazy_initialize! unless @initialized
|
145
|
+
@indirect_reflection
|
146
|
+
end
|
147
|
+
|
148
|
+
def direct_reflection_inverse(foreign_class = nil)
|
149
|
+
if direct_reflection.polymorphic?
|
150
|
+
direct_reflection.polymorphic_inverse_of(foreign_class)
|
151
|
+
else
|
152
|
+
direct_reflection.inverse_of
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def viewmodel_classes
|
157
|
+
lazy_initialize! unless @initialized
|
158
|
+
@viewmodel_classes
|
159
|
+
end
|
160
|
+
|
101
161
|
def viewmodel_class_for_model(model_class)
|
102
162
|
model_to_viewmodel[model_class]
|
103
163
|
end
|
@@ -132,47 +192,101 @@ class ViewModel::ActiveRecord::AssociationData
|
|
132
192
|
unless viewmodel_classes.size == 1
|
133
193
|
raise ArgumentError.new("More than one possible class for association '#{target_reflection.name}'")
|
134
194
|
end
|
195
|
+
|
135
196
|
viewmodel_classes.first
|
136
197
|
end
|
137
198
|
|
138
199
|
def through?
|
139
|
-
@
|
200
|
+
@indirect_association_name.present?
|
140
201
|
end
|
141
202
|
|
142
203
|
def direct_viewmodel
|
143
|
-
|
144
|
-
|
204
|
+
raise ArgumentError.new('not a through association') unless through?
|
205
|
+
lazy_initialize! unless @initialized
|
206
|
+
@direct_viewmodel
|
207
|
+
end
|
208
|
+
|
209
|
+
def collection?
|
210
|
+
through? || direct_reflection.collection?
|
211
|
+
end
|
145
212
|
|
146
|
-
|
213
|
+
def indirect_association_data
|
214
|
+
direct_viewmodel._association_data(indirect_reflection.name)
|
215
|
+
end
|
147
216
|
|
148
|
-
|
217
|
+
private
|
149
218
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
219
|
+
# Through associations must always be to a root viewmodel, via an owned
|
220
|
+
# has_many association to an intermediate model. A synthetic viewmodel is
|
221
|
+
# created to represent this intermediate, but is used only internally by the
|
222
|
+
# deserialization update operations, which directly understands the semantics
|
223
|
+
# of through associations.
|
224
|
+
def load_indirect_reflection(intermediate_model, indirect_association_name)
|
225
|
+
indirect_reflection =
|
226
|
+
intermediate_model.reflect_on_association(ActiveSupport::Inflector.singularize(indirect_association_name))
|
155
227
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
228
|
+
if indirect_reflection.nil?
|
229
|
+
raise InvalidAssociation.new(
|
230
|
+
"Indirect association '#{@indirect_association_name}' not found in "\
|
231
|
+
"intermediate model '#{intermediate_model.name}'")
|
232
|
+
end
|
233
|
+
|
234
|
+
unless direct_reflection.macro == :has_many
|
235
|
+
raise InvalidAssociation.new('Through associations must be `has_many`')
|
163
236
|
end
|
237
|
+
|
238
|
+
indirect_reflection
|
164
239
|
end
|
165
240
|
|
166
|
-
def indirect_reflection
|
167
|
-
|
168
|
-
|
241
|
+
def build_direct_viewmodel(direct_reflection, indirect_reflection, viewmodel_classes, through_order_attr)
|
242
|
+
# Join table viewmodel class. For A has_many B through T; where this association is defined on A
|
243
|
+
# direct_reflection = A -> T
|
244
|
+
# indirect_reflection = T -> B
|
245
|
+
|
246
|
+
Class.new(ViewModel::ActiveRecord) do
|
247
|
+
self.synthetic = true
|
248
|
+
self.model_class = direct_reflection.klass
|
249
|
+
self.view_name = direct_reflection.klass.name
|
250
|
+
association indirect_reflection.name, viewmodels: viewmodel_classes
|
251
|
+
acts_as_list through_order_attr if through_order_attr
|
252
|
+
end
|
169
253
|
end
|
170
254
|
|
171
|
-
def
|
172
|
-
|
255
|
+
def resolve_viewmodel_class(v)
|
256
|
+
case v
|
257
|
+
when String, Symbol
|
258
|
+
ViewModel::Registry.for_view_name(v.to_s)
|
259
|
+
when Class
|
260
|
+
v
|
261
|
+
else
|
262
|
+
raise InvalidAssociation.new("Invalid viewmodel class: #{v.inspect}")
|
263
|
+
end
|
173
264
|
end
|
174
265
|
|
175
|
-
def
|
176
|
-
|
266
|
+
def infer_viewmodel_class(model_class)
|
267
|
+
# If we weren't given explicit viewmodel classes, try to work out from the
|
268
|
+
# names. This should work unless the association is polymorphic.
|
269
|
+
if model_class.nil?
|
270
|
+
raise InvalidAssociation.new("Couldn't derive target class for model association '#{target_reflection.name}'")
|
271
|
+
end
|
272
|
+
|
273
|
+
inferred_view_name = ViewModel::Registry.default_view_name(model_class.name)
|
274
|
+
viewmodel_class = ViewModel::Registry.for_view_name(inferred_view_name) # TODO: improve error message to show it's looking for default name
|
275
|
+
[viewmodel_class]
|
276
|
+
end
|
277
|
+
|
278
|
+
def model_to_viewmodel
|
279
|
+
@model_to_viewmodel ||= viewmodel_classes.each_with_object({}) do |vm, h|
|
280
|
+
h[vm.model_class] = vm
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def name_to_viewmodel
|
285
|
+
@name_to_viewmodel ||= viewmodel_classes.each_with_object({}) do |vm, h|
|
286
|
+
h[vm.view_name] = vm
|
287
|
+
vm.view_aliases.each do |view_alias|
|
288
|
+
h[view_alias] = vm
|
289
|
+
end
|
290
|
+
end
|
177
291
|
end
|
178
292
|
end
|
@@ -52,9 +52,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
52
52
|
def replace_associated(association_name, update_hash, references: {}, deserialize_context: self.class.new_deserialize_context)
|
53
53
|
association_data = self.class._association_data(association_name)
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
if association_data.through? || association_data.shared?
|
55
|
+
if association_data.referenced?
|
58
56
|
is_fupdate =
|
59
57
|
association_data.collection? &&
|
60
58
|
update_hash.is_a?(Hash) &&
|
@@ -125,10 +123,6 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
125
123
|
|
126
124
|
update_context = ViewModel::ActiveRecord::UpdateContext.build!(root_update_data, referenced_update_data, root_type: direct_viewmodel_class)
|
127
125
|
|
128
|
-
# Provide information about what was updated
|
129
|
-
deserialize_context.updated_associations = root_update_data.map(&:updated_associations)
|
130
|
-
.inject({}) { |acc, assocs| acc.deep_merge(assocs) }
|
131
|
-
|
132
126
|
# Set new parent
|
133
127
|
new_parent = ViewModel::ActiveRecord::UpdateOperation::ParentData.new(direct_reflection.inverse_of, self)
|
134
128
|
update_context.root_updates.each { |update| update.reparent_to = new_parent }
|
@@ -177,15 +171,26 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
177
171
|
child_context = self.context_for_child(association_name, context: deserialize_context)
|
178
172
|
updated_viewmodels = update_context.run!(deserialize_context: child_context)
|
179
173
|
|
174
|
+
# Propagate changes and finalize the parent
|
175
|
+
updated_viewmodels.each do |child|
|
176
|
+
child_changes = child.previous_changes
|
177
|
+
|
178
|
+
if association_data.nested?
|
179
|
+
nested_children_changed! if child_changes.changed_nested_tree?
|
180
|
+
referenced_children_changed! if child_changes.changed_referenced_children?
|
181
|
+
elsif association_data.owned?
|
182
|
+
referenced_children_changed! if child_changes.changed_owned_tree?
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
final_changes = self.clear_changes!
|
187
|
+
|
180
188
|
if association_data.through?
|
181
189
|
updated_viewmodels.map! do |direct_vm|
|
182
190
|
direct_vm._read_association(association_data.indirect_reflection.name)
|
183
191
|
end
|
184
192
|
end
|
185
193
|
|
186
|
-
# Finalize the parent
|
187
|
-
final_changes = self.clear_changes!
|
188
|
-
|
189
194
|
# Could happen if hooks attempted to change the parent, which aren't
|
190
195
|
# valid since we're only editing children here.
|
191
196
|
unless final_changes.contained_to?(associations: [association_name.to_s])
|
@@ -269,7 +274,12 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
269
274
|
association.delete(child_vm.model)
|
270
275
|
end
|
271
276
|
|
272
|
-
|
277
|
+
if association_data.nested?
|
278
|
+
nested_children_changed!
|
279
|
+
elsif association_data.owned?
|
280
|
+
referenced_children_changed!
|
281
|
+
end
|
282
|
+
|
273
283
|
final_changes = self.clear_changes!
|
274
284
|
|
275
285
|
unless final_changes.contained_to?(associations: [association_name.to_s])
|
@@ -286,7 +296,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
286
296
|
|
287
297
|
private
|
288
298
|
|
289
|
-
def construct_direct_append_updates(
|
299
|
+
def construct_direct_append_updates(_association_data, subtree_hashes, references)
|
290
300
|
ViewModel::ActiveRecord::UpdateData.parse_hashes(subtree_hashes, references)
|
291
301
|
end
|
292
302
|
|
@@ -12,17 +12,7 @@ module ViewModel::ActiveRecord::Cache::CacheableView
|
|
12
12
|
CacheClearer = Struct.new(:cache, :id) do
|
13
13
|
include ViewModel::AfterTransactionRunner
|
14
14
|
|
15
|
-
|
16
|
-
# on database locking to prevent cache race conditions. We require
|
17
|
-
# reading/refreshing the cache to obtain a FOR SHARE lock, which means that
|
18
|
-
# a reader must wait for a concurrent writer to commit before continuing to
|
19
|
-
# the cache. If the writer cleared the cache after commit, the reader could
|
20
|
-
# obtain old data before the clear, and then save the old data after it.
|
21
|
-
def before_commit
|
22
|
-
cache.delete(id)
|
23
|
-
end
|
24
|
-
|
25
|
-
def after_rollback
|
15
|
+
def after_transaction
|
26
16
|
cache.delete(id)
|
27
17
|
end
|
28
18
|
|
@@ -49,12 +39,12 @@ module ViewModel::ActiveRecord::Cache::CacheableView
|
|
49
39
|
end
|
50
40
|
end
|
51
41
|
|
52
|
-
# Clear the cache if the view or its
|
42
|
+
# Clear the cache if the view or its nested children were changed during
|
53
43
|
# deserialization
|
54
44
|
def after_deserialize(deserialize_context:, changes:)
|
55
45
|
super if defined?(super)
|
56
46
|
|
57
|
-
if !changes.new? && changes.
|
47
|
+
if !changes.new? && changes.changed_nested_tree?
|
58
48
|
CacheClearer.new(self.class.viewmodel_cache, id).add_to_transaction
|
59
49
|
end
|
60
50
|
end
|
@@ -198,7 +198,7 @@ class ViewModel::ActiveRecord::Cache
|
|
198
198
|
end
|
199
199
|
|
200
200
|
ViewModel.preload_for_serialization(viewmodels,
|
201
|
-
|
201
|
+
include_referenced: false,
|
202
202
|
lock: "FOR SHARE",
|
203
203
|
serialize_context: serialize_context)
|
204
204
|
|
@@ -259,7 +259,7 @@ class ViewModel::ActiveRecord::Cache
|
|
259
259
|
end
|
260
260
|
|
261
261
|
def cache_version
|
262
|
-
version_string = @viewmodel_class.deep_schema_version(
|
262
|
+
version_string = @viewmodel_class.deep_schema_version(include_referenced: false).to_a.sort.join(',')
|
263
263
|
Base64.urlsafe_encode64(Digest::MD5.digest(version_string))
|
264
264
|
end
|
265
265
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Simple visitor for cloning models through the tree structure defined by
|
2
4
|
# ViewModel::ActiveRecord. Owned associations will be followed and cloned, while
|
3
|
-
#
|
4
|
-
# foreign keys not covered by ViewModel
|
5
|
-
# original.
|
5
|
+
# non-owned referenced associations will be copied directly as references.
|
6
|
+
# Attributes (including association foreign keys not covered by ViewModel
|
7
|
+
# `association`s) will be copied from the original.
|
6
8
|
#
|
7
9
|
# To customize, subclasses may define methods `visit_x_view(node, new_model)`
|
8
10
|
# for each type they wish to affect. These callbacks may update attributes of
|
@@ -19,9 +21,9 @@ class ViewModel::ActiveRecord::Cloner
|
|
19
21
|
return nil if ignored?
|
20
22
|
|
21
23
|
if node.class.name
|
22
|
-
class_name
|
23
|
-
visit
|
24
|
-
end_visit
|
24
|
+
class_name = node.class.name.underscore.gsub('/', '__')
|
25
|
+
visit = :"visit_#{class_name}"
|
26
|
+
end_visit = :"end_visit_#{class_name}"
|
25
27
|
end
|
26
28
|
|
27
29
|
if visit && respond_to?(visit, true)
|
@@ -44,7 +46,7 @@ class ViewModel::ActiveRecord::Cloner
|
|
44
46
|
|
45
47
|
if associated.nil?
|
46
48
|
new_associated = nil
|
47
|
-
elsif association_data.
|
49
|
+
elsif !association_data.owned? && !association_data.through?
|
48
50
|
# simply attach the associated target to the new model
|
49
51
|
new_associated = associated
|
50
52
|
else
|
@@ -82,11 +84,9 @@ class ViewModel::ActiveRecord::Cloner
|
|
82
84
|
new_model
|
83
85
|
end
|
84
86
|
|
85
|
-
def pre_visit(node, new_model)
|
86
|
-
end
|
87
|
+
def pre_visit(node, new_model); end
|
87
88
|
|
88
|
-
def post_visit(node, new_model)
|
89
|
-
end
|
89
|
+
def post_visit(node, new_model); end
|
90
90
|
|
91
91
|
private
|
92
92
|
|
@@ -46,8 +46,6 @@ module ViewModel::ActiveRecord::Controller
|
|
46
46
|
pre_rendered = viewmodel_class.transaction do
|
47
47
|
view = viewmodel_class.deserialize_from_view(update_hash, references: refs, deserialize_context: deserialize_context)
|
48
48
|
|
49
|
-
serialize_context.add_includes(deserialize_context.updated_associations)
|
50
|
-
|
51
49
|
view = yield(view) if block_given?
|
52
50
|
|
53
51
|
ViewModel.preload_for_serialization(view, serialize_context: serialize_context)
|
@@ -74,7 +74,7 @@ class ViewModel::ActiveRecord
|
|
74
74
|
|
75
75
|
def initialize
|
76
76
|
@root_update_operations = [] # The subject(s) of this update
|
77
|
-
@referenced_update_operations = {} #
|
77
|
+
@referenced_update_operations = {} # data updates to other root models, referred to by a ref hash
|
78
78
|
|
79
79
|
# Set of ViewModel::Reference used to assert only a single update is
|
80
80
|
# present for each viewmodel
|
@@ -178,8 +178,20 @@ class ViewModel::ActiveRecord
|
|
178
178
|
raise ViewModel::DeserializationError::ParentNotFound.new(@worklist.keys)
|
179
179
|
end
|
180
180
|
|
181
|
-
deferred_update
|
182
|
-
|
181
|
+
deferred_update = @worklist.delete(key)
|
182
|
+
released_viewmodel = @release_pool.claim_from_pool(key)
|
183
|
+
|
184
|
+
if deferred_update.viewmodel
|
185
|
+
# Deferred reference updates already have a viewmodel: ensure it
|
186
|
+
# matches the tree
|
187
|
+
unless deferred_update.viewmodel == released_viewmodel
|
188
|
+
raise ViewModel::DeserializationError::Internal.new(
|
189
|
+
"Released viewmodel doesn't match reference update", blame_reference)
|
190
|
+
end
|
191
|
+
else
|
192
|
+
deferred_update.viewmodel = released_viewmodel
|
193
|
+
end
|
194
|
+
|
183
195
|
deferred_update.build!(self)
|
184
196
|
end
|
185
197
|
|
@@ -201,6 +213,12 @@ class ViewModel::ActiveRecord
|
|
201
213
|
update_operation = ViewModel::ActiveRecord::UpdateOperation.new(
|
202
214
|
nil, update_data, reparent_to: reparent_to, reposition_to: reposition_to)
|
203
215
|
check_unique_update!(viewmodel_reference)
|
216
|
+
defer_update(viewmodel_reference, update_operation)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Defer an existing update: used if we need to ensure that an owned
|
220
|
+
# reference has been freed before we use it.
|
221
|
+
def defer_update(viewmodel_reference, update_operation)
|
204
222
|
@worklist[viewmodel_reference] = update_operation
|
205
223
|
end
|
206
224
|
|