mongoid 9.1.0 → 9.1.1
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/lib/config/locales/en.yml +49 -0
- data/lib/mongoid/association/accessors.rb +9 -1
- data/lib/mongoid/association/depending.rb +14 -10
- data/lib/mongoid/association/eager.rb +9 -0
- data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
- data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
- data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
- data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
- data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
- data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
- data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
- data/lib/mongoid/association/eager_loadable.rb +57 -100
- data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
- data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
- data/lib/mongoid/association/macros.rb +13 -1
- data/lib/mongoid/association/nested/many.rb +34 -5
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/nested/one.rb +6 -1
- data/lib/mongoid/association/options.rb +17 -0
- data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
- data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
- data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
- data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
- data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
- data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
- data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
- data/lib/mongoid/association/referenced/has_one.rb +1 -0
- data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
- data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
- data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
- data/lib/mongoid/association/referenced.rb +2 -0
- data/lib/mongoid/association/relatable.rb +40 -0
- data/lib/mongoid/association.rb +7 -0
- data/lib/mongoid/clients/factory.rb +7 -13
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +0 -1
- data/lib/mongoid/config/encryption.rb +36 -19
- data/lib/mongoid/config.rb +56 -5
- data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/includable.rb +35 -11
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
- data/lib/mongoid/deprecable.rb +45 -2
- data/lib/mongoid/encryptable.rb +46 -0
- data/lib/mongoid/errors/config_redactor.rb +43 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
- data/lib/mongoid/errors/no_client_database.rb +1 -1
- data/lib/mongoid/errors/no_client_hosts.rb +1 -1
- data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
- data/lib/mongoid/errors/readonly_association.rb +29 -0
- data/lib/mongoid/errors.rb +4 -0
- data/lib/mongoid/extensions/vector.rb +64 -0
- data/lib/mongoid/extensions.rb +1 -0
- data/lib/mongoid/field_readable.rb +69 -0
- data/lib/mongoid/indexable.rb +38 -26
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
- data/lib/mongoid/matcher/regex.rb +11 -12
- data/lib/mongoid/matcher/regexp_budget.rb +372 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistable/creatable.rb +1 -1
- data/lib/mongoid/persistable/updatable.rb +9 -11
- data/lib/mongoid/persistence_context.rb +35 -0
- data/lib/mongoid/search_indexable.rb +64 -23
- data/lib/mongoid/serializable.rb +12 -4
- data/lib/mongoid/tasks/database.rb +23 -9
- data/lib/mongoid/threaded.rb +36 -0
- data/lib/mongoid/timestamps/timeless.rb +84 -10
- data/lib/mongoid/traversable.rb +2 -2
- data/lib/mongoid/validatable/associated.rb +18 -2
- data/lib/mongoid/version.rb +1 -1
- data/spec/integration/app_spec.rb +7 -1
- data/spec/integration/associations/belongs_to_spec.rb +13 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +151 -0
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
- data/spec/mongoid/association/eager_spec.rb +2 -2
- data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
- data/spec/mongoid/association/fallback_spec.rb +425 -0
- data/spec/mongoid/association/macros_spec.rb +26 -0
- data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
- data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
- data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
- data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
- data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
- data/spec/mongoid/attributes/nested_spec.rb +249 -8
- data/spec/mongoid/config/defaults_spec.rb +33 -0
- data/spec/mongoid/config/encryption_spec.rb +228 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/includable_spec.rb +875 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
- data/spec/mongoid/criteria_spec.rb +3 -0
- data/spec/mongoid/encryptable_spec.rb +61 -0
- data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
- data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
- data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
- data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
- data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
- data/spec/mongoid/extensions/vector_spec.rb +175 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/search_indexable_spec.rb +295 -9
- data/spec/mongoid/tasks/database_spec.rb +18 -0
- data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
- data/spec/mongoid/touchable_spec.rb +25 -0
- data/spec/mongoid/touchable_spec_models.rb +10 -0
- data/spec/mongoid/validatable/associated_spec.rb +19 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/crypt/models.rb +157 -0
- metadata +52 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Mongoid::Association::Referenced::HasOneThrough::Eager do
|
|
6
|
+
before(:all) do
|
|
7
|
+
Object.const_set(:EgCustomer, Class.new do
|
|
8
|
+
include Mongoid::Document
|
|
9
|
+
|
|
10
|
+
store_in collection: 'eg_customers'
|
|
11
|
+
has_one :eg_franchise, class_name: 'EgFranchise', inverse_of: :eg_customer
|
|
12
|
+
has_one :eg_store, through: :eg_franchise, class_name: 'EgStore'
|
|
13
|
+
end)
|
|
14
|
+
Object.const_set(:EgFranchise, Class.new do
|
|
15
|
+
include Mongoid::Document
|
|
16
|
+
|
|
17
|
+
store_in collection: 'eg_franchises'
|
|
18
|
+
belongs_to :eg_customer, class_name: 'EgCustomer'
|
|
19
|
+
has_one :eg_store, class_name: 'EgStore', inverse_of: :eg_franchise
|
|
20
|
+
end)
|
|
21
|
+
Object.const_set(:EgStore, Class.new do
|
|
22
|
+
include Mongoid::Document
|
|
23
|
+
|
|
24
|
+
store_in collection: 'eg_stores'
|
|
25
|
+
belongs_to :eg_franchise, class_name: 'EgFranchise'
|
|
26
|
+
end)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
after(:all) do
|
|
30
|
+
%w[EgCustomer EgFranchise EgStore].each { |c| Object.send(:remove_const, c) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
before { [ EgCustomer, EgFranchise, EgStore ].each(&:delete_all) }
|
|
34
|
+
|
|
35
|
+
it 'preloads the through association for multiple owners' do
|
|
36
|
+
c1 = EgCustomer.create!
|
|
37
|
+
c2 = EgCustomer.create!
|
|
38
|
+
f1 = EgFranchise.create!(eg_customer: c1)
|
|
39
|
+
f2 = EgFranchise.create!(eg_customer: c2)
|
|
40
|
+
s1 = EgStore.create!(eg_franchise: f1)
|
|
41
|
+
s2 = EgStore.create!(eg_franchise: f2)
|
|
42
|
+
|
|
43
|
+
customers = EgCustomer.includes(:eg_store).to_a
|
|
44
|
+
by_id = customers.index_by(&:id)
|
|
45
|
+
|
|
46
|
+
expect(by_id[c1.id].eg_store).to eq(s1)
|
|
47
|
+
expect(by_id[c2.id].eg_store).to eq(s2)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'sets nil on owners without a store' do
|
|
51
|
+
lone = EgCustomer.create!
|
|
52
|
+
EgFranchise.create!(eg_customer: lone) # franchise but no store
|
|
53
|
+
|
|
54
|
+
customers = EgCustomer.includes(:eg_store).to_a
|
|
55
|
+
loaded = customers.find { |c| c.id == lone.id }
|
|
56
|
+
expect(loaded.eg_store).to be_nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Mongoid::Association::Referenced::HasOneThrough::Proxy do
|
|
6
|
+
before(:all) do
|
|
7
|
+
Object.const_set(:PxHotStore, Class.new { include Mongoid::Document })
|
|
8
|
+
Object.const_set(:PxHotFranchise, Class.new do
|
|
9
|
+
include Mongoid::Document
|
|
10
|
+
|
|
11
|
+
has_one :px_hot_store, class_name: 'PxHotStore', inverse_of: :px_hot_franchise
|
|
12
|
+
end)
|
|
13
|
+
Object.const_set(:PxHotCustomer, Class.new do
|
|
14
|
+
include Mongoid::Document
|
|
15
|
+
|
|
16
|
+
has_one :px_hot_franchise, class_name: 'PxHotFranchise',
|
|
17
|
+
inverse_of: :px_hot_customer
|
|
18
|
+
has_one :px_hot_store, through: :px_hot_franchise, class_name: 'PxHotStore',
|
|
19
|
+
source: :px_hot_store
|
|
20
|
+
end)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
after(:all) do
|
|
24
|
+
%w[PxHotCustomer PxHotFranchise PxHotStore].each { |c| Object.send(:remove_const, c) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
let(:customer) { PxHotCustomer.new }
|
|
28
|
+
|
|
29
|
+
describe 'setter' do
|
|
30
|
+
it 'raises ReadonlyAssociation' do
|
|
31
|
+
expect { customer.px_hot_store = PxHotStore.new }.to \
|
|
32
|
+
raise_error(Mongoid::Errors::ReadonlyAssociation)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '.eager_loader' do
|
|
37
|
+
it 'returns a HasOneThrough::Eager' do
|
|
38
|
+
assoc = PxHotCustomer.relations['px_hot_store']
|
|
39
|
+
result = described_class.eager_loader([ assoc ], [])
|
|
40
|
+
expect(result).to be_a(Mongoid::Association::Referenced::HasOneThrough::Eager)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '.embedded?' do
|
|
45
|
+
it 'returns false' do
|
|
46
|
+
expect(described_class.embedded?).to be false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Mongoid::Association::Referenced::HasOneThrough do
|
|
6
|
+
# Model setup:
|
|
7
|
+
# HotCustomer has_one :hot_franchise
|
|
8
|
+
# HotCustomer has_one :hot_store, through: :hot_franchise, source: :depot
|
|
9
|
+
# HotFranchise has_one :depot (class HotStore)
|
|
10
|
+
|
|
11
|
+
before(:all) do
|
|
12
|
+
Object.const_set(:HotStore, Class.new do
|
|
13
|
+
include Mongoid::Document
|
|
14
|
+
|
|
15
|
+
field :hot_franchise_id, type: BSON::ObjectId
|
|
16
|
+
end)
|
|
17
|
+
Object.const_set(:HotFranchise, Class.new do
|
|
18
|
+
include Mongoid::Document
|
|
19
|
+
|
|
20
|
+
field :hot_customer_id, type: BSON::ObjectId
|
|
21
|
+
has_one :depot, class_name: 'HotStore', inverse_of: :hot_franchise
|
|
22
|
+
end)
|
|
23
|
+
Object.const_set(:HotCustomer, Class.new do
|
|
24
|
+
include Mongoid::Document
|
|
25
|
+
|
|
26
|
+
has_one :hot_franchise, class_name: 'HotFranchise', inverse_of: :hot_customer
|
|
27
|
+
has_one :hot_store, through: :hot_franchise,
|
|
28
|
+
class_name: 'HotStore', source: :depot
|
|
29
|
+
end)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
after(:all) do
|
|
33
|
+
%w[HotCustomer HotFranchise HotStore].each { |c| Object.send(:remove_const, c) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
let(:assoc) { HotCustomer.relations['hot_store'] }
|
|
37
|
+
|
|
38
|
+
describe '#through_association' do
|
|
39
|
+
it 'returns the intermediate association metadata' do
|
|
40
|
+
expect(assoc.through_association).to eq(HotCustomer.relations['hot_franchise'])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#source_association' do
|
|
45
|
+
it 'returns the :depot association on HotFranchise' do
|
|
46
|
+
expect(assoc.source_association).to eq(HotFranchise.relations['depot'])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#embedded?' do
|
|
51
|
+
it 'returns false' do
|
|
52
|
+
expect(assoc.embedded?).to be false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe 'VALID_OPTIONS' do
|
|
57
|
+
it 'accepts :through' do
|
|
58
|
+
expect { HotCustomer.has_one(:foo, through: :hot_franchise) }.not_to raise_error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'accepts :source' do
|
|
62
|
+
expect { HotCustomer.has_one(:bar, through: :hot_franchise, source: :depot) }.not_to raise_error
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'rejects unknown options at definition time' do
|
|
66
|
+
expect do
|
|
67
|
+
HotCustomer.has_one(:baz, through: :hot_franchise, bogus_option: true)
|
|
68
|
+
end.to raise_error(Mongoid::Errors::InvalidRelationOption)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe '#criteria' do
|
|
73
|
+
it 'returns nil when the intermediate is nil' do
|
|
74
|
+
customer = HotCustomer.new
|
|
75
|
+
allow(customer).to receive(:hot_franchise).and_return(nil)
|
|
76
|
+
expect(assoc.criteria(customer)).to be_nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'delegates to the source association on the intermediate' do
|
|
80
|
+
franchise = HotFranchise.new
|
|
81
|
+
store = HotStore.new
|
|
82
|
+
customer = HotCustomer.new
|
|
83
|
+
allow(customer).to receive(:hot_franchise).and_return(franchise)
|
|
84
|
+
allow(franchise).to receive(:depot).and_return(store)
|
|
85
|
+
expect(assoc.criteria(customer)).to eq(store)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe ':through on an embedded association' do
|
|
90
|
+
it 'raises InvalidRelationOption when through association is embedded' do
|
|
91
|
+
embedded_owner = Class.new do
|
|
92
|
+
include Mongoid::Document
|
|
93
|
+
|
|
94
|
+
embeds_one :address
|
|
95
|
+
end
|
|
96
|
+
embedded_owner.has_one(:city, through: :address)
|
|
97
|
+
expect do
|
|
98
|
+
embedded_owner.relations['city'].through_association # triggers lazy validation
|
|
99
|
+
end.to raise_error(Mongoid::Errors::InvalidRelationOption)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
context 'integration', :integration do
|
|
104
|
+
before(:all) do
|
|
105
|
+
Object.const_set(:IntCustomer, Class.new do
|
|
106
|
+
include Mongoid::Document
|
|
107
|
+
|
|
108
|
+
store_in collection: 'int_customers'
|
|
109
|
+
has_one :int_franchise, class_name: 'IntFranchise', inverse_of: :int_customer
|
|
110
|
+
# source: :depot exercises the :source option end-to-end
|
|
111
|
+
has_one :int_store, through: :int_franchise, class_name: 'IntStore', source: :depot
|
|
112
|
+
end)
|
|
113
|
+
|
|
114
|
+
Object.const_set(:IntFranchise, Class.new do
|
|
115
|
+
include Mongoid::Document
|
|
116
|
+
|
|
117
|
+
store_in collection: 'int_franchises'
|
|
118
|
+
field :int_customer_id, type: BSON::ObjectId
|
|
119
|
+
belongs_to :int_customer, class_name: 'IntCustomer'
|
|
120
|
+
has_one :depot, class_name: 'IntStore', inverse_of: :int_franchise
|
|
121
|
+
end)
|
|
122
|
+
|
|
123
|
+
Object.const_set(:IntStore, Class.new do
|
|
124
|
+
include Mongoid::Document
|
|
125
|
+
|
|
126
|
+
store_in collection: 'int_stores'
|
|
127
|
+
field :int_franchise_id, type: BSON::ObjectId
|
|
128
|
+
belongs_to :int_franchise, class_name: 'IntFranchise'
|
|
129
|
+
end)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
after(:all) do
|
|
133
|
+
%w[IntCustomer IntFranchise IntStore].each { |c| Object.send(:remove_const, c) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
before { [ IntCustomer, IntFranchise, IntStore ].each(&:delete_all) }
|
|
137
|
+
|
|
138
|
+
let!(:customer) { IntCustomer.create! }
|
|
139
|
+
let!(:franchise) { IntFranchise.create!(int_customer: customer) }
|
|
140
|
+
let!(:store) { IntStore.create!(int_franchise: franchise) }
|
|
141
|
+
|
|
142
|
+
describe 'getter' do
|
|
143
|
+
it 'returns the store via the franchise' do
|
|
144
|
+
expect(customer.int_store).to eq(store)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'returns nil when the franchise is absent' do
|
|
148
|
+
lone = IntCustomer.create!
|
|
149
|
+
expect(lone.int_store).to be_nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'returns cached value before reload and fresh value after' do
|
|
153
|
+
first_store = customer.int_store # prime cache
|
|
154
|
+
expect(first_store).to eq(store)
|
|
155
|
+
|
|
156
|
+
new_store = IntStore.create!
|
|
157
|
+
franchise.depot = new_store
|
|
158
|
+
franchise.save!
|
|
159
|
+
|
|
160
|
+
expect(customer.int_store).to eq(store) # cached — still old
|
|
161
|
+
expect(customer.int_store(true)).to eq(new_store) # reloaded
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe 'setter' do
|
|
166
|
+
it 'raises ReadonlyAssociation' do
|
|
167
|
+
expect { customer.int_store = IntStore.new }.to \
|
|
168
|
+
raise_error(Mongoid::Errors::ReadonlyAssociation)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
context 'with :source option resolving a differently-named association', :integration do
|
|
174
|
+
before(:all) do
|
|
175
|
+
Object.const_set(:SrcOrg, Class.new do
|
|
176
|
+
include Mongoid::Document
|
|
177
|
+
|
|
178
|
+
store_in collection: 'src_orgs'
|
|
179
|
+
has_one :src_location, class_name: 'SrcLocation', inverse_of: :src_org
|
|
180
|
+
has_one :src_hq, through: :src_location,
|
|
181
|
+
class_name: 'SrcBuilding', source: :src_main_building
|
|
182
|
+
end)
|
|
183
|
+
Object.const_set(:SrcLocation, Class.new do
|
|
184
|
+
include Mongoid::Document
|
|
185
|
+
|
|
186
|
+
store_in collection: 'src_locations'
|
|
187
|
+
belongs_to :src_org, class_name: 'SrcOrg'
|
|
188
|
+
has_one :src_main_building, class_name: 'SrcBuilding', inverse_of: :src_location
|
|
189
|
+
end)
|
|
190
|
+
Object.const_set(:SrcBuilding, Class.new do
|
|
191
|
+
include Mongoid::Document
|
|
192
|
+
|
|
193
|
+
store_in collection: 'src_buildings'
|
|
194
|
+
belongs_to :src_location, class_name: 'SrcLocation'
|
|
195
|
+
end)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
after(:all) do
|
|
199
|
+
%w[SrcOrg SrcLocation SrcBuilding].each { |c| Object.send(:remove_const, c) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
before { [ SrcOrg, SrcLocation, SrcBuilding ].each(&:delete_all) }
|
|
203
|
+
|
|
204
|
+
it 'resolves the :source association on the intermediate' do
|
|
205
|
+
org = SrcOrg.create!
|
|
206
|
+
location = SrcLocation.create!(src_org: org)
|
|
207
|
+
building = SrcBuilding.create!(src_location: location)
|
|
208
|
+
expect(org.src_hq).to eq(building)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -7,6 +7,33 @@ require_relative '../association/referenced/has_and_belongs_to_many_models'
|
|
|
7
7
|
require_relative 'nested_spec_models'
|
|
8
8
|
|
|
9
9
|
describe Mongoid::Attributes::Nested do
|
|
10
|
+
# Expects the enclosing context to define `build_with_id`, taking an id
|
|
11
|
+
# value and returning the document built with it in nested attributes.
|
|
12
|
+
shared_examples 'rejects non-scalar ids' do |klass_name|
|
|
13
|
+
not_found = /not found for class #{klass_name}/
|
|
14
|
+
|
|
15
|
+
context 'when the id is an operator hash' do
|
|
16
|
+
it 'raises a document not found error' do
|
|
17
|
+
expect { build_with_id({ '$ne' => nil }) }
|
|
18
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context 'when the id is an array' do
|
|
23
|
+
it 'raises a document not found error' do
|
|
24
|
+
expect { build_with_id([ BSON::ObjectId.new ]) }
|
|
25
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
context 'when the id is a malformed object id hash' do
|
|
30
|
+
it 'raises a document not found error' do
|
|
31
|
+
expect { build_with_id({ '$oid' => 'junk' }) }
|
|
32
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
10
37
|
describe '.accepts_nested_attributes_for' do
|
|
11
38
|
context 'when the autosave option is not defined' do
|
|
12
39
|
let(:person) do
|
|
@@ -188,6 +215,77 @@ describe Mongoid::Attributes::Nested do
|
|
|
188
215
|
it 'sets the nested attributes' do
|
|
189
216
|
expect(person2.posts.map(&:title)).to eq([ 'Reparented!' ])
|
|
190
217
|
end
|
|
218
|
+
|
|
219
|
+
context 'when the id does not correspond to an existing document' do
|
|
220
|
+
let(:person2) do
|
|
221
|
+
Person.create!(posts_attributes: { '0' => { id: BSON::ObjectId.new, title: 'Ghost' } })
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
context 'when raise_not_found_error is true' do
|
|
225
|
+
config_override :raise_not_found_error, true
|
|
226
|
+
|
|
227
|
+
it 'raises a document not found error' do
|
|
228
|
+
expect { person2 }.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
229
|
+
/Document\(s\) not found for class Post/)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
context 'when raise_not_found_error is false' do
|
|
234
|
+
config_override :raise_not_found_error, false
|
|
235
|
+
|
|
236
|
+
it 'raises a document not found error' do
|
|
237
|
+
expect { person2 }.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
238
|
+
/Document\(s\) not found for class Post/)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
context 'when _destroy is true for a document not in the relation' do
|
|
245
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
246
|
+
|
|
247
|
+
before do
|
|
248
|
+
Person.send(:undef_method, :posts_attributes=)
|
|
249
|
+
Person.accepts_nested_attributes_for :posts, allow_destroy: true
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
let(:person3) do
|
|
253
|
+
Person.create!(
|
|
254
|
+
posts_attributes: { '0' => { id: post.id, _destroy: '1' } }
|
|
255
|
+
)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'does not raise UnknownAttribute' do
|
|
259
|
+
expect { person3 }.not_to raise_error
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'does not add the document to the relation' do
|
|
263
|
+
expect(person3.posts).to be_empty
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
context 'when the id in the attributes is not a scalar' do
|
|
269
|
+
let(:person) { Person.create! }
|
|
270
|
+
|
|
271
|
+
def build_with_id(id)
|
|
272
|
+
person.update!(posts_attributes: { '0' => { id: id, title: 'Crafted' } })
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
context 'when the association is empty' do
|
|
276
|
+
include_examples 'rejects non-scalar ids', 'Post'
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
context 'when the association is not empty' do
|
|
280
|
+
before { person.posts.create!(title: 'Existing') }
|
|
281
|
+
|
|
282
|
+
include_examples 'rejects non-scalar ids', 'Post'
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
286
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
287
|
+
|
|
288
|
+
include_examples 'rejects non-scalar ids', 'Post'
|
|
191
289
|
end
|
|
192
290
|
end
|
|
193
291
|
end
|
|
@@ -214,16 +312,100 @@ describe Mongoid::Attributes::Nested do
|
|
|
214
312
|
)
|
|
215
313
|
end
|
|
216
314
|
|
|
217
|
-
|
|
218
|
-
|
|
315
|
+
context 'when allow_reparenting_via_nested_attributes is false' do
|
|
316
|
+
config_override :allow_reparenting_via_nested_attributes, false
|
|
317
|
+
|
|
318
|
+
it 'raises a document not found error' do
|
|
319
|
+
expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
320
|
+
/Document\(s\) not found for class Preference/)
|
|
321
|
+
end
|
|
219
322
|
end
|
|
220
323
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
324
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
325
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
326
|
+
|
|
327
|
+
it 'sets the nested attributes' do
|
|
328
|
+
expect(person.preferences.map(&:name)).to eq([ preference.name ])
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it 'updates attributes of existing document which is added to relation' do
|
|
332
|
+
preference_name = 'updated preference'
|
|
333
|
+
person = Person.new(
|
|
334
|
+
preferences_attributes: { 0 => { id: preference.id, name: preference_name } }
|
|
335
|
+
)
|
|
336
|
+
expect(person.preferences.map(&:name)).to eq([ preference_name ])
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
context 'when the id does not correspond to an existing document' do
|
|
340
|
+
let(:person) do
|
|
341
|
+
Person.new(
|
|
342
|
+
preferences_attributes: { 0 => { id: BSON::ObjectId.new, name: 'Ghost' } }
|
|
343
|
+
)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
context 'when raise_not_found_error is true' do
|
|
347
|
+
config_override :raise_not_found_error, true
|
|
348
|
+
|
|
349
|
+
it 'raises a document not found error' do
|
|
350
|
+
expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
351
|
+
/Document\(s\) not found for class Preference/)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
context 'when raise_not_found_error is false' do
|
|
356
|
+
config_override :raise_not_found_error, false
|
|
357
|
+
|
|
358
|
+
it 'raises a document not found error' do
|
|
359
|
+
expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
360
|
+
/Document\(s\) not found for class Preference/)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
context 'when _destroy is true for a document not in the relation' do
|
|
367
|
+
before do
|
|
368
|
+
Person.send(:undef_method, :preferences_attributes=)
|
|
369
|
+
Person.accepts_nested_attributes_for :preferences, allow_destroy: true
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
let(:person) do
|
|
373
|
+
Person.new(
|
|
374
|
+
preferences_attributes: { 0 => { id: preference.id, _destroy: '1' } }
|
|
375
|
+
)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
it 'does not raise UnknownAttribute' do
|
|
379
|
+
expect { person }.not_to raise_error
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
it 'does not add the document to the relation' do
|
|
383
|
+
expect(person.preferences).to be_empty
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
context 'when the id in the attributes is not a scalar' do
|
|
389
|
+
let(:target) { Person.create! }
|
|
390
|
+
|
|
391
|
+
def build_with_id(id)
|
|
392
|
+
target.update!(preferences_attributes: { 0 => { id: id, name: 'Crafted' } })
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
context 'when the association is empty' do
|
|
396
|
+
include_examples 'rejects non-scalar ids', 'Preference'
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
context 'when the association is not empty' do
|
|
400
|
+
before { target.preferences.create!(name: 'Existing') }
|
|
401
|
+
|
|
402
|
+
include_examples 'rejects non-scalar ids', 'Preference'
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
406
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
407
|
+
|
|
408
|
+
include_examples 'rejects non-scalar ids', 'Preference'
|
|
227
409
|
end
|
|
228
410
|
end
|
|
229
411
|
end
|
|
@@ -4552,4 +4734,63 @@ describe Mongoid::Attributes::Nested do
|
|
|
4552
4734
|
end
|
|
4553
4735
|
end
|
|
4554
4736
|
end
|
|
4737
|
+
|
|
4738
|
+
context 'when an id in nested attributes belongs to another parent' do
|
|
4739
|
+
let(:victim) { Person.create! }
|
|
4740
|
+
let(:attacker) { Person.create! }
|
|
4741
|
+
|
|
4742
|
+
context 'when the association is a has-many' do
|
|
4743
|
+
before do
|
|
4744
|
+
Person.send(:undef_method, :posts_attributes=)
|
|
4745
|
+
Person.accepts_nested_attributes_for :posts
|
|
4746
|
+
end
|
|
4747
|
+
|
|
4748
|
+
let!(:victim_post) { victim.posts.create!(title: 'Private') }
|
|
4749
|
+
|
|
4750
|
+
it 'does not update the other parent\'s document' do
|
|
4751
|
+
expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
|
|
4752
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
4753
|
+
expect(victim_post.reload.title).to eq('Private')
|
|
4754
|
+
end
|
|
4755
|
+
|
|
4756
|
+
it 'does not add the other parent\'s document to the association' do
|
|
4757
|
+
expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
|
|
4758
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
4759
|
+
expect(attacker.reload.posts).to be_empty
|
|
4760
|
+
end
|
|
4761
|
+
|
|
4762
|
+
it 'does not update via a direct association assignment' do
|
|
4763
|
+
expect { attacker.update!(posts: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
|
|
4764
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
4765
|
+
expect(victim_post.reload.title).to eq('Private')
|
|
4766
|
+
end
|
|
4767
|
+
end
|
|
4768
|
+
|
|
4769
|
+
context 'when the association is a has-and-belongs-to-many' do
|
|
4770
|
+
before do
|
|
4771
|
+
Person.send(:undef_method, :preferences_attributes=)
|
|
4772
|
+
Person.accepts_nested_attributes_for :preferences
|
|
4773
|
+
end
|
|
4774
|
+
|
|
4775
|
+
let!(:victim_preference) { victim.preferences.create!(name: 'Private') }
|
|
4776
|
+
|
|
4777
|
+
it 'does not update the other parent\'s document' do
|
|
4778
|
+
expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
|
|
4779
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
4780
|
+
expect(victim_preference.reload.name).to eq('Private')
|
|
4781
|
+
end
|
|
4782
|
+
|
|
4783
|
+
it 'does not add the other parent\'s document to the association' do
|
|
4784
|
+
expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
|
|
4785
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
4786
|
+
expect(attacker.reload.preferences).to be_empty
|
|
4787
|
+
end
|
|
4788
|
+
|
|
4789
|
+
it 'does not update via a direct association assignment' do
|
|
4790
|
+
expect { attacker.update!(preferences: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
|
|
4791
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
4792
|
+
expect(victim_preference.reload.name).to eq('Private')
|
|
4793
|
+
end
|
|
4794
|
+
end
|
|
4795
|
+
end
|
|
4555
4796
|
end
|
|
@@ -38,6 +38,24 @@ describe Mongoid::Config::Defaults do
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
shared_examples 'uses settings for 9.0' do
|
|
42
|
+
it 'uses settings for 9.0' do
|
|
43
|
+
expect(Mongoid.autosave_saves_unchanged_documents).to be true
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
shared_examples 'does not use settings for 9.0' do
|
|
48
|
+
it 'does not use settings for 9.0' do
|
|
49
|
+
expect(Mongoid.autosave_saves_unchanged_documents).to be false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
shared_examples 'never allows reparenting via nested attributes' do
|
|
54
|
+
it 'leaves allow_reparenting_via_nested_attributes false' do
|
|
55
|
+
expect(Mongoid.allow_reparenting_via_nested_attributes).to be false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
41
59
|
context 'when giving a valid version' do
|
|
42
60
|
before do
|
|
43
61
|
config.load_defaults(version)
|
|
@@ -52,6 +70,8 @@ describe Mongoid::Config::Defaults do
|
|
|
52
70
|
|
|
53
71
|
it_behaves_like 'uses settings for 8.0'
|
|
54
72
|
it_behaves_like 'uses settings for 8.1'
|
|
73
|
+
it_behaves_like 'uses settings for 9.0'
|
|
74
|
+
it_behaves_like 'never allows reparenting via nested attributes'
|
|
55
75
|
end
|
|
56
76
|
|
|
57
77
|
context 'when the given version is 8.1' do
|
|
@@ -59,6 +79,8 @@ describe Mongoid::Config::Defaults do
|
|
|
59
79
|
|
|
60
80
|
it_behaves_like 'does not use settings for 8.0'
|
|
61
81
|
it_behaves_like 'uses settings for 8.1'
|
|
82
|
+
it_behaves_like 'uses settings for 9.0'
|
|
83
|
+
it_behaves_like 'never allows reparenting via nested attributes'
|
|
62
84
|
end
|
|
63
85
|
|
|
64
86
|
context 'when the given version is 9.0' do
|
|
@@ -66,6 +88,17 @@ describe Mongoid::Config::Defaults do
|
|
|
66
88
|
|
|
67
89
|
it_behaves_like 'does not use settings for 8.0'
|
|
68
90
|
it_behaves_like 'does not use settings for 8.1'
|
|
91
|
+
it_behaves_like 'uses settings for 9.0'
|
|
92
|
+
it_behaves_like 'never allows reparenting via nested attributes'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
context 'when the given version is 9.1' do
|
|
96
|
+
let(:version) { 9.1 }
|
|
97
|
+
|
|
98
|
+
it_behaves_like 'does not use settings for 8.0'
|
|
99
|
+
it_behaves_like 'does not use settings for 8.1'
|
|
100
|
+
it_behaves_like 'does not use settings for 9.0'
|
|
101
|
+
it_behaves_like 'never allows reparenting via nested attributes'
|
|
69
102
|
end
|
|
70
103
|
end
|
|
71
104
|
|