iknow_view_models 2.10.1 → 3.0.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 +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] =
|