mongoid 9.1.0 → 9.1.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/lib/config/locales/en.yml +69 -0
- data/lib/mongoid/association/accessors.rb +27 -5
- 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 +65 -101
- data/lib/mongoid/association/embedded/batchable.rb +17 -1
- 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/buildable.rb +6 -0
- 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 +58 -0
- data/lib/mongoid/association.rb +15 -0
- data/lib/mongoid/atomic/modifiers.rb +100 -16
- data/lib/mongoid/clients/factory.rb +9 -13
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +0 -1
- data/lib/mongoid/config/encryption.rb +170 -22
- 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/document.rb +3 -0
- data/lib/mongoid/encryptable.rb +73 -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/invalid_encryption_schema.rb +27 -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 +30 -0
- data/lib/mongoid/errors/readonly_association.rb +29 -0
- data/lib/mongoid/errors.rb +5 -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/fields.rb +7 -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 +120 -14
- data/lib/mongoid/persistable/updatable.rb +9 -11
- data/lib/mongoid/persistable.rb +15 -15
- data/lib/mongoid/persistence_context.rb +77 -0
- data/lib/mongoid/search_indexable.rb +69 -24
- data/lib/mongoid/serializable.rb +12 -4
- data/lib/mongoid/tasks/database.rb +33 -16
- data/lib/mongoid/threaded.rb +36 -0
- data/lib/mongoid/timestamps/timeless.rb +84 -10
- data/lib/mongoid/touchable.rb +28 -1
- data/lib/mongoid/traversable.rb +2 -2
- data/lib/mongoid/validatable/associated.rb +18 -2
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/app_spec.rb +7 -1
- data/spec/integration/associations/belongs_to_spec.rb +13 -0
- data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +31 -2
- data/spec/integration/atomic/modifiers_spec.rb +283 -0
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +300 -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/accessors_spec.rb +35 -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/buildable_spec.rb +11 -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/atomic/modifiers_spec.rb +347 -0
- data/spec/mongoid/attributes/nested_spec.rb +278 -8
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/defaults_spec.rb +33 -0
- data/spec/mongoid/config/encryption_spec.rb +451 -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 +1221 -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 +115 -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/fields_spec.rb +8 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/search_indexable_spec.rb +321 -9
- data/spec/mongoid/tasks/database_spec.rb +31 -0
- data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
- data/spec/mongoid/touchable_spec.rb +242 -0
- data/spec/mongoid/touchable_spec_models.rb +47 -0
- data/spec/mongoid/validatable/associated_spec.rb +19 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/crypt/models.rb +253 -0
- data/spec/support/models/modifier_models.rb +65 -0
- metadata +55 -2
|
@@ -13,13 +13,9 @@ module Mongoid
|
|
|
13
13
|
# @param [ Hash ] modifications The add to set modifiers.
|
|
14
14
|
def add_to_set(modifications)
|
|
15
15
|
modifications.each_pair do |field, value|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
20
|
-
else
|
|
21
|
-
add_to_sets[field] = { '$each' => value }
|
|
22
|
-
end
|
|
16
|
+
mods = add_to_set_conflict?(field) ? conflicting_add_to_sets : add_to_sets
|
|
17
|
+
add_each_operation(mods, field, value)
|
|
18
|
+
add_to_set_fields[field.split('.', 2)[0]] = field
|
|
23
19
|
end
|
|
24
20
|
end
|
|
25
21
|
|
|
@@ -31,7 +27,8 @@ module Mongoid
|
|
|
31
27
|
# @param [ Hash ] modifications The pull all modifiers.
|
|
32
28
|
def pull_all(modifications)
|
|
33
29
|
modifications.each_pair do |field, value|
|
|
34
|
-
|
|
30
|
+
mods = pull_all_conflict?(field) ? conflicting_pull_alls : pull_alls
|
|
31
|
+
add_operation(mods, field, value)
|
|
35
32
|
pull_fields[field.split('.', 2)[0]] = field
|
|
36
33
|
end
|
|
37
34
|
end
|
|
@@ -41,6 +38,14 @@ module Mongoid
|
|
|
41
38
|
# @example Add pull all operations.
|
|
42
39
|
# modifiers.pull({ "addresses" => { "_id" => { "$in" => [ 1, 2, 3 ]}}})
|
|
43
40
|
#
|
|
41
|
+
# A $pull is never split out into a separate write. It shifts the
|
|
42
|
+
# positions of the remaining array elements, and the paths of the other
|
|
43
|
+
# operations on that array are built for the positions after the $pull.
|
|
44
|
+
# In a separate write the $pull could run after them, so they would hit
|
|
45
|
+
# a different element. Kept in the main update, an operation that
|
|
46
|
+
# conflicts with the $pull is rejected by MongoDB (code 40) without
|
|
47
|
+
# changing stored data.
|
|
48
|
+
#
|
|
44
49
|
# @param [ Hash ] modifications The pull all modifiers.
|
|
45
50
|
def pull(modifications)
|
|
46
51
|
modifications.each_pair do |field, value|
|
|
@@ -99,6 +104,7 @@ module Mongoid
|
|
|
99
104
|
else
|
|
100
105
|
unsets.update(field => true)
|
|
101
106
|
end
|
|
107
|
+
unset_fields[field.split('.', 2)[0]] = field
|
|
102
108
|
end
|
|
103
109
|
end
|
|
104
110
|
|
|
@@ -166,17 +172,28 @@ module Mongoid
|
|
|
166
172
|
# @return [ true | false ] If this field is a conflict.
|
|
167
173
|
def set_conflict?(field)
|
|
168
174
|
name = field.split('.', 2)[0]
|
|
169
|
-
pull_fields.has_key?(name) || push_fields.has_key?(name)
|
|
175
|
+
pull_fields.has_key?(name) || push_fields.has_key?(name) ||
|
|
176
|
+
add_to_set_fields.has_key?(name)
|
|
170
177
|
end
|
|
171
178
|
|
|
172
179
|
# Is the operation going to be a conflict for an $unset?
|
|
173
180
|
#
|
|
181
|
+
# A $push or $addToSet does not shift existing array element positions,
|
|
182
|
+
# so a conflicting $unset can be safely applied in a separate write
|
|
183
|
+
# afterwards. A $pull does shift positions, so the $unset is not split
|
|
184
|
+
# out in that case; the operations stay in the same update and MongoDB
|
|
185
|
+
# rejects the update (code 40) without changing stored data, rather
|
|
186
|
+
# than risking the $unset hitting a different element.
|
|
187
|
+
#
|
|
174
188
|
# @param [ String ] field The field.
|
|
175
189
|
#
|
|
176
190
|
# @return [ true | false ] If this field is a conflict.
|
|
177
191
|
def unset_conflict?(field)
|
|
178
192
|
key = field.split('.', 2)[0]
|
|
179
|
-
set_fields.has_key?(key)
|
|
193
|
+
return true if set_fields.has_key?(key)
|
|
194
|
+
return false if pull_fields.has_key?(key)
|
|
195
|
+
|
|
196
|
+
push_fields.has_key?(key) || add_to_set_fields.has_key?(key)
|
|
180
197
|
end
|
|
181
198
|
|
|
182
199
|
# Is the operation going to be a conflict for a $push?
|
|
@@ -190,9 +207,36 @@ module Mongoid
|
|
|
190
207
|
def push_conflict?(field)
|
|
191
208
|
name = field.split('.', 2)[0]
|
|
192
209
|
set_fields.has_key?(name) || pull_fields.has_key?(name) ||
|
|
210
|
+
add_to_set_fields.has_key?(name) || unset_fields.has_key?(name) ||
|
|
193
211
|
(push_fields.keys.count { |item| item.split('.', 2).first == name } > 1)
|
|
194
212
|
end
|
|
195
213
|
|
|
214
|
+
# Is the operation going to be a conflict for an $addToSet?
|
|
215
|
+
#
|
|
216
|
+
# As with $push, an $addToSet is only split out when the other
|
|
217
|
+
# operation does not shift array element positions. A conflicting
|
|
218
|
+
# $pull is left as-is so MongoDB rejects the update rather than the
|
|
219
|
+
# $addToSet possibly hitting a different element in a separate write.
|
|
220
|
+
#
|
|
221
|
+
# @param [ String ] field The field.
|
|
222
|
+
#
|
|
223
|
+
# @return [ true | false ] If this field is a conflict.
|
|
224
|
+
def add_to_set_conflict?(field)
|
|
225
|
+
name = field.split('.', 2)[0]
|
|
226
|
+
set_fields.has_key?(name) || push_fields.has_key?(name)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Is the operation going to be a conflict for a $pullAll?
|
|
230
|
+
#
|
|
231
|
+
# @param [ String ] field The field.
|
|
232
|
+
#
|
|
233
|
+
# @return [ true | false ] If this field is a conflict.
|
|
234
|
+
def pull_all_conflict?(field)
|
|
235
|
+
name = field.split('.', 2)[0]
|
|
236
|
+
set_fields.has_key?(name) || push_fields.has_key?(name) ||
|
|
237
|
+
add_to_set_fields.has_key?(name) || pull_root?(name)
|
|
238
|
+
end
|
|
239
|
+
|
|
196
240
|
# Returns true if the $unset is made redundant by a $set that covers
|
|
197
241
|
# the entire root-level field. When $set "children" is issued, the
|
|
198
242
|
# current (live) state of the array already includes all pending changes
|
|
@@ -202,16 +246,26 @@ module Mongoid
|
|
|
202
246
|
# @param [ String ] field the field
|
|
203
247
|
def unset_superseded_by_set?(field)
|
|
204
248
|
key = field.split('.', 2).first
|
|
205
|
-
sets(initialize: false)
|
|
249
|
+
sets(initialize: false)&.has_key?(key)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Get the conflicting $addToSet operations.
|
|
253
|
+
#
|
|
254
|
+
# @example Get the conflicting add to sets.
|
|
255
|
+
# modifiers.conflicting_add_to_sets
|
|
256
|
+
#
|
|
257
|
+
# @return [ Hash ] The conflicting add to set operations.
|
|
258
|
+
def conflicting_add_to_sets
|
|
259
|
+
conflicts['$addToSet'] ||= {}
|
|
206
260
|
end
|
|
207
261
|
|
|
208
|
-
# Get the conflicting pull modifications.
|
|
262
|
+
# Get the conflicting pull all modifications.
|
|
209
263
|
#
|
|
210
|
-
# @example Get the conflicting
|
|
211
|
-
# modifiers.
|
|
264
|
+
# @example Get the conflicting pull alls.
|
|
265
|
+
# modifiers.conflicting_pull_alls
|
|
212
266
|
#
|
|
213
|
-
# @return [ Hash ] The conflicting pull operations.
|
|
214
|
-
def
|
|
267
|
+
# @return [ Hash ] The conflicting pull all operations.
|
|
268
|
+
def conflicting_pull_alls
|
|
215
269
|
conflicts['$pullAll'] ||= {}
|
|
216
270
|
end
|
|
217
271
|
|
|
@@ -282,6 +336,26 @@ module Mongoid
|
|
|
282
336
|
@set_fields ||= {}
|
|
283
337
|
end
|
|
284
338
|
|
|
339
|
+
# Get the names of the fields that need to be added to sets.
|
|
340
|
+
#
|
|
341
|
+
# @example Get the add to set fields.
|
|
342
|
+
# modifiers.add_to_set_fields
|
|
343
|
+
#
|
|
344
|
+
# @return [ Array<String> ] The add to set fields.
|
|
345
|
+
def add_to_set_fields
|
|
346
|
+
@add_to_set_fields ||= {}
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Get the names of the fields that need to be unset.
|
|
350
|
+
#
|
|
351
|
+
# @example Get the unset fields.
|
|
352
|
+
# modifiers.unset_fields
|
|
353
|
+
#
|
|
354
|
+
# @return [ Array<String> ] The unset fields.
|
|
355
|
+
def unset_fields
|
|
356
|
+
@unset_fields ||= {}
|
|
357
|
+
end
|
|
358
|
+
|
|
285
359
|
# Get the $pullAll operations or initialize a new one.
|
|
286
360
|
#
|
|
287
361
|
# @example Get the $pullAll operations.
|
|
@@ -333,6 +407,16 @@ module Mongoid
|
|
|
333
407
|
def unsets
|
|
334
408
|
self['$unset'] ||= {}
|
|
335
409
|
end
|
|
410
|
+
|
|
411
|
+
# Is there a $pull on the given root-level field? Does not initialize
|
|
412
|
+
# the $pull operations.
|
|
413
|
+
#
|
|
414
|
+
# @param [ String ] name The root-level field name.
|
|
415
|
+
#
|
|
416
|
+
# @return [ true | false ] If a $pull touches this field.
|
|
417
|
+
def pull_root?(name)
|
|
418
|
+
(self['$pull'] || {}).keys.any? { |key| key.split('.', 2)[0] == name }
|
|
419
|
+
end
|
|
336
420
|
end
|
|
337
421
|
end
|
|
338
422
|
end
|
|
@@ -90,6 +90,8 @@ module Mongoid
|
|
|
90
90
|
return nil unless opts[:auto_encryption_options]
|
|
91
91
|
|
|
92
92
|
opts[:auto_encryption_options].dup.tap do |auto_encryption_options|
|
|
93
|
+
Mongoid::Warnings.warn_bypass_auto_encryption if auto_encryption_options[:bypass_auto_encryption]
|
|
94
|
+
|
|
93
95
|
if auto_encryption_options.key?(:schema_map)
|
|
94
96
|
default_logger.warn(
|
|
95
97
|
'The :schema_map is configured in the :auto_encryption_options for the client;' +
|
|
@@ -111,10 +113,6 @@ module Mongoid
|
|
|
111
113
|
version: VERSION
|
|
112
114
|
}.freeze
|
|
113
115
|
|
|
114
|
-
def driver_version
|
|
115
|
-
Mongo::VERSION.split('.')[0...2].map(&:to_i)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
116
|
# Prepare options for Mongo::Client based on Mongoid client configuration.
|
|
119
117
|
#
|
|
120
118
|
# @param [ Hash ] opts Parameters from options section of Mongoid client configuration.
|
|
@@ -125,16 +123,14 @@ module Mongoid
|
|
|
125
123
|
options = opts.dup
|
|
126
124
|
options[:platform] = PLATFORM_DETAILS
|
|
127
125
|
options[:app_name] = Mongoid::Config.app_name if Mongoid::Config.app_name
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
wrap << { name: 'Rails', version: ::Rails.version } if defined?(::Rails) && ::Rails.respond_to?(:version)
|
|
135
|
-
end
|
|
136
|
-
options[:wrapping_libraries] = wrap_lib
|
|
126
|
+
wrap_lib = if options[:wrapping_libraries]
|
|
127
|
+
[ MONGOID_WRAPPING_LIBRARY ] + options[:wrapping_libraries]
|
|
128
|
+
else
|
|
129
|
+
[ MONGOID_WRAPPING_LIBRARY ]
|
|
130
|
+
end.tap do |wrap|
|
|
131
|
+
wrap << { name: 'Rails', version: ::Rails.version } if defined?(::Rails) && ::Rails.respond_to?(:version)
|
|
137
132
|
end
|
|
133
|
+
options[:wrapping_libraries] = wrap_lib
|
|
138
134
|
options.reject { |k, _v| k == :hosts }.to_hash.symbolize_keys!
|
|
139
135
|
end
|
|
140
136
|
end
|
|
@@ -20,6 +20,14 @@ module Mongoid
|
|
|
20
20
|
# @raise [ Errors::CreateCollectionFailure ] If collection creation failed.
|
|
21
21
|
# @raise [ Errors::DropCollectionFailure ] If an attempt to drop collection failed.
|
|
22
22
|
def create_collection(force: false)
|
|
23
|
+
Threaded.with_collection_management do
|
|
24
|
+
perform_create_collection(force: force)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def perform_create_collection(force:)
|
|
23
31
|
if collection_name.empty?
|
|
24
32
|
# This is most probably an anonymous class, we ignore them.
|
|
25
33
|
return
|
|
@@ -20,21 +20,51 @@ module Mongoid
|
|
|
20
20
|
#
|
|
21
21
|
# @return [ Hash ] The encryption schema map.
|
|
22
22
|
def encryption_schema_map(default_database, models = ::Mongoid.models)
|
|
23
|
-
|
|
24
|
-
models
|
|
25
|
-
|
|
23
|
+
# Subclasses share their parent's collection but are not registered in
|
|
24
|
+
# Mongoid.models, so they have to be added to the walk explicitly.
|
|
25
|
+
classes = models.flat_map { |model| [ model ] + model.descendants }.uniq
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
classes.each_with_object({}) do |model, map|
|
|
28
28
|
next if model.embedded?
|
|
29
|
-
next unless model.
|
|
29
|
+
next unless model.requires_encryption_schema?
|
|
30
30
|
|
|
31
31
|
database = model.storage_options.fetch(:database) { default_database }
|
|
32
|
+
# A callable database name cannot be resolved here: the documented
|
|
33
|
+
# multi-tenant idiom has no correct value while the client is being
|
|
34
|
+
# built. Interpolating the callable would produce a key that never
|
|
35
|
+
# matches any namespace, so leave the model out of the map. Writes
|
|
36
|
+
# are refused later, by PersistenceContext, rather than silently
|
|
37
|
+
# going out unencrypted.
|
|
38
|
+
next if database.respond_to?(:call)
|
|
39
|
+
|
|
32
40
|
key = "#{database}.#{model.collection_name}"
|
|
33
|
-
props = metadata_for(model).merge(properties_for(model,
|
|
34
|
-
|
|
41
|
+
props = metadata_for(model).merge(properties_for(model, [ model ], key))
|
|
42
|
+
next if props.empty?
|
|
43
|
+
|
|
44
|
+
# The root of a collection schema describes the document, so it is
|
|
45
|
+
# always an object. Saying so matters when a nested schema carries
|
|
46
|
+
# encryptMetadata: mongocryptd rejects the schema otherwise.
|
|
47
|
+
entry = { 'bsonType' => 'object' }.merge(props)
|
|
48
|
+
map[key] = map.key?(key) ? merge_schema_entries(map[key], entry, key) : entry
|
|
35
49
|
end
|
|
36
50
|
end
|
|
37
51
|
|
|
52
|
+
# Collect the paths of the fields encrypted by the schema generated for
|
|
53
|
+
# a single model.
|
|
54
|
+
#
|
|
55
|
+
# @param [ Mongoid::Document ] model The model.
|
|
56
|
+
#
|
|
57
|
+
# @return [ Array<Array<String>> ] The property paths, outermost first.
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
def encrypted_paths_for(model)
|
|
61
|
+
return model.instance_variable_get(:@encrypted_paths_for) if
|
|
62
|
+
model.instance_variable_defined?(:@encrypted_paths_for)
|
|
63
|
+
|
|
64
|
+
properties = properties_for(model, [ model ])['properties'] || {}
|
|
65
|
+
model.instance_variable_set(:@encrypted_paths_for, collect_encrypted_paths(properties, []))
|
|
66
|
+
end
|
|
67
|
+
|
|
38
68
|
private
|
|
39
69
|
|
|
40
70
|
# The algorithm to use for the deterministic encryption.
|
|
@@ -100,11 +130,14 @@ module Mongoid
|
|
|
100
130
|
# are marked as encrypted.
|
|
101
131
|
#
|
|
102
132
|
# @param [ Mongoid::Document ] model The model to generate the properties for.
|
|
103
|
-
# @param [
|
|
133
|
+
# @param [ Array<Mongoid::Document> ] path The models the walk is already
|
|
134
|
+
# inside of, outermost first.
|
|
135
|
+
# @param [ String | nil ] namespace The namespace the schema is built
|
|
136
|
+
# for, used for error reporting.
|
|
104
137
|
#
|
|
105
138
|
# @return [ Hash ] The encryption properties.
|
|
106
|
-
def properties_for(model,
|
|
107
|
-
result = properties_for_fields(model).merge(properties_for_relations(model,
|
|
139
|
+
def properties_for(model, path, namespace = nil)
|
|
140
|
+
result = properties_for_fields(model).merge(properties_for_relations(model, path, namespace))
|
|
108
141
|
if result.empty?
|
|
109
142
|
{}
|
|
110
143
|
else
|
|
@@ -141,26 +174,141 @@ module Mongoid
|
|
|
141
174
|
# are configured to be encrypted.
|
|
142
175
|
#
|
|
143
176
|
# @param [ Mongoid::Document ] model The model to generate the properties for.
|
|
144
|
-
# @param [
|
|
177
|
+
# @param [ Array<Mongoid::Document> ] path The models the walk is already
|
|
178
|
+
# inside of, outermost first.
|
|
179
|
+
# @param [ String | nil ] namespace The namespace the schema is built
|
|
180
|
+
# for, used for error reporting.
|
|
145
181
|
#
|
|
146
182
|
# @return [ Hash ] The encryption properties.
|
|
147
|
-
def properties_for_relations(model,
|
|
183
|
+
def properties_for_relations(model, path, namespace = nil)
|
|
148
184
|
model.relations.each_with_object({}) do |(name, relation), props|
|
|
149
|
-
|
|
185
|
+
# relation_class constantizes, and a polymorphic embedded_in has no
|
|
186
|
+
# class to resolve, so the relation type has to be checked first.
|
|
150
187
|
next unless relation.is_a?(Association::Embedded::EmbedsOne)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
|
|
189
|
+
klass = relation.try_relation_class
|
|
190
|
+
# An association target does not have to be a Mongoid document, and
|
|
191
|
+
# the class it names does not have to exist.
|
|
192
|
+
next unless klass.respond_to?(:requires_encryption_schema?)
|
|
193
|
+
|
|
194
|
+
# Any subclass of the declared class can be assigned to the
|
|
195
|
+
# association, so its encrypted fields have to be mapped here too.
|
|
196
|
+
properties = ([ klass ] + klass.descendants).uniq.inject({}) do |merged, candidate|
|
|
197
|
+
# Stop at a model the walk is already inside of, or a self-embedding
|
|
198
|
+
# model never terminates. The path covers the current branch only:
|
|
199
|
+
# a model embedded by two parents, or twice by one parent, has to be
|
|
200
|
+
# emitted at every place it appears.
|
|
201
|
+
next merged if path.include?(candidate)
|
|
202
|
+
next merged unless candidate.respond_to?(:requires_encryption_schema?)
|
|
203
|
+
next merged unless candidate.requires_encryption_schema?
|
|
204
|
+
|
|
205
|
+
merge_schema_entries(
|
|
206
|
+
merged,
|
|
207
|
+
metadata_for(candidate).merge(
|
|
208
|
+
properties_for(candidate, path + [ candidate ], namespace)
|
|
209
|
+
),
|
|
210
|
+
namespace
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
props[name] = { 'bsonType' => 'object' }.merge(properties) unless properties.empty?
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Walk the schema properties and collect the path of every encrypted
|
|
218
|
+
# leaf.
|
|
219
|
+
#
|
|
220
|
+
# @param [ Hash ] properties The properties to walk.
|
|
221
|
+
# @param [ Array<String> ] path The properties the walk is already
|
|
222
|
+
# inside of.
|
|
223
|
+
#
|
|
224
|
+
# @return [ Array<Array<String>> ] The property paths.
|
|
225
|
+
def collect_encrypted_paths(properties, path)
|
|
226
|
+
properties.each_with_object([]) do |(name, schema), paths|
|
|
227
|
+
if schema.key?('encrypt')
|
|
228
|
+
paths << (path + [ name ])
|
|
229
|
+
elsif schema['properties']
|
|
230
|
+
paths.concat(collect_encrypted_paths(schema['properties'], path + [ name ]))
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Merge two schema objects covering the same model or namespace.
|
|
236
|
+
#
|
|
237
|
+
# Every model writing to one namespace contributes an entry to the
|
|
238
|
+
# schema map. Those entries have to agree on everything they have in
|
|
239
|
+
# common, or the schema would silently encrypt a field differently for
|
|
240
|
+
# one model than for another.
|
|
241
|
+
#
|
|
242
|
+
# @param [ Hash ] left The first schema.
|
|
243
|
+
# @param [ Hash ] right The second schema.
|
|
244
|
+
# @param [ String | nil ] namespace The namespace the schema belongs to.
|
|
245
|
+
#
|
|
246
|
+
# @return [ Hash ] The merged schema.
|
|
247
|
+
def merge_schema_entries(left, right, namespace = nil)
|
|
248
|
+
left.merge(right) do |key, left_value, right_value|
|
|
249
|
+
case key
|
|
250
|
+
when 'bsonType'
|
|
251
|
+
left_value
|
|
252
|
+
when 'encryptMetadata'
|
|
253
|
+
merge_encrypt_metadata(left_value, right_value, namespace)
|
|
254
|
+
when 'properties'
|
|
255
|
+
merge_properties(left_value, right_value, namespace)
|
|
256
|
+
else
|
|
257
|
+
raise_invalid_encryption_schema(namespace, key)
|
|
160
258
|
end
|
|
161
259
|
end
|
|
162
260
|
end
|
|
163
261
|
|
|
262
|
+
# Merge two encryptMetadata objects.
|
|
263
|
+
#
|
|
264
|
+
# @param [ Hash ] left The first encryptMetadata object.
|
|
265
|
+
# @param [ Hash ] right The second encryptMetadata object.
|
|
266
|
+
# @param [ String | nil ] namespace The namespace the schema belongs to.
|
|
267
|
+
#
|
|
268
|
+
# @return [ Hash ] The merged encryptMetadata object.
|
|
269
|
+
def merge_encrypt_metadata(left, right, namespace)
|
|
270
|
+
left.merge(right) do |key, left_value, right_value|
|
|
271
|
+
unless left_value == right_value
|
|
272
|
+
raise_invalid_encryption_schema(namespace, "encryptMetadata.#{key}")
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
left_value
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Merge two properties objects.
|
|
280
|
+
#
|
|
281
|
+
# @param [ Hash ] left The first properties object.
|
|
282
|
+
# @param [ Hash ] right The second properties object.
|
|
283
|
+
# @param [ String | nil ] namespace The namespace the schema belongs to.
|
|
284
|
+
#
|
|
285
|
+
# @return [ Hash ] The merged properties object.
|
|
286
|
+
def merge_properties(left, right, namespace)
|
|
287
|
+
left.merge(right) do |name, left_value, right_value|
|
|
288
|
+
if left_value.key?('encrypt') && right_value.key?('encrypt')
|
|
289
|
+
unless left_value['encrypt'] == right_value['encrypt']
|
|
290
|
+
raise_invalid_encryption_schema(namespace, "properties.#{name}")
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
left_value
|
|
294
|
+
elsif left_value.key?('encrypt') || right_value.key?('encrypt')
|
|
295
|
+
raise_invalid_encryption_schema(namespace, "properties.#{name}")
|
|
296
|
+
else
|
|
297
|
+
merge_schema_entries(left_value, right_value, namespace)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Raise an error for a schema conflicting with another one.
|
|
303
|
+
#
|
|
304
|
+
# @param [ String | nil ] namespace The namespace the schema belongs to.
|
|
305
|
+
# @param [ String ] path The conflicting path in the schema.
|
|
306
|
+
#
|
|
307
|
+
# @raise [ Errors::InvalidEncryptionSchema ] Always raised.
|
|
308
|
+
def raise_invalid_encryption_schema(namespace, path)
|
|
309
|
+
raise Errors::InvalidEncryptionSchema.new(namespace, path)
|
|
310
|
+
end
|
|
311
|
+
|
|
164
312
|
# Get the BSON type identifier for the provided field according to the
|
|
165
313
|
# https://www.mongodb.com/docs/manual/reference/bson-types/#std-label-bson-types
|
|
166
314
|
#
|
data/lib/mongoid/config.rb
CHANGED
|
@@ -130,12 +130,14 @@ module Mongoid
|
|
|
130
130
|
validate_isolation_level!(level)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
# When this flag is true, it will be possible to
|
|
134
|
-
#
|
|
135
|
-
# nested attributes for another parent record
|
|
133
|
+
# When this flag is true, it will be possible to add a record to a
|
|
134
|
+
# "has_many" or "has_and_belongs_to_many" association by passing that
|
|
135
|
+
# record's id in the nested attributes for another parent record, even
|
|
136
|
+
# when the record does not already belong to that association. For a
|
|
137
|
+
# "has_many" association this moves the record to the new parent.
|
|
136
138
|
#
|
|
137
|
-
# When this flag is false,
|
|
138
|
-
#
|
|
139
|
+
# When this flag is false, an id in nested attributes is only resolved
|
|
140
|
+
# within the association itself, and anything else raises an error.
|
|
139
141
|
#
|
|
140
142
|
# The default is `false`. Note that allowing reparenting via nested attributes
|
|
141
143
|
# is a potential security risk, since it could allow a malicious user to move
|
|
@@ -294,6 +296,51 @@ module Mongoid
|
|
|
294
296
|
# See https://jira.mongodb.org/browse/MONGOID-5892 for more details.
|
|
295
297
|
option :serializable_hash_with_legacy_only, default: true
|
|
296
298
|
|
|
299
|
+
# When false (default), query operators are restricted when building a
|
|
300
|
+
# selector, so that user-supplied input reaching the query builder cannot
|
|
301
|
+
# make MongoDB execute arbitrary JavaScript. Two rules are enforced:
|
|
302
|
+
#
|
|
303
|
+
# - An operator at the top level of an expression must appear in
|
|
304
|
+
# +Criteria::Queryable::Selectable::ALLOWED_QUERY_OPERATORS+.
|
|
305
|
+
# - +$where+, +$function+, and +$accumulator+ are rejected at any depth,
|
|
306
|
+
# including inside +$expr+ and the logical operators.
|
|
307
|
+
#
|
|
308
|
+
# This applies to every query method that accepts an expression, including
|
|
309
|
+
# +where+, +find_by+, +and+, +or+, +nor+, +not+, +any_of+, and +none_of+.
|
|
310
|
+
# It also governs the string form of +where+, e.g.
|
|
311
|
+
# +where("this.name == 'admin'")+, which the server evaluates as +$where+.
|
|
312
|
+
# Applications relying on that form must set this option to true.
|
|
313
|
+
#
|
|
314
|
+
# The APIs that request JavaScript explicitly, +Criteria#for_js+ and
|
|
315
|
+
# +js_query+, are unaffected: there the developer has asked for it.
|
|
316
|
+
#
|
|
317
|
+
# Set to true to restore the unrestricted pass-through behavior.
|
|
318
|
+
#
|
|
319
|
+
# Note that this option is deliberately not tied to +load_defaults+: an
|
|
320
|
+
# application that has opted into older defaults still gets the guard, and
|
|
321
|
+
# must set this option explicitly to turn it off.
|
|
322
|
+
#
|
|
323
|
+
# See https://jira.mongodb.org/browse/MONGOID-5939,
|
|
324
|
+
# https://jira.mongodb.org/browse/MONGOID-5993,
|
|
325
|
+
# https://jira.mongodb.org/browse/MONGOID-5994 for details.
|
|
326
|
+
option :allow_unsafe_query_operators, default: false
|
|
327
|
+
|
|
328
|
+
# The maximum number of seconds that evaluating a single query in memory
|
|
329
|
+
# may spend executing regular expressions. Queries against an embedded
|
|
330
|
+
# association are evaluated in the calling thread, so a pattern built from
|
|
331
|
+
# user input runs locally and can otherwise consume unbounded CPU. The
|
|
332
|
+
# limit is cumulative over the whole query, since cost grows with the
|
|
333
|
+
# number of documents and conditions as well as with the pattern.
|
|
334
|
+
#
|
|
335
|
+
# Set to nil to remove the limit. On Ruby 3.2 and later the remaining
|
|
336
|
+
# budget is compiled into the pattern, so the limit counts only the time
|
|
337
|
+
# spent matching. Earlier Rubies have no per-Regexp timeout, so the query
|
|
338
|
+
# is bounded with Timeout instead and the limit is wall clock over the
|
|
339
|
+
# whole in-memory evaluation.
|
|
340
|
+
#
|
|
341
|
+
# See https://jira.mongodb.org/browse/MONGOID-5981 for details.
|
|
342
|
+
option :in_memory_regexp_time_limit, default: 5.0
|
|
343
|
+
|
|
297
344
|
# Returns the Config singleton, for use in the configure DSL.
|
|
298
345
|
#
|
|
299
346
|
# @return [ self ] The Config singleton.
|
|
@@ -539,6 +586,10 @@ module Mongoid
|
|
|
539
586
|
end
|
|
540
587
|
end
|
|
541
588
|
|
|
589
|
+
# Wraps configuration options that have been deprecated so that assigning
|
|
590
|
+
# them emits a deprecation warning. OPTIONS is intentionally empty when no
|
|
591
|
+
# options are currently deprecated; it is populated as options are retired,
|
|
592
|
+
# at which point this module rewrites their setters to warn.
|
|
542
593
|
module DeprecatedOptions
|
|
543
594
|
OPTIONS = %i[]
|
|
544
595
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'mongoid/field_readable'
|
|
4
|
+
|
|
3
5
|
module Mongoid
|
|
4
6
|
module Contextual
|
|
5
7
|
module Aggregable
|
|
6
8
|
# Contains behavior for aggregating values in memory.
|
|
7
9
|
module Memory
|
|
10
|
+
include FieldReadable
|
|
11
|
+
|
|
8
12
|
# Get all the aggregate values for the provided field.
|
|
9
13
|
# Provided for interface consistency with Aggregable::Mongo.
|
|
10
14
|
#
|
|
@@ -28,7 +32,7 @@ module Mongoid
|
|
|
28
32
|
#
|
|
29
33
|
# @return [ Numeric ] The average.
|
|
30
34
|
def avg(field)
|
|
31
|
-
total = count { |doc| !doc
|
|
35
|
+
total = count { |doc| !read_field_value(doc, field).nil? }
|
|
32
36
|
return nil unless total > 0
|
|
33
37
|
|
|
34
38
|
total = total.to_f if total.is_a?(Integer)
|
|
@@ -114,7 +118,7 @@ module Mongoid
|
|
|
114
118
|
def aggregate_by(field, method)
|
|
115
119
|
return nil unless any?
|
|
116
120
|
|
|
117
|
-
map { |doc| doc
|
|
121
|
+
map { |doc| read_field_value(doc, field) }.compact.public_send(method)
|
|
118
122
|
end
|
|
119
123
|
end
|
|
120
124
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'mongoid/contextual/aggregable/memory'
|
|
4
4
|
require 'mongoid/association/eager_loadable'
|
|
5
|
+
require 'mongoid/field_readable'
|
|
5
6
|
|
|
6
7
|
module Mongoid
|
|
7
8
|
module Contextual
|
|
@@ -15,6 +16,7 @@ module Mongoid
|
|
|
15
16
|
include Association::EagerLoadable
|
|
16
17
|
include Queryable
|
|
17
18
|
include Positional
|
|
19
|
+
include FieldReadable
|
|
18
20
|
|
|
19
21
|
# @attribute [r] root The root document.
|
|
20
22
|
# @attribute [r] path The atomic path.
|
|
@@ -174,10 +176,20 @@ module Mongoid
|
|
|
174
176
|
# @param [ Criteria ] criteria The criteria.
|
|
175
177
|
def initialize(criteria)
|
|
176
178
|
@criteria, @klass = criteria, criteria.klass
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
|
|
180
|
+
# Resolving the collection can build a Mongo::Client the first time it
|
|
181
|
+
# runs, so it happens before the budget opens. Where the budget is
|
|
182
|
+
# enforced with Timeout the exception is asynchronous, and landing in
|
|
183
|
+
# the middle of that would leave a half-built client behind.
|
|
184
|
+
if (first = criteria.documents.first)
|
|
185
|
+
@root = first._root
|
|
186
|
+
@collection = @root.collection
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# One regexp budget covers the whole scan, so that the limit bounds the
|
|
190
|
+
# query rather than each document individually.
|
|
191
|
+
@documents = Matcher::RegexpBudget.open(criteria.selector) do
|
|
192
|
+
criteria.documents.select { |doc| doc._matches?(criteria.selector) }
|
|
181
193
|
end
|
|
182
194
|
apply_sorting
|
|
183
195
|
apply_options
|
|
@@ -739,10 +751,9 @@ module Mongoid
|
|
|
739
751
|
# If this is a localized field, and there are remaining, get the
|
|
740
752
|
# _translations hash so that we can get the specified translation in
|
|
741
753
|
# the remaining
|
|
742
|
-
document.
|
|
754
|
+
document.public_send("#{segment}_translations") if field&.localized?
|
|
743
755
|
end
|
|
744
|
-
|
|
745
|
-
res.nil? ? document.try(meth) : res
|
|
756
|
+
res.nil? ? read_field_value(document, segment) : res
|
|
746
757
|
elsif document.is_a?(Hash)
|
|
747
758
|
# TODO: Remove the indifferent access when implementing MONGOID-5410.
|
|
748
759
|
if document.key?(segment.to_s)
|