iknow_view_models 2.10.1 → 3.0.0
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 +119 -0
- data/.travis.yml +31 -0
- data/Appraisals +6 -16
- data/gemfiles/{rails_7_0.gemfile → rails_6_0_beta.gemfile} +2 -2
- data/iknow_view_models.gemspec +3 -5
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model/active_record/association_data.rb +206 -92
- data/lib/view_model/active_record/association_manipulation.rb +22 -12
- data/lib/view_model/active_record/cache/cacheable_view.rb +3 -13
- data/lib/view_model/active_record/cache.rb +2 -2
- data/lib/view_model/active_record/cloner.rb +11 -11
- data/lib/view_model/active_record/controller.rb +0 -2
- data/lib/view_model/active_record/update_context.rb +21 -3
- data/lib/view_model/active_record/update_data.rb +43 -45
- data/lib/view_model/active_record/update_operation.rb +265 -153
- data/lib/view_model/active_record/visitor.rb +9 -6
- data/lib/view_model/active_record.rb +94 -74
- data/lib/view_model/after_transaction_runner.rb +3 -18
- data/lib/view_model/callbacks.rb +2 -2
- data/lib/view_model/changes.rb +24 -16
- data/lib/view_model/config.rb +6 -2
- data/lib/view_model/deserialization_error.rb +31 -0
- data/lib/view_model/deserialize_context.rb +2 -6
- data/lib/view_model/error_view.rb +6 -5
- data/lib/view_model/record/attribute_data.rb +11 -6
- data/lib/view_model/record.rb +44 -24
- data/lib/view_model/serialize_context.rb +2 -63
- data/lib/view_model/test_helpers/arvm_builder.rb +2 -4
- data/lib/view_model/traversal_context.rb +2 -2
- data/lib/view_model.rb +21 -13
- data/shell.nix +1 -1
- data/test/helpers/arvm_test_models.rb +4 -12
- data/test/helpers/arvm_test_utilities.rb +6 -0
- data/test/helpers/controller_test_helpers.rb +6 -6
- data/test/helpers/viewmodel_spec_helpers.rb +63 -52
- data/test/unit/view_model/access_control_test.rb +88 -37
- data/test/unit/view_model/active_record/belongs_to_test.rb +110 -178
- data/test/unit/view_model/active_record/cache_test.rb +11 -5
- data/test/unit/view_model/active_record/cloner_test.rb +1 -1
- data/test/unit/view_model/active_record/controller_test.rb +12 -20
- data/test/unit/view_model/active_record/has_many_test.rb +540 -316
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +12 -15
- data/test/unit/view_model/active_record/has_many_through_test.rb +15 -58
- data/test/unit/view_model/active_record/has_one_test.rb +288 -135
- data/test/unit/view_model/active_record/poly_test.rb +0 -1
- data/test/unit/view_model/active_record/shared_test.rb +21 -39
- data/test/unit/view_model/active_record/version_test.rb +3 -2
- data/test/unit/view_model/active_record_test.rb +5 -63
- data/test/unit/view_model/callbacks_test.rb +1 -0
- data/test/unit/view_model/record_test.rb +0 -32
- data/test/unit/view_model/traversal_context_test.rb +13 -12
- metadata +15 -25
- data/.github/workflows/gem-push.yml +0 -31
- data/.github/workflows/test.yml +0 -65
- data/gemfiles/rails_6_0.gemfile +0 -9
- data/gemfiles/rails_6_1.gemfile +0 -9
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +0 -58
@@ -12,17 +12,7 @@ module ViewModel::ActiveRecord::Cache::CacheableView
|
|
12
12
|
CacheClearer = Struct.new(:cache, :id) do
|
13
13
|
include ViewModel::AfterTransactionRunner
|
14
14
|
|
15
|
-
|
16
|
-
# on database locking to prevent cache race conditions. We require
|
17
|
-
# reading/refreshing the cache to obtain a FOR SHARE lock, which means that
|
18
|
-
# a reader must wait for a concurrent writer to commit before continuing to
|
19
|
-
# the cache. If the writer cleared the cache after commit, the reader could
|
20
|
-
# obtain old data before the clear, and then save the old data after it.
|
21
|
-
def before_commit
|
22
|
-
cache.delete(id)
|
23
|
-
end
|
24
|
-
|
25
|
-
def after_rollback
|
15
|
+
def after_transaction
|
26
16
|
cache.delete(id)
|
27
17
|
end
|
28
18
|
|
@@ -49,12 +39,12 @@ module ViewModel::ActiveRecord::Cache::CacheableView
|
|
49
39
|
end
|
50
40
|
end
|
51
41
|
|
52
|
-
# Clear the cache if the view or its
|
42
|
+
# Clear the cache if the view or its nested children were changed during
|
53
43
|
# deserialization
|
54
44
|
def after_deserialize(deserialize_context:, changes:)
|
55
45
|
super if defined?(super)
|
56
46
|
|
57
|
-
if !changes.new? && changes.
|
47
|
+
if !changes.new? && changes.changed_nested_tree?
|
58
48
|
CacheClearer.new(self.class.viewmodel_cache, id).add_to_transaction
|
59
49
|
end
|
60
50
|
end
|
@@ -198,7 +198,7 @@ class ViewModel::ActiveRecord::Cache
|
|
198
198
|
end
|
199
199
|
|
200
200
|
ViewModel.preload_for_serialization(viewmodels,
|
201
|
-
|
201
|
+
include_referenced: false,
|
202
202
|
lock: "FOR SHARE",
|
203
203
|
serialize_context: serialize_context)
|
204
204
|
|
@@ -259,7 +259,7 @@ class ViewModel::ActiveRecord::Cache
|
|
259
259
|
end
|
260
260
|
|
261
261
|
def cache_version
|
262
|
-
version_string = @viewmodel_class.deep_schema_version(
|
262
|
+
version_string = @viewmodel_class.deep_schema_version(include_referenced: false).to_a.sort.join(',')
|
263
263
|
Base64.urlsafe_encode64(Digest::MD5.digest(version_string))
|
264
264
|
end
|
265
265
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Simple visitor for cloning models through the tree structure defined by
|
2
4
|
# ViewModel::ActiveRecord. Owned associations will be followed and cloned, while
|
3
|
-
#
|
4
|
-
# foreign keys not covered by ViewModel
|
5
|
-
# original.
|
5
|
+
# non-owned referenced associations will be copied directly as references.
|
6
|
+
# Attributes (including association foreign keys not covered by ViewModel
|
7
|
+
# `association`s) will be copied from the original.
|
6
8
|
#
|
7
9
|
# To customize, subclasses may define methods `visit_x_view(node, new_model)`
|
8
10
|
# for each type they wish to affect. These callbacks may update attributes of
|
@@ -19,9 +21,9 @@ class ViewModel::ActiveRecord::Cloner
|
|
19
21
|
return nil if ignored?
|
20
22
|
|
21
23
|
if node.class.name
|
22
|
-
class_name
|
23
|
-
visit
|
24
|
-
end_visit
|
24
|
+
class_name = node.class.name.underscore.gsub('/', '__')
|
25
|
+
visit = :"visit_#{class_name}"
|
26
|
+
end_visit = :"end_visit_#{class_name}"
|
25
27
|
end
|
26
28
|
|
27
29
|
if visit && respond_to?(visit, true)
|
@@ -44,7 +46,7 @@ class ViewModel::ActiveRecord::Cloner
|
|
44
46
|
|
45
47
|
if associated.nil?
|
46
48
|
new_associated = nil
|
47
|
-
elsif association_data.
|
49
|
+
elsif !association_data.owned? && !association_data.through?
|
48
50
|
# simply attach the associated target to the new model
|
49
51
|
new_associated = associated
|
50
52
|
else
|
@@ -82,11 +84,9 @@ class ViewModel::ActiveRecord::Cloner
|
|
82
84
|
new_model
|
83
85
|
end
|
84
86
|
|
85
|
-
def pre_visit(node, new_model)
|
86
|
-
end
|
87
|
+
def pre_visit(node, new_model); end
|
87
88
|
|
88
|
-
def post_visit(node, new_model)
|
89
|
-
end
|
89
|
+
def post_visit(node, new_model); end
|
90
90
|
|
91
91
|
private
|
92
92
|
|
@@ -46,8 +46,6 @@ module ViewModel::ActiveRecord::Controller
|
|
46
46
|
pre_rendered = viewmodel_class.transaction do
|
47
47
|
view = viewmodel_class.deserialize_from_view(update_hash, references: refs, deserialize_context: deserialize_context)
|
48
48
|
|
49
|
-
serialize_context.add_includes(deserialize_context.updated_associations)
|
50
|
-
|
51
49
|
view = yield(view) if block_given?
|
52
50
|
|
53
51
|
ViewModel.preload_for_serialization(view, serialize_context: serialize_context)
|
@@ -74,7 +74,7 @@ class ViewModel::ActiveRecord
|
|
74
74
|
|
75
75
|
def initialize
|
76
76
|
@root_update_operations = [] # The subject(s) of this update
|
77
|
-
@referenced_update_operations = {} #
|
77
|
+
@referenced_update_operations = {} # data updates to other root models, referred to by a ref hash
|
78
78
|
|
79
79
|
# Set of ViewModel::Reference used to assert only a single update is
|
80
80
|
# present for each viewmodel
|
@@ -178,8 +178,20 @@ class ViewModel::ActiveRecord
|
|
178
178
|
raise ViewModel::DeserializationError::ParentNotFound.new(@worklist.keys)
|
179
179
|
end
|
180
180
|
|
181
|
-
deferred_update
|
182
|
-
|
181
|
+
deferred_update = @worklist.delete(key)
|
182
|
+
released_viewmodel = @release_pool.claim_from_pool(key)
|
183
|
+
|
184
|
+
if deferred_update.viewmodel
|
185
|
+
# Deferred reference updates already have a viewmodel: ensure it
|
186
|
+
# matches the tree
|
187
|
+
unless deferred_update.viewmodel == released_viewmodel
|
188
|
+
raise ViewModel::DeserializationError::Internal.new(
|
189
|
+
"Released viewmodel doesn't match reference update", blame_reference)
|
190
|
+
end
|
191
|
+
else
|
192
|
+
deferred_update.viewmodel = released_viewmodel
|
193
|
+
end
|
194
|
+
|
183
195
|
deferred_update.build!(self)
|
184
196
|
end
|
185
197
|
|
@@ -201,6 +213,12 @@ class ViewModel::ActiveRecord
|
|
201
213
|
update_operation = ViewModel::ActiveRecord::UpdateOperation.new(
|
202
214
|
nil, update_data, reparent_to: reparent_to, reposition_to: reposition_to)
|
203
215
|
check_unique_update!(viewmodel_reference)
|
216
|
+
defer_update(viewmodel_reference, update_operation)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Defer an existing update: used if we need to ensure that an owned
|
220
|
+
# reference has been freed before we use it.
|
221
|
+
def defer_update(viewmodel_reference, update_operation)
|
204
222
|
@worklist[viewmodel_reference] = update_operation
|
205
223
|
end
|
206
224
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'renum'
|
2
4
|
require 'view_model/schemas'
|
3
5
|
|
@@ -45,14 +47,13 @@ class ViewModel::ActiveRecord
|
|
45
47
|
end
|
46
48
|
end
|
47
49
|
|
48
|
-
# Parser for collection updates. Collection updates have a regular
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
# reference strings.
|
50
|
+
# Parser for collection updates. Collection updates have a regular structure,
|
51
|
+
# but vary based on the contents. Parsing a nested collection recurses deeply
|
52
|
+
# and creates a tree of UpdateDatas, while parsing a referenced collection
|
53
|
+
# collects reference strings.
|
53
54
|
class AbstractCollectionUpdate
|
54
|
-
# Wraps a complete collection of new data: either UpdateDatas for
|
55
|
-
#
|
55
|
+
# Wraps a complete collection of new data: either UpdateDatas for non-root
|
56
|
+
# associations or reference strings for root.
|
56
57
|
class Replace
|
57
58
|
attr_reader :contents
|
58
59
|
def initialize(contents)
|
@@ -61,7 +62,8 @@ class ViewModel::ActiveRecord
|
|
61
62
|
end
|
62
63
|
|
63
64
|
# Wraps an ordered list of FunctionalUpdates, each of whose `contents` are
|
64
|
-
# either UpdateData for
|
65
|
+
# either UpdateData for nested associations or references for referenced
|
66
|
+
# associations.
|
65
67
|
class Functional
|
66
68
|
attr_reader :actions
|
67
69
|
def initialize(actions)
|
@@ -81,8 +83,8 @@ class ViewModel::ActiveRecord
|
|
81
83
|
|
82
84
|
# Resolve ViewModel::References used in the update's contents, whether by
|
83
85
|
# reference or value.
|
84
|
-
def used_vm_refs(
|
85
|
-
raise
|
86
|
+
def used_vm_refs(_update_context)
|
87
|
+
raise RuntimeError.new('abstract method')
|
86
88
|
end
|
87
89
|
|
88
90
|
def removed_vm_refs
|
@@ -153,7 +155,8 @@ class ViewModel::ActiveRecord
|
|
153
155
|
# The shape of the actions are always the same
|
154
156
|
|
155
157
|
# Parse an anchor for a functional_update, before/after
|
156
|
-
# May only contain type and id fields, is never a reference even for
|
158
|
+
# May only contain type and id fields, is never a reference even for
|
159
|
+
# referenced associations.
|
157
160
|
def parse_anchor(child_hash) # final
|
158
161
|
child_metadata = ViewModel.extract_reference_only_metadata(child_hash)
|
159
162
|
|
@@ -271,9 +274,13 @@ class ViewModel::ActiveRecord
|
|
271
274
|
|
272
275
|
def used_vm_refs(update_context)
|
273
276
|
update_datas
|
274
|
-
.map { |upd| upd
|
277
|
+
.map { |upd| resolve_vm_reference(upd, update_context) }
|
275
278
|
.compact
|
276
279
|
end
|
280
|
+
|
281
|
+
def resolve_vm_reference(update_data, _update_context)
|
282
|
+
update_data.viewmodel_reference if update_data.id
|
283
|
+
end
|
277
284
|
end
|
278
285
|
|
279
286
|
class Parser < AbstractCollectionUpdate::Parser
|
@@ -319,9 +326,13 @@ class ViewModel::ActiveRecord
|
|
319
326
|
|
320
327
|
def used_vm_refs(update_context)
|
321
328
|
references.map do |ref|
|
322
|
-
|
329
|
+
resolve_vm_reference(ref, update_context)
|
323
330
|
end
|
324
331
|
end
|
332
|
+
|
333
|
+
def resolve_vm_reference(ref, update_context)
|
334
|
+
update_context.resolve_reference(ref, nil).viewmodel_reference
|
335
|
+
end
|
325
336
|
end
|
326
337
|
|
327
338
|
class Parser < AbstractCollectionUpdate::Parser
|
@@ -356,6 +367,7 @@ class ViewModel::ActiveRecord
|
|
356
367
|
unless valid_reference_keys.include?(ref)
|
357
368
|
raise ViewModel::DeserializationError::InvalidSharedReference.new(ref, blame_reference)
|
358
369
|
end
|
370
|
+
|
359
371
|
ref
|
360
372
|
end
|
361
373
|
end
|
@@ -421,7 +433,7 @@ class ViewModel::ActiveRecord
|
|
421
433
|
fupdate_owned =
|
422
434
|
fupdate_base.(ViewModel::Schemas::VIEWMODEL_UPDATE_SCHEMA)
|
423
435
|
|
424
|
-
fupdate_shared
|
436
|
+
fupdate_shared =
|
425
437
|
fupdate_base.({ 'oneOf' => [ViewModel::Schemas::VIEWMODEL_REFERENCE_SCHEMA,
|
426
438
|
viewmodel_reference_only] })
|
427
439
|
|
@@ -571,30 +583,15 @@ class ViewModel::ActiveRecord
|
|
571
583
|
case
|
572
584
|
when value.nil?
|
573
585
|
[]
|
574
|
-
when association_data.
|
586
|
+
when association_data.referenced?
|
575
587
|
[]
|
576
|
-
when association_data.collection? #
|
588
|
+
when association_data.collection? # nested, because of referenced? check above
|
577
589
|
value.update_datas
|
578
590
|
else
|
579
591
|
[value]
|
580
592
|
end
|
581
593
|
end
|
582
594
|
|
583
|
-
# Updates in terms of viewmodel associations
|
584
|
-
def updated_associations
|
585
|
-
deps = {}
|
586
|
-
|
587
|
-
(associations.merge(referenced_associations)).each do |assoc_name, assoc_update|
|
588
|
-
deps[assoc_name] =
|
589
|
-
to_sequence(assoc_name, assoc_update)
|
590
|
-
.each_with_object({}) do |update_data, updated_associations|
|
591
|
-
updated_associations.deep_merge!(update_data.updated_associations)
|
592
|
-
end
|
593
|
-
end
|
594
|
-
|
595
|
-
deps
|
596
|
-
end
|
597
|
-
|
598
595
|
def build_preload_specs(association_data, updates)
|
599
596
|
if association_data.polymorphic?
|
600
597
|
updates.map do |update_data|
|
@@ -682,6 +679,7 @@ class ViewModel::ActiveRecord
|
|
682
679
|
|
683
680
|
when AssociationData
|
684
681
|
association_data = member_data
|
682
|
+
|
685
683
|
case
|
686
684
|
when value.nil?
|
687
685
|
if association_data.collection?
|
@@ -689,28 +687,28 @@ class ViewModel::ActiveRecord
|
|
689
687
|
"Invalid collection update value 'nil' for association '#{name}'",
|
690
688
|
blame_reference)
|
691
689
|
end
|
692
|
-
if association_data.
|
690
|
+
if association_data.referenced?
|
693
691
|
referenced_associations[name] = nil
|
694
692
|
else
|
695
693
|
associations[name] = nil
|
696
694
|
end
|
697
695
|
|
698
|
-
when association_data.
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
696
|
+
when association_data.referenced?
|
697
|
+
if association_data.collection?
|
698
|
+
referenced_associations[name] =
|
699
|
+
ReferencedCollectionUpdate::Parser
|
700
|
+
.new(association_data, blame_reference, valid_reference_keys)
|
701
|
+
.parse(value)
|
702
|
+
else
|
703
|
+
# Extract and check reference
|
704
|
+
ref = ViewModel.extract_reference_metadata(value)
|
703
705
|
|
704
|
-
|
705
|
-
|
706
|
-
|
706
|
+
unless valid_reference_keys.include?(ref)
|
707
|
+
raise ViewModel::DeserializationError::InvalidSharedReference.new(ref, blame_reference)
|
708
|
+
end
|
707
709
|
|
708
|
-
|
709
|
-
raise ViewModel::DeserializationError::InvalidSharedReference.new(ref, blame_reference)
|
710
|
+
referenced_associations[name] = ref
|
710
711
|
end
|
711
|
-
|
712
|
-
referenced_associations[name] = ref
|
713
|
-
|
714
712
|
else
|
715
713
|
if association_data.collection?
|
716
714
|
associations[name] =
|