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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a86c98f9ceda1cb4bf6980a84eb2051b5bc3904c08ab85b8546c4b16c78d26ff
|
4
|
+
data.tar.gz: 3b988a29d63e06d929c3fa0ca31b7b11f585e03263653c8f5bea64f2583c2fa3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f281b79eda3e83689d5f2aec0239f89d73a4c1ad0778ee017c20af65088c50dc7356d2149bce7b487766a68947c85645cddd0f6e92f0312a7c2cad1489ccb6f0
|
7
|
+
data.tar.gz: a3f4960911039bc0679c068cc67c86bee0cdad278ab7acfbea66e36cdf8e064e45eed8ea709ecd3b75ddc42a7934645f93ad9699be5c40a0595a3d2b5db10dc6
|
@@ -0,0 +1,119 @@
|
|
1
|
+
version: 2.1
|
2
|
+
|
3
|
+
executors:
|
4
|
+
ruby-pg:
|
5
|
+
parameters:
|
6
|
+
ruby-version:
|
7
|
+
type: string
|
8
|
+
default: "2.6"
|
9
|
+
pg-version:
|
10
|
+
type: string
|
11
|
+
default: "11"
|
12
|
+
gemfile:
|
13
|
+
type: string
|
14
|
+
default: "Gemfile"
|
15
|
+
environment:
|
16
|
+
PGHOST: 127.0.0.1
|
17
|
+
PGUSER: eikaiwa
|
18
|
+
docker:
|
19
|
+
- image: circleci/ruby:<< parameters.ruby-version >>
|
20
|
+
environment:
|
21
|
+
BUNDLE_JOBS: 3
|
22
|
+
BUNDLE_RETRY: 3
|
23
|
+
BUNDLE_PATH: vendor/bundle
|
24
|
+
RAILS_ENV: test
|
25
|
+
BUNDLE_GEMFILE: << parameters.gemfile >>
|
26
|
+
- image: circleci/postgres:<< parameters.pg-version >>-alpine
|
27
|
+
environment:
|
28
|
+
POSTGRES_USER: eikaiwa
|
29
|
+
POSTGRES_DB: iknow_view_models
|
30
|
+
POSTGRES_PASSWORD: ""
|
31
|
+
|
32
|
+
jobs:
|
33
|
+
test:
|
34
|
+
parameters:
|
35
|
+
ruby-version:
|
36
|
+
type: string
|
37
|
+
pg-version:
|
38
|
+
type: string
|
39
|
+
gemfile:
|
40
|
+
type: string
|
41
|
+
executor:
|
42
|
+
name: ruby-pg
|
43
|
+
ruby-version: << parameters.ruby-version >>
|
44
|
+
pg-version: << parameters.pg-version >>
|
45
|
+
gemfile: << parameters.gemfile >>
|
46
|
+
parallelism: 1
|
47
|
+
steps:
|
48
|
+
- checkout
|
49
|
+
|
50
|
+
- run:
|
51
|
+
# Remove the non-appraisal gemfile for safety: we never want to use it.
|
52
|
+
name: Prepare bundler
|
53
|
+
command: bundle -v && rm Gemfile
|
54
|
+
|
55
|
+
- run:
|
56
|
+
name: Compute a gemfile lock
|
57
|
+
command: bundle lock && cp "${BUNDLE_GEMFILE}.lock" /tmp/gem-lock
|
58
|
+
|
59
|
+
- restore_cache:
|
60
|
+
keys:
|
61
|
+
- iknow_viewmodels-<< parameters.ruby-version >>-{{ checksum "/tmp/gem-lock" }}
|
62
|
+
- iknow_viewmodels-
|
63
|
+
|
64
|
+
- run:
|
65
|
+
name: Bundle Install
|
66
|
+
command: bundle check || bundle install
|
67
|
+
|
68
|
+
- save_cache:
|
69
|
+
key: iknow_viewmodels-<< parameters.ruby-version >>-{{ checksum "/tmp/gem-lock" }}
|
70
|
+
paths:
|
71
|
+
- vendor/bundle
|
72
|
+
|
73
|
+
- run:
|
74
|
+
name: Wait for DB
|
75
|
+
command: dockerize -wait tcp://localhost:5432 -timeout 1m
|
76
|
+
|
77
|
+
- run:
|
78
|
+
name: Run minitest
|
79
|
+
command: bundle exec rake test
|
80
|
+
|
81
|
+
- store_test_results:
|
82
|
+
path: test/reports
|
83
|
+
|
84
|
+
publish:
|
85
|
+
executor: ruby-pg
|
86
|
+
steps:
|
87
|
+
- checkout
|
88
|
+
- run:
|
89
|
+
name: Setup Rubygems
|
90
|
+
command: |
|
91
|
+
mkdir ~/.gem &&
|
92
|
+
echo -e "---\r\n:rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials &&
|
93
|
+
chmod 0600 ~/.gem/credentials
|
94
|
+
- run:
|
95
|
+
name: Publish to Rubygems
|
96
|
+
command: |
|
97
|
+
gem build iknow_view_models.gemspec
|
98
|
+
gem push iknow_view_models-*.gem
|
99
|
+
|
100
|
+
workflows:
|
101
|
+
version: 2.1
|
102
|
+
build:
|
103
|
+
jobs:
|
104
|
+
- test:
|
105
|
+
name: 'ruby 2.6 rails 5.2 pg 11'
|
106
|
+
ruby-version: "2.6"
|
107
|
+
pg-version: "11"
|
108
|
+
gemfile: gemfiles/rails_5_2.gemfile
|
109
|
+
- test:
|
110
|
+
name: 'ruby 2.6 rails 6.0 pg 11'
|
111
|
+
ruby-version: "2.6"
|
112
|
+
pg-version: "11"
|
113
|
+
gemfile: gemfiles/rails_6_0_beta.gemfile
|
114
|
+
- publish:
|
115
|
+
filters:
|
116
|
+
branches:
|
117
|
+
only: master
|
118
|
+
tags:
|
119
|
+
ignore: /.*/
|
data/.travis.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
dist: trusty
|
2
|
+
sudo: false
|
3
|
+
language: ruby
|
4
|
+
cache: bundler
|
5
|
+
|
6
|
+
rvm:
|
7
|
+
- 2.5
|
8
|
+
|
9
|
+
gemfile:
|
10
|
+
- gemfiles/rails_5_2.gemfile
|
11
|
+
|
12
|
+
addons:
|
13
|
+
postgresql: "10"
|
14
|
+
apt:
|
15
|
+
packages:
|
16
|
+
- postgresql-10
|
17
|
+
- postgresql-client-10
|
18
|
+
- postgresql-server-dev-10
|
19
|
+
env:
|
20
|
+
global:
|
21
|
+
- PGPORT=5433
|
22
|
+
|
23
|
+
before_install:
|
24
|
+
- gem update --system
|
25
|
+
- gem install bundler
|
26
|
+
|
27
|
+
before_script:
|
28
|
+
- psql -c 'CREATE DATABASE iknow_view_models;'
|
29
|
+
|
30
|
+
notifications:
|
31
|
+
email: false
|
data/Appraisals
CHANGED
@@ -1,19 +1,9 @@
|
|
1
|
-
appraise
|
2
|
-
gem
|
3
|
-
gem
|
1
|
+
appraise "rails-5-2" do
|
2
|
+
gem "activerecord", "~> 5.2.0"
|
3
|
+
gem "activesupport", "~> 5.2.0"
|
4
4
|
end
|
5
5
|
|
6
|
-
appraise
|
7
|
-
gem
|
8
|
-
gem
|
9
|
-
end
|
10
|
-
|
11
|
-
appraise 'rails-6-1' do
|
12
|
-
gem 'activerecord', '~> 6.1.0'
|
13
|
-
gem 'activesupport', '~> 6.1.0'
|
14
|
-
end
|
15
|
-
|
16
|
-
appraise 'rails-7-0' do
|
17
|
-
gem 'activerecord', '~> 7.0.0'
|
18
|
-
gem 'activesupport', '~> 7.0.0'
|
6
|
+
appraise "rails-6-0-beta" do
|
7
|
+
gem "activerecord", "~> 6.0.0.beta"
|
8
|
+
gem "activesupport", "~> 6.0.0.beta"
|
19
9
|
end
|
data/iknow_view_models.gemspec
CHANGED
@@ -20,15 +20,13 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
|
23
|
-
spec.required_ruby_version = '>= 2.7.3'
|
24
|
-
|
25
23
|
spec.add_dependency "activerecord", ">= 5.0"
|
26
24
|
spec.add_dependency "activesupport", ">= 5.0"
|
27
25
|
|
28
26
|
spec.add_dependency "acts_as_manual_list"
|
29
|
-
spec.add_dependency "deep_preloader"
|
27
|
+
spec.add_dependency "deep_preloader", ">= 1.0.1"
|
30
28
|
spec.add_dependency "iknow_cache"
|
31
|
-
spec.add_dependency "iknow_params", "
|
29
|
+
spec.add_dependency "iknow_params", "~> 2.2.0"
|
32
30
|
spec.add_dependency "safe_values"
|
33
31
|
spec.add_dependency "keyword_builder"
|
34
32
|
|
@@ -44,7 +42,7 @@ Gem::Specification.new do |spec|
|
|
44
42
|
spec.add_development_dependency "byebug"
|
45
43
|
spec.add_development_dependency "method_source"
|
46
44
|
spec.add_development_dependency "minitest-hooks"
|
47
|
-
spec.add_development_dependency "pg"
|
45
|
+
spec.add_development_dependency "pg", '~> 0.18' # As of 5.1.4, Rails runtime check excludes pg 1.x, see #31669
|
48
46
|
spec.add_development_dependency "pry"
|
49
47
|
spec.add_development_dependency "rake"
|
50
48
|
spec.add_development_dependency "rspec-expectations"
|
@@ -1,95 +1,137 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# TODO consider rephrase scope for consistency
|
4
3
|
class ViewModel::ActiveRecord::AssociationData
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
4
|
+
class InvalidAssociation < RuntimeError; end
|
5
|
+
|
6
|
+
attr_reader :association_name, :direct_reflection
|
7
|
+
|
8
|
+
def initialize(owner:,
|
9
|
+
association_name:,
|
10
|
+
direct_association_name:,
|
11
|
+
indirect_association_name:,
|
12
|
+
target_viewmodels:,
|
13
|
+
external:,
|
14
|
+
through_order_attr:,
|
15
|
+
read_only:)
|
16
|
+
@association_name = association_name
|
17
|
+
|
18
|
+
@direct_reflection = owner.model_class.reflect_on_association(direct_association_name)
|
19
|
+
if @direct_reflection.nil?
|
20
|
+
raise InvalidAssociation.new("Association '#{direct_association_name}' not found in model '#{model_class.name}'")
|
21
|
+
end
|
22
|
+
|
23
|
+
@indirect_association_name = indirect_association_name
|
24
|
+
|
25
|
+
@read_only = read_only
|
26
|
+
@external = external
|
27
|
+
@through_order_attr = through_order_attr
|
28
|
+
@target_viewmodels = target_viewmodels
|
29
|
+
|
30
|
+
# Target models/reflections/viewmodels are lazily evaluated so that we can
|
31
|
+
# safely express cycles.
|
32
|
+
@initialized = false
|
33
|
+
@mutex = Mutex.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def lazy_initialize!
|
37
|
+
@mutex.synchronize do
|
38
|
+
return if @initialized
|
39
|
+
|
40
|
+
if through?
|
41
|
+
intermediate_model = @direct_reflection.klass
|
42
|
+
@indirect_reflection = load_indirect_reflection(intermediate_model, @indirect_association_name)
|
43
|
+
target_reflection = @indirect_reflection
|
44
|
+
else
|
45
|
+
target_reflection = @direct_reflection
|
46
|
+
end
|
47
|
+
|
48
|
+
@viewmodel_classes =
|
49
|
+
if @target_viewmodels.present?
|
50
|
+
# Explicitly named
|
51
|
+
@target_viewmodels.map { |v| resolve_viewmodel_class(v) }
|
22
52
|
else
|
23
|
-
|
53
|
+
# Infer name from name of model
|
54
|
+
if target_reflection.polymorphic?
|
55
|
+
raise InvalidAssociation.new(
|
56
|
+
'Cannot automatically infer target viewmodels from polymorphic association')
|
57
|
+
end
|
58
|
+
infer_viewmodel_class(target_reflection.klass)
|
24
59
|
end
|
60
|
+
|
61
|
+
@referenced = @viewmodel_classes.first.root?
|
62
|
+
|
63
|
+
# Non-referenced viewmodels must be owned. For referenced viewmodels, we
|
64
|
+
# own it if it points to us. Through associations aren't considered
|
65
|
+
# `owned?`: while we do own the implicit direct viewmodel, we don't own
|
66
|
+
# the target of the association.
|
67
|
+
@owned = !@referenced || (target_reflection.macro != :belongs_to)
|
68
|
+
|
69
|
+
unless @viewmodel_classes.all? { |v| v.root? == @referenced }
|
70
|
+
raise InvalidAssociation.new('Invalid association target: mixed root and non-root viewmodels')
|
25
71
|
end
|
26
|
-
end
|
27
72
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
# express the ownership of the indirect target, but this direct
|
32
|
-
# association to the intermediate is in fact owned. This ownership
|
33
|
-
# property isn't directly used anywhere: the synthetic intermediate
|
34
|
-
# viewmodel is only used in the deserialization update operations, which
|
35
|
-
# directly understands the semantics of through associations.
|
36
|
-
raise ArgumentError.new("Through associations must be to a shared target") unless @shared
|
37
|
-
raise ArgumentError.new("Through associations must be `has_many`") unless direct_reflection.macro == :has_many
|
38
|
-
end
|
39
|
-
end
|
73
|
+
if external? && !@referenced
|
74
|
+
raise InvalidAssociation.new('External associations must be to root viewmodels')
|
75
|
+
end
|
40
76
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
77
|
+
if through?
|
78
|
+
unless @referenced
|
79
|
+
raise InvalidAssociation.new('Through associations must be to root viewmodels')
|
80
|
+
end
|
81
|
+
|
82
|
+
@direct_viewmodel = build_direct_viewmodel(@direct_reflection, @indirect_reflection,
|
83
|
+
@viewmodel_classes, @through_order_attr)
|
84
|
+
end
|
85
|
+
|
86
|
+
@initialized = true
|
47
87
|
end
|
48
88
|
end
|
49
89
|
|
50
|
-
def
|
51
|
-
|
90
|
+
def association?
|
91
|
+
true
|
52
92
|
end
|
53
93
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
@viewmodel_classes ||=
|
58
|
-
begin
|
59
|
-
model_class = target_reflection.klass
|
60
|
-
if model_class.nil?
|
61
|
-
raise "Couldn't derive target class for association '#{target_reflection.name}"
|
62
|
-
end
|
63
|
-
inferred_view_name = ViewModel::Registry.default_view_name(model_class.name)
|
64
|
-
viewmodel_class = ViewModel::Registry.for_view_name(inferred_view_name) # TODO: improve error message to show it's looking for default name
|
65
|
-
[viewmodel_class]
|
66
|
-
end
|
94
|
+
def referenced?
|
95
|
+
lazy_initialize! unless @initialized
|
96
|
+
@referenced
|
67
97
|
end
|
68
98
|
|
69
|
-
|
70
|
-
|
71
|
-
h[vm.model_class] = vm
|
72
|
-
end
|
99
|
+
def nested?
|
100
|
+
!referenced?
|
73
101
|
end
|
74
102
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
vm.view_aliases.each do |view_alias|
|
79
|
-
h[view_alias] = vm
|
80
|
-
end
|
81
|
-
end
|
103
|
+
def owned?
|
104
|
+
lazy_initialize! unless @initialized
|
105
|
+
@owned
|
82
106
|
end
|
83
107
|
|
84
108
|
def shared?
|
85
|
-
|
109
|
+
!owned?
|
110
|
+
end
|
111
|
+
|
112
|
+
def external?
|
113
|
+
@external
|
114
|
+
end
|
115
|
+
|
116
|
+
def read_only?
|
117
|
+
@read_only
|
118
|
+
end
|
119
|
+
|
120
|
+
# reflection for the target of this association: indirect if through, direct otherwise
|
121
|
+
def target_reflection
|
122
|
+
if through?
|
123
|
+
indirect_reflection
|
124
|
+
else
|
125
|
+
direct_reflection
|
126
|
+
end
|
86
127
|
end
|
87
128
|
|
88
|
-
def
|
89
|
-
|
129
|
+
def polymorphic?
|
130
|
+
target_reflection.polymorphic?
|
90
131
|
end
|
91
132
|
|
92
|
-
|
133
|
+
# The side of the immediate association that holds the pointer.
|
134
|
+
def pointer_location
|
93
135
|
case direct_reflection.macro
|
94
136
|
when :belongs_to
|
95
137
|
:local
|
@@ -98,6 +140,24 @@ class ViewModel::ActiveRecord::AssociationData
|
|
98
140
|
end
|
99
141
|
end
|
100
142
|
|
143
|
+
def indirect_reflection
|
144
|
+
lazy_initialize! unless @initialized
|
145
|
+
@indirect_reflection
|
146
|
+
end
|
147
|
+
|
148
|
+
def direct_reflection_inverse(foreign_class = nil)
|
149
|
+
if direct_reflection.polymorphic?
|
150
|
+
direct_reflection.polymorphic_inverse_of(foreign_class)
|
151
|
+
else
|
152
|
+
direct_reflection.inverse_of
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def viewmodel_classes
|
157
|
+
lazy_initialize! unless @initialized
|
158
|
+
@viewmodel_classes
|
159
|
+
end
|
160
|
+
|
101
161
|
def viewmodel_class_for_model(model_class)
|
102
162
|
model_to_viewmodel[model_class]
|
103
163
|
end
|
@@ -132,47 +192,101 @@ class ViewModel::ActiveRecord::AssociationData
|
|
132
192
|
unless viewmodel_classes.size == 1
|
133
193
|
raise ArgumentError.new("More than one possible class for association '#{target_reflection.name}'")
|
134
194
|
end
|
195
|
+
|
135
196
|
viewmodel_classes.first
|
136
197
|
end
|
137
198
|
|
138
199
|
def through?
|
139
|
-
@
|
200
|
+
@indirect_association_name.present?
|
140
201
|
end
|
141
202
|
|
142
203
|
def direct_viewmodel
|
143
|
-
|
144
|
-
|
204
|
+
raise ArgumentError.new('not a through association') unless through?
|
205
|
+
lazy_initialize! unless @initialized
|
206
|
+
@direct_viewmodel
|
207
|
+
end
|
208
|
+
|
209
|
+
def collection?
|
210
|
+
through? || direct_reflection.collection?
|
211
|
+
end
|
145
212
|
|
146
|
-
|
213
|
+
def indirect_association_data
|
214
|
+
direct_viewmodel._association_data(indirect_reflection.name)
|
215
|
+
end
|
147
216
|
|
148
|
-
|
217
|
+
private
|
149
218
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
219
|
+
# Through associations must always be to a root viewmodel, via an owned
|
220
|
+
# has_many association to an intermediate model. A synthetic viewmodel is
|
221
|
+
# created to represent this intermediate, but is used only internally by the
|
222
|
+
# deserialization update operations, which directly understands the semantics
|
223
|
+
# of through associations.
|
224
|
+
def load_indirect_reflection(intermediate_model, indirect_association_name)
|
225
|
+
indirect_reflection =
|
226
|
+
intermediate_model.reflect_on_association(ActiveSupport::Inflector.singularize(indirect_association_name))
|
155
227
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
228
|
+
if indirect_reflection.nil?
|
229
|
+
raise InvalidAssociation.new(
|
230
|
+
"Indirect association '#{@indirect_association_name}' not found in "\
|
231
|
+
"intermediate model '#{intermediate_model.name}'")
|
232
|
+
end
|
233
|
+
|
234
|
+
unless direct_reflection.macro == :has_many
|
235
|
+
raise InvalidAssociation.new('Through associations must be `has_many`')
|
163
236
|
end
|
237
|
+
|
238
|
+
indirect_reflection
|
164
239
|
end
|
165
240
|
|
166
|
-
def indirect_reflection
|
167
|
-
|
168
|
-
|
241
|
+
def build_direct_viewmodel(direct_reflection, indirect_reflection, viewmodel_classes, through_order_attr)
|
242
|
+
# Join table viewmodel class. For A has_many B through T; where this association is defined on A
|
243
|
+
# direct_reflection = A -> T
|
244
|
+
# indirect_reflection = T -> B
|
245
|
+
|
246
|
+
Class.new(ViewModel::ActiveRecord) do
|
247
|
+
self.synthetic = true
|
248
|
+
self.model_class = direct_reflection.klass
|
249
|
+
self.view_name = direct_reflection.klass.name
|
250
|
+
association indirect_reflection.name, viewmodels: viewmodel_classes
|
251
|
+
acts_as_list through_order_attr if through_order_attr
|
252
|
+
end
|
169
253
|
end
|
170
254
|
|
171
|
-
def
|
172
|
-
|
255
|
+
def resolve_viewmodel_class(v)
|
256
|
+
case v
|
257
|
+
when String, Symbol
|
258
|
+
ViewModel::Registry.for_view_name(v.to_s)
|
259
|
+
when Class
|
260
|
+
v
|
261
|
+
else
|
262
|
+
raise InvalidAssociation.new("Invalid viewmodel class: #{v.inspect}")
|
263
|
+
end
|
173
264
|
end
|
174
265
|
|
175
|
-
def
|
176
|
-
|
266
|
+
def infer_viewmodel_class(model_class)
|
267
|
+
# If we weren't given explicit viewmodel classes, try to work out from the
|
268
|
+
# names. This should work unless the association is polymorphic.
|
269
|
+
if model_class.nil?
|
270
|
+
raise InvalidAssociation.new("Couldn't derive target class for model association '#{target_reflection.name}'")
|
271
|
+
end
|
272
|
+
|
273
|
+
inferred_view_name = ViewModel::Registry.default_view_name(model_class.name)
|
274
|
+
viewmodel_class = ViewModel::Registry.for_view_name(inferred_view_name) # TODO: improve error message to show it's looking for default name
|
275
|
+
[viewmodel_class]
|
276
|
+
end
|
277
|
+
|
278
|
+
def model_to_viewmodel
|
279
|
+
@model_to_viewmodel ||= viewmodel_classes.each_with_object({}) do |vm, h|
|
280
|
+
h[vm.model_class] = vm
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def name_to_viewmodel
|
285
|
+
@name_to_viewmodel ||= viewmodel_classes.each_with_object({}) do |vm, h|
|
286
|
+
h[vm.view_name] = vm
|
287
|
+
vm.view_aliases.each do |view_alias|
|
288
|
+
h[view_alias] = vm
|
289
|
+
end
|
290
|
+
end
|
177
291
|
end
|
178
292
|
end
|
@@ -52,9 +52,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
52
52
|
def replace_associated(association_name, update_hash, references: {}, deserialize_context: self.class.new_deserialize_context)
|
53
53
|
association_data = self.class._association_data(association_name)
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
if association_data.through? || association_data.shared?
|
55
|
+
if association_data.referenced?
|
58
56
|
is_fupdate =
|
59
57
|
association_data.collection? &&
|
60
58
|
update_hash.is_a?(Hash) &&
|
@@ -125,10 +123,6 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
125
123
|
|
126
124
|
update_context = ViewModel::ActiveRecord::UpdateContext.build!(root_update_data, referenced_update_data, root_type: direct_viewmodel_class)
|
127
125
|
|
128
|
-
# Provide information about what was updated
|
129
|
-
deserialize_context.updated_associations = root_update_data.map(&:updated_associations)
|
130
|
-
.inject({}) { |acc, assocs| acc.deep_merge(assocs) }
|
131
|
-
|
132
126
|
# Set new parent
|
133
127
|
new_parent = ViewModel::ActiveRecord::UpdateOperation::ParentData.new(direct_reflection.inverse_of, self)
|
134
128
|
update_context.root_updates.each { |update| update.reparent_to = new_parent }
|
@@ -177,15 +171,26 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
177
171
|
child_context = self.context_for_child(association_name, context: deserialize_context)
|
178
172
|
updated_viewmodels = update_context.run!(deserialize_context: child_context)
|
179
173
|
|
174
|
+
# Propagate changes and finalize the parent
|
175
|
+
updated_viewmodels.each do |child|
|
176
|
+
child_changes = child.previous_changes
|
177
|
+
|
178
|
+
if association_data.nested?
|
179
|
+
nested_children_changed! if child_changes.changed_nested_tree?
|
180
|
+
referenced_children_changed! if child_changes.changed_referenced_children?
|
181
|
+
elsif association_data.owned?
|
182
|
+
referenced_children_changed! if child_changes.changed_owned_tree?
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
final_changes = self.clear_changes!
|
187
|
+
|
180
188
|
if association_data.through?
|
181
189
|
updated_viewmodels.map! do |direct_vm|
|
182
190
|
direct_vm._read_association(association_data.indirect_reflection.name)
|
183
191
|
end
|
184
192
|
end
|
185
193
|
|
186
|
-
# Finalize the parent
|
187
|
-
final_changes = self.clear_changes!
|
188
|
-
|
189
194
|
# Could happen if hooks attempted to change the parent, which aren't
|
190
195
|
# valid since we're only editing children here.
|
191
196
|
unless final_changes.contained_to?(associations: [association_name.to_s])
|
@@ -269,7 +274,12 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
269
274
|
association.delete(child_vm.model)
|
270
275
|
end
|
271
276
|
|
272
|
-
|
277
|
+
if association_data.nested?
|
278
|
+
nested_children_changed!
|
279
|
+
elsif association_data.owned?
|
280
|
+
referenced_children_changed!
|
281
|
+
end
|
282
|
+
|
273
283
|
final_changes = self.clear_changes!
|
274
284
|
|
275
285
|
unless final_changes.contained_to?(associations: [association_name.to_s])
|
@@ -286,7 +296,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
|
|
286
296
|
|
287
297
|
private
|
288
298
|
|
289
|
-
def construct_direct_append_updates(
|
299
|
+
def construct_direct_append_updates(_association_data, subtree_hashes, references)
|
290
300
|
ViewModel::ActiveRecord::UpdateData.parse_hashes(subtree_hashes, references)
|
291
301
|
end
|
292
302
|
|