iknow_view_models 3.2.14 → 3.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae5588513419124ea7eac511b571af52ea42e23d9bf87c01cf59a60af50e8055
4
- data.tar.gz: c7de424a0a465c65fa4ba6823f7aa3743c69c0d41a8efa9da9c67efb030931d8
3
+ metadata.gz: 10bc5dca413705470b182157c2a38ebc2e11a7ae5a121ad328d03b35ff64f1d6
4
+ data.tar.gz: c31bf72ad0a62316705618344963e7ad78d29224b33c5472dc00ca9ec9413d75
5
5
  SHA512:
6
- metadata.gz: dbcde44426aa3f49d963f4f6588ea49f2fce10ae616e8dd5d78af30bac20a09381fe2e369829b92888c23078136f12a2f6a4b2fd49be17e4920927ed918dc04d
7
- data.tar.gz: da13c596cb4ec09f17d74c79d9c2c17b8e3b9b2405856b49355f8d02be015d8cb430984fc2adf1c6f3fa668332152b8d440e5972204b66694d0a181cc1213f52
6
+ metadata.gz: 989ff044b31ae5bdcea3e489e0528c9d7d6115fc19193c11f0986985001c2bd0d6bb3c40ef99f331d525c466c09849b2218dcbad8169545106b3736e6da7360b
7
+ data.tar.gz: 6bb882161397c6ad72456dd8a5d9389fce1ce3be9c27347d8168790070072287e9e5ab669c6406504e67cff15fce1df6a58babc9ec59be542a9f2935f407a787
data/.circleci/config.yml CHANGED
@@ -5,7 +5,7 @@ executors:
5
5
  parameters:
6
6
  ruby-version:
7
7
  type: string
8
- default: "2.6"
8
+ default: "2.7"
9
9
  pg-version:
10
10
  type: string
11
11
  default: "11"
@@ -102,8 +102,8 @@ workflows:
102
102
  build:
103
103
  jobs:
104
104
  - test:
105
- name: 'ruby 2.6 rails 5.2 pg 12'
106
- ruby-version: "2.6"
105
+ name: 'ruby 2.7 rails 5.2 pg 12'
106
+ ruby-version: "2.7"
107
107
  pg-version: "12"
108
108
  gemfile: gemfiles/rails_5_2.gemfile
109
109
  - test:
@@ -116,6 +116,11 @@ workflows:
116
116
  ruby-version: "2.7"
117
117
  pg-version: "12"
118
118
  gemfile: gemfiles/rails_6_1.gemfile
119
+ - test:
120
+ name: 'ruby 3.0 rails 6.1 pg 12'
121
+ ruby-version: "3.0"
122
+ pg-version: "12"
123
+ gemfile: gemfiles/rails_6_1.gemfile
119
124
  - publish:
120
125
  filters:
121
126
  branches:
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
20
  spec.require_paths = ['lib']
21
21
 
22
- spec.required_ruby_version = 2.6
22
+ spec.required_ruby_version = '>= 2.7'
23
23
 
24
24
  spec.add_dependency 'activerecord', '>= 5.0'
25
25
  spec.add_dependency 'activesupport', '>= 5.0'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.2.14'
4
+ VERSION = '3.4.2'
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
 
@@ -238,16 +238,16 @@ class ViewModel
238
238
  ViewModel::SerializeContext
239
239
  end
240
240
 
241
- def new_serialize_context(*args)
242
- serialize_context_class.new(*args)
241
+ def new_serialize_context(...)
242
+ serialize_context_class.new(...)
243
243
  end
244
244
 
245
245
  def deserialize_context_class
246
246
  ViewModel::DeserializeContext
247
247
  end
248
248
 
249
- def new_deserialize_context(*args)
250
- deserialize_context_class.new(*args)
249
+ def new_deserialize_context(...)
250
+ deserialize_context_class.new(...)
251
251
  end
252
252
 
253
253
  def accepts_schema_version?(schema_version)
@@ -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
@@ -196,6 +196,21 @@ class ViewModel::ActiveRecord::AssociationData
196
196
  viewmodel_classes.first
197
197
  end
198
198
 
199
+ def ordered?
200
+ @ordered ||=
201
+ if through?
202
+ direct_viewmodel._list_member?
203
+ else
204
+ list_members = viewmodel_classes.map { |c| c._list_member? }.uniq
205
+
206
+ if list_members.size > 1
207
+ raise ArgumentError.new('Inconsistent associated views: mixed list membership')
208
+ end
209
+
210
+ list_members[0]
211
+ end
212
+ end
213
+
199
214
  def through?
200
215
  @indirect_association_name.present?
201
216
  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.viewmodel_classes.size > 1
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.viewmodel_classes.size > 1
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.viewmodel_classes.size > 1
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)
@@ -76,7 +76,7 @@ module ViewModel::ActiveRecord::Controller
76
76
  end
77
77
  end
78
78
 
79
- def prerender_viewmodel(*)
79
+ def prerender_viewmodel(...)
80
80
  super do |jbuilder|
81
81
  yield(jbuilder) if block_given?
82
82
 
@@ -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)
@@ -24,8 +24,8 @@ class ViewModel::TraversalContext
24
24
 
25
25
  delegate :access_control, :callbacks, to: :shared_context
26
26
 
27
- def self.new_child(*args)
28
- self.allocate.tap { |c| c.initialize_as_child(*args) }
27
+ def self.new_child(...)
28
+ self.allocate.tap { |c| c.initialize_as_child(...) }
29
29
  end
30
30
 
31
31
  def initialize(shared_context: nil, **shared_context_params)
@@ -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,131 @@ 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
+ end
1025
+
868
1026
  describe 'owned reference children' do
869
1027
  def child_attributes
870
1028
  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.2.14
4
+ version: 3.4.2
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-03-26 00:00:00.000000000 Z
11
+ date: 2021-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -486,14 +486,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
486
486
  requirements:
487
487
  - - ">="
488
488
  - !ruby/object:Gem::Version
489
- version: '0'
489
+ version: '2.7'
490
490
  required_rubygems_version: !ruby/object:Gem::Requirement
491
491
  requirements:
492
492
  - - ">="
493
493
  - !ruby/object:Gem::Version
494
494
  version: '0'
495
495
  requirements: []
496
- rubygems_version: 3.0.3
496
+ rubygems_version: 3.1.6
497
497
  signing_key:
498
498
  specification_version: 4
499
499
  summary: ViewModels provide a means of encapsulating a collection of related data