iknow_view_models 3.2.13 → 3.2.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +5 -0
- data/Appraisals +5 -0
- data/README.md +1 -1
- data/gemfiles/rails_6_1.gemfile +9 -0
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model.rb +11 -0
- data/lib/view_model/active_record/cache.rb +95 -58
- data/lib/view_model/active_record/cache/cacheable_view.rb +0 -8
- data/test/unit/view_model/active_record/cache_test.rb +19 -6
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae5588513419124ea7eac511b571af52ea42e23d9bf87c01cf59a60af50e8055
|
4
|
+
data.tar.gz: c7de424a0a465c65fa4ba6823f7aa3743c69c0d41a8efa9da9c67efb030931d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbcde44426aa3f49d963f4f6588ea49f2fce10ae616e8dd5d78af30bac20a09381fe2e369829b92888c23078136f12a2f6a4b2fd49be17e4920927ed918dc04d
|
7
|
+
data.tar.gz: da13c596cb4ec09f17d74c79d9c2c17b8e3b9b2405856b49355f8d02be015d8cb430984fc2adf1c6f3fa668332152b8d440e5972204b66694d0a181cc1213f52
|
data/.circleci/config.yml
CHANGED
@@ -111,6 +111,11 @@ 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
|
114
119
|
- publish:
|
115
120
|
filters:
|
116
121
|
branches:
|
data/Appraisals
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# IknowViewModels
|
2
2
|
|
3
|
-
[![Build Status](https://
|
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
|
|
data/lib/view_model.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
75
|
+
class CacheWorker
|
76
|
+
SENTINEL = Object.new
|
77
|
+
WorklistEntry = Struct.new(:ref_name, :viewmodel)
|
62
78
|
|
63
|
-
|
64
|
-
ids = positions.keys
|
79
|
+
attr_reader :migration_versions, :serialize_context, :resolved_references
|
65
80
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
99
|
-
|
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
|
-
|
102
|
-
|
103
|
-
end
|
138
|
+
viewmodels = find_and_preload_viewmodels(viewmodel_class, ids_to_render.to_a,
|
139
|
+
available_viewmodels: available_viewmodels)
|
104
140
|
|
105
|
-
|
106
|
-
SENTINEL = Object.new
|
107
|
-
WorklistEntry = Struct.new(:ref_name, :viewmodel)
|
141
|
+
loaded_serializations = serialize_and_cache(viewmodels)
|
108
142
|
|
109
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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!
|
@@ -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
|
@@ -135,8 +135,8 @@ class ViewModel::ActiveRecord
|
|
135
135
|
[data, refs]
|
136
136
|
end
|
137
137
|
|
138
|
-
def fetch_with_cache
|
139
|
-
|
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
|
-
|
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
|
-
|
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
|
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.
|
4
|
+
version: 3.2.14
|
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-
|
11
|
+
date: 2021-03-26 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
|