iknow_view_models 3.1.8 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fc6b7d1f2e4c48c65f3f603d8e322afe7f26d52e08bb418dc746f9c5b6bd6c0
4
- data.tar.gz: 39eab5e4ae3423805b3c3d931504d08c623d8fbf602b58e4d978aeb7202aee71
3
+ metadata.gz: 8031b0e3cf50931cfdc65ee1c530efc4210566f2951ab5d6187f80d75848ac2a
4
+ data.tar.gz: bad21c087fff5def9362bd93576e0f9f0aeb8ec8ddc4f5ff6eb6849e879168c7
5
5
  SHA512:
6
- metadata.gz: 13f7e88bc34527984aea0d73518acb2e2fa8e2a6cc4cbb408f325afd39bfedd738be907f0922efcfacebe1802002e8f8d1c86ca9c37fc9a810df767f923d4455
7
- data.tar.gz: 71b8b322ce29db0b042c28b40c94820433d96beade6d991c5759a7828a7ce8101fcccc47fea3545538e522355fe949fd5a03493e26b22601c22e8bc31c8b2d92
6
+ metadata.gz: 17edcc7ab94fa8fa3a9b34e209a3d2feebb1c31fc1d3d619c36d3d9f602312e9ff13c1e0bc0974c2df070084b8b3fa7de3a2f7c74c38a3dbd04976ee166854aa
7
+ data.tar.gz: e176a0df288c029bea2d083c03492b6a5370e78a4c35164eaadf284b66de9120dfe60eda3dc6bca5e2845b71a865f4996fd17124462d06a70156249fd709a066
@@ -102,15 +102,15 @@ workflows:
102
102
  build:
103
103
  jobs:
104
104
  - test:
105
- name: 'ruby 2.6 rails 5.2 pg 11'
105
+ name: 'ruby 2.6 rails 5.2 pg 12'
106
106
  ruby-version: "2.6"
107
- pg-version: "11"
107
+ pg-version: "12"
108
108
  gemfile: gemfiles/rails_5_2.gemfile
109
109
  - test:
110
- name: 'ruby 2.6 rails 6.0 pg 11'
111
- ruby-version: "2.6"
112
- pg-version: "11"
113
- gemfile: gemfiles/rails_6_0_beta.gemfile
110
+ name: 'ruby 2.7 rails 6.0 pg 12'
111
+ ruby-version: "2.7"
112
+ pg-version: "12"
113
+ gemfile: gemfiles/rails_6_0.gemfile
114
114
  - publish:
115
115
  filters:
116
116
  branches:
@@ -0,0 +1,5 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+
4
+ inherit_gem:
5
+ rubocop-iknow: rubocop.yml
data/Appraisals CHANGED
@@ -3,7 +3,7 @@ appraise "rails-5-2" do
3
3
  gem "activesupport", "~> 5.2.0"
4
4
  end
5
5
 
6
- appraise "rails-6-0-beta" do
7
- gem "activerecord", "~> 6.0.0.beta"
8
- gem "activesupport", "~> 6.0.0.beta"
6
+ appraise "rails-6-0" do
7
+ gem "activerecord", "~> 6.0.0"
8
+ gem "activesupport", "~> 6.0.0"
9
9
  end
data/Gemfile CHANGED
@@ -3,9 +3,13 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in cerego_view_models.gemspec
4
4
  gemspec
5
5
 
6
+ # Add our linter rules as a development dependency
7
+ gem 'rubocop'
8
+ gem 'rubocop-iknow'
9
+
6
10
  # Test metadata collection for circleci
7
11
  gem 'minitest-ci'
8
12
 
9
13
  # Override gemspec for development version preferences
10
- gem 'activerecord', '~> 5.2.0'
11
- gem 'activesupport', '~> 5.2.0'
14
+ gem 'activerecord', '~> 6.0.0'
15
+ gem 'activesupport', '~> 6.0.0'
@@ -3,7 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "minitest-ci"
6
- gem "activerecord", "~> 6.0.0.beta"
7
- gem "activesupport", "~> 6.0.0.beta"
6
+ gem "activerecord", "~> 6.0.0"
7
+ gem "activesupport", "~> 6.0.0"
8
8
 
9
9
  gemspec path: "../"
@@ -36,13 +36,14 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency "lazily"
37
37
  spec.add_dependency "renum"
38
38
  spec.add_dependency "oj"
39
+ spec.add_dependency "rgl"
39
40
 
40
41
  spec.add_development_dependency "appraisal"
41
42
  spec.add_development_dependency "bundler"
42
43
  spec.add_development_dependency "byebug"
43
44
  spec.add_development_dependency "method_source"
44
45
  spec.add_development_dependency "minitest-hooks"
45
- spec.add_development_dependency "pg", '~> 0.18' # As of 5.1.4, Rails runtime check excludes pg 1.x, see #31669
46
+ spec.add_development_dependency "pg"
46
47
  spec.add_development_dependency "pry"
47
48
  spec.add_development_dependency "rake"
48
49
  spec.add_development_dependency "rspec-expectations"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.1.8'
4
+ VERSION = '3.2.0'
5
5
  end
@@ -12,7 +12,12 @@ class ViewModel
12
12
  VERSION_ATTRIBUTE = "_version"
13
13
  NEW_ATTRIBUTE = "_new"
14
14
 
15
- Metadata = Struct.new(:id, :view_name, :schema_version, :new) do
15
+ # Migrations leave a metadata attribute _migrated on any views that they
16
+ # alter. This attribute is accessible as metadata when deserializing migrated
17
+ # input, and is included in the output serialization sent to clients.
18
+ MIGRATED_ATTRIBUTE = "_migrated"
19
+
20
+ Metadata = Struct.new(:id, :view_name, :schema_version, :new, :migrated) do
16
21
  alias :new? :new
17
22
  end
18
23
 
@@ -99,8 +104,9 @@ class ViewModel
99
104
  type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
100
105
  schema_version = hash.delete(ViewModel::VERSION_ATTRIBUTE)
101
106
  new = hash.delete(ViewModel::NEW_ATTRIBUTE) { false }
107
+ migrated = hash.delete(ViewModel::MIGRATED_ATTRIBUTE) { false }
102
108
 
103
- Metadata.new(id, type_name, schema_version, new)
109
+ Metadata.new(id, type_name, schema_version, new, migrated)
104
110
  end
105
111
 
106
112
  def extract_reference_only_metadata(hash)
@@ -108,7 +114,7 @@ class ViewModel
108
114
  id = hash.delete(ViewModel::ID_ATTRIBUTE)
109
115
  type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
110
116
 
111
- Metadata.new(id, type_name, nil, false)
117
+ Metadata.new(id, type_name, nil, false, false)
112
118
  end
113
119
 
114
120
  def extract_reference_metadata(hash)
@@ -234,6 +240,11 @@ class ViewModel
234
240
  schema_version == self.schema_version
235
241
  end
236
242
 
243
+ def schema_hash(schema_versions)
244
+ version_string = schema_versions.to_a.sort.join(',')
245
+ Base64.urlsafe_encode64(Digest::MD5.digest(version_string))
246
+ end
247
+
237
248
  def preload_for_serialization(viewmodels, serialize_context: new_serialize_context, include_referenced: true, lock: nil)
238
249
  Array.wrap(viewmodels).group_by(&:class).each do |type, views|
239
250
  DeepPreloader.preload(views.map(&:model),
@@ -245,7 +245,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
245
245
  begin
246
246
  dependent_viewmodels(include_referenced: include_referenced).each_with_object({}) do |view, h|
247
247
  h[view.view_name] = view.schema_version
248
- end
248
+ end.freeze
249
249
  end
250
250
  end
251
251
 
@@ -5,6 +5,7 @@ require 'iknow_cache'
5
5
  # Cache for ViewModels that wrap ActiveRecord models.
6
6
  class ViewModel::ActiveRecord::Cache
7
7
  require 'view_model/active_record/cache/cacheable_view'
8
+ require 'view_model/migrator'
8
9
 
9
10
  class UncacheableViewModelError < RuntimeError; end
10
11
 
@@ -13,13 +14,20 @@ class ViewModel::ActiveRecord::Cache
13
14
  # If cache_group: is specified, it must be a group of a single key: `:id`
14
15
  def initialize(viewmodel_class, cache_group: nil)
15
16
  @viewmodel_class = viewmodel_class
16
- @cache_group = cache_group || create_default_cache_group # requires @viewmodel_class
17
+
18
+ @cache_group = cache_group || create_default_cache_group
19
+ @migrated_cache_group = @cache_group.register_child_group(:migrated, :version)
20
+
21
+ # /viewname/:id/viewname-currentversion
17
22
  @cache = @cache_group.register_cache(cache_name)
23
+
24
+ # /viewname/:id/migrated/:oldversion/viewname-currentversion
25
+ @migrated_cache = @migrated_cache_group.register_cache(cache_name)
18
26
  end
19
27
 
20
28
  def delete(*ids)
21
29
  ids.each do |id|
22
- @cache_group.delete_all(key_for(id))
30
+ @cache_group.delete_all(@cache.key.new(id))
23
31
  end
24
32
  end
25
33
 
@@ -27,14 +35,14 @@ class ViewModel::ActiveRecord::Cache
27
35
  @cache_group.invalidate_cache_group
28
36
  end
29
37
 
30
- def fetch_by_viewmodel(viewmodels, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
38
+ def fetch_by_viewmodel(viewmodels, migration_versions: {}, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
31
39
  ids = viewmodels.map(&:id)
32
- fetch(ids, initial_viewmodels: viewmodels, locked: false, serialize_context: serialize_context)
40
+ fetch(ids, initial_viewmodels: viewmodels, migration_versions: migration_versions, locked: locked, serialize_context: serialize_context)
33
41
  end
34
42
 
35
- def fetch(ids, initial_viewmodels: nil, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
43
+ def fetch(ids, initial_viewmodels: nil, migration_versions: {}, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
36
44
  data_serializations = Array.new(ids.size)
37
- worker = CacheWorker.new(serialize_context: serialize_context)
45
+ worker = CacheWorker.new(migration_versions: migration_versions, serialize_context: serialize_context)
38
46
 
39
47
  # If initial root viewmodels were provided, visit them to ensure that they
40
48
  # are visible. Other than this, no traversal callbacks are performed, as a
@@ -98,12 +106,14 @@ class ViewModel::ActiveRecord::Cache
98
106
  SENTINEL = Object.new
99
107
  WorklistEntry = Struct.new(:ref_name, :viewmodel)
100
108
 
101
- attr_reader :serialize_context, :resolved_references
109
+ attr_reader :migration_versions, :serialize_context, :resolved_references
102
110
 
103
- def initialize(serialize_context:)
104
- @worklist = {}
105
- @resolved_references = {}
106
- @serialize_context = serialize_context
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
107
117
  end
108
118
 
109
119
  def resolve_references!
@@ -141,11 +151,15 @@ class ViewModel::ActiveRecord::Cache
141
151
  end
142
152
  end
143
153
 
154
+ def migrated_cache_version(viewmodel_cache)
155
+ @migrated_cache_versions[viewmodel_cache] ||= viewmodel_cache.migrated_cache_version(migration_versions)
156
+ end
157
+
144
158
  # Loads the specified entities from the cache and returns a hash of
145
159
  # {id=>serialized_view}. Any references encountered are added to the
146
160
  # worklist.
147
161
  def load_from_cache(viewmodel_cache, ids)
148
- cached_serializations = viewmodel_cache.load(ids, serialize_context: serialize_context)
162
+ cached_serializations = viewmodel_cache.load(ids, migrated_cache_version(viewmodel_cache), serialize_context: serialize_context)
149
163
 
150
164
  cached_serializations.each_with_object({}) do |(id, cached_serialization), result|
151
165
  add_refs_to_worklist(cached_serialization[:ref_cache])
@@ -159,17 +173,54 @@ class ViewModel::ActiveRecord::Cache
159
173
  # added to the worklist.
160
174
  def serialize_and_cache(viewmodels)
161
175
  viewmodels.each_with_object({}) do |viewmodel, result|
162
- data_serialization = Jbuilder.encode do |json|
176
+ builder = Jbuilder.new do |json|
163
177
  ViewModel.serialize(viewmodel, json, serialize_context: serialize_context)
164
178
  end
165
179
 
166
180
  referenced_viewmodels = serialize_context.extract_referenced_views!
181
+
182
+ if migration_versions.present?
183
+ migrator = ViewModel::DownMigrator.new(migration_versions)
184
+
185
+ # This migration isn't given the chance to inspect/alter the contents
186
+ # of referenced views, only their presence: it's strictly less
187
+ # powerful than migrations on a fully serialized tree, as the only
188
+ # possible action on a referenced child is to delete it. The effect of
189
+ # this is that for sufficiently complex migrations where a parent view
190
+ # must introduce children or alter the contents of its referenced
191
+ # children, we may have to avoid caching while the migration is in
192
+ # use.
193
+ dummy_references = referenced_viewmodels.transform_values do |ref_vm|
194
+ {
195
+ ViewModel::TYPE_ATTRIBUTE => ref_vm.class.view_name,
196
+ ViewModel::VERSION_ATTRIBUTE => ref_vm.class.schema_version,
197
+ ViewModel::ID_ATTRIBUTE => ref_vm.id,
198
+ }.freeze
199
+ end
200
+
201
+ migrator.migrate!(builder.attributes!, references: dummy_references)
202
+
203
+ # Removed dummy references can be removed from referenced_viewmodels.
204
+ referenced_viewmodels.keep_if { |k, _| dummy_references.has_key?(k) }
205
+
206
+ # Introduced dummy references cannot be handled.
207
+ if dummy_references.keys != referenced_viewmodels.keys
208
+ version = migration_versions[viewmodel.class]
209
+ raise ViewModel::Error.new(
210
+ status: 500,
211
+ detail: "Down-migration for cacheable view #{viewmodel.class} to v#{version} must not introduce new shared references")
212
+ end
213
+ end
214
+
215
+ data_serialization = builder.target!
216
+
167
217
  add_viewmodels_to_worklist(referenced_viewmodels)
168
218
 
169
219
  if viewmodel.class < CacheableView
170
220
  cacheable_references = referenced_viewmodels.transform_values { |vm| cacheable_reference(vm) }
171
- viewmodel.class.viewmodel_cache.store(viewmodel.id, data_serialization, cacheable_references,
172
- serialize_context: serialize_context)
221
+ target_cache = viewmodel.class.viewmodel_cache
222
+ target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references,
223
+ serialize_context: serialize_context)
173
224
  end
174
225
 
175
226
  result[viewmodel.id] = data_serialization
@@ -225,8 +276,20 @@ class ViewModel::ActiveRecord::Cache
225
276
  end
226
277
  end
227
278
 
228
- def key_for(id)
229
- cache.key.new(id)
279
+ def cache_for(migration_version)
280
+ if migration_version
281
+ @migrated_cache
282
+ else
283
+ @cache
284
+ end
285
+ end
286
+
287
+ def key_for(id, migration_version)
288
+ if migration_version
289
+ @migrated_cache.key.new(id, migration_version)
290
+ else
291
+ @cache.key.new(id)
292
+ end
230
293
  end
231
294
 
232
295
  def id_for(key)
@@ -234,32 +297,47 @@ class ViewModel::ActiveRecord::Cache
234
297
  end
235
298
 
236
299
  # Save the provided serialization and reference data in the cache
237
- def store(id, data_serialization, ref_cache, serialize_context:)
238
- cache.write(key_for(id), { data: data_serialization, ref_cache: ref_cache })
300
+ def store(id, migration_version, data_serialization, ref_cache, serialize_context:)
301
+ key = key_for(id, migration_version)
302
+ cache_for(migration_version).write(key, { data: data_serialization, ref_cache: ref_cache })
239
303
  end
240
304
 
241
- def load(ids, serialize_context:)
242
- keys = ids.map { |id| key_for(id) }
243
- results = cache.read_multi(keys)
305
+ def load(ids, migration_version, serialize_context:)
306
+ keys = ids.map { |id| key_for(id, migration_version) }
307
+ results = cache_for(migration_version).read_multi(keys)
244
308
  results.transform_keys! { |key| id_for(key) }
245
309
  end
246
310
 
247
- private
311
+ def cache_version
312
+ @cache_version ||=
313
+ begin
314
+ versions = @viewmodel_class.deep_schema_version(include_referenced: false)
315
+ ViewModel.schema_hash(versions)
316
+ end
317
+ end
248
318
 
249
- attr_reader :cache
319
+ def migrated_cache_version(migration_versions)
320
+ versions = ViewModel::Migrator.migrated_deep_schema_version(viewmodel_class, migration_versions, include_referenced: false)
321
+ version_hash = ViewModel.schema_hash(versions)
322
+
323
+ if version_hash == cache_version
324
+ # no migrations affect this view
325
+ nil
326
+ else
327
+ version_hash
328
+ end
329
+ end
330
+
331
+ private
250
332
 
251
333
  def create_default_cache_group
252
334
  IknowCache.register_group(@viewmodel_class.name, :id)
253
335
  end
254
336
 
255
- # Statically version the terminal cache based on the deep schema versions of
256
- # the constituent viewmodels, so that viewmodel changes force invalidation.
337
+ # Statically version the cache name based on the (current) deep schema
338
+ # versions of the constituent viewmodels, so that viewmodel changes force
339
+ # invalidation.
257
340
  def cache_name
258
- "#{@viewmodel_class.name}_#{cache_version}"
259
- end
260
-
261
- def cache_version
262
- version_string = @viewmodel_class.deep_schema_version(include_referenced: false).to_a.sort.join(',')
263
- Base64.urlsafe_encode64(Digest::MD5.digest(version_string))
341
+ "vmcache_#{cache_version}"
264
342
  end
265
343
  end
@@ -40,10 +40,10 @@ module ViewModel::ActiveRecord::Cache::CacheableView
40
40
  @viewmodel_cache
41
41
  end
42
42
 
43
- def serialize_from_cache(views, serialize_context:)
43
+ def serialize_from_cache(views, migration_versions: {}, locked: false, serialize_context:)
44
44
  plural = views.is_a?(Array)
45
45
  views = Array.wrap(views)
46
- json_views, json_refs = viewmodel_cache.fetch_by_viewmodel(views, serialize_context: serialize_context)
46
+ json_views, json_refs = viewmodel_cache.fetch_by_viewmodel(views, locked: locked, migration_versions: migration_versions, serialize_context: serialize_context)
47
47
  json_views = json_views.first unless plural
48
48
  return json_views, json_refs
49
49
  end
@@ -62,7 +62,29 @@ module ViewModel::ActiveRecord::Controller
62
62
  end
63
63
 
64
64
  included do
65
- etag { self.viewmodel_class.deep_schema_version }
65
+ etag { migrated_deep_schema_version }
66
+ end
67
+
68
+ def parse_viewmodel_updates
69
+ super.tap do |update_hash, refs|
70
+ if migration_versions.present?
71
+ migrator = ViewModel::UpMigrator.new(migration_versions)
72
+ migrator.migrate!([update_hash, refs], references: refs)
73
+ end
74
+ end
75
+ end
76
+
77
+ def prerender_viewmodel(*)
78
+ super do |jbuilder|
79
+ yield(jbuilder) if block_given?
80
+
81
+ # migrate the resulting structure before it's serialized to a json string
82
+ if migration_versions.present?
83
+ tree = jbuilder.attributes!
84
+ migrator = ViewModel::DownMigrator.new(migration_versions)
85
+ migrator.migrate!(tree, references: tree['references'])
86
+ end
87
+ end
66
88
  end
67
89
 
68
90
  private
@@ -70,4 +92,34 @@ module ViewModel::ActiveRecord::Controller
70
92
  def viewmodel_id
71
93
  parse_param(:id)
72
94
  end
95
+
96
+ def migration_versions
97
+ @migration_versions ||=
98
+ begin
99
+ versions = parse_param(
100
+ :versions,
101
+ default: {},
102
+ with: IknowParams::Serializer::HashOf.new(
103
+ IknowParams::Serializer::String, IknowParams::Serializer::Integer))
104
+
105
+ migration_versions = {}
106
+
107
+ versions.each do |view_name, required_version|
108
+ viewmodel_class = ViewModel::Registry.for_view_name(view_name)
109
+
110
+ if viewmodel_class.schema_version != required_version
111
+ migration_versions[viewmodel_class] = required_version
112
+ end
113
+ rescue ViewModel::DeserializationError::UnknownView
114
+ # Ignore requests to migrate types that no longer exist
115
+ next
116
+ end
117
+
118
+ migration_versions.freeze
119
+ end
120
+ end
121
+
122
+ def migrated_deep_schema_version
123
+ ViewModel::Migrator.migrated_deep_schema_version(viewmodel_class, migration_versions, include_referenced: true)
124
+ end
73
125
  end