iknow_view_models 3.3.0 → 3.4.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 680a05c75dacc9f5380b920439650249c11d04ab3f1b685e88081414fc4a72d3
4
- data.tar.gz: 7328f45000837430c6627ecb1f916f529172b53fe0266aba8a0d02c48d131935
3
+ metadata.gz: 3560a98616dd2f0548ac7e663827e68d86fa9991969ac6ee49633a0090e7d628
4
+ data.tar.gz: deec44e5132f91dfc1deef0dd889ee20c1b01c6d5d7791d356f1e9dc99ec1c8a
5
5
  SHA512:
6
- metadata.gz: 5ae7508982f9e95354f137e84e8bb844b5373f5236526c7640276cc93f3bdb78846864a0cc35209ded4cd24963bbab248010849bfe6cbabcf8c01046288c97bd
7
- data.tar.gz: 459318dca0636a4381b96e83ce6d260380b74106834b6d60b88af886363c846233c9dc2547e082e009b799d8e674d2cf48811bd49765d3bd2e2a4f2f8e307882
6
+ metadata.gz: 5af2b38b2e8fb8805480bd665ea24e7ad4b489951c8b72660e502a053a945798f18269b60991eb416ab6959fc46e4831a911a15005810b57e288e2eeaaf37fd5
7
+ data.tar.gz: 5a73108237e3ffae5751f50a6e693e8fd737a4d1936f087095138ec2db47cbd7ec852f81e6ef881f794a38d67ba85429825b2393a850c149f363121f321adf30
@@ -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.1'
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'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.3.0'
4
+ VERSION = '3.4.3'
5
5
  end
data/lib/view_model.rb CHANGED
@@ -134,7 +134,7 @@ class ViewModel
134
134
  # If this viewmodel represents an AR model, what associations does it make
135
135
  # use of? Returns a includes spec appropriate for DeepPreloader, either as
136
136
  # AR-style nested hashes or DeepPreloader::Spec.
137
- def eager_includes(serialize_context: new_serialize_context, include_referenced: true)
137
+ def eager_includes(include_referenced: true)
138
138
  {}
139
139
  end
140
140
 
@@ -267,10 +267,10 @@ class ViewModel
267
267
  Base64.urlsafe_encode64(hash, padding: false)
268
268
  end
269
269
 
270
- def preload_for_serialization(viewmodels, serialize_context: new_serialize_context, include_referenced: true, lock: nil)
270
+ def preload_for_serialization(viewmodels, include_referenced: true, lock: nil)
271
271
  Array.wrap(viewmodels).group_by(&:class).each do |type, views|
272
272
  DeepPreloader.preload(views.map(&:model),
273
- type.eager_includes(serialize_context: serialize_context, include_referenced: include_referenced),
273
+ type.eager_includes(include_referenced: include_referenced),
274
274
  lock: lock)
275
275
  end
276
276
  end
@@ -354,8 +354,8 @@ class ViewModel
354
354
  context.for_child(self, association_name: member_name)
355
355
  end
356
356
 
357
- def preload_for_serialization(lock: nil, serialize_context: self.class.new_serialize_context)
358
- ViewModel.preload_for_serialization([self], lock: lock, serialize_context: serialize_context)
357
+ def preload_for_serialization(lock: nil)
358
+ ViewModel.preload_for_serialization([self], lock: lock)
359
359
  end
360
360
 
361
361
  def ==(other)
@@ -135,7 +135,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
135
135
  end
136
136
 
137
137
  ## Load instances of the viewmodel by id(s)
138
- def find(id_or_ids, scope: nil, lock: nil, eager_include: true, serialize_context: new_serialize_context)
138
+ def find(id_or_ids, scope: nil, lock: nil, eager_include: true)
139
139
  find_scope = self.model_class.all
140
140
  find_scope = find_scope.order(:id).lock(lock) if lock
141
141
  find_scope = find_scope.merge(scope) if scope
@@ -152,19 +152,19 @@ class ViewModel::ActiveRecord < ViewModel::Record
152
152
  end
153
153
 
154
154
  vms = models.map { |m| self.new(m) }
155
- ViewModel.preload_for_serialization(vms, lock: lock, serialize_context: serialize_context) if eager_include
155
+ ViewModel.preload_for_serialization(vms, lock: lock) if eager_include
156
156
  vms
157
157
  end
158
158
  end
159
159
 
160
160
  ## Load instances of the viewmodel by scope
161
161
  ## TODO: is this too much of a encapsulation violation?
162
- def load(scope: nil, eager_include: true, lock: nil, serialize_context: new_serialize_context)
162
+ def load(scope: nil, eager_include: true, lock: nil)
163
163
  load_scope = self.model_class.all
164
164
  load_scope = load_scope.lock(lock) if lock
165
165
  load_scope = load_scope.merge(scope) if scope
166
166
  vms = load_scope.map { |model| self.new(model) }
167
- ViewModel.preload_for_serialization(vms, lock: lock, serialize_context: serialize_context) if eager_include
167
+ ViewModel.preload_for_serialization(vms, lock: lock) if eager_include
168
168
  vms
169
169
  end
170
170
 
@@ -184,7 +184,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
184
184
  # Constructs a preload specification of the required models for
185
185
  # serializing/deserializing this view. Cycles in the schema will be broken
186
186
  # after two layers of eager loading.
187
- def eager_includes(serialize_context: new_serialize_context, include_referenced: true, vm_path: [])
187
+ def eager_includes(include_referenced: true, vm_path: [])
188
188
  association_specs = {}
189
189
 
190
190
  return nil if vm_path.count(self) > 2
@@ -194,19 +194,10 @@ class ViewModel::ActiveRecord < ViewModel::Record
194
194
  next unless association_data.is_a?(AssociationData)
195
195
  next if association_data.external?
196
196
 
197
- child_context =
198
- if self.synthetic
199
- serialize_context
200
- elsif association_data.referenced?
201
- serialize_context.for_references
202
- else
203
- serialize_context.for_child(nil, association_name: assoc_name)
204
- end
205
-
206
197
  case
207
198
  when association_data.through?
208
199
  viewmodel = association_data.direct_viewmodel
209
- children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced, vm_path: child_path)
200
+ children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path)
210
201
 
211
202
  when !include_referenced && association_data.referenced?
212
203
  children = nil # Load up to the root viewmodel, but no further
@@ -215,13 +206,13 @@ class ViewModel::ActiveRecord < ViewModel::Record
215
206
  children_by_klass = {}
216
207
  association_data.viewmodel_classes.each do |vm_class|
217
208
  klass = vm_class.model_class.name
218
- children_by_klass[klass] = vm_class.eager_includes(serialize_context: child_context, include_referenced: include_referenced, vm_path: child_path)
209
+ children_by_klass[klass] = vm_class.eager_includes(include_referenced: include_referenced, vm_path: child_path)
219
210
  end
220
211
  children = DeepPreloader::PolymorphicSpec.new(children_by_klass)
221
212
 
222
213
  else
223
214
  viewmodel = association_data.viewmodel_class
224
- children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced, vm_path: child_path)
215
+ children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path)
225
216
  end
226
217
 
227
218
  association_specs[association_data.direct_reflection.name.to_s] = children
@@ -348,7 +339,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
348
339
  # associated here are join-table models; we need to get the far side out
349
340
  join_models = associated
350
341
 
351
- if association_data.direct_viewmodel._list_member?
342
+ if association_data.ordered?
352
343
  attr = association_data.direct_viewmodel._list_attribute_name
353
344
  join_models = join_models.sort_by { |j| j[attr] }
354
345
  end
@@ -359,11 +350,16 @@ class ViewModel::ActiveRecord < ViewModel::Record
359
350
  end
360
351
 
361
352
  when association_data.collection?
362
- associated_viewmodel_class = association_data.viewmodel_class
363
- associated_viewmodels = associated.map { |x| associated_viewmodel_class.new(x) }
364
- if associated_viewmodel_class._list_member?
353
+ associated_viewmodels = associated.map do |x|
354
+ associated_viewmodel_class = association_data.viewmodel_class_for_model!(x.class)
355
+ associated_viewmodel_class.new(x)
356
+ end
357
+
358
+ # If any associated type is a list member, they must all be
359
+ if association_data.ordered?
365
360
  associated_viewmodels.sort_by!(&:_list_attribute)
366
361
  end
362
+
367
363
  associated_viewmodels
368
364
 
369
365
  else
@@ -127,7 +127,9 @@ class ViewModel::ActiveRecord::AssociationData
127
127
  end
128
128
 
129
129
  def polymorphic?
130
- target_reflection.polymorphic?
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.
@@ -196,6 +198,21 @@ class ViewModel::ActiveRecord::AssociationData
196
198
  viewmodel_classes.first
197
199
  end
198
200
 
201
+ def ordered?
202
+ @ordered ||=
203
+ if through?
204
+ direct_viewmodel._list_member?
205
+ else
206
+ list_members = viewmodel_classes.map { |c| c._list_member? }.uniq
207
+
208
+ if list_members.size > 1
209
+ raise ArgumentError.new('Inconsistent associated views: mixed list membership')
210
+ end
211
+
212
+ list_members[0]
213
+ end
214
+ end
215
+
199
216
  def through?
200
217
  @indirect_association_name.present?
201
218
  end
@@ -16,11 +16,13 @@ module ViewModel::ActiveRecord::AssociationManipulation
16
16
  associated_viewmodel = association_data.viewmodel_class
17
17
  direct_viewmodel = association_data.direct_viewmodel
18
18
  else
19
+ raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic?
20
+
19
21
  associated_viewmodel = association.klass.try { |k| association_data.viewmodel_class_for_model!(k) }
20
22
  direct_viewmodel = associated_viewmodel
21
23
  end
22
24
 
23
- if direct_viewmodel._list_member?
25
+ if association_data.ordered?
24
26
  association_scope = association_scope.order(direct_viewmodel._list_attribute_name)
25
27
  end
26
28
 
@@ -34,10 +36,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
34
36
 
35
37
  vms = association_scope.map { |model| associated_viewmodel.new(model) }
36
38
 
37
- if eager_include
38
- child_context = self.context_for_child(association_name, context: serialize_context)
39
- ViewModel.preload_for_serialization(vms, serialize_context: child_context)
40
- end
39
+ ViewModel.preload_for_serialization(vms) if eager_include
41
40
 
42
41
  if association_data.collection?
43
42
  vms
@@ -119,6 +118,8 @@ module ViewModel::ActiveRecord::AssociationManipulation
119
118
  direct_viewmodel_class = association_data.direct_viewmodel
120
119
  root_update_data, referenced_update_data = construct_indirect_append_updates(association_data, subtree_hashes, references)
121
120
  else
121
+ raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic?
122
+
122
123
  direct_viewmodel_class = association_data.viewmodel_class
123
124
  root_update_data, referenced_update_data = construct_direct_append_updates(association_data, subtree_hashes, references)
124
125
  end
@@ -130,7 +131,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
130
131
  update_context.root_updates.each { |update| update.reparent_to = new_parent }
131
132
 
132
133
  # Set place in list.
133
- if direct_viewmodel_class._list_member?
134
+ if association_data.ordered?
134
135
  new_positions = select_append_positions(association_data,
135
136
  direct_viewmodel_class._list_attribute_name,
136
137
  update_context.root_updates.count,
@@ -241,6 +242,8 @@ module ViewModel::ActiveRecord::AssociationManipulation
241
242
  direct_viewmodel = association_data.direct_viewmodel
242
243
  association_scope = association_scope.where(association_data.indirect_reflection.foreign_key => associated_id)
243
244
  else
245
+ raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic?
246
+
244
247
  # viewmodel type for current association: nil in case of empty polymorphic association
245
248
  direct_viewmodel = association.klass.try { |k| association_data.viewmodel_class_for_model!(k) }
246
249
 
@@ -196,7 +196,7 @@ class ViewModel::ActiveRecord::Cache
196
196
  # {id=>serialized_view}. Any references encountered are added to the
197
197
  # worklist.
198
198
  def load_from_cache(viewmodel_cache, ids)
199
- cached_serializations = viewmodel_cache.load(ids, migrated_cache_version(viewmodel_cache), serialize_context: serialize_context)
199
+ cached_serializations = viewmodel_cache.load(ids, migrated_cache_version(viewmodel_cache))
200
200
 
201
201
  cached_serializations.each_with_object({}) do |(id, cached_serialization), result|
202
202
  add_refs_to_worklist(cached_serialization[:ref_cache])
@@ -255,8 +255,7 @@ class ViewModel::ActiveRecord::Cache
255
255
  if viewmodel.class < CacheableView
256
256
  cacheable_references = referenced_viewmodels.transform_values { |vm| cacheable_reference(vm) }
257
257
  target_cache = viewmodel.class.viewmodel_cache
258
- target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references,
259
- serialize_context: serialize_context)
258
+ target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references)
260
259
  end
261
260
 
262
261
  result[viewmodel.id] = data_serialization
@@ -279,15 +278,13 @@ class ViewModel::ActiveRecord::Cache
279
278
  if ids.present?
280
279
  found = viewmodel_class.find(ids,
281
280
  eager_include: false,
282
- lock: 'FOR SHARE',
283
- serialize_context: serialize_context)
281
+ lock: 'FOR SHARE')
284
282
  viewmodels.concat(found)
285
283
  end
286
284
 
287
285
  ViewModel.preload_for_serialization(viewmodels,
288
286
  include_referenced: false,
289
- lock: 'FOR SHARE',
290
- serialize_context: serialize_context)
287
+ lock: 'FOR SHARE')
291
288
 
292
289
  viewmodels
293
290
  end
@@ -335,12 +332,12 @@ class ViewModel::ActiveRecord::Cache
335
332
  end
336
333
 
337
334
  # Save the provided serialization and reference data in the cache
338
- def store(id, migration_version, data_serialization, ref_cache, serialize_context:)
335
+ def store(id, migration_version, data_serialization, ref_cache)
339
336
  key = key_for(id, migration_version)
340
337
  cache_for(migration_version).write(key, { data: data_serialization, ref_cache: ref_cache })
341
338
  end
342
339
 
343
- def load(ids, migration_version, serialize_context:)
340
+ def load(ids, migration_version)
344
341
  keys = ids.map { |id| key_for(id, migration_version) }
345
342
  results = cache_for(migration_version).read_multi(keys)
346
343
  results.transform_keys! { |key| id_for(key) }
@@ -22,23 +22,23 @@ module ViewModel::ActiveRecord::CollectionNestedController
22
22
  extend ActiveSupport::Concern
23
23
  include ViewModel::ActiveRecord::NestedControllerBase
24
24
 
25
- def index_associated(scope: nil, serialize_context: new_serialize_context, &block)
26
- show_association(scope: scope, serialize_context: serialize_context, &block)
25
+ def index_associated(scope: nil, serialize_context: new_serialize_context, lock_owner: nil, &block)
26
+ show_association(scope: scope, serialize_context: serialize_context, lock_owner: lock_owner, &block)
27
27
  end
28
28
 
29
29
  # Deserialize items of the associated type and associate them with the owner,
30
30
  # replacing previously associated items.
31
- def replace(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, &block)
32
- write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, &block)
31
+ def replace(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil, &block)
32
+ write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner, &block)
33
33
  end
34
34
 
35
- def disassociate_all(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
36
- destroy_association(true, serialize_context: serialize_context, deserialize_context: deserialize_context)
35
+ def disassociate_all(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
36
+ destroy_association(true, serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner)
37
37
  end
38
38
 
39
39
  # Deserialize items of the associated type and append them to the owner's
40
40
  # collection.
41
- def append(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
41
+ def append(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
42
42
  assoc_view = nil
43
43
  pre_rendered = owner_viewmodel.transaction do
44
44
  update_hash, refs = parse_viewmodel_updates
@@ -50,7 +50,7 @@ module ViewModel::ActiveRecord::CollectionNestedController
50
50
  raise ViewModel::DeserializationError::InvalidSyntax.new('Can not provide both `before` and `after` anchors for a collection append')
51
51
  end
52
52
 
53
- owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, serialize_context: serialize_context)
53
+ owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
54
54
 
55
55
  assoc_view = owner_view.append_associated(association_name,
56
56
  update_hash,
@@ -60,7 +60,7 @@ module ViewModel::ActiveRecord::CollectionNestedController
60
60
  deserialize_context: deserialize_context)
61
61
  ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
62
62
  child_context = owner_view.context_for_child(association_name, context: serialize_context)
63
- ViewModel.preload_for_serialization(assoc_view, serialize_context: child_context)
63
+ ViewModel.preload_for_serialization(assoc_view)
64
64
  assoc_view = yield(assoc_view) if block_given?
65
65
  prerender_viewmodel(assoc_view, serialize_context: child_context)
66
66
  end
@@ -69,9 +69,9 @@ module ViewModel::ActiveRecord::CollectionNestedController
69
69
  assoc_view
70
70
  end
71
71
 
72
- def disassociate(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
72
+ def disassociate(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
73
73
  owner_viewmodel.transaction do
74
- owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, serialize_context: serialize_context)
74
+ owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
75
75
  owner_view.delete_associated(association_name, viewmodel_id, type: viewmodel_class, deserialize_context: deserialize_context)
76
76
  render_viewmodel(nil)
77
77
  end
@@ -22,7 +22,7 @@ module ViewModel::ActiveRecord::Controller
22
22
  def show(scope: nil, viewmodel_class: self.viewmodel_class, serialize_context: new_serialize_context(viewmodel_class: viewmodel_class))
23
23
  view = nil
24
24
  pre_rendered = viewmodel_class.transaction do
25
- view = viewmodel_class.find(viewmodel_id, scope: scope, serialize_context: serialize_context)
25
+ view = viewmodel_class.find(viewmodel_id, scope: scope)
26
26
  view = yield(view) if block_given?
27
27
  prerender_viewmodel(view, serialize_context: serialize_context)
28
28
  end
@@ -33,7 +33,7 @@ module ViewModel::ActiveRecord::Controller
33
33
  def index(scope: nil, viewmodel_class: self.viewmodel_class, serialize_context: new_serialize_context(viewmodel_class: viewmodel_class))
34
34
  views = nil
35
35
  pre_rendered = viewmodel_class.transaction do
36
- views = viewmodel_class.load(scope: scope, serialize_context: serialize_context)
36
+ views = viewmodel_class.load(scope: scope)
37
37
  views = yield(views) if block_given?
38
38
  prerender_viewmodel(views, serialize_context: serialize_context)
39
39
  end
@@ -47,7 +47,7 @@ module ViewModel::ActiveRecord::Controller
47
47
  view = nil
48
48
  pre_rendered = viewmodel_class.transaction do
49
49
  view = viewmodel_class.deserialize_from_view(update_hash, references: refs, deserialize_context: deserialize_context)
50
- ViewModel.preload_for_serialization(view, serialize_context: serialize_context)
50
+ ViewModel.preload_for_serialization(view)
51
51
  view = yield(view) if block_given?
52
52
  prerender_viewmodel(view, serialize_context: serialize_context)
53
53
  end
@@ -57,7 +57,7 @@ module ViewModel::ActiveRecord::Controller
57
57
 
58
58
  def destroy(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
59
59
  viewmodel_class.transaction do
60
- view = viewmodel_class.find(viewmodel_id, eager_include: false, serialize_context: serialize_context)
60
+ view = viewmodel_class.find(viewmodel_id, eager_include: false)
61
61
  view.destroy!(deserialize_context: deserialize_context)
62
62
  end
63
63
  render_viewmodel(nil)
@@ -9,10 +9,10 @@ module ViewModel::ActiveRecord::NestedControllerBase
9
9
 
10
10
  protected
11
11
 
12
- def show_association(scope: nil, serialize_context: new_serialize_context)
12
+ def show_association(scope: nil, serialize_context: new_serialize_context, lock_owner: nil)
13
13
  associated_views = nil
14
14
  pre_rendered = owner_viewmodel.transaction do
15
- owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, serialize_context: serialize_context)
15
+ owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
16
16
  ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
17
17
  # Association manipulation methods construct child contexts internally
18
18
  associated_views = owner_view.load_associated(association_name, scope: scope, serialize_context: serialize_context)
@@ -27,12 +27,12 @@ module ViewModel::ActiveRecord::NestedControllerBase
27
27
  associated_views
28
28
  end
29
29
 
30
- def write_association(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
30
+ def write_association(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
31
31
  association_view = nil
32
32
  pre_rendered = owner_viewmodel.transaction do
33
33
  update_hash, refs = parse_viewmodel_updates
34
34
 
35
- owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, serialize_context: serialize_context)
35
+ owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
36
36
 
37
37
  association_view = owner_view.replace_associated(association_name, update_hash,
38
38
  references: refs,
@@ -40,7 +40,7 @@ module ViewModel::ActiveRecord::NestedControllerBase
40
40
 
41
41
  ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
42
42
  child_context = owner_view.context_for_child(association_name, context: serialize_context)
43
- ViewModel.preload_for_serialization(association_view, serialize_context: child_context)
43
+ ViewModel.preload_for_serialization(association_view)
44
44
  association_view = yield(association_view) if block_given?
45
45
  prerender_viewmodel(association_view, serialize_context: child_context)
46
46
  end
@@ -49,7 +49,11 @@ module ViewModel::ActiveRecord::NestedControllerBase
49
49
  association_view
50
50
  end
51
51
 
52
- def destroy_association(collection, serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
52
+ def destroy_association(collection, serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
53
+ if lock_owner
54
+ owner_viewmodel.find(owner_viewmodel_id, eager_include: false, lock: lock_owner)
55
+ end
56
+
53
57
  empty_update = collection ? [] : nil
54
58
  owner_viewmodel.deserialize_from_view(owner_update_hash(empty_update),
55
59
  deserialize_context: deserialize_context)
@@ -20,15 +20,15 @@ module ViewModel::ActiveRecord::SingularNestedController
20
20
  extend ActiveSupport::Concern
21
21
  include ViewModel::ActiveRecord::NestedControllerBase
22
22
 
23
- def show_associated(scope: nil, serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, &block)
24
- show_association(scope: scope, serialize_context: serialize_context, &block)
23
+ def show_associated(scope: nil, serialize_context: new_serialize_context, lock_owner: nil, &block)
24
+ show_association(scope: scope, serialize_context: serialize_context, lock_owner: lock_owner, &block)
25
25
  end
26
26
 
27
- def create_associated(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, &block)
28
- write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, &block)
27
+ def create_associated(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil, &block)
28
+ write_association(serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner, &block)
29
29
  end
30
30
 
31
- def destroy_associated(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
32
- destroy_association(false, serialize_context: serialize_context, deserialize_context: deserialize_context)
31
+ def destroy_associated(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context, lock_owner: nil)
32
+ destroy_association(false, serialize_context: serialize_context, deserialize_context: deserialize_context, lock_owner: lock_owner)
33
33
  end
34
34
  end
@@ -450,14 +450,13 @@ class ViewModel::ActiveRecord
450
450
  parent_data = ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
451
451
 
452
452
  # load children already attached to this model
453
- child_viewmodel_class = association_data.viewmodel_class
454
-
455
453
  previous_child_viewmodels =
456
454
  model.public_send(association_data.direct_reflection.name).map do |child_model|
455
+ child_viewmodel_class = association_data.viewmodel_class_for_model!(child_model.class)
457
456
  child_viewmodel_class.new(child_model)
458
457
  end
459
458
 
460
- if child_viewmodel_class._list_member?
459
+ if association_data.ordered?
461
460
  previous_child_viewmodels.sort_by!(&:_list_attribute)
462
461
  end
463
462
 
@@ -604,7 +603,7 @@ class ViewModel::ActiveRecord
604
603
  # need to be updated anyway since their parent pointer will change.
605
604
  new_positions = Array.new(resolved_children.length)
606
605
 
607
- if association_data.viewmodel_class._list_member?
606
+ if association_data.ordered?
608
607
  set_position = ->(index, pos) { new_positions[index] = pos }
609
608
 
610
609
  get_previous_position = ->(index) do
@@ -802,7 +801,7 @@ class ViewModel::ActiveRecord
802
801
  ReferencedCollectionMember.new(indirect_viewmodel_ref, direct_vm)
803
802
  end
804
803
 
805
- if direct_viewmodel_class._list_member?
804
+ if association_data.ordered?
806
805
  previous_members.sort_by!(&:position)
807
806
  end
808
807
 
@@ -868,7 +867,7 @@ class ViewModel::ActiveRecord
868
867
  viewmodel.association_changed!(association_data.association_name)
869
868
  end
870
869
 
871
- if direct_viewmodel_class._list_member?
870
+ if association_data.ordered?
872
871
  ActsAsManualList.update_positions(target_collection.members)
873
872
  end
874
873
 
@@ -102,11 +102,18 @@ class ViewModel
102
102
  class DownMigrator < Migrator
103
103
  private
104
104
 
105
- def migrate_viewmodel!(view_name, _, view_hash, references)
105
+ def migrate_viewmodel!(view_name, source_version, view_hash, references)
106
106
  path = @paths[view_name]
107
107
  return false unless path
108
108
 
109
- required_version, _current_version = @versions[view_name]
109
+ # In a serialized output, the source version should always be the present
110
+ # and the current version, unless already modified by a parent migration
111
+ required_version, current_version = @versions[view_name]
112
+ return false if source_version == required_version
113
+
114
+ unless source_version == current_version
115
+ raise ViewModel::Migration::UnspecifiedVersionError.new(view_name, source_version)
116
+ end
110
117
 
111
118
  path.reverse_each do |migration|
112
119
  migration.down(view_hash, references)
@@ -168,26 +168,61 @@ class ViewModel::ActiveRecord::BelongsToTest < ActiveSupport::TestCase
168
168
 
169
169
  class GCTests < ActiveSupport::TestCase
170
170
  include ARVMTestUtilities
171
- include ViewModelSpecHelpers::ParentAndBelongsToChild
171
+ include ViewModelSpecHelpers::Base
172
172
 
173
173
  def model_attributes
174
174
  super.merge(
175
175
  schema: ->(t) do
176
+ t.integer :destroyed_child_id
176
177
  t.integer :deleted_child_id
177
178
  t.integer :ignored_child_id
179
+ t.foreign_key :children, column: :destroyed_child_id
180
+ t.foreign_key :children, column: :deleted_child_id
181
+ t.foreign_key :children, column: :ignored_child_id
178
182
  end,
179
183
  model: ->(_m) do
180
- belongs_to :deleted_child, class_name: Child.name, dependent: :delete
181
- belongs_to :ignored_child, class_name: Child.name
184
+ belongs_to :destroyed_child, class_name: Child.name, inverse_of: :destroyed_model, dependent: :destroy
185
+ belongs_to :deleted_child, class_name: Child.name, inverse_of: :deleted_model, dependent: :delete
186
+ belongs_to :ignored_child, class_name: Child.name, inverse_of: :ignored_model
182
187
  end,
183
188
  viewmodel: ->(_v) do
184
- associations :deleted_child, :ignored_child
189
+ associations :destroyed_child, :deleted_child, :ignored_child
185
190
  end)
186
191
  end
187
192
 
188
- # test belongs_to garbage collection - dependent: delete_all
189
- def test_gc_dependent_delete_all
193
+ def child_attributes
194
+ super.merge(
195
+ model: ->(_m) do
196
+ has_one :destroyed_model, class_name: 'Model', inverse_of: :destroyed_child, foreign_key: 'destroyed_child_id'
197
+ has_one :deleted_model, class_name: 'Model', inverse_of: :deleted_child, foreign_key: 'deleted_child_id'
198
+ has_one :ignored_model, class_name: 'Model', inverse_of: :ignored_child, foreign_key: 'ignored_child_id'
199
+ end)
200
+ end
201
+
202
+ def viewmodel_class
203
+ child_viewmodel_class
204
+ super
205
+ end
206
+
207
+ # test belongs_to garbage collection - dependent: destroy
208
+ def test_gc_dependent_destroy
209
+ model = model_class.create(destroyed_child: Child.new(name: 'one'))
210
+
211
+ old_child = model.destroyed_child
212
+
213
+ alter_by_view!(ModelView, model) do |ov, _refs|
214
+ ov['destroyed_child'] = { '_type' => 'Child', 'name' => 'two' }
215
+ end
216
+
217
+ assert_equal('two', model.destroyed_child.name)
218
+ refute_equal(old_child, model.destroyed_child)
219
+ assert(Child.where(id: old_child.id).blank?)
220
+ end
221
+
222
+ # test belongs_to garbage collection - dependent: delete
223
+ def test_gc_dependent_delete
190
224
  model = model_class.create(deleted_child: Child.new(name: 'one'))
225
+
191
226
  old_child = model.deleted_child
192
227
 
193
228
  alter_by_view!(ModelView, model) do |ov, _refs|
@@ -808,6 +808,39 @@ class ViewModel::ActiveRecord::HasManyTest < ActiveSupport::TestCase
808
808
  assert_match(/Removed entities must have only _type and id fields/, ex.message)
809
809
  end
810
810
 
811
+ def test_functional_update_move
812
+ c1_id, c2_id, c3_id = @model1.children.pluck(:id)
813
+ c4_id, c5_id = @model2.children.pluck(:id)
814
+
815
+ remove_fupdate = build_fupdate do
816
+ remove([{ '_type' => 'Child', 'id' => c2_id }])
817
+ end
818
+
819
+ append_fupdate = build_fupdate do
820
+ append([{ '_type' => 'Child', 'id' => c2_id }])
821
+ end
822
+
823
+ move_view = [
824
+ {
825
+ '_type' => 'Model',
826
+ 'id' => @model1.id,
827
+ 'children' => remove_fupdate
828
+ },
829
+ {
830
+ '_type' => 'Model',
831
+ 'id' => @model2.id,
832
+ 'children' => append_fupdate
833
+ }
834
+ ]
835
+
836
+ viewmodel_class.deserialize_from_view(move_view)
837
+ @model1.reload
838
+ @model2.reload
839
+
840
+ assert_equal([c1_id, c3_id], @model1.children.pluck(:id))
841
+ assert_equal([c4_id, c5_id, c2_id], @model2.children.pluck(:id))
842
+ end
843
+
811
844
  def test_functional_update_update_success
812
845
  c1_id, c2_id, c3_id = @model1.children.pluck(:id)
813
846
 
@@ -865,6 +898,143 @@ class ViewModel::ActiveRecord::HasManyTest < ActiveSupport::TestCase
865
898
  assert_match(/Duplicate functional update targets\b.*\bChild\b/, ex.message)
866
899
  end
867
900
 
901
+ describe 'sti polymorphic children' do
902
+ def setup
903
+ child_viewmodel_class
904
+ dog_viewmodel_class
905
+ cat_viewmodel_class
906
+ enable_logging!
907
+ end
908
+
909
+ def child_attributes
910
+ super().merge(schema: ->(t) do
911
+ t.string :type, null: false
912
+ t.integer :dog_number
913
+ t.integer :cat_number
914
+ end)
915
+ end
916
+
917
+ def subject_association_features
918
+ { viewmodels: [:Dog, :Cat] }
919
+ end
920
+
921
+ def dog_viewmodel_class
922
+ @dog_viewmodel_class ||= define_viewmodel_class(:Dog, namespace: namespace, viewmodel_base: viewmodel_base, model_base: child_model_class) do
923
+ define_model {}
924
+ define_viewmodel do
925
+ attribute :dog_number
926
+ acts_as_list :position
927
+ end
928
+ end
929
+ end
930
+
931
+ def cat_viewmodel_class
932
+ @cat_viewmodel_class ||= define_viewmodel_class(:Cat, namespace: namespace, viewmodel_base: viewmodel_base, model_base: child_model_class) do
933
+ define_model {}
934
+ define_viewmodel do
935
+ attribute :cat_number
936
+ acts_as_list :position
937
+ end
938
+ end
939
+ end
940
+
941
+ def new_model
942
+ model_class.new(name: 'p', children: [Dog.new(position: 1, dog_number: 1), Cat.new(position: 2, cat_number: 2)])
943
+ end
944
+
945
+ it 'creates the model structure' do
946
+ m = create_model!
947
+ m.reload
948
+ assert(m.is_a?(Model))
949
+ children = m.children.order(:position)
950
+ assert_equal(2, children.size)
951
+ assert_kind_of(Dog, children[0])
952
+ assert_kind_of(Cat, children[1])
953
+ end
954
+
955
+ it 'serializes' do
956
+ model = create_model!
957
+ view = serialize(ModelView.new(model))
958
+ expected_view = {
959
+ 'id' => 1, '_type' => 'Model', '_version' => 1, 'name' => 'p',
960
+ 'children' => [
961
+ { 'id' => 1, '_type' => 'Dog', '_version' => 1, 'dog_number' => 1 },
962
+ { 'id' => 2, '_type' => 'Cat', '_version' => 1, 'cat_number' => 2 },
963
+ ]
964
+ }
965
+ assert_equal(expected_view, view)
966
+ end
967
+
968
+ it 'creates from view' do
969
+ view = {
970
+ '_type' => 'Model',
971
+ 'name' => 'p',
972
+ 'children' => [
973
+ { '_type' => 'Dog', 'dog_number' => 1 },
974
+ { '_type' => 'Cat', 'cat_number' => 2 },
975
+ ],
976
+ }
977
+
978
+ pv = ModelView.deserialize_from_view(view)
979
+ p = pv.model
980
+
981
+ assert(!p.changed?)
982
+ assert(!p.new_record?)
983
+
984
+ assert_equal('p', p.name)
985
+
986
+ children = p.children.order(:position)
987
+
988
+ assert_equal(2, children.size)
989
+ assert_kind_of(Dog, children[0])
990
+ assert_equal(1, children[0].dog_number)
991
+ assert_kind_of(Cat, children[1])
992
+ assert_equal(2, children[1].cat_number)
993
+ end
994
+
995
+ it 'updates with reordering' do
996
+ model = create_model!
997
+
998
+ alter_by_view!(ModelView, model) do |view, _refs|
999
+ view['children'].reverse!
1000
+ end
1001
+
1002
+ children = model.children.order(:position)
1003
+ assert_equal(2, children.size)
1004
+ assert_kind_of(Cat, children[0])
1005
+ assert_equal(2, children[0].cat_number)
1006
+ assert_kind_of(Dog, children[1])
1007
+ assert_equal(1, children[1].dog_number)
1008
+ end
1009
+
1010
+ it 'functional updates' do
1011
+ model = create_model!
1012
+
1013
+ alter_by_view!(ModelView, model) do |view, _refs|
1014
+ view['children'] = build_fupdate do
1015
+ append([{ '_type' => 'Cat', 'cat_number' => 100 }])
1016
+ end
1017
+ end
1018
+
1019
+ assert_equal(3, model.children.size)
1020
+ new_child = model.children.order(:position).last
1021
+ assert_kind_of(Cat, new_child)
1022
+ assert_equal(100, new_child.cat_number)
1023
+ end
1024
+
1025
+ it 'calculates eager_includes' do
1026
+ includes = viewmodel_class.eager_includes
1027
+ expected = DeepPreloader::Spec.new(
1028
+ 'children' => DeepPreloader::PolymorphicSpec.new(
1029
+ {
1030
+ 'Dog' => DeepPreloader::Spec.new,
1031
+ 'Cat' => DeepPreloader::Spec.new,
1032
+ }))
1033
+
1034
+ assert_equal(includes, expected)
1035
+ end
1036
+ end
1037
+
868
1038
  describe 'owned reference children' do
869
1039
  def child_attributes
870
1040
  super.merge(viewmodel: ->(_v) { root! })
@@ -118,7 +118,7 @@ class ViewModel::ActiveRecord::HasManyThroughPolyTest < ActiveSupport::TestCase
118
118
  def test_loading_batching
119
119
  context = ParentView.new_serialize_context
120
120
  log_queries do
121
- parent_views = ParentView.load(serialize_context: context)
121
+ parent_views = ParentView.load
122
122
  serialize(parent_views, serialize_context: context)
123
123
  end
124
124
 
@@ -112,7 +112,7 @@ class ViewModel::ActiveRecord::HasManyThroughTest < ActiveSupport::TestCase
112
112
  def test_loading_batching
113
113
  context = ParentView.new_serialize_context
114
114
  log_queries do
115
- parent_views = ParentView.load(serialize_context: context)
115
+ parent_views = ParentView.load
116
116
  serialize(parent_views, serialize_context: context)
117
117
  end
118
118
 
@@ -494,7 +494,7 @@ class ViewModel::CallbacksTest < ActiveSupport::TestCase
494
494
  let(:callback) { CallbackCrasher.new }
495
495
 
496
496
  it 'raises the callback error' do
497
- proc { serialize(vm) }.must_raise(Crash)
497
+ _(-> { serialize(vm) }).must_raise(Crash)
498
498
  end
499
499
 
500
500
  describe 'with an access control that rejects' do
@@ -503,7 +503,7 @@ class ViewModel::CallbacksTest < ActiveSupport::TestCase
503
503
  end
504
504
 
505
505
  it 'fails access control first' do
506
- proc { serialize(vm) }.must_raise(ViewModel::AccessControlError)
506
+ _(-> { serialize(vm) }).must_raise(ViewModel::AccessControlError)
507
507
  end
508
508
 
509
509
  describe 'and a view-mutating callback that crashes' do
@@ -514,7 +514,7 @@ class ViewModel::CallbacksTest < ActiveSupport::TestCase
514
514
  let(:callback) { MutatingCrasher.new }
515
515
 
516
516
  it 'raises the callback error first' do
517
- proc { serialize(vm) }.must_raise(Crash)
517
+ _(-> { serialize(vm) }).must_raise(Crash)
518
518
  end
519
519
  end
520
520
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iknow_view_models
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - iKnow Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-13 00:00:00.000000000 Z
11
+ date: 2021-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 1.0.1
61
+ version: 1.0.2
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 1.0.1
68
+ version: 1.0.2
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: iknow_cache
71
71
  requirement: !ruby/object:Gem::Requirement