mongoid 9.0.0 → 9.0.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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/Rakefile +44 -21
  4. data/lib/config/locales/en.yml +20 -0
  5. data/lib/mongoid/association/accessors.rb +7 -2
  6. data/lib/mongoid/association/nested/one.rb +14 -1
  7. data/lib/mongoid/association/referenced/belongs_to/binding.rb +7 -1
  8. data/lib/mongoid/association/referenced/belongs_to/buildable.rb +1 -1
  9. data/lib/mongoid/association/referenced/belongs_to.rb +15 -0
  10. data/lib/mongoid/association/referenced/has_many.rb +9 -8
  11. data/lib/mongoid/association/referenced/has_one/buildable.rb +3 -8
  12. data/lib/mongoid/association/referenced/with_polymorphic_criteria.rb +41 -0
  13. data/lib/mongoid/atomic_update_preparer.rb +7 -6
  14. data/lib/mongoid/attributes/nested.rb +2 -1
  15. data/lib/mongoid/clients/sessions.rb +12 -15
  16. data/lib/mongoid/composable.rb +2 -0
  17. data/lib/mongoid/config.rb +9 -0
  18. data/lib/mongoid/contextual/aggregable/memory.rb +3 -2
  19. data/lib/mongoid/contextual/aggregable/mongo.rb +5 -2
  20. data/lib/mongoid/criteria/findable.rb +2 -2
  21. data/lib/mongoid/criteria/queryable/extensions/numeric.rb +15 -1
  22. data/lib/mongoid/document.rb +2 -0
  23. data/lib/mongoid/errors/invalid_around_callback.rb +16 -0
  24. data/lib/mongoid/errors/unrecognized_model_alias.rb +53 -0
  25. data/lib/mongoid/errors/unrecognized_resolver.rb +27 -0
  26. data/lib/mongoid/errors/unregistered_class.rb +47 -0
  27. data/lib/mongoid/errors.rb +4 -0
  28. data/lib/mongoid/fields.rb +13 -7
  29. data/lib/mongoid/identifiable.rb +28 -0
  30. data/lib/mongoid/interceptable.rb +18 -13
  31. data/lib/mongoid/model_resolver.rb +154 -0
  32. data/lib/mongoid/persistence_context.rb +2 -1
  33. data/lib/mongoid/scopable.rb +7 -1
  34. data/lib/mongoid/touchable.rb +1 -7
  35. data/lib/mongoid/traversable.rb +5 -0
  36. data/lib/mongoid/version.rb +1 -1
  37. data/spec/integration/associations/belongs_to_spec.rb +129 -0
  38. data/spec/integration/persistence/collection_options_spec.rb +36 -0
  39. data/spec/mongoid/association/embedded/embeds_many_query_spec.rb +4 -0
  40. data/spec/mongoid/association/referenced/belongs_to/proxy_spec.rb +1 -0
  41. data/spec/mongoid/association/referenced/belongs_to_spec.rb +58 -21
  42. data/spec/mongoid/association/referenced/has_many/buildable_spec.rb +4 -0
  43. data/spec/mongoid/association_spec.rb +14 -0
  44. data/spec/mongoid/attributes/nested_spec.rb +1 -0
  45. data/spec/mongoid/attributes_spec.rb +16 -0
  46. data/spec/mongoid/clients/transactions_spec.rb +2 -2
  47. data/spec/mongoid/contextual/aggregable/memory_spec.rb +11 -0
  48. data/spec/mongoid/contextual/aggregable/mongo_spec.rb +11 -0
  49. data/spec/mongoid/contextual/mongo_spec.rb +72 -3
  50. data/spec/mongoid/fields_spec.rb +2 -2
  51. data/spec/mongoid/interceptable_spec.rb +31 -0
  52. data/spec/mongoid/model_resolver_spec.rb +167 -0
  53. data/spec/mongoid/monkey_patches_spec.rb +1 -1
  54. data/spec/mongoid/persistence_context_spec.rb +17 -4
  55. data/spec/mongoid/scopable_spec.rb +88 -85
  56. data/spec/mongoid/serializable_spec.rb +16 -9
  57. data/spec/mongoid/touchable_spec.rb +75 -0
  58. data/spec/mongoid/touchable_spec_models.rb +16 -0
  59. data/spec/support/models/band.rb +1 -0
  60. data/spec/support/models/lat_lng.rb +6 -0
  61. metadata +20 -82
  62. checksums.yaml.gz.sig +0 -1
  63. data/spec/shared/LICENSE +0 -20
  64. data/spec/shared/bin/get-mongodb-download-url +0 -17
  65. data/spec/shared/bin/s3-copy +0 -45
  66. data/spec/shared/bin/s3-upload +0 -69
  67. data/spec/shared/lib/mrss/child_process_helper.rb +0 -80
  68. data/spec/shared/lib/mrss/cluster_config.rb +0 -231
  69. data/spec/shared/lib/mrss/constraints.rb +0 -378
  70. data/spec/shared/lib/mrss/docker_runner.rb +0 -298
  71. data/spec/shared/lib/mrss/eg_config_utils.rb +0 -51
  72. data/spec/shared/lib/mrss/event_subscriber.rb +0 -210
  73. data/spec/shared/lib/mrss/lite_constraints.rb +0 -238
  74. data/spec/shared/lib/mrss/server_version_registry.rb +0 -113
  75. data/spec/shared/lib/mrss/session_registry.rb +0 -69
  76. data/spec/shared/lib/mrss/session_registry_legacy.rb +0 -60
  77. data/spec/shared/lib/mrss/spec_organizer.rb +0 -179
  78. data/spec/shared/lib/mrss/utils.rb +0 -37
  79. data/spec/shared/share/Dockerfile.erb +0 -281
  80. data/spec/shared/share/haproxy-1.conf +0 -16
  81. data/spec/shared/share/haproxy-2.conf +0 -17
  82. data/spec/shared/shlib/config.sh +0 -27
  83. data/spec/shared/shlib/distro.sh +0 -74
  84. data/spec/shared/shlib/server.sh +0 -417
  85. data/spec/shared/shlib/set_env.sh +0 -146
  86. data.tar.gz.sig +0 -0
  87. metadata.gz.sig +0 -1
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Errors
5
+ # Raised when a polymorphic association is queried, but the type of the
6
+ # association cannot be resolved. This usually happens when the data in
7
+ # the database references a type that no longer exists.
8
+ #
9
+ # For example, consider the following model:
10
+ #
11
+ # class Manager
12
+ # include Mongoid::Document
13
+ # belongs_to :unit, polymorphic: true
14
+ # end
15
+ #
16
+ # Imagine there is a document in the `managers` collection that looks
17
+ # something like this:
18
+ #
19
+ # { _id: ..., unit_id: ..., unit_type: 'Department::Engineering' }
20
+ #
21
+ # If, at some point in your refactoring, you rename the `Department::Engineering`
22
+ # model to something else, Mongoid will no longer be able to resolve the
23
+ # type of this association, and asking for `manager.unit` will raise this
24
+ # exception.
25
+ #
26
+ # To fix this exception, you can add an alias to the model class so that it
27
+ # can still be found, even after renaming it:
28
+ #
29
+ # module Engineering
30
+ # class Department
31
+ # include Mongoid::Document
32
+ #
33
+ # identify_as 'Department::Engineering'
34
+ #
35
+ # # ...
36
+ # end
37
+ # end
38
+ #
39
+ # Better practice would be to use unique strings instead of class names to
40
+ # identify these polymorphic types in the database (e.g. 'dept' instead of
41
+ # 'Department::Engineering').
42
+ class UnrecognizedModelAlias < MongoidError
43
+ def initialize(model_alias)
44
+ super(
45
+ compose_message(
46
+ 'unrecognized_model_alias',
47
+ model_alias: model_alias.inspect
48
+ )
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Errors
5
+ # Raised when a model resolver is referenced, but not registered.
6
+ #
7
+ # class Manager
8
+ # include Mongoid::Document
9
+ # belongs_to :unit, polymorphic: :org
10
+ # end
11
+ #
12
+ # If `:org` has not previously been registered as a model resolver,
13
+ # Mongoid will raise UnrecognizedResolver when it tries to resolve
14
+ # a manager's unit.
15
+ class UnrecognizedResolver < MongoidError
16
+ def initialize(resolver)
17
+ super(
18
+ compose_message(
19
+ 'unrecognized_resolver',
20
+ resolver: resolver.inspect,
21
+ resolvers: [ :default, *Mongoid::ModelResolver.resolvers.keys ].inspect
22
+ )
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Errors
5
+ # Raised when Mongoid tries to query the identifier to use for a given
6
+ # class in a polymorphic association, but the class has not previously
7
+ # been registered by resolver that was used for the query.
8
+ #
9
+ # Here's an exammple:
10
+ #
11
+ # class Department
12
+ # include Mongoid::Document
13
+ # has_many :managers, as: :unit
14
+ # end
15
+ #
16
+ # class Manager
17
+ # include Mongoid::Document
18
+ # belongs_to :unit, polymorphic: :org
19
+ # end
20
+ #
21
+ # The Manager class is configured to use a custom resolver named `:org`
22
+ # when resolving the polymorphic `unit` association. However, the `Department`
23
+ # class is not registered with that resolver. When the program tries to
24
+ # associate a manager record with a department, it will not be able to find
25
+ # the required key in the `:org` resolver, and will fail with this exception.
26
+ #
27
+ # The solution is to make sure the `Department` class is properly registered
28
+ # with the `:org` resolver:
29
+ #
30
+ # class Department
31
+ # include Mongoid::Document
32
+ # identify_as resolver: :org
33
+ # has_many :managers, as: :unit
34
+ # end
35
+ class UnregisteredClass < MongoidError
36
+ def initialize(klass, resolver)
37
+ super(
38
+ compose_message(
39
+ 'unregistered_class',
40
+ klass: klass,
41
+ resolver: resolver.inspect
42
+ )
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
@@ -13,6 +13,7 @@ require "mongoid/errors/empty_config_file"
13
13
  require "mongoid/errors/immutable_attribute"
14
14
  require "mongoid/errors/in_memory_collation_not_supported"
15
15
  require "mongoid/errors/invalid_auto_encryption_configuration"
16
+ require "mongoid/errors/invalid_around_callback"
16
17
  require "mongoid/errors/invalid_async_query_executor"
17
18
  require "mongoid/errors/invalid_collection"
18
19
  require "mongoid/errors/invalid_config_file"
@@ -68,6 +69,9 @@ require "mongoid/errors/transaction_error"
68
69
  require "mongoid/errors/transactions_not_supported"
69
70
  require "mongoid/errors/unknown_attribute"
70
71
  require "mongoid/errors/unknown_model"
72
+ require 'mongoid/errors/unrecognized_model_alias'
73
+ require 'mongoid/errors/unrecognized_resolver'
74
+ require 'mongoid/errors/unregistered_class'
71
75
  require "mongoid/errors/unsaved_document"
72
76
  require "mongoid/errors/unsupported_javascript"
73
77
  require "mongoid/errors/validations"
@@ -49,6 +49,11 @@ module Mongoid
49
49
  # @api private
50
50
  INVALID_BSON_CLASSES = [ BSON::Decimal128, BSON::Int32, BSON::Int64 ].freeze
51
51
 
52
+ # The suffix for generated translated fields.
53
+ #
54
+ # @api private
55
+ TRANSLATIONS_SFX = '_translations'
56
+
52
57
  module ClassMethods
53
58
  # Returns the list of id fields for this model class, as both strings
54
59
  # and symbols.
@@ -101,8 +106,8 @@ module Mongoid
101
106
  ar.each_with_index do |fn, i|
102
107
  key = fn
103
108
  unless klass.fields.key?(fn) || klass.relations.key?(fn)
104
- if tr = fn.match(/(.*)_translations\z/)&.captures&.first
105
- key = tr
109
+ if fn.end_with?(TRANSLATIONS_SFX)
110
+ key = fn.delete_suffix(TRANSLATIONS_SFX)
106
111
  else
107
112
  key = fn
108
113
  end
@@ -408,7 +413,8 @@ module Mongoid
408
413
  #
409
414
  # @api private
410
415
  def database_field_name(name, relations, aliased_fields, aliased_associations)
411
- return nil unless name.present?
416
+ return "" unless name.present?
417
+
412
418
  key = name.to_s
413
419
  segment, remaining = key.split('.', 2)
414
420
 
@@ -725,11 +731,11 @@ module Mongoid
725
731
  # @api private
726
732
  def create_translations_getter(name, meth)
727
733
  generated_methods.module_eval do
728
- re_define_method("#{meth}_translations") do
734
+ re_define_method("#{meth}#{TRANSLATIONS_SFX}") do
729
735
  attributes[name] ||= {}
730
736
  attributes[name].with_indifferent_access
731
737
  end
732
- alias_method :"#{meth}_t", :"#{meth}_translations"
738
+ alias_method :"#{meth}_t", :"#{meth}#{TRANSLATIONS_SFX}"
733
739
  end
734
740
  end
735
741
 
@@ -745,14 +751,14 @@ module Mongoid
745
751
  # @api private
746
752
  def create_translations_setter(name, meth, field)
747
753
  generated_methods.module_eval do
748
- re_define_method("#{meth}_translations=") do |value|
754
+ re_define_method("#{meth}#{TRANSLATIONS_SFX}=") do |value|
749
755
  attribute_will_change!(name)
750
756
  value&.transform_values! do |_value|
751
757
  field.type.mongoize(_value)
752
758
  end
753
759
  attributes[name] = value
754
760
  end
755
- alias_method :"#{meth}_t=", :"#{meth}_translations="
761
+ alias_method :"#{meth}_t=", :"#{meth}#{TRANSLATIONS_SFX}="
756
762
  end
757
763
  end
758
764
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mongoid/model_resolver'
4
+
5
+ module Mongoid
6
+ # Implements the "identify_as" interface (for specifying type aliases
7
+ # for document classes).
8
+ module Identifiable
9
+ extend ActiveSupport::Concern
10
+
11
+ class_methods do
12
+ # Specifies aliases that may be used to identify this document
13
+ # class in polymorphic situations. By default, classes are identified
14
+ # by their class names, but alternative aliases may be used instead,
15
+ # if desired.
16
+ #
17
+ # @param [ Array<String | Symbol> ] aliases the list of aliases to
18
+ # assign to this class.
19
+ # @param [ Mongoid::ModelResolver::Interface | Symbol | :default ] resolver the
20
+ # resolver instance to use when registering the type. If :default, the default
21
+ # `ModelResolver` instance will be used. If any other symbol, it must identify a
22
+ # previously registered ModelResolver instance.
23
+ def identify_as(*aliases, resolver: :default)
24
+ Mongoid::ModelResolver.resolver(resolver).register(self, *aliases)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -161,11 +161,6 @@ module Mongoid
161
161
  # Execute the callbacks of given kind for embedded documents including
162
162
  # around callbacks.
163
163
  #
164
- # @note This method is prone to stack overflow errors if the document
165
- # has a large number of embedded documents. It is recommended to avoid
166
- # using around callbacks for embedded documents until a proper solution
167
- # is implemented.
168
- #
169
164
  # @param [ Symbol ] kind The type of callback to execute.
170
165
  # @param [ Array<Document> ] children Children to execute callbacks on. If
171
166
  # nil, callbacks will be executed on all cascadable children of
@@ -173,17 +168,27 @@ module Mongoid
173
168
  #
174
169
  # @api private
175
170
  def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block)
176
- child, *tail = (children || cascadable_children(kind))
171
+ children = (children || cascadable_children(kind))
177
172
  with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks
178
- if child.nil?
179
- block&.call
180
- elsif tail.empty?
181
- child.run_callbacks(child_callback_type(kind, child), with_children: with_children, &block)
182
- else
183
- child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do
184
- _mongoid_run_child_callbacks_with_around(kind, children: tail, &block)
173
+
174
+ return block&.call if children.empty?
175
+
176
+ fibers = children.map do |child|
177
+ Fiber.new do
178
+ child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do
179
+ Fiber.yield
180
+ end
185
181
  end
186
182
  end
183
+
184
+ fibers.each do |fiber|
185
+ fiber.resume
186
+ raise Mongoid::Errors::InvalidAroundCallback unless fiber.alive?
187
+ end
188
+
189
+ block&.call
190
+
191
+ fibers.reverse.each(&:resume)
187
192
  end
188
193
 
189
194
  # Execute the callbacks of given kind for embedded documents without
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Mongoid
6
+ # The default class for resolving model classes based on discriminant keys.
7
+ # Given a key, it will return the corresponding model class, if any. By
8
+ # default, it looks for classes with names that match the given keys, but
9
+ # additional mappings may be provided.
10
+ #
11
+ # It is also possible to instantiate multiple resolvers---and even implement
12
+ # your own---so that different sets of classes can use independent resolution
13
+ # mechanics.
14
+ class ModelResolver
15
+ # The mutex instance used to make the `.instance` method thread-safe.
16
+ #
17
+ # @api private
18
+ INSTANCE_MUTEX = Mutex.new
19
+
20
+ class << self
21
+ extend Forwardable
22
+ def_delegators :instance, :register
23
+
24
+ # Returns the default instance of the ModelResolver.
25
+ #
26
+ # @return [ Mongoid::ModelResolver ] the default ModelResolver instance.
27
+ def instance
28
+ @instance ||= INSTANCE_MUTEX.synchronize { @instance ||= new }
29
+ end
30
+
31
+ # Returns the map of registered resolvers. The default resolver is not
32
+ # included here.
33
+ #
34
+ # @return [ Hash<Symbol => Mongoid::ModelResolver::Interface> ] the hash of
35
+ # resolver instances, mapped by symbol identifier.
36
+ def resolvers
37
+ @resolvers ||= {}
38
+ end
39
+
40
+ # Returns the resolver instance that corresponds to the argument.
41
+ #
42
+ # @param [ nil | true | false Symbol | String | Mongoid::ModelResolver::Interface ] identifier_or_object
43
+ # When nil or false, returns nil. When true or :default, corresponds to the default resolver.
44
+ # When any other symbol or string, corresponds to the registered resolver with that identifier.
45
+ # Otherwise, it must be a resolver instance itself.
46
+ #
47
+ # @raise Mongoid::Errors::UnrecognizedResolver if the given identifier is a
48
+ # symbol or string and it does not match any registered resolver.
49
+ #
50
+ # @return [ Mongoid::ModelResolver::Interface ] the resolver instance corresponding to the
51
+ # given argument.
52
+ def resolver(identifier_or_object = :default)
53
+ case identifier_or_object
54
+ when nil, false then nil
55
+ when true, :default then instance
56
+ when String, Symbol
57
+ resolvers.fetch(identifier_or_object.to_sym) do |key|
58
+ raise Mongoid::Errors::UnrecognizedResolver, key
59
+ end
60
+ else identifier_or_object
61
+ end
62
+ end
63
+
64
+ # Register the given resolver under the given name.
65
+ #
66
+ # @param [ Mongoid::ModelResolver::Interface ] resolver the resolver to register.
67
+ # @param [ String | Symbol ] name the identifier to use to register the resolver.
68
+ def register_resolver(resolver, name)
69
+ resolvers[name.to_sym] = resolver
70
+ self
71
+ end
72
+ end
73
+
74
+ # Instantiates a new ModelResolver instance.
75
+ def initialize
76
+ @key_to_model = {}
77
+ @model_to_keys = {}
78
+ end
79
+
80
+ # Registers the given model class with the given keys. In addition to the given keys, the
81
+ # class name itself will be included as a key to identify the class. Keys are given in priority
82
+ # order, with highest priority keys first and lowest last. The class name, if not given explicitly,
83
+ # is always given lowest priority.
84
+ #
85
+ # If called more than once, newer keys have higher priority than older keys. All duplicate keys will
86
+ # be removed.
87
+ #
88
+ # @param [ Mongoid::Document ] klass the document class to register
89
+ # @param [ Array<String> ] *keys the list of keys to use as an alias (optional)
90
+ def register(klass, *keys)
91
+ default_key = klass.name
92
+
93
+ @model_to_keys[klass] = [ *keys, *@model_to_keys[klass], default_key ].uniq
94
+ @key_to_model[default_key] = klass
95
+
96
+ keys.each do |key|
97
+ @key_to_model[key] = klass
98
+ end
99
+
100
+ self
101
+ end
102
+
103
+ # The `Interface` concern represents the interface that custom resolvers
104
+ # must implement.
105
+ concerning :Interface do
106
+ # Returns the default (highest priority) key for the given record. This is typically
107
+ # the key that will be used when saving a new polymorphic association.
108
+ #
109
+ # @param [ Mongoid::Document ] record the record instance for which to query the default key.
110
+ #
111
+ # @raise Mongoid::Errors::UnregisteredClass if the record's class has not been registered with this resolver.
112
+ #
113
+ # @return [ String ] the default key for the record's class.
114
+ def default_key_for(record)
115
+ keys_for(record).first
116
+ end
117
+
118
+ # Returns the list of all keys for the given record's class, in priority order (with highest
119
+ # priority keys first).
120
+ #
121
+ # @param [ Mongoid::Document] record the record instance for which to query the registered keys.
122
+ #
123
+ # @raise Mongoid::Errors::UnregisteredClass if the record's class has not been registered with this resolver.
124
+ #
125
+ # @return [ Array<String> ] the list of keys that have been registered for the given class.
126
+ def keys_for(record)
127
+ @model_to_keys.fetch(record.class) do |klass|
128
+ # figure out which resolver this is
129
+ resolver = if self == Mongoid::ModelResolver.instance
130
+ :default
131
+ else
132
+ Mongoid::ModelResolver.resolvers.keys.detect { |k| Mongoid::ModelResolver.resolvers[k] == self }
133
+ end
134
+ resolver ||= self # if it hasn't been registered, we'll show it the best we can
135
+ raise Mongoid::Errors::UnregisteredClass.new(klass, resolver)
136
+ end
137
+ end
138
+
139
+ # Returns the document class that has been registered by the given key.
140
+ #
141
+ # @param [ String ] key the key by which to query the corresponding class.
142
+ #
143
+ # @raise Mongoid::Errors::UnrecognizedModelAlias if the given key has not
144
+ # been registered with this resolver.
145
+ #
146
+ # @return [ Class ] the document class that has been registered with the given key.
147
+ def model_for(key)
148
+ @key_to_model.fetch(key) do
149
+ raise Mongoid::Errors::UnrecognizedModelAlias, key
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -25,7 +25,8 @@ module Mongoid
25
25
  # @return [ Array<Symbol> ] The list of extra options besides client options
26
26
  # that determine the persistence context.
27
27
  EXTRA_OPTIONS = [ :client,
28
- :collection
28
+ :collection,
29
+ :collection_options
29
30
  ].freeze
30
31
 
31
32
  # The full list of valid persistence context options.
@@ -294,7 +294,13 @@ module Mongoid
294
294
  scope = instance_exec(*args, **kwargs, &scoping[:scope])
295
295
  extension = scoping[:extension]
296
296
  to_merge = scope || queryable
297
- criteria = to_merge.empty_and_chainable? ? to_merge : with_default_scope.merge(to_merge)
297
+
298
+ criteria = if Mongoid.allow_scopes_to_unset_default_scope
299
+ to_merge
300
+ else
301
+ to_merge.empty_and_chainable? ? to_merge : with_default_scope.merge(to_merge)
302
+ end
303
+
298
304
  criteria.extend(extension)
299
305
  criteria
300
306
  end
@@ -78,7 +78,7 @@ module Mongoid
78
78
  field = database_field_name(field)
79
79
 
80
80
  write_attribute(:updated_at, now) if respond_to?("updated_at=")
81
- write_attribute(field, now) if field
81
+ write_attribute(field, now) if field.present?
82
82
 
83
83
  touches = _extract_touches_from_atomic_sets(field) || {}
84
84
  touches.merge!(_parent._gather_touch_updates(now) || {}) if _touchable_parent?
@@ -212,12 +212,6 @@ module Mongoid
212
212
  #
213
213
  # @return [ Symbol ] The method name.
214
214
  def define_relation_touch_method(name, association)
215
- relation_classes = if association.polymorphic?
216
- association.send(:inverse_association_classes)
217
- else
218
- [ association.relation_class ]
219
- end
220
-
221
215
  method_name = "touch_#{name}_after_create_or_destroy"
222
216
  association.inverse_class.class_eval do
223
217
  define_method(method_name) do
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'mongoid/fields/validators/macro'
4
+ require 'mongoid/model_resolver'
4
5
 
5
6
  module Mongoid
6
7
  # Mixin module included in Mongoid::Document to provide behavior
@@ -32,6 +33,10 @@ module Mongoid
32
33
  # rubocop:disable Metrics/AbcSize
33
34
  def inherited(subclass)
34
35
  super
36
+
37
+ # Register the new subclass with the resolver subsystem
38
+ Mongoid::ModelResolver.register(subclass)
39
+
35
40
  @_type = nil
36
41
  subclass.aliased_fields = aliased_fields.dup
37
42
  subclass.localized_fields = localized_fields.dup
@@ -2,5 +2,5 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  module Mongoid
5
- VERSION = "9.0.0"
5
+ VERSION = "9.0.2"
6
6
  end
@@ -2,8 +2,40 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  require 'spec_helper'
5
+ require 'support/feature_sandbox'
6
+
5
7
  require_relative '../../mongoid/association/referenced/has_one_models'
6
8
 
9
+ def quarantine(context, polymorphic:, dept_aliases:, team_aliases:)
10
+ state = {}
11
+
12
+ context.before(:context) do
13
+ state[:quarantine] = FeatureSandbox.start_quarantine
14
+
15
+ # Have to eval this, because otherwise we get syntax errors when defining a class
16
+ # inside a method.
17
+ #
18
+ # I know the scissors are sharp! But I want to run with them anwyay!
19
+ Object.class_eval <<-RUBY
20
+ class SandboxManager; include Mongoid::Document; end
21
+ class SandboxDepartment; include Mongoid::Document; end
22
+ class SandboxTeam; include Mongoid::Document; end
23
+ RUBY
24
+
25
+ SandboxManager.belongs_to :unit, polymorphic: polymorphic
26
+
27
+ SandboxDepartment.identify_as *dept_aliases, resolver: polymorphic
28
+ SandboxDepartment.has_many :sandbox_managers, as: :unit
29
+
30
+ SandboxTeam.identify_as *team_aliases, resolver: polymorphic
31
+ SandboxTeam.has_one :sandbox_manager, as: :unit
32
+ end
33
+
34
+ context.after(:context) do
35
+ FeatureSandbox.end_quarantine(state[:quarantine])
36
+ end
37
+ end
38
+
7
39
  describe 'belongs_to associations' do
8
40
  context 'referencing top level classes when source class is namespaced' do
9
41
  let(:college) { HomCollege.create! }
@@ -31,4 +63,101 @@ describe 'belongs_to associations' do
31
63
  expect(instance.movie).to eq movie
32
64
  end
33
65
  end
66
+
67
+ context 'when the association is polymorphic' do
68
+ let(:dept_manager) { SandboxManager.create(unit: department) }
69
+ let(:team_manager) { SandboxManager.create(unit: team) }
70
+ let(:department) { SandboxDepartment.create }
71
+ let(:team) { SandboxTeam.create }
72
+
73
+ shared_context 'it finds the associated records' do
74
+ it 'successfully finds the manager\'s unit' do
75
+ expect(dept_manager.reload.unit).to be == department
76
+ expect(team_manager.reload.unit).to be == team
77
+ end
78
+
79
+ it 'successfully finds the unit\'s manager' do
80
+ dept_manager; team_manager # make sure these are created first...
81
+
82
+ expect(department.reload.sandbox_managers).to be == [ dept_manager ]
83
+ expect(team.reload.sandbox_manager).to be == team_manager
84
+ end
85
+ end
86
+
87
+ shared_context 'it searches for alternative aliases' do
88
+ it 'successfully finds the corresponding unit when unit_type is a different alias' do
89
+ dept_manager.update unit_type: 'sandbox_dept'
90
+ dept_manager.reload
91
+
92
+ team_manager.update unit_type: 'group'
93
+ team_manager.reload
94
+
95
+ expect(dept_manager.reload.unit_type).to be == 'sandbox_dept'
96
+ expect(dept_manager.unit).to be == department
97
+
98
+ expect(team_manager.reload.unit_type).to be == 'group'
99
+ expect(team_manager.unit).to be == team
100
+ end
101
+ end
102
+
103
+ context 'when the association uses the default resolver' do
104
+ context 'when there are no aliases given' do
105
+ quarantine(self, polymorphic: true, dept_aliases: [], team_aliases: [])
106
+
107
+ it 'populates the unit_type with the class name' do
108
+ expect(dept_manager.unit_type).to be == 'SandboxDepartment'
109
+ expect(team_manager.unit_type).to be == 'SandboxTeam'
110
+ end
111
+
112
+ it_behaves_like 'it finds the associated records'
113
+ end
114
+
115
+ context 'when there are multiple aliases given' do
116
+ quarantine(self, polymorphic: true, dept_aliases: %w[ dept sandbox_dept ], team_aliases: %w[ team group ])
117
+
118
+ it 'populates the unit_type with the first alias' do
119
+ expect(dept_manager.unit_type).to be == 'dept'
120
+ expect(team_manager.unit_type).to be == 'team'
121
+ end
122
+
123
+ it_behaves_like 'it finds the associated records'
124
+ it_behaves_like 'it searches for alternative aliases'
125
+ end
126
+ end
127
+
128
+ context 'when the association uses a registered resolver' do
129
+ before(:context) { Mongoid::ModelResolver.register_resolver Mongoid::ModelResolver.new, :sandbox }
130
+ quarantine(self, polymorphic: :sandbox, dept_aliases: %w[ dept sandbox_dept ], team_aliases: %w[ team group ])
131
+
132
+ it 'does not include the aliases in the default resolver' do
133
+ expect(Mongoid::ModelResolver.instance.keys_for(SandboxDepartment.new)).not_to include('dept')
134
+ end
135
+
136
+ it 'populates the unit_type with the first alias' do
137
+ expect(dept_manager.unit_type).to be == 'dept'
138
+ expect(team_manager.unit_type).to be == 'team'
139
+ end
140
+
141
+ it_behaves_like 'it finds the associated records'
142
+ it_behaves_like 'it searches for alternative aliases'
143
+ end
144
+
145
+ context 'when the association uses an unregistered resolver' do
146
+ quarantine(self, polymorphic: Mongoid::ModelResolver.new,
147
+ dept_aliases: %w[ dept sandbox_dept ],
148
+ team_aliases: %w[ team group ])
149
+
150
+ it 'does not include the aliases in the default resolver' do
151
+ expect(Mongoid::ModelResolver.instance.keys_for(SandboxDepartment.new)).not_to include('dept')
152
+ end
153
+
154
+ it 'populates the unit_type with the first alias' do
155
+ expect(dept_manager.unit_type).to be == 'dept'
156
+ expect(team_manager.unit_type).to be == 'team'
157
+ end
158
+
159
+ it_behaves_like 'it finds the associated records'
160
+ it_behaves_like 'it searches for alternative aliases'
161
+ end
162
+ end
34
163
  end