iknow_view_models 2.8.4
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 +7 -0
- data/.circleci/config.yml +115 -0
- data/.gitignore +36 -0
- data/.travis.yml +31 -0
- data/Appraisals +9 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +19 -0
- data/Rakefile +21 -0
- data/appveyor.yml +22 -0
- data/gemfiles/rails_5_2.gemfile +15 -0
- data/gemfiles/rails_6_0_beta.gemfile +15 -0
- data/iknow_view_models.gemspec +49 -0
- data/lib/iknow_view_models.rb +12 -0
- data/lib/iknow_view_models/railtie.rb +8 -0
- data/lib/iknow_view_models/version.rb +5 -0
- data/lib/view_model.rb +333 -0
- data/lib/view_model/access_control.rb +154 -0
- data/lib/view_model/access_control/composed.rb +216 -0
- data/lib/view_model/access_control/open.rb +13 -0
- data/lib/view_model/access_control/read_only.rb +13 -0
- data/lib/view_model/access_control/tree.rb +264 -0
- data/lib/view_model/access_control_error.rb +10 -0
- data/lib/view_model/active_record.rb +383 -0
- data/lib/view_model/active_record/association_data.rb +178 -0
- data/lib/view_model/active_record/association_manipulation.rb +389 -0
- data/lib/view_model/active_record/cache.rb +265 -0
- data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
- data/lib/view_model/active_record/cloner.rb +113 -0
- data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
- data/lib/view_model/active_record/controller.rb +77 -0
- data/lib/view_model/active_record/controller_base.rb +185 -0
- data/lib/view_model/active_record/nested_controller_base.rb +93 -0
- data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
- data/lib/view_model/active_record/update_context.rb +252 -0
- data/lib/view_model/active_record/update_data.rb +749 -0
- data/lib/view_model/active_record/update_operation.rb +810 -0
- data/lib/view_model/active_record/visitor.rb +77 -0
- data/lib/view_model/after_transaction_runner.rb +29 -0
- data/lib/view_model/callbacks.rb +219 -0
- data/lib/view_model/changes.rb +62 -0
- data/lib/view_model/config.rb +29 -0
- data/lib/view_model/controller.rb +142 -0
- data/lib/view_model/deserialization_error.rb +437 -0
- data/lib/view_model/deserialize_context.rb +16 -0
- data/lib/view_model/error.rb +191 -0
- data/lib/view_model/error_view.rb +35 -0
- data/lib/view_model/record.rb +367 -0
- data/lib/view_model/record/attribute_data.rb +48 -0
- data/lib/view_model/reference.rb +31 -0
- data/lib/view_model/references.rb +48 -0
- data/lib/view_model/registry.rb +73 -0
- data/lib/view_model/schemas.rb +45 -0
- data/lib/view_model/serialization_error.rb +10 -0
- data/lib/view_model/serialize_context.rb +118 -0
- data/lib/view_model/test_helpers.rb +103 -0
- data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
- data/lib/view_model/traversal_context.rb +126 -0
- data/lib/view_model/utils.rb +24 -0
- data/lib/view_model/utils/collections.rb +49 -0
- data/test/helpers/arvm_test_models.rb +59 -0
- data/test/helpers/arvm_test_utilities.rb +187 -0
- data/test/helpers/callback_tracer.rb +27 -0
- data/test/helpers/controller_test_helpers.rb +270 -0
- data/test/helpers/match_enumerator.rb +58 -0
- data/test/helpers/query_logging.rb +71 -0
- data/test/helpers/test_access_control.rb +56 -0
- data/test/helpers/viewmodel_spec_helpers.rb +326 -0
- data/test/unit/view_model/access_control_test.rb +769 -0
- data/test/unit/view_model/active_record/alias_test.rb +35 -0
- data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
- data/test/unit/view_model/active_record/cache_test.rb +351 -0
- data/test/unit/view_model/active_record/cloner_test.rb +313 -0
- data/test/unit/view_model/active_record/controller_test.rb +561 -0
- data/test/unit/view_model/active_record/counter_test.rb +80 -0
- data/test/unit/view_model/active_record/customization_test.rb +388 -0
- data/test/unit/view_model/active_record/has_many_test.rb +957 -0
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
- data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
- data/test/unit/view_model/active_record/has_one_test.rb +334 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
- data/test/unit/view_model/active_record/poly_test.rb +320 -0
- data/test/unit/view_model/active_record/shared_test.rb +285 -0
- data/test/unit/view_model/active_record/version_test.rb +121 -0
- data/test/unit/view_model/active_record_test.rb +542 -0
- data/test/unit/view_model/callbacks_test.rb +582 -0
- data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
- data/test/unit/view_model/record_test.rb +524 -0
- data/test/unit/view_model/traversal_context_test.rb +371 -0
- data/test/unit/view_model_test.rb +62 -0
- metadata +490 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require_relative "../../../helpers/arvm_test_utilities.rb"
|
|
2
|
+
require_relative "../../../helpers/arvm_test_models.rb"
|
|
3
|
+
require_relative "../../../helpers/viewmodel_spec_helpers.rb"
|
|
4
|
+
|
|
5
|
+
require "minitest/autorun"
|
|
6
|
+
|
|
7
|
+
require "view_model/active_record"
|
|
8
|
+
|
|
9
|
+
class ViewModel::ActiveRecord::Alias < ActiveSupport::TestCase
|
|
10
|
+
include ARVMTestUtilities
|
|
11
|
+
extend Minitest::Spec::DSL
|
|
12
|
+
|
|
13
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
|
14
|
+
|
|
15
|
+
def child_attributes
|
|
16
|
+
super.merge(
|
|
17
|
+
viewmodel: ->(v) do
|
|
18
|
+
add_view_alias "ChildA"
|
|
19
|
+
add_view_alias "ChildB"
|
|
20
|
+
end
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "permits association types to be aliased" do
|
|
25
|
+
%w(Child ChildA ChildB).each do |view_alias|
|
|
26
|
+
view = {
|
|
27
|
+
"_type" => viewmodel_class.view_name,
|
|
28
|
+
"child" => { "_type" => view_alias },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
parent = viewmodel_class.deserialize_from_view(view).model
|
|
32
|
+
assert_instance_of(child_model_class, parent.child)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
require_relative "../../../helpers/arvm_test_utilities.rb"
|
|
2
|
+
require_relative "../../../helpers/arvm_test_models.rb"
|
|
3
|
+
|
|
4
|
+
require "minitest/autorun"
|
|
5
|
+
|
|
6
|
+
require "view_model/active_record"
|
|
7
|
+
|
|
8
|
+
class ViewModel::ActiveRecord::BelongsToTest < ActiveSupport::TestCase
|
|
9
|
+
include ARVMTestUtilities
|
|
10
|
+
|
|
11
|
+
module WithLabel
|
|
12
|
+
def before_all
|
|
13
|
+
super
|
|
14
|
+
|
|
15
|
+
build_viewmodel(:Label) do
|
|
16
|
+
define_schema do |t|
|
|
17
|
+
t.string :text
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
define_model do
|
|
21
|
+
has_one :parent, inverse_of: :label
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
define_viewmodel do
|
|
25
|
+
attributes :text
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module WithParent
|
|
32
|
+
def before_all
|
|
33
|
+
super
|
|
34
|
+
|
|
35
|
+
build_viewmodel(:Parent) do
|
|
36
|
+
define_schema do |t|
|
|
37
|
+
t.string :name
|
|
38
|
+
t.references :label, foreign_key: true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
define_model do
|
|
42
|
+
belongs_to :label, inverse_of: :parent, dependent: :destroy
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
define_viewmodel do
|
|
46
|
+
attributes :name
|
|
47
|
+
associations :label
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module WithOwner
|
|
54
|
+
def before_all
|
|
55
|
+
super
|
|
56
|
+
|
|
57
|
+
build_viewmodel(:Owner) do
|
|
58
|
+
define_schema do |t|
|
|
59
|
+
t.integer :deleted_id
|
|
60
|
+
t.integer :ignored_id
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
define_model do
|
|
64
|
+
belongs_to :deleted, class_name: Label.name, dependent: :delete
|
|
65
|
+
belongs_to :ignored, class_name: Label.name
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
define_viewmodel do
|
|
69
|
+
associations :deleted, :ignored
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
include WithLabel
|
|
76
|
+
include WithParent
|
|
77
|
+
|
|
78
|
+
def setup
|
|
79
|
+
super
|
|
80
|
+
|
|
81
|
+
# TODO make a `has_list?` that allows a parent to set all children as an array
|
|
82
|
+
@parent1 = Parent.new(name: "p1",
|
|
83
|
+
label: Label.new(text: "p1l"))
|
|
84
|
+
@parent1.save!
|
|
85
|
+
|
|
86
|
+
@parent2 = Parent.new(name: "p2",
|
|
87
|
+
label: Label.new(text: "p2l"))
|
|
88
|
+
|
|
89
|
+
@parent2.save!
|
|
90
|
+
|
|
91
|
+
enable_logging!
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_serialize_view
|
|
95
|
+
view, _refs = serialize_with_references(ParentView.new(@parent1))
|
|
96
|
+
|
|
97
|
+
assert_equal({ "_type" => "Parent",
|
|
98
|
+
"_version" => 1,
|
|
99
|
+
"id" => @parent1.id,
|
|
100
|
+
"name" => @parent1.name,
|
|
101
|
+
"label" => { "_type" => "Label",
|
|
102
|
+
"_version" => 1,
|
|
103
|
+
"id" => @parent1.label.id,
|
|
104
|
+
"text" => @parent1.label.text },
|
|
105
|
+
},
|
|
106
|
+
view)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_loading_batching
|
|
110
|
+
log_queries do
|
|
111
|
+
serialize(ParentView.load)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
assert_equal(['Parent Load', 'Label Load'],
|
|
115
|
+
logged_load_queries)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_create_from_view
|
|
119
|
+
view = {
|
|
120
|
+
"_type" => "Parent",
|
|
121
|
+
"name" => "p",
|
|
122
|
+
"label" => { "_type" => "Label", "text" => "l" },
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pv = ParentView.deserialize_from_view(view)
|
|
126
|
+
p = pv.model
|
|
127
|
+
|
|
128
|
+
assert(!p.changed?)
|
|
129
|
+
assert(!p.new_record?)
|
|
130
|
+
|
|
131
|
+
assert_equal("p", p.name)
|
|
132
|
+
|
|
133
|
+
assert(p.label.present?)
|
|
134
|
+
assert_equal("l", p.label.text)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_create_belongs_to_nil
|
|
138
|
+
view = { '_type' => 'Parent', 'name' => 'p', 'label' => nil }
|
|
139
|
+
pv = ParentView.deserialize_from_view(view)
|
|
140
|
+
assert_nil(pv.model.label)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_create_invalid_child_type
|
|
144
|
+
view = { '_type' => 'Parent', 'name' => 'p', 'label' => { '_type' => 'Parent', 'name' => 'q' } }
|
|
145
|
+
assert_raises(ViewModel::DeserializationError::InvalidAssociationType) do
|
|
146
|
+
ParentView.deserialize_from_view(view)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_belongs_to_create
|
|
151
|
+
@parent1.update(label: nil)
|
|
152
|
+
|
|
153
|
+
alter_by_view!(ParentView, @parent1) do |view, refs|
|
|
154
|
+
view['label'] = { '_type' => 'Label', 'text' => 'cheese' }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
assert_equal('cheese', @parent1.label.text)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_belongs_to_replace
|
|
161
|
+
old_label = @parent1.label
|
|
162
|
+
|
|
163
|
+
alter_by_view!(ParentView, @parent1) do |view, refs|
|
|
164
|
+
view['label'] = { '_type' => 'Label', 'text' => 'cheese' }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
assert_equal('cheese', @parent1.label.text)
|
|
168
|
+
assert(Label.where(id: old_label).blank?)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_belongs_to_move_and_replace
|
|
172
|
+
old_p1_label = @parent1.label
|
|
173
|
+
old_p2_label = @parent2.label
|
|
174
|
+
|
|
175
|
+
set_by_view!(ParentView, [@parent1, @parent2]) do |(p1, p2), refs|
|
|
176
|
+
p1['label'] = nil
|
|
177
|
+
p2['label'] = update_hash_for(LabelView, old_p1_label)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
assert(@parent1.label.blank?, 'l1 label reference removed')
|
|
181
|
+
assert_equal(old_p1_label, @parent2.label, 'p2 has label from p1')
|
|
182
|
+
assert(Label.where(id: old_p2_label).blank?, 'p2 old label deleted')
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def test_belongs_to_swap
|
|
186
|
+
old_p1_label = @parent1.label
|
|
187
|
+
old_p2_label = @parent2.label
|
|
188
|
+
|
|
189
|
+
alter_by_view!(ParentView, [@parent1, @parent2]) do |(p1, p2), refs|
|
|
190
|
+
p1['label'] = update_hash_for(LabelView, old_p2_label)
|
|
191
|
+
p2['label'] = update_hash_for(LabelView, old_p1_label)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
assert_equal(old_p2_label, @parent1.label, 'p1 has label from p2')
|
|
195
|
+
assert_equal(old_p1_label, @parent2.label, 'p2 has label from p1')
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_moved_child_is_not_delete_checked
|
|
199
|
+
# move from p1 to p3
|
|
200
|
+
d_context = ParentView.new_deserialize_context
|
|
201
|
+
|
|
202
|
+
target_label = Label.create
|
|
203
|
+
from_parent = Parent.create(name: 'from', label: target_label)
|
|
204
|
+
to_parent = Parent.create(name: 'p3')
|
|
205
|
+
|
|
206
|
+
alter_by_view!(
|
|
207
|
+
ParentView, [from_parent, to_parent],
|
|
208
|
+
deserialize_context: d_context
|
|
209
|
+
) do |(from, to), refs|
|
|
210
|
+
from['label'] = nil
|
|
211
|
+
to['label'] = update_hash_for(LabelView, target_label)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
assert_equal(target_label, to_parent.label, 'target label moved')
|
|
215
|
+
assert_equal([ViewModel::Reference.new(ParentView, from_parent.id),
|
|
216
|
+
ViewModel::Reference.new(ParentView, to_parent.id)],
|
|
217
|
+
d_context.valid_edit_refs,
|
|
218
|
+
"only parents are checked for change; child was not")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def test_implicit_release_invalid_belongs_to
|
|
222
|
+
taken_label_ref = update_hash_for(LabelView, @parent1.label)
|
|
223
|
+
assert_raises(ViewModel::DeserializationError::ParentNotFound) do
|
|
224
|
+
ParentView.deserialize_from_view(
|
|
225
|
+
[{ '_type' => 'Parent',
|
|
226
|
+
'name' => 'newp',
|
|
227
|
+
'label' => taken_label_ref }])
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
class GCTests < ActiveSupport::TestCase
|
|
232
|
+
include ARVMTestUtilities
|
|
233
|
+
include WithLabel
|
|
234
|
+
include WithOwner
|
|
235
|
+
include WithParent
|
|
236
|
+
|
|
237
|
+
# test belongs_to garbage collection - dependent: delete_all
|
|
238
|
+
def test_gc_dependent_delete_all
|
|
239
|
+
owner = Owner.create(deleted: Label.new(text: 'one'))
|
|
240
|
+
old_label = owner.deleted
|
|
241
|
+
|
|
242
|
+
alter_by_view!(OwnerView, owner) do |ov, refs|
|
|
243
|
+
ov['deleted'] = { '_type' => 'Label', 'text' => 'two' }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
assert_equal('two', owner.deleted.text)
|
|
247
|
+
refute_equal(old_label, owner.deleted)
|
|
248
|
+
assert(Label.where(id: old_label.id).blank?)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def test_no_gc_dependent_ignore
|
|
252
|
+
owner = Owner.create(ignored: Label.new(text: "one"))
|
|
253
|
+
old_label = owner.ignored
|
|
254
|
+
|
|
255
|
+
alter_by_view!(OwnerView, owner) do |ov, refs|
|
|
256
|
+
ov['ignored'] = { '_type' => 'Label', 'text' => 'two' }
|
|
257
|
+
end
|
|
258
|
+
assert_equal('two', owner.ignored.text)
|
|
259
|
+
refute_equal(old_label, owner.ignored)
|
|
260
|
+
assert_equal(1, Label.where(id: old_label.id).count)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
class RenamedTest < ActiveSupport::TestCase
|
|
265
|
+
include ARVMTestUtilities
|
|
266
|
+
include WithLabel
|
|
267
|
+
|
|
268
|
+
def before_all
|
|
269
|
+
super
|
|
270
|
+
|
|
271
|
+
build_viewmodel(:Parent) do
|
|
272
|
+
define_schema do |t|
|
|
273
|
+
t.string :name
|
|
274
|
+
t.references :label, foreign_key: true
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
define_model do
|
|
278
|
+
belongs_to :label, inverse_of: :parent, dependent: :destroy
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
define_viewmodel do
|
|
282
|
+
attributes :name
|
|
283
|
+
association :label, as: :something_else
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def setup
|
|
289
|
+
super
|
|
290
|
+
|
|
291
|
+
@parent = Parent.create(name: 'p1', label: Label.new(text: 'l1'))
|
|
292
|
+
|
|
293
|
+
enable_logging!
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def test_dependencies
|
|
297
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent', 'something_else' => nil }])
|
|
298
|
+
assert_equal(DeepPreloader::Spec.new('label' => DeepPreloader::Spec.new), root_updates.first.preload_dependencies)
|
|
299
|
+
assert_equal({ 'something_else' => {} }, root_updates.first.updated_associations)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def test_renamed_roundtrip
|
|
303
|
+
alter_by_view!(ParentView, @parent) do |view, refs|
|
|
304
|
+
assert_equal({ 'id' => @parent.label.id,
|
|
305
|
+
'_type' => 'Label',
|
|
306
|
+
'_version' => 1,
|
|
307
|
+
'text' => 'l1' },
|
|
308
|
+
view['something_else'])
|
|
309
|
+
view['something_else']['text'] = 'new l1 text'
|
|
310
|
+
end
|
|
311
|
+
assert_equal('new l1 text', @parent.label.text)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
class FreedChildrenTest < ActiveSupport::TestCase
|
|
316
|
+
include ARVMTestUtilities
|
|
317
|
+
|
|
318
|
+
def before_all
|
|
319
|
+
build_viewmodel(:Aye) do
|
|
320
|
+
define_schema do |t|
|
|
321
|
+
t.references :bee
|
|
322
|
+
end
|
|
323
|
+
define_model do
|
|
324
|
+
belongs_to :bee, inverse_of: :aye, dependent: :destroy
|
|
325
|
+
end
|
|
326
|
+
define_viewmodel do
|
|
327
|
+
association :bee
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
build_viewmodel(:Bee) do
|
|
332
|
+
define_schema do |t|
|
|
333
|
+
t.references :cee
|
|
334
|
+
end
|
|
335
|
+
define_model do
|
|
336
|
+
has_one :aye, inverse_of: :bee
|
|
337
|
+
belongs_to :cee, inverse_of: :bee, dependent: :destroy
|
|
338
|
+
end
|
|
339
|
+
define_viewmodel do
|
|
340
|
+
association :cee
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
build_viewmodel(:Cee) do
|
|
345
|
+
define_schema do |t|
|
|
346
|
+
end
|
|
347
|
+
define_model do
|
|
348
|
+
has_one :bee, inverse_of: :cee
|
|
349
|
+
end
|
|
350
|
+
define_viewmodel do
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# Do we support replacing a node in the tree and reparenting its children
|
|
357
|
+
# back to it? In theory we want to, but currently we don't: the child node
|
|
358
|
+
# is unresolvable.
|
|
359
|
+
|
|
360
|
+
# To support it we could maintain a list of child elements that will be
|
|
361
|
+
# implicitly freed by each freelist entry. Then worklist entries could
|
|
362
|
+
# resolve themselves from these children, and nil out the association target
|
|
363
|
+
# in the freelist to prevent them from being deleted when the freelist is
|
|
364
|
+
# cleaned. If the freelist entry is subsequently reclaimed, double update
|
|
365
|
+
# protection should prevent the child from being reused, but that will need
|
|
366
|
+
# testing.
|
|
367
|
+
def test_move
|
|
368
|
+
model = Aye.create(bee: Bee.new(cee: Cee.new))
|
|
369
|
+
assert_raises(ViewModel::DeserializationError::ParentNotFound) do
|
|
370
|
+
alter_by_view!(AyeView, model) do |view, refs|
|
|
371
|
+
view['bee'].delete("id")
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "minitest/unit"
|
|
5
|
+
require "minitest/hooks"
|
|
6
|
+
|
|
7
|
+
require_relative "../../../helpers/arvm_test_models.rb"
|
|
8
|
+
require_relative "../../../helpers/arvm_test_utilities.rb"
|
|
9
|
+
require_relative "../../../helpers/viewmodel_spec_helpers.rb"
|
|
10
|
+
|
|
11
|
+
require "view_model"
|
|
12
|
+
require "view_model/active_record"
|
|
13
|
+
|
|
14
|
+
# IknowCache uses Rails.cache: create a dummy cache.
|
|
15
|
+
|
|
16
|
+
DUMMY_RAILS_CACHE = ActiveSupport::Cache::MemoryStore.new
|
|
17
|
+
|
|
18
|
+
module Rails
|
|
19
|
+
def self.cache
|
|
20
|
+
DUMMY_RAILS_CACHE
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ViewModel::ActiveRecord
|
|
25
|
+
class CacheTest < ActiveSupport::TestCase
|
|
26
|
+
using ViewModel::Utils::Collections
|
|
27
|
+
extend Minitest::Spec::DSL
|
|
28
|
+
include ARVMTestUtilities
|
|
29
|
+
|
|
30
|
+
# Defines a cacheable parent Model with a owned Child and a cachable shared Shared.
|
|
31
|
+
module CacheableParentAndChildren
|
|
32
|
+
extend ActiveSupport::Concern
|
|
33
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
|
34
|
+
|
|
35
|
+
def model_attributes
|
|
36
|
+
super.merge(
|
|
37
|
+
schema: ->(t) { t.references :shared, foreign_key: true },
|
|
38
|
+
model: ->(_) { belongs_to :shared, inverse_of: :models },
|
|
39
|
+
viewmodel: ->(_) {
|
|
40
|
+
association :shared, shared: true, optional: false
|
|
41
|
+
cacheable!
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def shared_cache_group
|
|
47
|
+
@shared_cache_group ||= IknowCache.register_group(:shared, :id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def shared_viewmodel_class
|
|
51
|
+
shared_cache_group = self.shared_cache_group
|
|
52
|
+
@shared_viewmodel_class ||= define_viewmodel_class(:Shared, namespace: namespace) do
|
|
53
|
+
define_schema do |t|
|
|
54
|
+
t.string :name
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
define_model do
|
|
58
|
+
has_many :models
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
define_viewmodel do
|
|
62
|
+
attributes :name
|
|
63
|
+
cacheable!(cache_group: shared_cache_group)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def shared_model_class
|
|
69
|
+
shared_viewmodel_class.model_class
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# parent depends on children, ensure it's touched first
|
|
73
|
+
def viewmodel_class
|
|
74
|
+
shared_viewmodel_class
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
included do
|
|
79
|
+
let(:shared) { shared_model_class.create!(name: "shared1") }
|
|
80
|
+
let(:root) { model_class.create!(name: "root1", child: Child.new(name: "owned1"), shared: shared) }
|
|
81
|
+
let(:root_view) { viewmodel_class.new(root) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
before(:each) do
|
|
86
|
+
DUMMY_RAILS_CACHE.clear
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Extract the iKnowCaches to verify their contents
|
|
90
|
+
def read_cache(viewmodel_class, id)
|
|
91
|
+
vm_cache = viewmodel_class.viewmodel_cache
|
|
92
|
+
vm_cache.send(:cache).read(vm_cache.key_for(id))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def serialize_from_database
|
|
96
|
+
view = viewmodel_class.new(model_class.find(root.id))
|
|
97
|
+
context = viewmodel_class.new_serialize_context
|
|
98
|
+
data = ViewModel.serialize_to_hash([view], serialize_context: context)
|
|
99
|
+
refs = context.serialize_references_to_hash
|
|
100
|
+
[data, refs]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_result(result)
|
|
104
|
+
data_json, refs_json = result
|
|
105
|
+
data = data_json.map { |d| JSON.parse(d) }
|
|
106
|
+
refs = refs_json.transform_values { |v| JSON.parse(v) }
|
|
107
|
+
[data, refs]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def fetch_with_cache
|
|
111
|
+
viewmodel_class.viewmodel_cache.fetch([root.id])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def serialize_with_cache
|
|
115
|
+
parse_result(fetch_with_cache)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
module BehavesLikeACache
|
|
119
|
+
extend ActiveSupport::Concern
|
|
120
|
+
included do
|
|
121
|
+
it 'returns the right serialization' do
|
|
122
|
+
value(serialize_with_cache).must_equal(serialize_from_database)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'returns the right serialization after caching' do
|
|
126
|
+
fetch_with_cache
|
|
127
|
+
value(serialize_from_database).must_equal(serialize_with_cache)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'writes to the cache after fetching' do
|
|
131
|
+
cached_value = read_cache(viewmodel_class, root.id)
|
|
132
|
+
value(cached_value).wont_be(:present?)
|
|
133
|
+
|
|
134
|
+
fetch_with_cache
|
|
135
|
+
|
|
136
|
+
cached_value = read_cache(viewmodel_class, root.id)
|
|
137
|
+
value(cached_value).must_be(:present?)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'saves the returned serialization in the cache' do
|
|
141
|
+
data, refs = fetch_with_cache
|
|
142
|
+
value(data.size).must_equal(1)
|
|
143
|
+
|
|
144
|
+
cached_root = read_cache(viewmodel_class, root.id)
|
|
145
|
+
value(cached_root).must_be(:present?)
|
|
146
|
+
value(cached_root[:data]).must_equal(data.first)
|
|
147
|
+
|
|
148
|
+
ref_cache = cached_root[:ref_cache]
|
|
149
|
+
value(refs.size).must_equal(ref_cache.size)
|
|
150
|
+
|
|
151
|
+
refs.each do |key, ref_data|
|
|
152
|
+
view_name, id = ref_cache[key]
|
|
153
|
+
value(view_name).must_be(:present?)
|
|
154
|
+
value(id).must_be(:present?)
|
|
155
|
+
|
|
156
|
+
# The cached reference must correspond to the returned data.
|
|
157
|
+
parsed_data = JSON.parse(ref_data)
|
|
158
|
+
value(parsed_data["id"]).must_equal(id)
|
|
159
|
+
value(parsed_data["_type"]).must_equal(view_name)
|
|
160
|
+
|
|
161
|
+
# When the cached reference is to independently cached data
|
|
162
|
+
# (SharedView in this test), make sure that data is correctly
|
|
163
|
+
# cached.
|
|
164
|
+
next unless view_name == "Shared"
|
|
165
|
+
value(id).must_equal(shared.id)
|
|
166
|
+
cached_shared = read_cache(shared_viewmodel_class, id)
|
|
167
|
+
value(cached_shared).must_be(:present?)
|
|
168
|
+
value(cached_shared[:data]).must_equal(ref_data)
|
|
169
|
+
value(cached_shared[:ref_cache]).must_be(:blank?)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
describe 'with owned and shared children' do
|
|
176
|
+
include CacheableParentAndChildren
|
|
177
|
+
include BehavesLikeACache
|
|
178
|
+
|
|
179
|
+
describe 'with a record in the cache' do
|
|
180
|
+
# Fetch the root record to ensure it's in the cache
|
|
181
|
+
before(:each) do
|
|
182
|
+
viewmodel_class.viewmodel_cache.fetch([root.id])
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def change_in_database
|
|
186
|
+
root.update_attribute(:name, "CHANGEDROOT")
|
|
187
|
+
shared.update_attribute(:name, "CHANGEDSHARED")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'resolves from the cache' do
|
|
191
|
+
before_data, before_refs = serialize_from_database
|
|
192
|
+
change_in_database
|
|
193
|
+
|
|
194
|
+
cache_data, cache_refs = serialize_with_cache
|
|
195
|
+
value(cache_data).must_equal(before_data)
|
|
196
|
+
value(cache_refs).must_equal(before_refs)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'can clear the root cache' do
|
|
200
|
+
_before_data, before_refs = serialize_from_database
|
|
201
|
+
change_in_database
|
|
202
|
+
viewmodel_class.viewmodel_cache.clear
|
|
203
|
+
|
|
204
|
+
cache_data, cache_refs = serialize_with_cache
|
|
205
|
+
value(cache_data[0]["name"]).must_equal("CHANGEDROOT") # Root view invalidated
|
|
206
|
+
value(cache_refs).must_equal(before_refs) # Shared view not invalidated
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
describe 'when deserializing' do
|
|
210
|
+
it 'does not clear the cache on round-trip' do
|
|
211
|
+
alter_by_view!(viewmodel_class, root) {}
|
|
212
|
+
|
|
213
|
+
cached_root_value = read_cache(viewmodel_class, root.id)
|
|
214
|
+
value(cached_root_value).must_be(:present?)
|
|
215
|
+
|
|
216
|
+
cached_root_value = read_cache(shared_viewmodel_class, shared.id)
|
|
217
|
+
value(cached_root_value).must_be(:present?)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'clears only the root cache on edit to root' do
|
|
221
|
+
alter_by_view!(viewmodel_class, root) do |data, _refs|
|
|
222
|
+
data['name'] = 'new name'
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
cached_root_value = read_cache(viewmodel_class, root.id)
|
|
226
|
+
value(cached_root_value).wont_be(:present?)
|
|
227
|
+
|
|
228
|
+
cached_root_value = read_cache(shared_viewmodel_class, shared.id)
|
|
229
|
+
value(cached_root_value).must_be(:present?)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'clears only the root cache on edit to owned child' do
|
|
233
|
+
alter_by_view!(viewmodel_class, root) do |data, _refs|
|
|
234
|
+
data['child']['name'] = 'new child name'
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
cached_root_value = read_cache(viewmodel_class, root.id)
|
|
238
|
+
value(cached_root_value).wont_be(:present?)
|
|
239
|
+
|
|
240
|
+
cached_root_value = read_cache(shared_viewmodel_class, shared.id)
|
|
241
|
+
value(cached_root_value).must_be(:present?)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'clears only the shared child cache on edit to shared child' do
|
|
245
|
+
alter_by_view!(viewmodel_class, root) do |_data, refs|
|
|
246
|
+
refs.values.first['name'] = 'new shared name'
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
cached_root_value = read_cache(viewmodel_class, root.id)
|
|
250
|
+
value(cached_root_value).must_be(:present?)
|
|
251
|
+
|
|
252
|
+
cached_root_value = read_cache(shared_viewmodel_class, shared.id)
|
|
253
|
+
value(cached_root_value).wont_be(:present?)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'can delete an entity from a cache' do
|
|
258
|
+
_before_data, before_refs = serialize_from_database
|
|
259
|
+
change_in_database
|
|
260
|
+
viewmodel_class.viewmodel_cache.delete(root.id)
|
|
261
|
+
|
|
262
|
+
cache_data, cache_refs = serialize_with_cache
|
|
263
|
+
value(cache_data[0]["name"]).must_equal("CHANGEDROOT")
|
|
264
|
+
value(cache_refs).must_equal(before_refs)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it 'can clear a referenced cache' do
|
|
268
|
+
change_in_database
|
|
269
|
+
shared_viewmodel_class.viewmodel_cache.clear
|
|
270
|
+
|
|
271
|
+
# Shared view invalidated, but root view not
|
|
272
|
+
cache_data, cache_hrefs = serialize_with_cache
|
|
273
|
+
value(cache_data[0]["name"]).must_equal("root1")
|
|
274
|
+
value(cache_hrefs.values[0]["name"]).must_equal("CHANGEDSHARED")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it 'can clear a cache via its external cache group' do
|
|
278
|
+
change_in_database
|
|
279
|
+
shared_cache_group.invalidate_cache_group
|
|
280
|
+
|
|
281
|
+
# Shared view invalidated, but root view not
|
|
282
|
+
cache_data, cache_hrefs = serialize_with_cache
|
|
283
|
+
value(cache_data[0]["name"]).must_equal("root1")
|
|
284
|
+
value(cache_hrefs.values[0]["name"]).must_equal("CHANGEDSHARED")
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
describe 'and a record not in the cache' do
|
|
288
|
+
let(:root2) { model_class.create!(name: "root2", child: Child.new(name: "owned2"), shared: shared) }
|
|
289
|
+
|
|
290
|
+
def serialize_from_database
|
|
291
|
+
views = model_class.find(root.id, root2.id).map { |r| viewmodel_class.new(r) }
|
|
292
|
+
context = viewmodel_class.new_serialize_context
|
|
293
|
+
data = ViewModel.serialize_to_hash(views, serialize_context: context)
|
|
294
|
+
refs = context.serialize_references_to_hash
|
|
295
|
+
[data, refs]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def fetch_with_cache
|
|
299
|
+
viewmodel_class.viewmodel_cache.fetch([root.id, root2.id])
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
it 'merges matching shared references between cache hits and misses' do
|
|
303
|
+
db_data, db_refs = serialize_from_database
|
|
304
|
+
value(db_refs.size).must_equal(1)
|
|
305
|
+
|
|
306
|
+
cache_data, cache_refs = serialize_with_cache
|
|
307
|
+
value(cache_data).must_equal(db_data)
|
|
308
|
+
value(cache_refs).must_equal(db_refs)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'merges cache hits and misses' do
|
|
312
|
+
_, refs = serialize_from_database
|
|
313
|
+
change_in_database
|
|
314
|
+
|
|
315
|
+
cache_data, cache_refs = serialize_with_cache
|
|
316
|
+
value(cache_data[0]["name"]).must_equal("root1")
|
|
317
|
+
value(cache_data[1]["name"]).must_equal("root2")
|
|
318
|
+
value(cache_refs).must_equal(refs)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
describe "with a non-cacheable shared child" do
|
|
325
|
+
include ViewModelSpecHelpers::ParentAndSharedChild
|
|
326
|
+
def model_attributes
|
|
327
|
+
super.merge(viewmodel: ->(_) { cacheable! })
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
let(:root) { model_class.create!(name: "root1", child: Child.new(name: "owned1")) }
|
|
331
|
+
let(:root_view) { viewmodel_class.new(root) }
|
|
332
|
+
|
|
333
|
+
include BehavesLikeACache
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
describe 'when fetched by viewmodel' do
|
|
337
|
+
def fetch_with_cache
|
|
338
|
+
viewmodel_class.viewmodel_cache.fetch_by_viewmodel([root_view])
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
include CacheableParentAndChildren
|
|
342
|
+
include BehavesLikeACache
|
|
343
|
+
|
|
344
|
+
it 'can handle duplicates' do
|
|
345
|
+
data, _refs = viewmodel_class.viewmodel_cache.fetch_by_viewmodel([root_view, root_view])
|
|
346
|
+
value(data.size).must_equal(2)
|
|
347
|
+
value(data[0]).must_equal(data[1])
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|