iknow_view_models 3.1.6 → 3.2.2
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 +6 -6
- data/.rubocop.yml +18 -0
- data/Appraisals +6 -6
- data/Gemfile +6 -2
- data/Rakefile +5 -5
- data/gemfiles/rails_5_2.gemfile +5 -5
- data/gemfiles/rails_6_0.gemfile +9 -0
- data/iknow_view_models.gemspec +40 -38
- data/lib/iknow_view_models.rb +9 -7
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model.rb +31 -17
- data/lib/view_model/access_control.rb +5 -2
- data/lib/view_model/access_control/composed.rb +10 -9
- data/lib/view_model/access_control/open.rb +2 -0
- data/lib/view_model/access_control/read_only.rb +2 -0
- data/lib/view_model/access_control/tree.rb +11 -6
- data/lib/view_model/access_control_error.rb +4 -1
- data/lib/view_model/active_record.rb +13 -12
- data/lib/view_model/active_record/association_data.rb +3 -2
- data/lib/view_model/active_record/association_manipulation.rb +6 -4
- data/lib/view_model/active_record/cache.rb +114 -34
- data/lib/view_model/active_record/cache/cacheable_view.rb +2 -2
- data/lib/view_model/active_record/collection_nested_controller.rb +3 -3
- data/lib/view_model/active_record/controller.rb +68 -1
- data/lib/view_model/active_record/controller_base.rb +4 -1
- data/lib/view_model/active_record/nested_controller_base.rb +1 -0
- data/lib/view_model/active_record/update_context.rb +8 -6
- data/lib/view_model/active_record/update_data.rb +32 -30
- data/lib/view_model/active_record/update_operation.rb +17 -13
- data/lib/view_model/active_record/visitor.rb +0 -1
- data/lib/view_model/after_transaction_runner.rb +2 -2
- data/lib/view_model/callbacks.rb +3 -1
- data/lib/view_model/controller.rb +13 -3
- data/lib/view_model/deserialization_error.rb +15 -12
- data/lib/view_model/error.rb +12 -10
- data/lib/view_model/error_view.rb +3 -1
- data/lib/view_model/migratable_view.rb +78 -0
- data/lib/view_model/migration.rb +48 -0
- data/lib/view_model/migration/no_path_error.rb +26 -0
- data/lib/view_model/migration/one_way_error.rb +24 -0
- data/lib/view_model/migration/unspecified_version_error.rb +24 -0
- data/lib/view_model/migrator.rb +108 -0
- data/lib/view_model/record.rb +15 -14
- data/lib/view_model/reference.rb +3 -1
- data/lib/view_model/references.rb +8 -5
- data/lib/view_model/registry.rb +1 -1
- data/lib/view_model/schemas.rb +9 -4
- data/lib/view_model/serialization_error.rb +4 -1
- data/lib/view_model/serialize_context.rb +4 -4
- data/lib/view_model/test_helpers.rb +8 -3
- data/lib/view_model/test_helpers/arvm_builder.rb +21 -15
- data/lib/view_model/traversal_context.rb +2 -1
- data/nix/dependencies.nix +5 -0
- data/nix/gem/generate.rb +2 -1
- data/shell.nix +8 -3
- data/test/.rubocop.yml +14 -0
- data/test/helpers/arvm_test_models.rb +12 -9
- data/test/helpers/arvm_test_utilities.rb +5 -3
- data/test/helpers/controller_test_helpers.rb +55 -32
- data/test/helpers/match_enumerator.rb +1 -0
- data/test/helpers/query_logging.rb +2 -1
- data/test/helpers/test_access_control.rb +5 -3
- data/test/helpers/viewmodel_spec_helpers.rb +88 -22
- data/test/unit/view_model/access_control_test.rb +144 -144
- data/test/unit/view_model/active_record/alias_test.rb +15 -13
- data/test/unit/view_model/active_record/belongs_to_test.rb +40 -39
- data/test/unit/view_model/active_record/cache_test.rb +68 -31
- data/test/unit/view_model/active_record/cloner_test.rb +67 -63
- data/test/unit/view_model/active_record/controller_test.rb +113 -65
- data/test/unit/view_model/active_record/counter_test.rb +10 -9
- data/test/unit/view_model/active_record/customization_test.rb +59 -58
- data/test/unit/view_model/active_record/has_many_test.rb +112 -111
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +15 -14
- data/test/unit/view_model/active_record/has_many_through_test.rb +33 -38
- data/test/unit/view_model/active_record/has_one_test.rb +37 -36
- data/test/unit/view_model/active_record/migration_test.rb +161 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +19 -17
- data/test/unit/view_model/active_record/poly_test.rb +44 -45
- data/test/unit/view_model/active_record/shared_test.rb +30 -28
- data/test/unit/view_model/active_record/version_test.rb +9 -7
- data/test/unit/view_model/active_record_test.rb +72 -72
- data/test/unit/view_model/callbacks_test.rb +19 -15
- data/test/unit/view_model/controller_test.rb +4 -2
- data/test/unit/view_model/record_test.rb +92 -97
- data/test/unit/view_model/traversal_context_test.rb +4 -5
- data/test/unit/view_model_test.rb +18 -16
- metadata +36 -12
- data/.travis.yml +0 -31
- data/appveyor.yml +0 -22
- data/gemfiles/rails_6_0_beta.gemfile +0 -9
@@ -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
|
@@ -47,7 +47,7 @@ module ViewModel::ActiveRecord::CollectionNestedController
|
|
47
47
|
after = parse_relative_position(:after)
|
48
48
|
|
49
49
|
if before && after
|
50
|
-
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
50
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new('Can not provide both `before` and `after` anchors for a collection append')
|
51
51
|
end
|
52
52
|
|
53
53
|
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, serialize_context: serialize_context)
|
@@ -55,8 +55,8 @@ module ViewModel::ActiveRecord::CollectionNestedController
|
|
55
55
|
assoc_view = owner_view.append_associated(association_name,
|
56
56
|
update_hash,
|
57
57
|
references: refs,
|
58
|
-
before:
|
59
|
-
after:
|
58
|
+
before: before,
|
59
|
+
after: after,
|
60
60
|
deserialize_context: deserialize_context)
|
61
61
|
ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
|
62
62
|
child_context = owner_view.context_for_child(association_name, context: serialize_context)
|
@@ -17,6 +17,8 @@ module ViewModel::ActiveRecord::Controller
|
|
17
17
|
include ViewModel::ActiveRecord::CollectionNestedController
|
18
18
|
include ViewModel::ActiveRecord::SingularNestedController
|
19
19
|
|
20
|
+
MIGRATION_VERSION_HEADER = 'X-ViewModel-Versions'
|
21
|
+
|
20
22
|
def show(scope: nil, viewmodel_class: self.viewmodel_class, serialize_context: new_serialize_context(viewmodel_class: viewmodel_class))
|
21
23
|
view = nil
|
22
24
|
pre_rendered = viewmodel_class.transaction do
|
@@ -62,7 +64,29 @@ module ViewModel::ActiveRecord::Controller
|
|
62
64
|
end
|
63
65
|
|
64
66
|
included do
|
65
|
-
etag {
|
67
|
+
etag { migrated_deep_schema_version }
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_viewmodel_updates
|
71
|
+
super.tap do |update_hash, refs|
|
72
|
+
if migration_versions.present?
|
73
|
+
migrator = ViewModel::UpMigrator.new(migration_versions)
|
74
|
+
migrator.migrate!([update_hash, refs], references: refs)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def prerender_viewmodel(*)
|
80
|
+
super do |jbuilder|
|
81
|
+
yield(jbuilder) if block_given?
|
82
|
+
|
83
|
+
# migrate the resulting structure before it's serialized to a json string
|
84
|
+
if migration_versions.present?
|
85
|
+
tree = jbuilder.attributes!
|
86
|
+
migrator = ViewModel::DownMigrator.new(migration_versions)
|
87
|
+
migrator.migrate!(tree, references: tree['references'])
|
88
|
+
end
|
89
|
+
end
|
66
90
|
end
|
67
91
|
|
68
92
|
private
|
@@ -70,4 +94,47 @@ module ViewModel::ActiveRecord::Controller
|
|
70
94
|
def viewmodel_id
|
71
95
|
parse_param(:id)
|
72
96
|
end
|
97
|
+
|
98
|
+
def migration_versions
|
99
|
+
@migration_versions ||=
|
100
|
+
begin
|
101
|
+
version_spec =
|
102
|
+
if params.include?(:versions)
|
103
|
+
params[:versions]
|
104
|
+
elsif request.headers.include?(MIGRATION_VERSION_HEADER)
|
105
|
+
begin
|
106
|
+
JSON.parse(request.headers[MIGRATION_VERSION_HEADER])
|
107
|
+
rescue JSON::ParserError
|
108
|
+
raise ViewModel::Error.new(status: 400, detail: "Invalid JSON in #{MIGRATION_VERSION_HEADER}")
|
109
|
+
end
|
110
|
+
else
|
111
|
+
{}
|
112
|
+
end
|
113
|
+
|
114
|
+
versions =
|
115
|
+
IknowParams::Parser.parse_value(
|
116
|
+
version_spec,
|
117
|
+
with: IknowParams::Serializer::HashOf.new(
|
118
|
+
IknowParams::Serializer::String, IknowParams::Serializer::Integer))
|
119
|
+
|
120
|
+
migration_versions = {}
|
121
|
+
|
122
|
+
versions.each do |view_name, required_version|
|
123
|
+
viewmodel_class = ViewModel::Registry.for_view_name(view_name)
|
124
|
+
|
125
|
+
if viewmodel_class.schema_version != required_version
|
126
|
+
migration_versions[viewmodel_class] = required_version
|
127
|
+
end
|
128
|
+
rescue ViewModel::DeserializationError::UnknownView
|
129
|
+
# Ignore requests to migrate types that no longer exist
|
130
|
+
next
|
131
|
+
end
|
132
|
+
|
133
|
+
migration_versions.freeze
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def migrated_deep_schema_version
|
138
|
+
ViewModel::Migrator.migrated_deep_schema_version(viewmodel_class, migration_versions, include_referenced: true)
|
139
|
+
end
|
73
140
|
end
|
@@ -33,7 +33,7 @@ module ViewModel::ActiveRecord::ControllerBase
|
|
33
33
|
if (match = /(.*)Controller$/.match(self.name))
|
34
34
|
self.viewmodel_name = match[1].singularize
|
35
35
|
else
|
36
|
-
raise ArgumentError.new("Could not auto-determine ViewModel from Controller name '#{self.name}'")
|
36
|
+
raise ArgumentError.new("Could not auto-determine ViewModel from Controller name '#{self.name}'")
|
37
37
|
end
|
38
38
|
end
|
39
39
|
@viewmodel_class
|
@@ -43,6 +43,7 @@ module ViewModel::ActiveRecord::ControllerBase
|
|
43
43
|
unless instance_variable_defined?(:@access_control)
|
44
44
|
raise ArgumentError.new("AccessControl instance not set for Controller '#{self.name}'")
|
45
45
|
end
|
46
|
+
|
46
47
|
@access_control
|
47
48
|
end
|
48
49
|
|
@@ -64,6 +65,7 @@ module ViewModel::ActiveRecord::ControllerBase
|
|
64
65
|
unless type < ViewModel
|
65
66
|
raise ArgumentError.new("'#{type.inspect}' is not a valid ViewModel")
|
66
67
|
end
|
68
|
+
|
67
69
|
@viewmodel_class = type
|
68
70
|
end
|
69
71
|
|
@@ -75,6 +77,7 @@ module ViewModel::ActiveRecord::ControllerBase
|
|
75
77
|
unless access_control.is_a?(Class) && access_control < ViewModel::AccessControl
|
76
78
|
raise ArgumentError.new("'#{access_control.inspect}' is not a valid AccessControl")
|
77
79
|
end
|
80
|
+
|
78
81
|
@access_control = access_control
|
79
82
|
end
|
80
83
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Assembles an update operation tree from user input. Handles the interlinking
|
2
4
|
# and model of update operations, but does not handle the actual user data nor
|
3
5
|
# the mechanism by which it is applied to models.
|
@@ -66,7 +68,7 @@ class ViewModel::ActiveRecord
|
|
66
68
|
.assemble_update_tree
|
67
69
|
end
|
68
70
|
|
69
|
-
# TODO an unfortunate abstraction violation. The `append` case constructs an
|
71
|
+
# TODO: an unfortunate abstraction violation. The `append` case constructs an
|
70
72
|
# update tree and later injects the context of parent and position.
|
71
73
|
def root_updates
|
72
74
|
@root_update_operations
|
@@ -143,7 +145,7 @@ class ViewModel::ActiveRecord
|
|
143
145
|
if ref.nil?
|
144
146
|
@root_update_operations << update_op
|
145
147
|
else
|
146
|
-
# TODO make sure that referenced subtree hashes are unique and provide a decent error message
|
148
|
+
# TODO: make sure that referenced subtree hashes are unique and provide a decent error message
|
147
149
|
# not strictly necessary, but will save confusion
|
148
150
|
@referenced_update_operations[ref] = update_op
|
149
151
|
end
|
@@ -200,9 +202,9 @@ class ViewModel::ActiveRecord
|
|
200
202
|
deferred_update.build!(self)
|
201
203
|
end
|
202
204
|
|
203
|
-
dangling_references = @referenced_update_operations.reject { |
|
205
|
+
dangling_references = @referenced_update_operations.reject { |_ref, upd| upd.built? }.map { |_ref, upd| upd.viewmodel.to_reference }
|
204
206
|
if dangling_references.present?
|
205
|
-
raise ViewModel::DeserializationError::InvalidStructure.new(
|
207
|
+
raise ViewModel::DeserializationError::InvalidStructure.new('References not referred to from roots', dangling_references)
|
206
208
|
end
|
207
209
|
|
208
210
|
self
|
@@ -265,8 +267,8 @@ class ViewModel::ActiveRecord
|
|
265
267
|
# individual node that caused it, without attempting to parse Postgres'
|
266
268
|
# human-readable error details.
|
267
269
|
def check_deferred_constraints!(model_class)
|
268
|
-
if model_class.connection.adapter_name ==
|
269
|
-
model_class.connection.execute(
|
270
|
+
if model_class.connection.adapter_name == 'PostgreSQL'
|
271
|
+
model_class.connection.execute('SET CONSTRAINTS ALL IMMEDIATE')
|
270
272
|
end
|
271
273
|
rescue ::ActiveRecord::StatementInvalid => ex
|
272
274
|
raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(ex)
|
@@ -24,6 +24,7 @@ class ViewModel::ActiveRecord
|
|
24
24
|
NAME = 'append'
|
25
25
|
attr_accessor :before, :after
|
26
26
|
attr_reader :contents
|
27
|
+
|
27
28
|
def initialize(contents)
|
28
29
|
@contents = contents
|
29
30
|
end
|
@@ -32,6 +33,7 @@ class ViewModel::ActiveRecord
|
|
32
33
|
class Update
|
33
34
|
NAME = 'update'
|
34
35
|
attr_reader :contents
|
36
|
+
|
35
37
|
def initialize(contents)
|
36
38
|
@contents = contents
|
37
39
|
end
|
@@ -56,6 +58,7 @@ class ViewModel::ActiveRecord
|
|
56
58
|
# associations or reference strings for root.
|
57
59
|
class Replace
|
58
60
|
attr_reader :contents
|
61
|
+
|
59
62
|
def initialize(contents)
|
60
63
|
@contents = contents
|
61
64
|
end
|
@@ -66,6 +69,7 @@ class ViewModel::ActiveRecord
|
|
66
69
|
# associations.
|
67
70
|
class Functional
|
68
71
|
attr_reader :actions
|
72
|
+
|
69
73
|
def initialize(actions)
|
70
74
|
@actions = actions
|
71
75
|
end
|
@@ -215,23 +219,23 @@ class ViewModel::ActiveRecord
|
|
215
219
|
# The contents of the actions are determined by the subclasses
|
216
220
|
|
217
221
|
def functional_update_schema # abstract
|
218
|
-
raise
|
222
|
+
raise 'abstract'
|
219
223
|
end
|
220
224
|
|
221
225
|
def append_action_schema # abstract
|
222
|
-
raise
|
226
|
+
raise 'abstract'
|
223
227
|
end
|
224
228
|
|
225
229
|
def remove_action_schema # abstract
|
226
|
-
raise
|
230
|
+
raise 'abstract'
|
227
231
|
end
|
228
232
|
|
229
233
|
def update_action_schema # abstract
|
230
|
-
raise
|
234
|
+
raise 'abstract'
|
231
235
|
end
|
232
236
|
|
233
|
-
def parse_contents(
|
234
|
-
raise
|
237
|
+
def parse_contents(_values) # abstract
|
238
|
+
raise 'abstract'
|
235
239
|
end
|
236
240
|
|
237
241
|
# Remove values are always anchors
|
@@ -255,11 +259,11 @@ class ViewModel::ActiveRecord
|
|
255
259
|
# behaviour, so we parameterise the result type as well.
|
256
260
|
|
257
261
|
def replace_update_type # abstract
|
258
|
-
raise
|
262
|
+
raise 'abstract'
|
259
263
|
end
|
260
264
|
|
261
265
|
def functional_update_type # abstract
|
262
|
-
raise
|
266
|
+
raise 'abstract'
|
263
267
|
end
|
264
268
|
end
|
265
269
|
end
|
@@ -390,6 +394,7 @@ class ViewModel::ActiveRecord
|
|
390
394
|
|
391
395
|
class UpdateData
|
392
396
|
attr_accessor :viewmodel_class, :metadata, :attributes, :associations, :referenced_associations
|
397
|
+
|
393
398
|
delegate :id, :view_name, :schema_version, to: :metadata
|
394
399
|
|
395
400
|
module Schemas
|
@@ -400,7 +405,7 @@ class ViewModel::ActiveRecord
|
|
400
405
|
'properties' => { ViewModel::TYPE_ATTRIBUTE => { 'type' => 'string' },
|
401
406
|
ViewModel::ID_ATTRIBUTE => ViewModel::Schemas::ID_SCHEMA },
|
402
407
|
'additionalProperties' => false,
|
403
|
-
'required' => [ViewModel::TYPE_ATTRIBUTE, ViewModel::ID_ATTRIBUTE]
|
408
|
+
'required' => [ViewModel::TYPE_ATTRIBUTE, ViewModel::ID_ATTRIBUTE],
|
404
409
|
}
|
405
410
|
|
406
411
|
VIEWMODEL_REFERENCE_ONLY = JsonSchema.parse!(viewmodel_reference_only)
|
@@ -409,14 +414,14 @@ class ViewModel::ActiveRecord
|
|
409
414
|
{
|
410
415
|
'description' => 'functional update',
|
411
416
|
'type' => 'object',
|
417
|
+
'required' => [ViewModel::TYPE_ATTRIBUTE, VALUES_ATTRIBUTE],
|
412
418
|
'properties' => {
|
413
419
|
ViewModel::TYPE_ATTRIBUTE => { 'enum' => [FunctionalUpdate::Append::NAME,
|
414
420
|
FunctionalUpdate::Update::NAME,
|
415
|
-
FunctionalUpdate::Remove::NAME] },
|
421
|
+
FunctionalUpdate::Remove::NAME,] },
|
416
422
|
VALUES_ATTRIBUTE => { 'type' => 'array',
|
417
|
-
'items' => value_schema }
|
423
|
+
'items' => value_schema },
|
418
424
|
},
|
419
|
-
'required' => [ViewModel::TYPE_ATTRIBUTE, VALUES_ATTRIBUTE]
|
420
425
|
}
|
421
426
|
end
|
422
427
|
|
@@ -426,7 +431,7 @@ class ViewModel::ActiveRecord
|
|
426
431
|
'properties' => {
|
427
432
|
ViewModel::TYPE_ATTRIBUTE => { 'enum' => [FunctionalUpdate::Append::NAME] },
|
428
433
|
BEFORE_ATTRIBUTE => viewmodel_reference_only,
|
429
|
-
AFTER_ATTRIBUTE => viewmodel_reference_only
|
434
|
+
AFTER_ATTRIBUTE => viewmodel_reference_only,
|
430
435
|
},
|
431
436
|
}
|
432
437
|
|
@@ -435,7 +440,7 @@ class ViewModel::ActiveRecord
|
|
435
440
|
|
436
441
|
fupdate_shared =
|
437
442
|
fupdate_base.({ 'oneOf' => [ViewModel::Schemas::VIEWMODEL_REFERENCE_SCHEMA,
|
438
|
-
viewmodel_reference_only] })
|
443
|
+
viewmodel_reference_only,] })
|
439
444
|
|
440
445
|
# Referenced updates are special:
|
441
446
|
# - Append requires `_ref` hashes
|
@@ -450,7 +455,7 @@ class ViewModel::ActiveRecord
|
|
450
455
|
'description' => 'collection update',
|
451
456
|
'additionalProperties' => false,
|
452
457
|
'properties' => {
|
453
|
-
ViewModel::TYPE_ATTRIBUTE => { 'enum' => [FunctionalUpdate::Update::NAME] }
|
458
|
+
ViewModel::TYPE_ATTRIBUTE => { 'enum' => [FunctionalUpdate::Update::NAME] },
|
454
459
|
},
|
455
460
|
}
|
456
461
|
|
@@ -471,7 +476,6 @@ class ViewModel::ActiveRecord
|
|
471
476
|
REMOVE_ACTION = JsonSchema.parse!(fupdate_owned.deep_merge(remove_mixin))
|
472
477
|
REFERENCED_REMOVE_ACTION = JsonSchema.parse!(fupdate_shared.deep_merge(remove_mixin))
|
473
478
|
|
474
|
-
|
475
479
|
collection_update = ->(base_schema) do
|
476
480
|
{
|
477
481
|
'type' => 'object',
|
@@ -480,7 +484,7 @@ class ViewModel::ActiveRecord
|
|
480
484
|
'required' => [ViewModel::TYPE_ATTRIBUTE, ACTIONS_ATTRIBUTE],
|
481
485
|
'properties' => {
|
482
486
|
ViewModel::TYPE_ATTRIBUTE => { 'enum' => [FUNCTIONAL_UPDATE_TYPE] },
|
483
|
-
ACTIONS_ATTRIBUTE => { 'type' => 'array', 'items' => base_schema }
|
487
|
+
ACTIONS_ATTRIBUTE => { 'type' => 'array', 'items' => base_schema },
|
484
488
|
# The ACTIONS_ATTRIBUTE could be accurately expressed as
|
485
489
|
#
|
486
490
|
# { 'oneOf' => [append, update, remove] }
|
@@ -488,7 +492,7 @@ class ViewModel::ActiveRecord
|
|
488
492
|
# but this produces completely unusable error messages. Instead we
|
489
493
|
# specify it must be an array, and defer checking to the code that
|
490
494
|
# can determine the schema by inspecting the type field.
|
491
|
-
}
|
495
|
+
},
|
492
496
|
}
|
493
497
|
end
|
494
498
|
|
@@ -503,7 +507,7 @@ class ViewModel::ActiveRecord
|
|
503
507
|
when :_type
|
504
508
|
viewmodel_class.view_name
|
505
509
|
else
|
506
|
-
attributes.fetch(name) { associations.fetch(name) { referenced_associations.fetch(name) }}
|
510
|
+
attributes.fetch(name) { associations.fetch(name) { referenced_associations.fetch(name) } }
|
507
511
|
end
|
508
512
|
end
|
509
513
|
|
@@ -538,7 +542,7 @@ class ViewModel::ActiveRecord
|
|
538
542
|
end
|
539
543
|
|
540
544
|
# Ensure that no root is referred to more than once
|
541
|
-
check_duplicate_updates(root_updates, type:
|
545
|
+
check_duplicate_updates(root_updates, type: 'root')
|
542
546
|
|
543
547
|
# Construct reference UpdateData
|
544
548
|
referenced_updates = referenced_subtree_hashes.transform_values do |subtree_hash|
|
@@ -549,7 +553,7 @@ class ViewModel::ActiveRecord
|
|
549
553
|
UpdateData.new(viewmodel_class, metadata, subtree_hash, valid_reference_keys)
|
550
554
|
end
|
551
555
|
|
552
|
-
check_duplicate_updates(referenced_updates.values, type:
|
556
|
+
check_duplicate_updates(referenced_updates.values, type: 'reference')
|
553
557
|
|
554
558
|
return root_updates, referenced_updates
|
555
559
|
end
|
@@ -614,7 +618,7 @@ class ViewModel::ActiveRecord
|
|
614
618
|
def preload_dependencies
|
615
619
|
deps = {}
|
616
620
|
|
617
|
-
|
621
|
+
associations.merge(referenced_associations).each do |assoc_name, reference|
|
618
622
|
association_data = self.viewmodel_class._association_data(assoc_name)
|
619
623
|
|
620
624
|
preload_specs = build_preload_specs(association_data,
|
@@ -665,7 +669,7 @@ class ViewModel::ActiveRecord
|
|
665
669
|
|
666
670
|
# handle view pre-parsing if defined
|
667
671
|
self.viewmodel_class.pre_parse(viewmodel_reference, metadata, hash_data) if self.viewmodel_class.respond_to?(:pre_parse)
|
668
|
-
hash_data.keys.each do |key|
|
672
|
+
hash_data.keys.each do |key| # rubocop:disable Style/HashEachMethods
|
669
673
|
if self.viewmodel_class.respond_to?(:"pre_parse_#{key}")
|
670
674
|
self.viewmodel_class.public_send("pre_parse_#{key}", viewmodel_reference, metadata, hash_data, hash_data.delete(key))
|
671
675
|
end
|
@@ -710,19 +714,18 @@ class ViewModel::ActiveRecord
|
|
710
714
|
referenced_associations[name] = ref
|
711
715
|
end
|
712
716
|
else
|
713
|
-
|
714
|
-
|
717
|
+
associations[name] =
|
718
|
+
if association_data.collection?
|
715
719
|
OwnedCollectionUpdate::Parser
|
716
720
|
.new(association_data, blame_reference, valid_reference_keys)
|
717
721
|
.parse(value)
|
718
|
-
|
719
|
-
|
720
|
-
if value.nil?
|
722
|
+
else # not a collection
|
723
|
+
if value.nil? # rubocop:disable Style/IfInsideElse
|
721
724
|
nil
|
722
725
|
else
|
723
726
|
self.class.parse_associated(association_data, blame_reference, valid_reference_keys, value)
|
724
727
|
end
|
725
|
-
|
728
|
+
end
|
726
729
|
end
|
727
730
|
else
|
728
731
|
raise ViewModel::DeserializationError::UnknownAttribute.new(name, blame_reference)
|
@@ -730,7 +733,6 @@ class ViewModel::ActiveRecord
|
|
730
733
|
end
|
731
734
|
end
|
732
735
|
|
733
|
-
|
734
736
|
def blame_reference
|
735
737
|
ViewModel::Reference.new(self.viewmodel_class, self.id)
|
736
738
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'renum'
|
2
4
|
|
3
5
|
# Partially parsed tree of user-specified update hashes, created during deserialization.
|
4
6
|
class ViewModel::ActiveRecord
|
@@ -14,7 +16,7 @@ class ViewModel::ActiveRecord
|
|
14
16
|
:update_data,
|
15
17
|
:points_to, # AssociationData => UpdateOperation (returns single new viewmodel to update fkey)
|
16
18
|
:pointed_to, # AssociationData => UpdateOperation(s) (returns viewmodel(s) with which to update assoc cache)
|
17
|
-
:reparent_to,
|
19
|
+
:reparent_to, # If node needs to update its pointer to a new parent, ParentData for the parent
|
18
20
|
:reposition_to, # if this node participates in a list under its parent, what should its position be?
|
19
21
|
:released_children # Set of children that have been released
|
20
22
|
|
@@ -46,11 +48,11 @@ class ViewModel::ActiveRecord
|
|
46
48
|
|
47
49
|
# Evaluate a built update tree, applying and saving changes to the models.
|
48
50
|
def run!(deserialize_context:)
|
49
|
-
raise ViewModel::DeserializationError::Internal.new(
|
51
|
+
raise ViewModel::DeserializationError::Internal.new('Internal error: UpdateOperation run before build') unless built?
|
50
52
|
|
51
53
|
case @run_state
|
52
54
|
when RunState::Running
|
53
|
-
raise ViewModel::DeserializationError::Internal.new(
|
55
|
+
raise ViewModel::DeserializationError::Internal.new('Internal error: Cycle found in running UpdateOperation')
|
54
56
|
when RunState::Run
|
55
57
|
return viewmodel
|
56
58
|
end
|
@@ -217,7 +219,7 @@ class ViewModel::ActiveRecord
|
|
217
219
|
|
218
220
|
# Recursively builds UpdateOperations for the associations in our UpdateData
|
219
221
|
def build!(update_context)
|
220
|
-
raise ViewModel::DeserializationError::Internal.new(
|
222
|
+
raise ViewModel::DeserializationError::Internal.new('Internal error: UpdateOperation cannot build a deferred update') if viewmodel.nil?
|
221
223
|
return self if built?
|
222
224
|
|
223
225
|
update_data.associations.each do |association_name, association_update_data|
|
@@ -254,8 +256,8 @@ class ViewModel::ActiveRecord
|
|
254
256
|
def add_update(association_data, update)
|
255
257
|
target =
|
256
258
|
case association_data.pointer_location
|
257
|
-
when :remote
|
258
|
-
when :local
|
259
|
+
when :remote then pointed_to
|
260
|
+
when :local then points_to
|
259
261
|
end
|
260
262
|
|
261
263
|
target[association_data] = update
|
@@ -658,7 +660,7 @@ class ViewModel::ActiveRecord
|
|
658
660
|
other.indirect_viewmodel_reference == self.indirect_viewmodel_reference
|
659
661
|
end
|
660
662
|
|
661
|
-
alias
|
663
|
+
alias eql? ==
|
662
664
|
end
|
663
665
|
|
664
666
|
# Helper class to wrap the previous members of a referenced collection and
|
@@ -767,6 +769,7 @@ class ViewModel::ActiveRecord
|
|
767
769
|
member.ref_string = ref_string if ref_string
|
768
770
|
member
|
769
771
|
end
|
772
|
+
|
770
773
|
def remove_from_members(removed_members)
|
771
774
|
s = removed_members.to_set
|
772
775
|
members.reject! { |m| s.include?(m) }
|
@@ -900,11 +903,12 @@ class ViewModel::ActiveRecord
|
|
900
903
|
|
901
904
|
def clear_association_cache(model, reflection)
|
902
905
|
association = model.association(reflection.name)
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
906
|
+
association.target =
|
907
|
+
if reflection.collection?
|
908
|
+
[]
|
909
|
+
else
|
910
|
+
nil
|
911
|
+
end
|
908
912
|
end
|
909
913
|
|
910
914
|
def blame_reference
|