iknow_view_models 3.2.12 → 3.4.0

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: 284af292a0f1a70b761afd2e3ef084ad9f8f5853f00c8af8ac2d3b1227eb9cb5
4
- data.tar.gz: 61fdea22a0b13db50e7b237e472900eef376e95d04fed5f21a8c1ef7422aaa56
3
+ metadata.gz: 1e0fb612b3ee4fc28b8bba11d40dfcd2da75775af0c8154a83a913f179824ccd
4
+ data.tar.gz: 6027d6f5a46e2654432e8c7b28cfcde8d2cb07f523efa7f87be199241246b83e
5
5
  SHA512:
6
- metadata.gz: 64ffd808756c298236f4e00b11cdf7f1410019bc506f306c6cfbaa97b953212a5c58e7b56263deac0aac5e80398fa2d551922d6b90d7a3c60732a50b66467384
7
- data.tar.gz: ccd3301209a94c8e9cd9a44166b153ae3edafab81cfe49498849e6c22dc413956d33e3cdba0ab001456122f47400df47dfbe327d9a9005f1947c512da3d9e523
6
+ metadata.gz: 7301fdf85df162d86aaaa558e136dd1df2f047aec97abd084ba83369cac83b5d1b7904a8d0930c9cfb34a9c08aa49e835f89d0ea5c83eef007dbec4ff92a4b31
7
+ data.tar.gz: 861f4a2b0ab0eb4a48fa6ef1a2116307dd9d8409508e40c2534cb3e07b6dfe92e843f2eeb4904249d97326d83147eccf3b1757c0200b2ce796aa83eeb1eb3c0f
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:
@@ -111,6 +111,16 @@ workflows:
111
111
  ruby-version: "2.7"
112
112
  pg-version: "12"
113
113
  gemfile: gemfiles/rails_6_0.gemfile
114
+ - test:
115
+ name: 'ruby 2.7 rails 6.1 pg 12'
116
+ ruby-version: "2.7"
117
+ pg-version: "12"
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
114
124
  - publish:
115
125
  filters:
116
126
  branches:
data/Appraisals CHANGED
@@ -7,3 +7,8 @@ appraise 'rails-6-0' do
7
7
  gem 'activerecord', '~> 6.0.0'
8
8
  gem 'activesupport', '~> 6.0.0'
9
9
  end
10
+
11
+ appraise 'rails-6-1' do
12
+ gem 'activerecord', '~> 6.1.0'
13
+ gem 'activesupport', '~> 6.1.0'
14
+ end
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # IknowViewModels
2
2
 
3
- [![Build Status](https://travis-ci.org/iknow/iknow_view_models.svg?branch=master)](https://travis-ci.org/iknow/iknow_view_models)
3
+ [![Build Status](https://circleci.com/gh/iknow/iknow_view_models.svg?style=svg)](https://circleci.com/gh/iknow/iknow_view_models/)
4
4
 
5
5
  ViewModels provide a means of encapsulating a collection of related data and specifying its JSON serialization.
6
6
 
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'minitest-ci'
6
+ gem 'activerecord', '~> 6.1.0'
7
+ gem 'activesupport', '~> 6.1.0'
8
+
9
+ gemspec path: '../'
@@ -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.12'
4
+ VERSION = '3.4.0'
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
 
@@ -173,6 +173,17 @@ class ViewModel
173
173
  Jbuilder.new { |json| serialize(viewmodel, json, serialize_context: serialize_context) }.attributes!
174
174
  end
175
175
 
176
+ def serialize_from_cache(views, migration_versions: {}, locked: false, serialize_context:)
177
+ plural = views.is_a?(Array)
178
+ views = Array.wrap(views)
179
+
180
+ json_views, json_refs = ViewModel::ActiveRecord::Cache.render_viewmodels_from_cache(
181
+ views, locked: locked, migration_versions: migration_versions, serialize_context: serialize_context)
182
+
183
+ json_views = json_views.first unless plural
184
+ return json_views, json_refs
185
+ end
186
+
176
187
  def encode_json(value)
177
188
  # Jbuilder#encode no longer uses MultiJson, but instead calls `.to_json`. In
178
189
  # the context of ActiveSupport, we don't want this, because AS replaces the
@@ -227,16 +238,16 @@ class ViewModel
227
238
  ViewModel::SerializeContext
228
239
  end
229
240
 
230
- def new_serialize_context(*args)
231
- serialize_context_class.new(*args)
241
+ def new_serialize_context(...)
242
+ serialize_context_class.new(...)
232
243
  end
233
244
 
234
245
  def deserialize_context_class
235
246
  ViewModel::DeserializeContext
236
247
  end
237
248
 
238
- def new_deserialize_context(*args)
239
- deserialize_context_class.new(*args)
249
+ def new_deserialize_context(...)
250
+ deserialize_context_class.new(...)
240
251
  end
241
252
 
242
253
  def accepts_schema_version?(schema_version)
@@ -256,10 +267,10 @@ class ViewModel
256
267
  Base64.urlsafe_encode64(hash, padding: false)
257
268
  end
258
269
 
259
- 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)
260
271
  Array.wrap(viewmodels).group_by(&:class).each do |type, views|
261
272
  DeepPreloader.preload(views.map(&:model),
262
- type.eager_includes(serialize_context: serialize_context, include_referenced: include_referenced),
273
+ type.eager_includes(include_referenced: include_referenced),
263
274
  lock: lock)
264
275
  end
265
276
  end
@@ -343,8 +354,8 @@ class ViewModel
343
354
  context.for_child(self, association_name: member_name)
344
355
  end
345
356
 
346
- def preload_for_serialization(lock: nil, serialize_context: self.class.new_serialize_context)
347
- 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)
348
359
  end
349
360
 
350
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
 
@@ -182,26 +182,22 @@ class ViewModel::ActiveRecord < ViewModel::Record
182
182
  end
183
183
 
184
184
  # Constructs a preload specification of the required models for
185
- # serializing/deserializing this view.
186
- def eager_includes(serialize_context: new_serialize_context, include_referenced: true)
185
+ # serializing/deserializing this view. Cycles in the schema will be broken
186
+ # after two layers of eager loading.
187
+ def eager_includes(include_referenced: true, vm_path: [])
187
188
  association_specs = {}
189
+
190
+ return nil if vm_path.count(self) > 2
191
+
192
+ child_path = vm_path + [self]
188
193
  _members.each do |assoc_name, association_data|
189
194
  next unless association_data.is_a?(AssociationData)
190
195
  next if association_data.external?
191
196
 
192
- child_context =
193
- if self.synthetic
194
- serialize_context
195
- elsif association_data.referenced?
196
- serialize_context.for_references
197
- else
198
- serialize_context.for_child(nil, association_name: assoc_name)
199
- end
200
-
201
197
  case
202
198
  when association_data.through?
203
199
  viewmodel = association_data.direct_viewmodel
204
- children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced)
200
+ children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path)
205
201
 
206
202
  when !include_referenced && association_data.referenced?
207
203
  children = nil # Load up to the root viewmodel, but no further
@@ -210,13 +206,13 @@ class ViewModel::ActiveRecord < ViewModel::Record
210
206
  children_by_klass = {}
211
207
  association_data.viewmodel_classes.each do |vm_class|
212
208
  klass = vm_class.model_class.name
213
- children_by_klass[klass] = vm_class.eager_includes(serialize_context: child_context, include_referenced: include_referenced)
209
+ children_by_klass[klass] = vm_class.eager_includes(include_referenced: include_referenced, vm_path: child_path)
214
210
  end
215
211
  children = DeepPreloader::PolymorphicSpec.new(children_by_klass)
216
212
 
217
213
  else
218
214
  viewmodel = association_data.viewmodel_class
219
- children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced)
215
+ children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path)
220
216
  end
221
217
 
222
218
  association_specs[association_data.direct_reflection.name.to_s] = children
@@ -34,10 +34,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
34
34
 
35
35
  vms = association_scope.map { |model| associated_viewmodel.new(model) }
36
36
 
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
37
+ ViewModel.preload_for_serialization(vms) if eager_include
41
38
 
42
39
  if association_data.collection?
43
40
  vms
@@ -11,6 +11,30 @@ class ViewModel::ActiveRecord::Cache
11
11
 
12
12
  attr_reader :viewmodel_class
13
13
 
14
+ class << self
15
+ def render_viewmodels_from_cache(viewmodels, migration_versions: {}, locked: false, serialize_context: nil)
16
+ if viewmodels.empty?
17
+ return [], {}
18
+ end
19
+
20
+ ids = viewmodels.map(&:id)
21
+ # ideally the roots wouldn't have to all be the same type
22
+ viewmodel_class = viewmodels.first.class
23
+ serialize_context ||= viewmodel_class.new_serialize_context
24
+
25
+ render_from_cache(viewmodel_class, ids,
26
+ initial_viewmodels: viewmodels,
27
+ migration_versions: migration_versions,
28
+ locked: locked,
29
+ serialize_context: serialize_context)
30
+ end
31
+
32
+ def render_from_cache(viewmodel_class, ids, initial_viewmodels: nil, migration_versions: {}, locked: false, serialize_context: viewmodel_class.new_serialize_context)
33
+ worker = CacheWorker.new(migration_versions: migration_versions, serialize_context: serialize_context)
34
+ worker.render_from_cache(viewmodel_class, ids, initial_viewmodels: initial_viewmodels, locked: locked)
35
+ end
36
+ end
37
+
14
38
  # If cache_group: is specified, it must be a group of a single key: `:id`
15
39
  def initialize(viewmodel_class, cache_group: nil)
16
40
  @viewmodel_class = viewmodel_class
@@ -35,85 +59,98 @@ class ViewModel::ActiveRecord::Cache
35
59
  @cache_group.invalidate_cache_group
36
60
  end
37
61
 
62
+ # @deprecated Replaced by class methods
38
63
  def fetch_by_viewmodel(viewmodels, migration_versions: {}, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
39
64
  ids = viewmodels.map(&:id)
40
65
  fetch(ids, initial_viewmodels: viewmodels, migration_versions: migration_versions, locked: locked, serialize_context: serialize_context)
41
66
  end
42
67
 
68
+ # @deprecated Replaced by class methods
43
69
  def fetch(ids, initial_viewmodels: nil, migration_versions: {}, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
44
- data_serializations = Array.new(ids.size)
45
- worker = CacheWorker.new(migration_versions: migration_versions, serialize_context: serialize_context)
46
-
47
- # If initial root viewmodels were provided, visit them to ensure that they
48
- # are visible. Other than this, no traversal callbacks are performed, as a
49
- # view may be resolved from the cache without ever loading its viewmodel.
50
- # Note that if unlocked, these views will be reloaded as part of obtaining a
51
- # share lock. If the visibility of this viewmodel can change due to edits,
52
- # it is necessary to obtain a lock before calling `fetch`.
53
- initial_viewmodels&.each do |v|
54
- serialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeVisit, v)
55
- serialize_context.run_callback(ViewModel::Callbacks::Hook::AfterVisit, v)
56
- end
70
+ self.class.render_from_cache(@viewmodel_class, ids,
71
+ initial_viewmodels: initial_viewmodels, locked: locked,
72
+ migration_versions: migration_versions, serialize_context: serialize_context)
73
+ end
57
74
 
58
- # Collect input array positions for each id, allowing duplicates
59
- positions = ids.each_with_index.with_object({}) do |(id, i), h|
60
- (h[id] ||= []) << i
61
- end
75
+ class CacheWorker
76
+ SENTINEL = Object.new
77
+ WorklistEntry = Struct.new(:ref_name, :viewmodel)
62
78
 
63
- # Fetch duplicates only once
64
- ids = positions.keys
79
+ attr_reader :migration_versions, :serialize_context, :resolved_references
65
80
 
66
- # Load existing serializations from the cache
67
- cached_serializations = worker.load_from_cache(self, ids)
68
- cached_serializations.each do |id, data|
69
- positions[id].each do |idx|
70
- data_serializations[idx] = data
71
- end
81
+ def initialize(migration_versions:, serialize_context:)
82
+ @worklist = {} # Hash[type_name, Hash[id, WorklistEntry]]
83
+ @resolved_references = {} # Hash[refname, json]
84
+ @migration_versions = migration_versions
85
+ @migrated_cache_versions = {}
86
+ @serialize_context = serialize_context
72
87
  end
73
88
 
74
- # Resolve and serialize missing views
75
- missing_ids = ids.to_set.subtract(cached_serializations.keys)
89
+ def render_from_cache(viewmodel_class, ids, initial_viewmodels: nil, locked: false)
90
+ viewmodel_class.transaction do
91
+ root_serializations = Array.new(ids.size)
76
92
 
77
- # If initial viewmodels have been locked, we can serialize them for cache
78
- # misses.
79
- available_viewmodels =
80
- if locked
81
- initial_viewmodels&.each_with_object({}) do |vm, h|
82
- h[vm.id] = vm if missing_ids.include?(vm.id)
93
+ # Collect input array positions for each id, allowing duplicates
94
+ positions = ids.each_with_index.with_object({}) do |(id, i), h|
95
+ (h[id] ||= []) << i
83
96
  end
84
- end
85
97
 
86
- @viewmodel_class.transaction do
87
- # Load remaining views and serialize
88
- viewmodels = worker.find_and_preload_viewmodels(@viewmodel_class, missing_ids.to_a,
89
- available_viewmodels: available_viewmodels)
98
+ # If duplicates are specified, fetch each only once
99
+ ids = positions.keys
100
+
101
+ ids_to_render = ids.to_set
102
+
103
+ if viewmodel_class < CacheableView
104
+ # Load existing serializations from the cache
105
+ cached_serializations = load_from_cache(viewmodel_class.viewmodel_cache, ids)
106
+ cached_serializations.each do |id, data|
107
+ positions[id].each do |idx|
108
+ root_serializations[idx] = data
109
+ end
110
+ end
90
111
 
91
- loaded_serializations = worker.serialize_and_cache(viewmodels)
92
- loaded_serializations.each do |id, data|
93
- positions[id].each do |idx|
94
- data_serializations[idx] = data
112
+ ids_to_render.subtract(cached_serializations.keys)
113
+
114
+ # If initial root viewmodels were provided, call hooks on any
115
+ # viewmodels which were rendered from the cache to ensure that the
116
+ # root is visible (in isolation). Other than this, no traversal
117
+ # callbacks are performed for cache-rendered views. This particularly
118
+ # requires care for references: if a visible view may refer to
119
+ # non-visible cacheable views, those referenced views will not be
120
+ # access control checked.
121
+ initial_viewmodels&.each do |v|
122
+ next unless cached_serializations.has_key?(v.id)
123
+ serialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeVisit, v)
124
+ serialize_context.run_callback(ViewModel::Callbacks::Hook::AfterVisit, v)
125
+ end
95
126
  end
96
- end
97
127
 
98
- # Resolve references
99
- worker.resolve_references!
128
+ # Render remaining views. If initial viewmodels have been locked, we may
129
+ # use them to serialize from, otherwise we must reload with share lock
130
+ # in find_and_preload.
131
+ available_viewmodels =
132
+ if locked
133
+ initial_viewmodels&.each_with_object({}) do |vm, h|
134
+ h[vm.id] = vm if ids_to_render.include?(vm.id)
135
+ end
136
+ end
100
137
 
101
- return data_serializations, worker.resolved_references
102
- end
103
- end
138
+ viewmodels = find_and_preload_viewmodels(viewmodel_class, ids_to_render.to_a,
139
+ available_viewmodels: available_viewmodels)
104
140
 
105
- class CacheWorker
106
- SENTINEL = Object.new
107
- WorklistEntry = Struct.new(:ref_name, :viewmodel)
141
+ loaded_serializations = serialize_and_cache(viewmodels)
108
142
 
109
- attr_reader :migration_versions, :serialize_context, :resolved_references
143
+ loaded_serializations.each do |id, data|
144
+ positions[id].each do |idx|
145
+ root_serializations[idx] = data
146
+ end
147
+ end
110
148
 
111
- def initialize(migration_versions:, serialize_context:)
112
- @worklist = {}
113
- @resolved_references = {}
114
- @migration_versions = migration_versions
115
- @migrated_cache_versions = {}
116
- @serialize_context = serialize_context
149
+ # recursively resolve referenced views
150
+ self.resolve_references!
151
+
152
+ [root_serializations, self.resolved_references]
153
+ end
117
154
  end
118
155
 
119
156
  def resolve_references!
@@ -159,7 +196,7 @@ class ViewModel::ActiveRecord::Cache
159
196
  # {id=>serialized_view}. Any references encountered are added to the
160
197
  # worklist.
161
198
  def load_from_cache(viewmodel_cache, ids)
162
- 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))
163
200
 
164
201
  cached_serializations.each_with_object({}) do |(id, cached_serialization), result|
165
202
  add_refs_to_worklist(cached_serialization[:ref_cache])
@@ -218,8 +255,7 @@ class ViewModel::ActiveRecord::Cache
218
255
  if viewmodel.class < CacheableView
219
256
  cacheable_references = referenced_viewmodels.transform_values { |vm| cacheable_reference(vm) }
220
257
  target_cache = viewmodel.class.viewmodel_cache
221
- target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references,
222
- serialize_context: serialize_context)
258
+ target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references)
223
259
  end
224
260
 
225
261
  result[viewmodel.id] = data_serialization
@@ -242,15 +278,13 @@ class ViewModel::ActiveRecord::Cache
242
278
  if ids.present?
243
279
  found = viewmodel_class.find(ids,
244
280
  eager_include: false,
245
- lock: 'FOR SHARE',
246
- serialize_context: serialize_context)
281
+ lock: 'FOR SHARE')
247
282
  viewmodels.concat(found)
248
283
  end
249
284
 
250
285
  ViewModel.preload_for_serialization(viewmodels,
251
286
  include_referenced: false,
252
- lock: 'FOR SHARE',
253
- serialize_context: serialize_context)
287
+ lock: 'FOR SHARE')
254
288
 
255
289
  viewmodels
256
290
  end
@@ -298,12 +332,12 @@ class ViewModel::ActiveRecord::Cache
298
332
  end
299
333
 
300
334
  # Save the provided serialization and reference data in the cache
301
- def store(id, migration_version, data_serialization, ref_cache, serialize_context:)
335
+ def store(id, migration_version, data_serialization, ref_cache)
302
336
  key = key_for(id, migration_version)
303
337
  cache_for(migration_version).write(key, { data: data_serialization, ref_cache: ref_cache })
304
338
  end
305
339
 
306
- def load(ids, migration_version, serialize_context:)
340
+ def load(ids, migration_version)
307
341
  keys = ids.map { |id| key_for(id, migration_version) }
308
342
  results = cache_for(migration_version).read_multi(keys)
309
343
  results.transform_keys! { |key| id_for(key) }
@@ -39,14 +39,6 @@ module ViewModel::ActiveRecord::Cache::CacheableView
39
39
  def viewmodel_cache
40
40
  @viewmodel_cache
41
41
  end
42
-
43
- def serialize_from_cache(views, migration_versions: {}, locked: false, serialize_context:)
44
- plural = views.is_a?(Array)
45
- views = Array.wrap(views)
46
- json_views, json_refs = viewmodel_cache.fetch_by_viewmodel(views, locked: locked, migration_versions: migration_versions, serialize_context: serialize_context)
47
- json_views = json_views.first unless plural
48
- return json_views, json_refs
49
- end
50
42
  end
51
43
 
52
44
  # Clear the cache if the view or its nested children were changed during
@@ -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,13 +9,13 @@ 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
- associated_views = owner_view.load_associated(association_name, scope: scope, serialize_context: serialize_context)
18
+ associated_views = owner_view.load_associated(association_name, scope: scope)
19
19
 
20
20
  associated_views = yield(associated_views) if block_given?
21
21
 
@@ -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
@@ -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)
@@ -135,8 +135,8 @@ class ViewModel::ActiveRecord
135
135
  [data, refs]
136
136
  end
137
137
 
138
- def fetch_with_cache
139
- viewmodel_class.viewmodel_cache.fetch([root.id], migration_versions: migration_versions)
138
+ def fetch_with_cache(**rest)
139
+ ViewModel::ActiveRecord::Cache.render_from_cache(viewmodel_class, [root.id], migration_versions: migration_versions, **rest)
140
140
  end
141
141
 
142
142
  def serialize_with_cache
@@ -206,6 +206,19 @@ class ViewModel::ActiveRecord
206
206
 
207
207
  describe 'without migrations' do
208
208
  include BehavesLikeACache
209
+
210
+ it 'returns the right serialization with provided initial viewmodel' do
211
+ fetched_result = parse_result(fetch_with_cache(initial_viewmodels: [root_view]))
212
+
213
+ value(fetched_result).must_equal(serialize_from_database)
214
+ end
215
+
216
+ it 'returns the right serialization with provided locked initial viewmodel' do
217
+ locked_root_view = viewmodel_class.new(model_class.lock("FOR SHARE").find(root.id))
218
+ fetched_result = parse_result(fetch_with_cache(initial_viewmodels: [locked_root_view], locked: true))
219
+
220
+ value(fetched_result).must_equal(serialize_from_database)
221
+ end
209
222
  end
210
223
 
211
224
  describe 'with migrations' do
@@ -337,8 +350,8 @@ class ViewModel::ActiveRecord
337
350
  [data, refs]
338
351
  end
339
352
 
340
- def fetch_with_cache
341
- viewmodel_class.viewmodel_cache.fetch([root.id, root2.id])
353
+ def fetch_with_cache(**rest)
354
+ ViewModel::ActiveRecord::Cache.render_from_cache(viewmodel_class, [root.id, root2.id], **rest)
342
355
  end
343
356
 
344
357
  it 'merges matching shared references between cache hits and misses' do
@@ -376,8 +389,8 @@ class ViewModel::ActiveRecord
376
389
  end
377
390
 
378
391
  describe 'when fetched by viewmodel' do
379
- def fetch_with_cache
380
- viewmodel_class.viewmodel_cache.fetch_by_viewmodel([root_view])
392
+ def fetch_with_cache(**rest)
393
+ ViewModel::ActiveRecord::Cache.render_viewmodels_from_cache([root_view], **rest)
381
394
  end
382
395
 
383
396
  include CacheableParentAndChildren
@@ -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
 
@@ -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
 
@@ -416,6 +416,11 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
416
416
  @list = List.new(child: List.new(child: nil))
417
417
  end
418
418
 
419
+ def test_list_eager_includes
420
+ expected_includes = 3.times.inject(nil) { |x| DeepPreloader::Spec.new({ 'child' => x }) }
421
+ assert_equal(expected_includes, ListView.eager_includes)
422
+ end
423
+
419
424
  def test_deserialize_context
420
425
  view = {
421
426
  '_type' => 'List',
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.12
4
+ version: 3.4.0
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-02-15 00:00:00.000000000 Z
11
+ date: 2021-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -379,6 +379,7 @@ files:
379
379
  - Rakefile
380
380
  - gemfiles/rails_5_2.gemfile
381
381
  - gemfiles/rails_6_0.gemfile
382
+ - gemfiles/rails_6_1.gemfile
382
383
  - iknow_view_models.gemspec
383
384
  - lib/iknow_view_models.rb
384
385
  - lib/iknow_view_models/railtie.rb
@@ -485,14 +486,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
485
486
  requirements:
486
487
  - - ">="
487
488
  - !ruby/object:Gem::Version
488
- version: '0'
489
+ version: '2.7'
489
490
  required_rubygems_version: !ruby/object:Gem::Requirement
490
491
  requirements:
491
492
  - - ">="
492
493
  - !ruby/object:Gem::Version
493
494
  version: '0'
494
495
  requirements: []
495
- rubygems_version: 3.0.3
496
+ rubygems_version: 3.1.6
496
497
  signing_key:
497
498
  specification_version: 4
498
499
  summary: ViewModels provide a means of encapsulating a collection of related data