mongoid 9.0.7 → 9.0.9

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mongoid/association/embedded/batchable.rb +11 -10
  3. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +64 -29
  4. data/lib/mongoid/association/nested/many.rb +2 -0
  5. data/lib/mongoid/association/nested/one.rb +1 -1
  6. data/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb +0 -6
  7. data/lib/mongoid/association/referenced/has_many/enumerable.rb +40 -0
  8. data/lib/mongoid/association/referenced/has_many/proxy.rb +17 -5
  9. data/lib/mongoid/attributes.rb +19 -1
  10. data/lib/mongoid/changeable.rb +10 -1
  11. data/lib/mongoid/clients/sessions.rb +3 -4
  12. data/lib/mongoid/config.rb +1 -1
  13. data/lib/mongoid/contextual/aggregable/mongo.rb +6 -1
  14. data/lib/mongoid/contextual/mongo.rb +8 -89
  15. data/lib/mongoid/pluckable.rb +132 -0
  16. data/lib/mongoid/railties/bson_object_id_serializer.rb +7 -0
  17. data/lib/mongoid/reloadable.rb +6 -0
  18. data/lib/mongoid/traversable.rb +0 -2
  19. data/lib/mongoid/version.rb +1 -1
  20. data/spec/integration/associations/embeds_many_spec.rb +110 -0
  21. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +81 -0
  22. data/spec/integration/associations/has_many_spec.rb +56 -0
  23. data/spec/integration/associations/has_one_spec.rb +55 -3
  24. data/spec/mongoid/association/referenced/has_many/enumerable_spec.rb +394 -0
  25. data/spec/mongoid/association/referenced/has_many_models.rb +24 -0
  26. data/spec/mongoid/association/referenced/has_one_models.rb +10 -2
  27. data/spec/mongoid/attributes_spec.rb +13 -0
  28. data/spec/mongoid/clients/transactions_spec.rb +162 -1
  29. data/spec/mongoid/clients/transactions_spec_models.rb +93 -0
  30. data/spec/mongoid/contextual/aggregable/mongo_spec.rb +33 -0
  31. data/spec/mongoid/reloadable_spec.rb +24 -0
  32. data/spec/shared/CANDIDATE.md +28 -0
  33. data/spec/shared/lib/mrss/spec_organizer.rb +32 -3
  34. data/spec/shared/shlib/server.sh +1 -1
  35. data/spec/support/models/company.rb +2 -0
  36. data/spec/support/models/passport.rb +1 -0
  37. data/spec/support/models/product.rb +2 -0
  38. data/spec/support/models/seo.rb +2 -0
  39. metadata +7 -4
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ # Provides shared behavior for any document with "pluck" functionality.
5
+ #
6
+ # @api private
7
+ module Pluckable
8
+ extend ActiveSupport::Concern
9
+
10
+ private
11
+
12
+ # Prepares the field names for plucking by normalizing them to their
13
+ # database field names. Also prepares a projection hash if requested.
14
+ def prepare_pluck(field_names, document_class: klass, prepare_projection: false)
15
+ normalized_field_names = []
16
+ projection = {}
17
+
18
+ field_names.each do |f|
19
+ db_fn = document_class.database_field_name(f)
20
+ normalized_field_names.push(db_fn)
21
+
22
+ next unless prepare_projection
23
+
24
+ cleaned_name = document_class.cleanse_localized_field_names(f)
25
+ canonical_name = document_class.database_field_name(cleaned_name)
26
+ projection[canonical_name] = true
27
+ end
28
+
29
+ { field_names: normalized_field_names, projection: projection }
30
+ end
31
+
32
+ # Plucks the given field names from the given documents.
33
+ def pluck_from_documents(documents, field_names, document_class: klass)
34
+ documents.reduce([]) do |plucked, doc|
35
+ values = field_names.map { |name| extract_value(doc, name.to_s, document_class) }
36
+ plucked << ((values.size == 1) ? values.first : values)
37
+ end
38
+ end
39
+
40
+ # Fetch the element from the given hash and demongoize it using the
41
+ # given field. If the obj is an array, map over it and call this method
42
+ # on all of its elements.
43
+ #
44
+ # @param [ Hash | Array<Hash> ] obj The hash or array of hashes to fetch from.
45
+ # @param [ String ] key The key to fetch from the hash.
46
+ # @param [ Field ] field The field to use for demongoization.
47
+ #
48
+ # @return [ Object ] The demongoized value.
49
+ def fetch_and_demongoize(obj, key, field)
50
+ if obj.is_a?(Array)
51
+ obj.map { |doc| fetch_and_demongoize(doc, key, field) }
52
+ else
53
+ value = obj.try(:fetch, key, nil)
54
+ field ? field.demongoize(value) : value.class.demongoize(value)
55
+ end
56
+ end
57
+
58
+ # Extracts the value for the given field name from the given attribute
59
+ # hash.
60
+ #
61
+ # @param [ Hash ] attrs The attributes hash.
62
+ # @param [ String ] field_name The name of the field to extract.
63
+ #
64
+ # @return [ Object ] The value for the given field name
65
+ def extract_value(attrs, field_name, document_class)
66
+ i = 1
67
+ num_meths = field_name.count('.') + 1
68
+ curr = attrs.dup
69
+
70
+ document_class.traverse_association_tree(field_name) do |meth, obj, is_field|
71
+ field = obj if is_field
72
+
73
+ # use the correct document class to check for localized fields on
74
+ # embedded documents.
75
+ document_class = obj.klass if obj.respond_to?(:klass)
76
+
77
+ is_translation = false
78
+ # If no association or field was found, check if the meth is an
79
+ # _translations field.
80
+ if obj.nil? && (tr = meth.match(/(.*)_translations\z/)&.captures&.first)
81
+ is_translation = true
82
+ meth = document_class.database_field_name(tr)
83
+ end
84
+
85
+ curr = descend(i, curr, meth, field, num_meths, is_translation)
86
+
87
+ i += 1
88
+ end
89
+ curr
90
+ end
91
+
92
+ # Descend one level in the attribute hash.
93
+ #
94
+ # @param [ Integer ] part The current part index.
95
+ # @param [ Hash | Array<Hash> ] current The current level in the attribute hash.
96
+ # @param [ String ] method_name The method name to descend to.
97
+ # @param [ Field|nil ] field The field to use for demongoization.
98
+ # @param [ Boolean ] is_translation Whether the method is an _translations field.
99
+ # @param [ Integer ] part_count The total number of parts in the field name.
100
+ #
101
+ # @return [ Object ] The value at the next level.
102
+ #
103
+ # rubocop:disable Metrics/ParameterLists
104
+ def descend(part, current, method_name, field, part_count, is_translation)
105
+ # 1. If curr is an array fetch from all elements in the array.
106
+ # 2. If the field is localized, and is not an _translations field
107
+ # (_translations fields don't show up in the fields hash).
108
+ # - If this is the end of the methods, return the translation for
109
+ # the current locale.
110
+ # - Otherwise, return the whole translations hash so the next method
111
+ # can select the language it wants.
112
+ # 3. If the meth is an _translations field, do not demongoize the
113
+ # value so the full hash is returned.
114
+ # 4. Otherwise, fetch and demongoize the value for the key meth.
115
+ if current.is_a? Array
116
+ res = fetch_and_demongoize(current, method_name, field)
117
+ res.empty? ? nil : res
118
+ elsif !is_translation && field&.localized?
119
+ if part < part_count
120
+ current.try(:fetch, method_name, nil)
121
+ else
122
+ fetch_and_demongoize(current, method_name, field)
123
+ end
124
+ elsif is_translation
125
+ current.try(:fetch, method_name, nil)
126
+ else
127
+ fetch_and_demongoize(current, method_name, field)
128
+ end
129
+ end
130
+ # rubocop:enable Metrics/ParameterLists
131
+ end
132
+ end
@@ -33,6 +33,13 @@ module Mongoid
33
33
  def deserialize(string)
34
34
  BSON::ObjectId.from_string(string)
35
35
  end
36
+
37
+ # Returns the klass this serializer handles.
38
+ #
39
+ # @return [ BSON::ObjectId ] The class this serializer handles.
40
+ def klass
41
+ BSON::ObjectId
42
+ end
36
43
  end
37
44
  end
38
45
  end
@@ -17,6 +17,12 @@ module Mongoid
17
17
  reloaded = _reload
18
18
  check_for_deleted_document!(reloaded)
19
19
 
20
+ # In an instance where we create a new document, but set the ID to an existing one,
21
+ # when the document is reloaded, we want to set new_record to false.
22
+ # This is necessary otherwise saving will fail, as it will try to insert the document,
23
+ # instead of attempting to update the existing document.
24
+ @new_record = false unless reloaded.nil? || reloaded.empty?
25
+
20
26
  reset_object!(reloaded)
21
27
 
22
28
  run_callbacks(:find) unless _find_callbacks.empty?
@@ -131,7 +131,6 @@ module Mongoid
131
131
  # @param [ String ] value The discriminator key to set.
132
132
  #
133
133
  # @api private
134
- # rubocop:disable Metrics/AbcSize
135
134
  def discriminator_key=(value)
136
135
  raise Errors::InvalidDiscriminatorKeyTarget.new(self, superclass) if hereditary?
137
136
 
@@ -159,7 +158,6 @@ module Mongoid
159
158
  default_proc = -> { self.class.discriminator_value }
160
159
  field(discriminator_key, default: default_proc, type: String)
161
160
  end
162
- # rubocop:enable Metrics/AbcSize
163
161
 
164
162
  # Returns the discriminator key.
165
163
  #
@@ -5,5 +5,5 @@ module Mongoid
5
5
  #
6
6
  # Note that this file is automatically updated via `rake candidate:create`.
7
7
  # Manual changes to this file will be overwritten by that rake task.
8
- VERSION = '9.0.7'
8
+ VERSION = '9.0.9'
9
9
  end
@@ -3,6 +3,24 @@
3
3
 
4
4
  require 'spec_helper'
5
5
 
6
+ module EmbedsManySpec
7
+ class Post
8
+ include Mongoid::Document
9
+ field :title, type: String
10
+ embeds_many :comments, class_name: 'EmbedsManySpec::Comment', as: :container
11
+ accepts_nested_attributes_for :comments
12
+ end
13
+
14
+ class Comment
15
+ include Mongoid::Document
16
+ field :content, type: String
17
+ validates :content, presence: true
18
+ embedded_in :container, polymorphic: true
19
+ embeds_many :comments, class_name: 'EmbedsManySpec::Comment', as: :container
20
+ accepts_nested_attributes_for :comments
21
+ end
22
+ end
23
+
6
24
  describe 'embeds_many associations' do
7
25
 
8
26
  context 're-associating the same object' do
@@ -201,6 +219,47 @@ describe 'embeds_many associations' do
201
219
  include_examples 'persists correctly'
202
220
  end
203
221
  end
222
+
223
+ context 'including duplicates in the assignment' do
224
+ let(:canvas) do
225
+ Canvas.create!(shapes: [Shape.new])
226
+ end
227
+
228
+ shared_examples 'persists correctly' do
229
+ it 'persists correctly' do
230
+ canvas.shapes.length.should eq 2
231
+ _canvas = Canvas.find(canvas.id)
232
+ _canvas.shapes.length.should eq 2
233
+ end
234
+ end
235
+
236
+ context 'via assignment operator' do
237
+ before do
238
+ canvas.shapes = [ canvas.shapes.first, Shape.new, canvas.shapes.first ]
239
+ canvas.save!
240
+ end
241
+
242
+ include_examples 'persists correctly'
243
+ end
244
+
245
+ context 'via attributes=' do
246
+ before do
247
+ canvas.attributes = { shapes: [ canvas.shapes.first, Shape.new, canvas.shapes.first ] }
248
+ canvas.save!
249
+ end
250
+
251
+ include_examples 'persists correctly'
252
+ end
253
+
254
+ context 'via assign_attributes' do
255
+ before do
256
+ canvas.assign_attributes(shapes: [ canvas.shapes.first, Shape.new, canvas.shapes.first ])
257
+ canvas.save!
258
+ end
259
+
260
+ include_examples 'persists correctly'
261
+ end
262
+ end
204
263
  end
205
264
 
206
265
  context 'when an anonymous class defines an embeds_many association' do
@@ -217,4 +276,55 @@ describe 'embeds_many associations' do
217
276
  expect(klass.new.addresses.build).to be_a Address
218
277
  end
219
278
  end
279
+
280
+ context 'with deeply nested trees' do
281
+ let(:post) { EmbedsManySpec::Post.create!(title: 'Post') }
282
+ let(:child) { post.comments.create!(content: 'Child') }
283
+
284
+ # creating grandchild will cascade to create the other documents
285
+ let!(:grandchild) { child.comments.create!(content: 'Grandchild') }
286
+
287
+ let(:updated_parent_title) { 'Post Updated' }
288
+ let(:updated_grandchild_content) { 'Grandchild Updated' }
289
+
290
+ context 'with nested attributes' do
291
+ let(:attributes) do
292
+ {
293
+ title: updated_parent_title,
294
+ comments_attributes: [
295
+ {
296
+ # no change for comment1
297
+ _id: child.id,
298
+ comments_attributes: [
299
+ {
300
+ _id: grandchild.id,
301
+ content: updated_grandchild_content,
302
+ }
303
+ ]
304
+ }
305
+ ]
306
+ }
307
+ end
308
+
309
+ context 'when the grandchild is invalid' do
310
+ let(:updated_grandchild_content) { '' } # invalid value
311
+
312
+ it 'will not save the parent' do
313
+ expect(post.update(attributes)).to be_falsey
314
+ expect(post.errors).not_to be_empty
315
+ expect(post.reload.title).not_to eq(updated_parent_title)
316
+ expect(grandchild.reload.content).not_to eq(updated_grandchild_content)
317
+ end
318
+ end
319
+
320
+ context 'when the grandchild is valid' do
321
+ it 'will save the parent' do
322
+ expect(post.update(attributes)).to be_truthy
323
+ expect(post.errors).to be_empty
324
+ expect(post.reload.title).to eq(updated_parent_title)
325
+ expect(grandchild.reload.content).to eq(updated_grandchild_content)
326
+ end
327
+ end
328
+ end
329
+ end
220
330
  end
@@ -23,6 +23,38 @@ module HabtmSpec
23
23
  include Mongoid::Document
24
24
  field :file, type: String
25
25
  end
26
+
27
+ class Item
28
+ include Mongoid::Document
29
+
30
+ field :title, type: String
31
+
32
+ has_and_belongs_to_many :colors, class_name: 'HabtmSpec::Color', inverse_of: :items
33
+
34
+ accepts_nested_attributes_for :colors
35
+ end
36
+
37
+ class Beam
38
+ include Mongoid::Document
39
+
40
+ field :name, type: String
41
+ validates :name, presence: true
42
+
43
+ has_and_belongs_to_many :colors, class_name: 'HabtmSpec::Color', inverse_of: :beams
44
+
45
+ accepts_nested_attributes_for :colors
46
+ end
47
+
48
+ class Color
49
+ include Mongoid::Document
50
+
51
+ field :name, type: String
52
+
53
+ has_and_belongs_to_many :items, class_name: 'HabtmSpec::Item', inverse_of: :colors
54
+ has_and_belongs_to_many :beams, class_name: 'HabtmSpec::Beam', inverse_of: :colors
55
+
56
+ accepts_nested_attributes_for :items, :beams
57
+ end
26
58
  end
27
59
 
28
60
  describe 'has_and_belongs_to_many associations' do
@@ -59,4 +91,53 @@ describe 'has_and_belongs_to_many associations' do
59
91
  expect { image_block.save! }.not_to raise_error
60
92
  end
61
93
  end
94
+
95
+ context 'with deeply nested trees' do
96
+ let(:item) { HabtmSpec::Item.create!(title: 'Item') }
97
+ let(:beam) { HabtmSpec::Beam.create!(name: 'Beam') }
98
+ let!(:color) { HabtmSpec::Color.create!(name: 'Red', items: [ item ], beams: [ beam ]) }
99
+
100
+ let(:updated_item_title) { 'Item Updated' }
101
+ let(:updated_beam_name) { 'Beam Updated' }
102
+
103
+ context 'with nested attributes' do
104
+ let(:attributes) do
105
+ {
106
+ title: updated_item_title,
107
+ colors_attributes: [
108
+ {
109
+ # no change for color
110
+ _id: color.id,
111
+ beams_attributes: [
112
+ {
113
+ _id: beam.id,
114
+ name: updated_beam_name,
115
+ }
116
+ ]
117
+ }
118
+ ]
119
+ }
120
+ end
121
+
122
+ context 'when the beam is invalid' do
123
+ let(:updated_beam_name) { '' } # invalid value
124
+
125
+ it 'will not save the parent' do
126
+ expect(item.update(attributes)).to be_falsey
127
+ expect(item.errors).not_to be_empty
128
+ expect(item.reload.title).not_to eq(updated_item_title)
129
+ expect(beam.reload.name).not_to eq(updated_beam_name)
130
+ end
131
+ end
132
+
133
+ context 'when the beam is valid' do
134
+ it 'will save the parent' do
135
+ expect(item.update(attributes)).to be_truthy
136
+ expect(item.errors).to be_empty
137
+ expect(item.reload.title).to eq(updated_item_title)
138
+ expect(beam.reload.name).to eq(updated_beam_name)
139
+ end
140
+ end
141
+ end
142
+ end
62
143
  end
@@ -126,4 +126,60 @@ describe 'has_many associations' do
126
126
  end
127
127
  end
128
128
  end
129
+
130
+ context 'with deeply nested trees' do
131
+ let(:post) { HmmPost.create!(title: 'Post') }
132
+ let(:child) { post.comments.create!(title: 'Child') }
133
+
134
+ # creating grandchild will cascade to create the other documents
135
+ let!(:grandchild) { child.comments.create!(title: 'Grandchild') }
136
+
137
+ let(:updated_parent_title) { 'Post Updated' }
138
+ let(:updated_grandchild_title) { 'Grandchild Updated' }
139
+
140
+ context 'with nested attributes' do
141
+ let(:attributes) do
142
+ {
143
+ title: updated_parent_title,
144
+ comments_attributes: [
145
+ {
146
+ # no change for comment1
147
+ _id: child.id,
148
+ comments_attributes: [
149
+ {
150
+ _id: grandchild.id,
151
+ title: updated_grandchild_title,
152
+ num: updated_grandchild_num,
153
+ }
154
+ ]
155
+ }
156
+ ]
157
+ }
158
+ end
159
+
160
+ context 'when the grandchild is invalid' do
161
+ let(:updated_grandchild_num) { -1 } # invalid value
162
+
163
+ it 'will not save the parent' do
164
+ expect(post.update(attributes)).to be_falsey
165
+ expect(post.errors).not_to be_empty
166
+ expect(post.reload.title).not_to eq(updated_parent_title)
167
+ expect(grandchild.reload.title).not_to eq(updated_grandchild_title)
168
+ expect(grandchild.num).not_to eq(updated_grandchild_num)
169
+ end
170
+ end
171
+
172
+ context 'when the grandchild is valid' do
173
+ let(:updated_grandchild_num) { 1 }
174
+
175
+ it 'will save the parent' do
176
+ expect(post.update(attributes)).to be_truthy
177
+ expect(post.errors).to be_empty
178
+ expect(post.reload.title).to eq(updated_parent_title)
179
+ expect(grandchild.reload.title).to eq(updated_grandchild_title)
180
+ expect(grandchild.num).to eq(updated_grandchild_num)
181
+ end
182
+ end
183
+ end
184
+ end
129
185
  end
@@ -224,7 +224,7 @@ describe 'has_one associations' do
224
224
  end
225
225
 
226
226
  context "when explicitly setting the foreign key" do
227
- let(:comment2) { HomComment.new(post_id: post.id, content: "2") }
227
+ let(:comment2) { HomComment.new(container_id: post.id, container_type: post.class.name, content: "2") }
228
228
 
229
229
  it "persists the new comment" do
230
230
  post.comment = comment1
@@ -264,10 +264,62 @@ describe 'has_one associations' do
264
264
 
265
265
  it "does not overwrite the original value" do
266
266
  pending "MONGOID-3999"
267
- p1 = comment.post
267
+ p1 = comment.container
268
268
  expect(p1.title).to eq("post 1")
269
- comment.post = post2
269
+ comment.container = post2
270
270
  expect(p1.title).to eq("post 1")
271
271
  end
272
272
  end
273
+
274
+ context 'with deeply nested trees' do
275
+ let(:post) { HomPost.create!(title: 'Post') }
276
+ let(:child) { post.create_comment(content: 'Child') }
277
+
278
+ # creating grandchild will cascade to create the other documents
279
+ let!(:grandchild) { child.create_comment(content: 'Grandchild') }
280
+
281
+ let(:updated_parent_title) { 'Post Updated' }
282
+ let(:updated_grandchild_content) { 'Grandchild Updated' }
283
+
284
+ context 'with nested attributes' do
285
+ let(:attributes) do
286
+ {
287
+ title: updated_parent_title,
288
+ comment_attributes: {
289
+ # no change for child
290
+ _id: child.id,
291
+ comment_attributes: {
292
+ _id: grandchild.id,
293
+ content: updated_grandchild_content,
294
+ num: updated_grandchild_num,
295
+ }
296
+ }
297
+ }
298
+ end
299
+
300
+ context 'when the grandchild is invalid' do
301
+ let(:updated_grandchild_num) { -1 } # invalid value
302
+
303
+ it 'will not save the parent' do
304
+ expect(post.update(attributes)).to be_falsey
305
+ expect(post.errors).not_to be_empty
306
+ expect(post.reload.title).not_to eq(updated_parent_title)
307
+ expect(grandchild.reload.content).not_to eq(updated_grandchild_content)
308
+ expect(grandchild.num).not_to eq(updated_grandchild_num)
309
+ end
310
+ end
311
+
312
+ context 'when the grandchild is valid' do
313
+ let(:updated_grandchild_num) { 1 }
314
+
315
+ it 'will save the parent' do
316
+ expect(post.update(attributes)).to be_truthy
317
+ expect(post.errors).to be_empty
318
+ expect(post.reload.title).to eq(updated_parent_title)
319
+ expect(grandchild.reload.content).to eq(updated_grandchild_content)
320
+ expect(grandchild.num).to eq(updated_grandchild_num)
321
+ end
322
+ end
323
+ end
324
+ end
273
325
  end