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
data/lib/mongoid/serializable.rb
CHANGED
|
@@ -127,10 +127,18 @@ module Mongoid
|
|
|
127
127
|
inclusions = options[:include]
|
|
128
128
|
relation_names(inclusions).each do |name|
|
|
129
129
|
association = relations[name.to_s]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
next unless association
|
|
131
|
+
|
|
132
|
+
relation =
|
|
133
|
+
if association.fallback?
|
|
134
|
+
without_autobuild { send(association.name) }
|
|
135
|
+
else
|
|
136
|
+
send(association.name)
|
|
137
|
+
end
|
|
138
|
+
next unless relation
|
|
139
|
+
|
|
140
|
+
attributes[association.name.to_s] =
|
|
141
|
+
relation.serializable_hash(relation_options(inclusions, options, name))
|
|
134
142
|
end
|
|
135
143
|
end
|
|
136
144
|
|
|
@@ -83,6 +83,12 @@ module Mongoid
|
|
|
83
83
|
#
|
|
84
84
|
# @return [ Array<Hash> ] The list of undefined indexes by model.
|
|
85
85
|
def undefined_indexes(models = ::Mongoid.models)
|
|
86
|
+
Threaded.with_collection_management do
|
|
87
|
+
undefined_indexes_for(models)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def undefined_indexes_for(models)
|
|
86
92
|
undefined_by_model = {}
|
|
87
93
|
|
|
88
94
|
models.each do |model|
|
|
@@ -116,15 +122,17 @@ module Mongoid
|
|
|
116
122
|
#
|
|
117
123
|
# @return [ Hash{Class => Array(Hash)}] The list of indexes that were removed by model.
|
|
118
124
|
def remove_undefined_indexes(models = ::Mongoid.models)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
Threaded.with_collection_management do
|
|
126
|
+
undefined_indexes(models).each do |model, indexes|
|
|
127
|
+
indexes.each do |index|
|
|
128
|
+
key = index['key'].symbolize_keys
|
|
129
|
+
collection = model.collection
|
|
130
|
+
collection.indexes(session: model.send(:_session)).drop_one(key)
|
|
131
|
+
logger.info(
|
|
132
|
+
"MONGOID: Removed index '#{index['name']}' on collection " +
|
|
133
|
+
"'#{collection.name}' in database '#{collection.database.name}'."
|
|
134
|
+
)
|
|
135
|
+
end
|
|
128
136
|
end
|
|
129
137
|
end
|
|
130
138
|
end
|
|
@@ -172,6 +180,12 @@ module Mongoid
|
|
|
172
180
|
#
|
|
173
181
|
# @return [ Array<Class> ] The sharded models
|
|
174
182
|
def shard_collections(models = ::Mongoid.models)
|
|
183
|
+
Threaded.with_collection_management do
|
|
184
|
+
shard_collections_for(models)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def shard_collections_for(models)
|
|
175
189
|
models.map do |model|
|
|
176
190
|
next if model.shard_config.nil?
|
|
177
191
|
|
data/lib/mongoid/threaded.rb
CHANGED
|
@@ -32,6 +32,9 @@ module Mongoid
|
|
|
32
32
|
hash[key] = "#{key}-stack"
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# The name of the stack tracking collection and index management.
|
|
36
|
+
COLLECTION_MANAGEMENT = :collection_management
|
|
37
|
+
|
|
35
38
|
# The key for the current thread's sessions.
|
|
36
39
|
SESSIONS_KEY = 'sessions'
|
|
37
40
|
|
|
@@ -42,6 +45,9 @@ module Mongoid
|
|
|
42
45
|
# executed on documents.
|
|
43
46
|
EXECUTE_CALLBACKS = 'execute-callbacks'
|
|
44
47
|
|
|
48
|
+
# The key for the time left in the current in-memory regexp budget.
|
|
49
|
+
REGEXP_BUDGET_KEY = 'regexp-budget'
|
|
50
|
+
|
|
45
51
|
extend self
|
|
46
52
|
|
|
47
53
|
# Resets the current thread- or fiber-local storage to its initial state.
|
|
@@ -165,6 +171,36 @@ module Mongoid
|
|
|
165
171
|
!stack(name).empty?
|
|
166
172
|
end
|
|
167
173
|
|
|
174
|
+
# Execute the block as collection or index management.
|
|
175
|
+
#
|
|
176
|
+
# Creating, dropping and inspecting collections and indexes sends no
|
|
177
|
+
# document data, so these operations are exempt from the encryption schema
|
|
178
|
+
# check that PersistenceContext applies to reads and writes. Without the
|
|
179
|
+
# exemption, tasks such as db:mongoid:create_collections would need an
|
|
180
|
+
# encryption-capable client to run.
|
|
181
|
+
#
|
|
182
|
+
# @example Create a collection.
|
|
183
|
+
# Threaded.with_collection_management { model.create_collection }
|
|
184
|
+
#
|
|
185
|
+
# @return [ Object ] The result of the block.
|
|
186
|
+
def with_collection_management
|
|
187
|
+
begin_execution(COLLECTION_MANAGEMENT)
|
|
188
|
+
yield
|
|
189
|
+
ensure
|
|
190
|
+
exit_execution(COLLECTION_MANAGEMENT)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Is collection or index management being executed?
|
|
194
|
+
#
|
|
195
|
+
# @example Is a collection being managed?
|
|
196
|
+
# Threaded.managing_collection?
|
|
197
|
+
#
|
|
198
|
+
# @return [ true | false ] Whether collection or index management is in
|
|
199
|
+
# progress on the current thread.
|
|
200
|
+
def managing_collection?
|
|
201
|
+
executing?(COLLECTION_MANAGEMENT)
|
|
202
|
+
end
|
|
203
|
+
|
|
168
204
|
# Exit from a named thread local stack.
|
|
169
205
|
#
|
|
170
206
|
# @example Exit from the stack.
|
|
@@ -22,13 +22,20 @@ module Mongoid
|
|
|
22
22
|
true
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# Skip timestamping for the duration of the given block, or (in the
|
|
26
|
+
# deprecated, block-less form) for the next persistence operation.
|
|
26
27
|
#
|
|
27
|
-
# @example Save a document but don't timestamp.
|
|
28
|
+
# @example Save a document but don't timestamp (block form).
|
|
29
|
+
# person.timeless { person.save }
|
|
30
|
+
#
|
|
31
|
+
# @example Save a document but don't timestamp (deprecated chained form).
|
|
28
32
|
# person.timeless.save
|
|
29
33
|
#
|
|
30
|
-
# @return [ Document ] The
|
|
31
|
-
|
|
34
|
+
# @return [ Object | Document ] The return value of the block, or (in the
|
|
35
|
+
# block-less form) the document this was called on.
|
|
36
|
+
def timeless(&block)
|
|
37
|
+
return Timeless.with_timeless(&block) if block
|
|
38
|
+
|
|
32
39
|
self.class.timeless
|
|
33
40
|
self
|
|
34
41
|
end
|
|
@@ -47,6 +54,9 @@ module Mongoid
|
|
|
47
54
|
# The key to use to store the timeless table
|
|
48
55
|
TIMELESS_TABLE_KEY = '[mongoid]:timeless'
|
|
49
56
|
|
|
57
|
+
# The key to use to store the block-based timeless flag.
|
|
58
|
+
TIMELESS_FLAG_KEY = '[mongoid]:timeless-flag'
|
|
59
|
+
|
|
50
60
|
# Returns the in-memory thread cache of classes
|
|
51
61
|
# for which to skip timestamping.
|
|
52
62
|
#
|
|
@@ -58,16 +68,78 @@ module Mongoid
|
|
|
58
68
|
end
|
|
59
69
|
|
|
60
70
|
def_delegators :timeless_table, :[]=, :[]
|
|
71
|
+
|
|
72
|
+
# Skip timestamping for the duration of the given block, on the
|
|
73
|
+
# current thread or fiber. This applies to every document persisted
|
|
74
|
+
# while the block is executing, regardless of class, including
|
|
75
|
+
# cascaded embedded children at any nesting depth.
|
|
76
|
+
#
|
|
77
|
+
# @example Skip timestamping for a block.
|
|
78
|
+
# Mongoid::Timestamps::Timeless.with_timeless do
|
|
79
|
+
# person.save
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# @return [ Object ] The return value of the block.
|
|
83
|
+
def with_timeless
|
|
84
|
+
# Only the outermost block owns the flag: if we are already inside a
|
|
85
|
+
# timeless scope, we leave the suppression in place when this block
|
|
86
|
+
# ends. This avoids tracking a nesting depth that could drift out of
|
|
87
|
+
# sync.
|
|
88
|
+
already_timeless = suppressing_timestamps?
|
|
89
|
+
set_suppressing_timestamps(true) unless already_timeless
|
|
90
|
+
yield
|
|
91
|
+
ensure
|
|
92
|
+
set_suppressing_timestamps(false) unless already_timeless
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether a block-based timeless scope is currently active on this
|
|
96
|
+
# thread/fiber.
|
|
97
|
+
#
|
|
98
|
+
# @return [ true | false ] Whether timestamps are being suppressed.
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
def suppressing_timestamps?
|
|
102
|
+
!!Threaded.get(TIMELESS_FLAG_KEY) { false }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Set whether a block-based timeless scope is active on this
|
|
106
|
+
# thread/fiber.
|
|
107
|
+
#
|
|
108
|
+
# @param [ true | false ] value Whether to suppress timestamps.
|
|
109
|
+
#
|
|
110
|
+
# @api private
|
|
111
|
+
def set_suppressing_timestamps(value)
|
|
112
|
+
Threaded.set(TIMELESS_FLAG_KEY, value)
|
|
113
|
+
end
|
|
61
114
|
end
|
|
62
115
|
|
|
63
116
|
module ClassMethods
|
|
64
|
-
#
|
|
117
|
+
# Skip timestamping for the duration of the given block, or (in the
|
|
118
|
+
# deprecated, block-less form) for the next persistence operation.
|
|
119
|
+
#
|
|
120
|
+
# @example Create a document but don't timestamp (block form).
|
|
121
|
+
# Person.timeless { Person.create(title: "Sir") }
|
|
65
122
|
#
|
|
66
|
-
# @example Create a document but don't timestamp.
|
|
123
|
+
# @example Create a document but don't timestamp (deprecated form).
|
|
67
124
|
# Person.timeless.create(:title => "Sir")
|
|
68
125
|
#
|
|
69
|
-
# @return [ Class ] The
|
|
70
|
-
|
|
126
|
+
# @return [ Object | Class ] The return value of the block, or (in the
|
|
127
|
+
# block-less form) the class this was called on.
|
|
128
|
+
def timeless(&block)
|
|
129
|
+
return Timeless.with_timeless(&block) if block
|
|
130
|
+
|
|
131
|
+
locations = caller_locations
|
|
132
|
+
|
|
133
|
+
# if this is called from the #timeless instance method, look up one
|
|
134
|
+
# level higher for the call site
|
|
135
|
+
locations.shift if locations[0].path == __FILE__
|
|
136
|
+
|
|
137
|
+
Mongoid.deprecation_warning(
|
|
138
|
+
:timeless_sans_block,
|
|
139
|
+
'Calling #timeless without a block is deprecated; pass a block ' \
|
|
140
|
+
'instead, e.g. `record.timeless { record.save }`.',
|
|
141
|
+
locations
|
|
142
|
+
)
|
|
71
143
|
counter = 0
|
|
72
144
|
counter += 1 if self < Mongoid::Timestamps::Created
|
|
73
145
|
counter += 1 if self < Mongoid::Timestamps::Updated
|
|
@@ -109,12 +181,14 @@ module Mongoid
|
|
|
109
181
|
Timeless[name] = (counter == 0) ? nil : counter
|
|
110
182
|
end
|
|
111
183
|
|
|
112
|
-
# Returns whether the current class should skip timestamping.
|
|
184
|
+
# Returns whether the current class should skip timestamping. This is
|
|
185
|
+
# true when either a block-based timeless scope is active on the
|
|
186
|
+
# current thread/fiber, or the deprecated per-class counter is set.
|
|
113
187
|
#
|
|
114
188
|
# @return [ true | false ] Whether the current class should
|
|
115
189
|
# skip timestamping.
|
|
116
190
|
def timeless?
|
|
117
|
-
!!Timeless[name]
|
|
191
|
+
Timeless.suppressing_timestamps? || !!Timeless[name]
|
|
118
192
|
end
|
|
119
193
|
end
|
|
120
194
|
end
|
data/lib/mongoid/traversable.rb
CHANGED
|
@@ -211,7 +211,7 @@ module Mongoid
|
|
|
211
211
|
# @api private
|
|
212
212
|
def self.add_discriminator_mapping(value, klass = self)
|
|
213
213
|
self.discriminator_mapping ||= {}
|
|
214
|
-
|
|
214
|
+
discriminator_mapping[value] = klass
|
|
215
215
|
superclass.add_discriminator_mapping(value, klass) if hereditary?
|
|
216
216
|
end
|
|
217
217
|
|
|
@@ -225,7 +225,7 @@ module Mongoid
|
|
|
225
225
|
#
|
|
226
226
|
# @api private
|
|
227
227
|
def self.get_discriminator_mapping(value)
|
|
228
|
-
|
|
228
|
+
discriminator_mapping[value] if discriminator_mapping
|
|
229
229
|
end
|
|
230
230
|
end
|
|
231
231
|
|
|
@@ -67,12 +67,13 @@ module Mongoid
|
|
|
67
67
|
valid = document.validating do
|
|
68
68
|
# Now, treating the target as an array, look at each element
|
|
69
69
|
# and see if it is valid, but only if it has already been
|
|
70
|
-
# persisted, or changed, and hasn't been flagged for destroy
|
|
70
|
+
# persisted, or changed, and hasn't been flagged for destroy
|
|
71
|
+
# or already destroyed.
|
|
71
72
|
#
|
|
72
73
|
# use map.all? instead of just all?, because all? will do short-circuit
|
|
73
74
|
# evaluation and terminate on the first failed validation.
|
|
74
75
|
list.map do |value|
|
|
75
|
-
if
|
|
76
|
+
if needs_validation?(value)
|
|
76
77
|
value.validated? || value.valid?
|
|
77
78
|
else
|
|
78
79
|
true
|
|
@@ -120,6 +121,21 @@ module Mongoid
|
|
|
120
121
|
def get_target_documents_for_other(target)
|
|
121
122
|
Array.wrap(target)
|
|
122
123
|
end
|
|
124
|
+
|
|
125
|
+
# Returns true if the given value should be validated as part of
|
|
126
|
+
# an associated validation. Destroyed and flagged-for-destroy
|
|
127
|
+
# documents are skipped, as are persisted documents that haven't
|
|
128
|
+
# changed.
|
|
129
|
+
#
|
|
130
|
+
# @param [ Mongoid::Document | nil ] value The value to check.
|
|
131
|
+
#
|
|
132
|
+
# @return [ true | false ] Whether the value needs validation.
|
|
133
|
+
def needs_validation?(value)
|
|
134
|
+
value &&
|
|
135
|
+
!value.flagged_for_destroy? &&
|
|
136
|
+
!value.destroyed? &&
|
|
137
|
+
(!value.persisted? || value.changed?)
|
|
138
|
+
end
|
|
123
139
|
end
|
|
124
140
|
end
|
|
125
141
|
end
|
data/lib/mongoid/version.rb
CHANGED
|
@@ -435,7 +435,13 @@ describe 'Mongoid application tests' do
|
|
|
435
435
|
end
|
|
436
436
|
gemfile_lines << "gem 'mongoid', path: '#{File.expand_path(BASE)}'\n"
|
|
437
437
|
|
|
438
|
-
#
|
|
438
|
+
# json 3.0 removed the quirks_mode and max_nesting keywords that
|
|
439
|
+
# ActiveSupport::JSON (every 7.x) still passes to JSON.generate and
|
|
440
|
+
# JSON.parse, so a freshly resolved bundle picks a 3.x json and the app
|
|
441
|
+
# fails to encode any response with ArgumentError: unknown keyword:
|
|
442
|
+
# quirks_mode. Cap json at 2.x, which still accepts those keywords.
|
|
443
|
+
gemfile_lines << "gem 'json', '< 3'\n" # Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
|
|
444
|
+
|
|
439
445
|
gemfile_lines << "gem 'logger'\n" if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
|
|
440
446
|
|
|
441
447
|
if rails_version
|
|
@@ -99,6 +99,19 @@ describe 'belongs_to associations' do
|
|
|
99
99
|
expect(team_manager.reload.unit_type).to eq 'group'
|
|
100
100
|
expect(team_manager.unit).to eq team
|
|
101
101
|
end
|
|
102
|
+
|
|
103
|
+
it 'eager-loads the corresponding unit when unit_type is a different alias' do
|
|
104
|
+
dept_manager.update unit_type: 'sandbox_dept'
|
|
105
|
+
team_manager.update unit_type: 'group'
|
|
106
|
+
|
|
107
|
+
managers = SandboxManager.where(:_id.in => [ dept_manager.id, team_manager.id ])
|
|
108
|
+
.includes(:unit)
|
|
109
|
+
.to_a
|
|
110
|
+
units_by_id = managers.to_h { |m| [ m.id, m.ivar(:unit) ] }
|
|
111
|
+
|
|
112
|
+
expect(units_by_id[dept_manager.id]).to eq department
|
|
113
|
+
expect(units_by_id[team_manager.id]).to eq team
|
|
114
|
+
end
|
|
102
115
|
end
|
|
103
116
|
|
|
104
117
|
context 'when the association uses the default resolver' do
|
|
@@ -90,8 +90,23 @@ describe 'has_and_belongs_to_many associations' do
|
|
|
90
90
|
})
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
# The nested attributes are processed before the attachment_ids
|
|
94
|
+
# assignment is applied, so at that point the id is not yet in the
|
|
95
|
+
# association and resolving it requires a collection-wide lookup.
|
|
96
|
+
context 'when allow_reparenting_via_nested_attributes is false' do
|
|
97
|
+
config_override :allow_reparenting_via_nested_attributes, false
|
|
98
|
+
|
|
99
|
+
it 'raises a document not found error' do
|
|
100
|
+
expect { image_block.save! }.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
105
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
106
|
+
|
|
107
|
+
it 'does not raise on save' do
|
|
108
|
+
expect { image_block.save! }.not_to raise_error
|
|
109
|
+
end
|
|
95
110
|
end
|
|
96
111
|
end
|
|
97
112
|
|
|
@@ -253,10 +253,20 @@ describe 'Dots and Dollars' do
|
|
|
253
253
|
DADMUser.where('$_amount': 0).first
|
|
254
254
|
end
|
|
255
255
|
|
|
256
|
-
it '
|
|
256
|
+
it 'raises an error' do
|
|
257
257
|
expect do
|
|
258
258
|
queried
|
|
259
|
-
end.to raise_error(
|
|
259
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
context 'when allow_unsafe_query_operators is true' do
|
|
263
|
+
config_override :allow_unsafe_query_operators, true
|
|
264
|
+
|
|
265
|
+
it 'raises an error from the server' do
|
|
266
|
+
expect do
|
|
267
|
+
queried
|
|
268
|
+
end.to raise_error(Mongo::Error::OperationFailure)
|
|
269
|
+
end
|
|
260
270
|
end
|
|
261
271
|
end
|
|
262
272
|
end
|
|
@@ -33,19 +33,24 @@ describe 'Encryption' do
|
|
|
33
33
|
around do |example|
|
|
34
34
|
Mongoid.default_client[Crypt::Patient.collection_name].drop
|
|
35
35
|
Mongoid.default_client[Crypt::Car.collection_name].drop
|
|
36
|
+
Mongoid.default_client[Crypt::Folder.collection_name].drop
|
|
36
37
|
existing_key_id = Crypt::Patient.encrypt_metadata[:key_id]
|
|
37
38
|
Crypt::Patient.set_key_id(data_key_id)
|
|
38
39
|
Crypt::Car.set_key_id(data_key_id)
|
|
40
|
+
Crypt::Note.set_key_id(data_key_id)
|
|
39
41
|
Mongoid::Config.send(:clients=, config)
|
|
40
42
|
Mongoid::Clients.with_name(:key_vault)[key_vault_collection].drop
|
|
41
43
|
Crypt::Patient.store_in(client: :encrypted)
|
|
42
44
|
Crypt::Car.store_in(client: :encrypted, database: Crypt::Car.storage_options[:database])
|
|
45
|
+
Crypt::Folder.store_in(client: :encrypted)
|
|
43
46
|
|
|
44
47
|
example.run
|
|
45
48
|
|
|
46
49
|
Crypt::Patient.reset_storage_options!
|
|
50
|
+
Crypt::Folder.reset_storage_options!
|
|
47
51
|
Crypt::Patient.set_key_id(existing_key_id)
|
|
48
52
|
Crypt::Car.set_key_id(existing_key_id)
|
|
53
|
+
Crypt::Note.set_key_id(existing_key_id)
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
it 'encrypts and decrypts fields' do
|
|
@@ -85,6 +90,17 @@ describe 'Encryption' do
|
|
|
85
90
|
end
|
|
86
91
|
end
|
|
87
92
|
|
|
93
|
+
# Nothing about the parent document says it needs a schema: it has no
|
|
94
|
+
# encrypted field of its own and no encrypt_with. Its collection still needs
|
|
95
|
+
# one, or the embedded field goes out in the clear.
|
|
96
|
+
it 'stores an embedded field encrypted when the parent has no encrypted field' do
|
|
97
|
+
folder = Crypt::Folder.create!(note: Crypt::Note.new(text: 'SECRET'))
|
|
98
|
+
unencrypted_client[Crypt::Folder.collection.name].find(_id: folder.id).first.tap do |doc|
|
|
99
|
+
expect(doc['note']['text']).to be_a(BSON::Binary)
|
|
100
|
+
expect(doc['note']['text'].type).to eq(:ciphertext)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
88
104
|
it 'stores data encrypted in the non-default database' do
|
|
89
105
|
car = Crypt::Car.create!(vin: 'VA1234')
|
|
90
106
|
unencrypted_client
|
|
@@ -94,4 +110,139 @@ describe 'Encryption' do
|
|
|
94
110
|
expect(doc[:vin].type).to eq(:ciphertext)
|
|
95
111
|
end
|
|
96
112
|
end
|
|
113
|
+
|
|
114
|
+
# The encryption schema map is keyed by namespace and built once, when the
|
|
115
|
+
# client is constructed. Whenever the namespace an encrypted model actually
|
|
116
|
+
# writes to is not in that map, the driver encrypts nothing and reports no
|
|
117
|
+
# error, so the field lands in the clear. Mongoid fails closed instead.
|
|
118
|
+
describe 'when the target namespace is not in the encryption schema map' do
|
|
119
|
+
let(:scratch_databases) do
|
|
120
|
+
%w[vehicles_dynamic vehicles_tenant_b vehicles_tenant_c tenant_a]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
around do |example|
|
|
124
|
+
original_options = {
|
|
125
|
+
Crypt::DynamicCar => Crypt::DynamicCar.storage_options,
|
|
126
|
+
Crypt::TenantCar => Crypt::TenantCar.storage_options
|
|
127
|
+
}
|
|
128
|
+
existing_key_ids = original_options.keys.to_h { |model| [ model, model.encrypt_metadata[:key_id] ] }
|
|
129
|
+
original_options.each_key do |model|
|
|
130
|
+
model.set_key_id(data_key_id)
|
|
131
|
+
model.store_in(client: :encrypted)
|
|
132
|
+
end
|
|
133
|
+
clean_scratch_data
|
|
134
|
+
# The schema map is generated when the client is built, so the client has
|
|
135
|
+
# to be built after the storage options above are in place.
|
|
136
|
+
Mongoid::Clients.clear
|
|
137
|
+
|
|
138
|
+
example.run
|
|
139
|
+
|
|
140
|
+
clean_scratch_data
|
|
141
|
+
original_options.each { |model, options| model.storage_options = options }
|
|
142
|
+
existing_key_ids.each { |model, key_id| model.set_key_id(key_id) }
|
|
143
|
+
Mongoid::Clients.clear
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def clean_scratch_data
|
|
147
|
+
scratch_databases.each { |database| unencrypted_client.use(database).database.drop }
|
|
148
|
+
# Crypt::Car writes into the shared 'vehicles' database, which other
|
|
149
|
+
# examples rely on, so remove only the documents these examples create.
|
|
150
|
+
unencrypted_client
|
|
151
|
+
.use(Crypt::Car.storage_options[:database])[Crypt::Car.collection_name.to_s]
|
|
152
|
+
.delete_many(vin: { '$in' => %w[CLIENT-1 CLIENT-2] })
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Reads with a client that has no automatic encryption, to see what really
|
|
156
|
+
# landed on disk.
|
|
157
|
+
def documents_with_plaintext_vin(database, collection, vin)
|
|
158
|
+
unencrypted_client.use(database)[collection].find(vin: vin).to_a
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
context 'when the database name is a callable' do
|
|
162
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
163
|
+
expect { Crypt::DynamicCar.create!(vin: 'DYNAMIC-1') }
|
|
164
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
165
|
+
|
|
166
|
+
expect(
|
|
167
|
+
documents_with_plaintext_vin('vehicles_dynamic', Crypt::DynamicCar.collection_name.to_s, 'DYNAMIC-1')
|
|
168
|
+
).to be_empty
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
context 'when the database name is a callable resolved per tenant' do
|
|
173
|
+
around do |example|
|
|
174
|
+
Thread.current[:tenant_database] = 'tenant_a'
|
|
175
|
+
example.run
|
|
176
|
+
Thread.current[:tenant_database] = nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
180
|
+
expect { Crypt::TenantCar.create!(vin: 'TENANT-1') }
|
|
181
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
182
|
+
|
|
183
|
+
expect(
|
|
184
|
+
documents_with_plaintext_vin('tenant_a', Crypt::TenantCar.collection_name.to_s, 'TENANT-1')
|
|
185
|
+
).to be_empty
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
context 'when the database is overridden for the block' do
|
|
190
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
191
|
+
expect { Crypt::Car.with(database: 'vehicles_tenant_b') { |car| car.create!(vin: 'BLOCK-1') } }
|
|
192
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
193
|
+
|
|
194
|
+
expect(
|
|
195
|
+
documents_with_plaintext_vin('vehicles_tenant_b', Crypt::Car.collection_name.to_s, 'BLOCK-1')
|
|
196
|
+
).to be_empty
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
context 'when the database is overridden globally' do
|
|
201
|
+
persistence_context_override :database, 'vehicles_tenant_c'
|
|
202
|
+
|
|
203
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
204
|
+
expect { Crypt::Car.create!(vin: 'GLOBAL-1') }
|
|
205
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
206
|
+
|
|
207
|
+
expect(
|
|
208
|
+
documents_with_plaintext_vin('vehicles_tenant_c', Crypt::Car.collection_name.to_s, 'GLOBAL-1')
|
|
209
|
+
).to be_empty
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
context 'when the model is routed to a client without automatic encryption' do
|
|
214
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
215
|
+
expect { Crypt::Car.with(client: :default) { |car| car.create!(vin: 'CLIENT-1') } }
|
|
216
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
217
|
+
|
|
218
|
+
expect(
|
|
219
|
+
documents_with_plaintext_vin('vehicles', Crypt::Car.collection_name.to_s, 'CLIENT-1')
|
|
220
|
+
).to be_empty
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
context 'when the parent has no encrypted field of its own' do
|
|
225
|
+
it 'does not store the embedded field in plaintext', :aggregate_failures do
|
|
226
|
+
expect { Crypt::Folder.with(client: :default) { |folder| folder.create!(note: Crypt::Note.new(text: 'EMBEDDED-1')) } }
|
|
227
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
228
|
+
|
|
229
|
+
expect(
|
|
230
|
+
unencrypted_client[Crypt::Folder.collection_name.to_s].find('note.text' => 'EMBEDDED-1').to_a
|
|
231
|
+
).to be_empty
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
context 'when the client is overridden globally to one without automatic encryption' do
|
|
236
|
+
persistence_context_override :client, :default
|
|
237
|
+
|
|
238
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
239
|
+
expect { Crypt::Car.create!(vin: 'CLIENT-2') }
|
|
240
|
+
.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
241
|
+
|
|
242
|
+
expect(
|
|
243
|
+
documents_with_plaintext_vin('vehicles', Crypt::Car.collection_name.to_s, 'CLIENT-2')
|
|
244
|
+
).to be_empty
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
97
248
|
end
|
|
@@ -135,6 +135,27 @@
|
|
|
135
135
|
$regex: bar
|
|
136
136
|
matches: false
|
|
137
137
|
|
|
138
|
+
# Object#=~ is gone as of Ruby 3.2, so an element that cannot be matched
|
|
139
|
+
# against has to be skipped rather than passed to =~.
|
|
140
|
+
- name: array field value - element that cannot be matched against
|
|
141
|
+
document:
|
|
142
|
+
title:
|
|
143
|
+
- 42
|
|
144
|
+
- foo
|
|
145
|
+
query:
|
|
146
|
+
title:
|
|
147
|
+
$regex: foo
|
|
148
|
+
matches: true
|
|
149
|
+
|
|
150
|
+
- name: array field value - no element can be matched against
|
|
151
|
+
document:
|
|
152
|
+
title:
|
|
153
|
+
- 42
|
|
154
|
+
query:
|
|
155
|
+
title:
|
|
156
|
+
$regex: foo
|
|
157
|
+
matches: false
|
|
158
|
+
|
|
138
159
|
- name: true field value
|
|
139
160
|
document:
|
|
140
161
|
title: true
|