mongoid 5.0.2 → 5.1.0

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 (55) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/LICENSE +1 -1
  5. data/README.md +1 -1
  6. data/lib/mongoid/changeable.rb +1 -1
  7. data/lib/mongoid/clients.rb +1 -0
  8. data/lib/mongoid/clients/options.rb +119 -7
  9. data/lib/mongoid/config.rb +7 -0
  10. data/lib/mongoid/config/options.rb +15 -0
  11. data/lib/mongoid/contextual/geo_near.rb +12 -0
  12. data/lib/mongoid/contextual/mongo.rb +6 -0
  13. data/lib/mongoid/criteria.rb +2 -56
  14. data/lib/mongoid/criteria/findable.rb +4 -1
  15. data/lib/mongoid/criteria/includable.rb +142 -0
  16. data/lib/mongoid/criteria/modifiable.rb +13 -1
  17. data/lib/mongoid/document.rb +21 -0
  18. data/lib/mongoid/fields/foreign_key.rb +5 -1
  19. data/lib/mongoid/fields/localized.rb +16 -1
  20. data/lib/mongoid/fields/validators/macro.rb +1 -0
  21. data/lib/mongoid/findable.rb +1 -0
  22. data/lib/mongoid/loggable.rb +1 -1
  23. data/lib/mongoid/matchable.rb +6 -1
  24. data/lib/mongoid/persistable.rb +2 -2
  25. data/lib/mongoid/relations/eager.rb +12 -5
  26. data/lib/mongoid/relations/eager/base.rb +3 -1
  27. data/lib/mongoid/relations/referenced/many.rb +39 -6
  28. data/lib/mongoid/relations/targets/enumerable.rb +3 -3
  29. data/lib/mongoid/scopable.rb +5 -2
  30. data/lib/mongoid/version.rb +1 -1
  31. data/spec/app/models/address.rb +2 -0
  32. data/spec/app/models/agent.rb +2 -0
  33. data/spec/app/models/alert.rb +2 -0
  34. data/spec/app/models/post.rb +1 -0
  35. data/spec/config/mongoid.yml +1 -0
  36. data/spec/mongoid/changeable_spec.rb +1 -1
  37. data/spec/mongoid/clients/options_spec.rb +57 -0
  38. data/spec/mongoid/config_spec.rb +38 -0
  39. data/spec/mongoid/contextual/geo_near_spec.rb +19 -0
  40. data/spec/mongoid/criteria/findable_spec.rb +11 -0
  41. data/spec/mongoid/criteria/modifiable_spec.rb +126 -0
  42. data/spec/mongoid/criteria_spec.rb +81 -5
  43. data/spec/mongoid/document_spec.rb +56 -0
  44. data/spec/mongoid/fields/foreign_key_spec.rb +23 -0
  45. data/spec/mongoid/fields/localized_spec.rb +32 -14
  46. data/spec/mongoid/matchable_spec.rb +127 -1
  47. data/spec/mongoid/persistable_spec.rb +24 -0
  48. data/spec/mongoid/relations/embedded/many_spec.rb +16 -0
  49. data/spec/mongoid/relations/referenced/many_spec.rb +60 -0
  50. data/spec/mongoid/relations/referenced/many_to_many_spec.rb +40 -0
  51. data/spec/mongoid/scopable_spec.rb +67 -0
  52. data/spec/spec_helper.rb +4 -0
  53. data/spec/support/authorization.rb +2 -1
  54. metadata +6 -5
  55. metadata.gz.sig +0 -0
@@ -55,6 +55,18 @@ module Mongoid
55
55
  create_document(:create!, attrs, &block)
56
56
  end
57
57
 
58
+ # Define attributes with which new documents will be created.
59
+ #
60
+ # @example Define attributes to be used when a new document is created.
61
+ # Person.create_with(job: 'Engineer').find_or_create_by(employer: 'MongoDB')
62
+ #
63
+ # @return [ Mongoid::Criteria ] A criteria.
64
+ #
65
+ # @since 5.1.0
66
+ def create_with(attrs = {})
67
+ where(selector.merge(attrs))
68
+ end
69
+
58
70
  # Find the first +Document+ given the conditions, or creates a new document
59
71
  # with the conditions that were supplied.
60
72
  #
@@ -160,7 +172,7 @@ module Mongoid
160
172
  #
161
173
  # @since 3.0.0
162
174
  def create_document(method, attrs = nil, &block)
163
- attributes = selector.reduce(attrs || {}) do |hash, (key, value)|
175
+ attributes = selector.reduce(attrs ? attrs.dup : {}) do |hash, (key, value)|
164
176
  unless key.to_s =~ /\$/ || value.is_a?(Hash)
165
177
  hash[key] = value
166
178
  end
@@ -180,6 +180,27 @@ module Mongoid
180
180
  attributes
181
181
  end
182
182
 
183
+ # Calls #as_json on the document with additional, Mongoid-specific options.
184
+ #
185
+ # @example Get the document as json.
186
+ # document.as_json(compact: true)
187
+ #
188
+ # @param [ Hash ] options The options.
189
+ #
190
+ # @option options [ true, false ] :compact Whether to include fields with
191
+ # nil values in the json document.
192
+ #
193
+ # @return [ Hash ] The document as json.
194
+ #
195
+ # @since 5.1.0
196
+ def as_json(options = nil)
197
+ if options && (options[:compact] == true)
198
+ super(options).reject! { |k,v| v.nil? }
199
+ else
200
+ super(options)
201
+ end
202
+ end
203
+
183
204
  # Returns an instance of the specified class with the attributes,
184
205
  # errors, and embedded documents of the current document.
185
206
  #
@@ -63,7 +63,11 @@ module Mongoid
63
63
  # @since 3.0.0
64
64
  def evolve(object)
65
65
  if object_id_field? || object.is_a?(Document)
66
- object.__evolve_object_id__
66
+ if metadata.polymorphic? && constraint
67
+ constraint.convert(object)
68
+ else
69
+ object.__evolve_object_id__
70
+ end
67
71
  else
68
72
  related_id_field.evolve(object)
69
73
  end
@@ -48,6 +48,21 @@ module Mongoid
48
48
 
49
49
  private
50
50
 
51
+ # Are fallbacks being used for this localized field.
52
+ #
53
+ # @api private
54
+ #
55
+ # @example Should fallbacks be used.
56
+ # field.fallbacks?
57
+ #
58
+ # @return [ true, false ] If fallbacks should be used.
59
+ #
60
+ # @since 5.1.0
61
+ def fallbacks?
62
+ return true if options[:fallbacks].nil?
63
+ !!options[:fallbacks]
64
+ end
65
+
51
66
  # Lookup the value from the provided object.
52
67
  #
53
68
  # @api private
@@ -62,7 +77,7 @@ module Mongoid
62
77
  # @since 3.0.0
63
78
  def lookup(object)
64
79
  locale = ::I18n.locale
65
- if ::I18n.respond_to?(:fallbacks)
80
+ if fallbacks? && ::I18n.respond_to?(:fallbacks)
66
81
  object[::I18n.fallbacks[locale].map(&:to_s).find{ |loc| object.has_key?(loc) }]
67
82
  else
68
83
  object[locale.to_s]
@@ -13,6 +13,7 @@ module Mongoid
13
13
  :identity,
14
14
  :label,
15
15
  :localize,
16
+ :fallbacks,
16
17
  :metadata,
17
18
  :pre_processed,
18
19
  :subtype,
@@ -15,6 +15,7 @@ module Mongoid
15
15
  delegate \
16
16
  :aggregates,
17
17
  :avg,
18
+ :create_with,
18
19
  :distinct,
19
20
  :each,
20
21
  :each_with_index,
@@ -48,7 +48,7 @@ module Mongoid
48
48
  # @since 3.0.0
49
49
  def default_logger
50
50
  logger = Logger.new($stdout)
51
- logger.level = Logger::INFO
51
+ logger.level = Mongoid::Config.log_level
52
52
  logger
53
53
  end
54
54
 
@@ -55,7 +55,12 @@ module Mongoid
55
55
  selector.each_pair do |key, value|
56
56
  if value.is_a?(Hash)
57
57
  value.each do |item|
58
- return false unless matcher(self, key, Hash[*item]).matches?(Hash[*item])
58
+ if item[0].to_s == "$not".freeze
59
+ item = item[1]
60
+ return false if matcher(self, key, item).matches?(item)
61
+ else
62
+ return false unless matcher(self, key, Hash[*item]).matches?(Hash[*item])
63
+ end
59
64
  end
60
65
  else
61
66
  return false unless matcher(self, key, value).matches?(value)
@@ -55,7 +55,7 @@ module Mongoid
55
55
  # @since 4.0.0
56
56
  def atomically
57
57
  begin
58
- @atomic_updates_to_execute = {}
58
+ @atomic_updates_to_execute = @atomic_updates_to_execute || {}
59
59
  yield(self) if block_given?
60
60
  persist_atomic_operations(@atomic_updates_to_execute)
61
61
  true
@@ -207,7 +207,7 @@ module Mongoid
207
207
  #
208
208
  # @since 4.0.0
209
209
  def persist_atomic_operations(operations)
210
- if persisted?
210
+ if persisted? && operations
211
211
  selector = atomic_selector
212
212
  _root.collection.find(selector).update_one(positionally(selector, operations))
213
213
  end
@@ -34,11 +34,18 @@ module Mongoid
34
34
  end
35
35
 
36
36
  def preload(relations, docs)
37
-
38
- relations.group_by do |metadata|
39
- metadata.relation
40
- end.each do |relation, associations|
41
- relation.eager_load_klass.new(associations, docs).run
37
+ grouped_relations = relations.group_by do |metadata|
38
+ metadata.inverse_class_name
39
+ end
40
+ grouped_relations.keys.each do |_klass|
41
+ grouped_relations[_klass] = grouped_relations[_klass].group_by do |metadata|
42
+ metadata.relation
43
+ end
44
+ end
45
+ grouped_relations.each do |_klass, associations|
46
+ docs = associations.collect do |_relation, association|
47
+ _relation.eager_load_klass.new(association, docs).run
48
+ end.flatten
42
49
  end
43
50
  end
44
51
  end
@@ -45,10 +45,12 @@ module Mongoid
45
45
  #
46
46
  # @since 4.0.0
47
47
  def run
48
+ @loaded = []
48
49
  while shift_metadata
49
50
  preload
51
+ @loaded << @docs.collect { |d| d.send(@metadata.name) }
50
52
  end
51
- @docs
53
+ @loaded.flatten
52
54
  end
53
55
 
54
56
  # Preload the current relation.
@@ -320,14 +320,47 @@ module Mongoid
320
320
  #
321
321
  # @since 2.0.0.rc.1
322
322
  def append(document)
323
- # @todo: remove?
324
323
  document.with(@persistence_options) if @persistence_options
324
+ with_add_callbacks(document, already_related?(document)) do
325
+ target.push(document)
326
+ characterize_one(document)
327
+ bind_one(document)
328
+ end
329
+ end
325
330
 
326
- execute_callback :before_add, document
327
- target.push(document)
328
- characterize_one(document)
329
- bind_one(document)
330
- execute_callback :after_add, document
331
+ # Execute before/after add callbacks around the block unless the objects
332
+ # already have a persisted relation.
333
+ #
334
+ # @example Execute before/after add callbacks around the block.
335
+ # relation.with_add_callbacks(document, false)
336
+ #
337
+ # @param [ Document ] document The document to append to the target.
338
+ # @param [ true, false ] already_related Whether the document is already related
339
+ # to the target.
340
+ #
341
+ # @since 5.1.0
342
+ def with_add_callbacks(document, already_related)
343
+ execute_callback :before_add, document unless already_related
344
+ yield
345
+ execute_callback :after_add, document unless already_related
346
+ end
347
+
348
+ # Whether the document and the base already have a persisted relation.
349
+ #
350
+ # @example Is the document already related to the base.
351
+ # relation.already_related?(document)
352
+ #
353
+ # @param [ Document ] document The document to possibly append to the target.
354
+ #
355
+ # @return [ true, false ] Whether the document is already related to the base and the
356
+ # relation is persisted.
357
+ #
358
+ # @since 5.1.0
359
+ def already_related?(document)
360
+ document.persisted? &&
361
+ document.__metadata &&
362
+ document.respond_to?(document.__metadata.foreign_key) &&
363
+ document.__send__(document.__metadata.foreign_key) == base.id
331
364
  end
332
365
 
333
366
  # Instantiate the binding associated with this relation.
@@ -241,7 +241,7 @@ module Mongoid
241
241
  else
242
242
  @_added, @executed = {}, true
243
243
  @_loaded = target.inject({}) do |_target, doc|
244
- _target[doc._id] = doc
244
+ _target[doc._id] = doc if doc
245
245
  _target
246
246
  end
247
247
  end
@@ -340,7 +340,7 @@ module Mongoid
340
340
  #
341
341
  # @since 3.0.15
342
342
  def marshal_dump
343
- [ _added, _loaded, _unloaded ]
343
+ [ _added, _loaded, _unloaded, @executed]
344
344
  end
345
345
 
346
346
  # Loads the data needed to Marshal.load an enumerable proxy.
@@ -352,7 +352,7 @@ module Mongoid
352
352
  #
353
353
  # @since 3.0.15
354
354
  def marshal_load(data)
355
- @_added, @_loaded, @_unloaded = data
355
+ @_added, @_loaded, @_unloaded, @executed = data
356
356
  end
357
357
 
358
358
  # Reset the enumerable back to its persisted state.
@@ -87,7 +87,8 @@ module Mongoid
87
87
  # @return [ Proc ] The default scope.
88
88
  #
89
89
  # @since 1.0.0
90
- def default_scope(value)
90
+ def default_scope(value = nil)
91
+ value = Proc.new { yield } if block_given?
91
92
  check_scope_validity(value)
92
93
  self.default_scoping = process_default_scope(value)
93
94
  end
@@ -115,7 +116,9 @@ module Mongoid
115
116
  #
116
117
  # @since 3.0.0
117
118
  def queryable
118
- Threaded.current_scope(self) || Criteria.new(self)
119
+ crit = Threaded.current_scope(self) || Criteria.new(self)
120
+ crit.embedded = true if crit.klass.embedded?
121
+ crit
119
122
  end
120
123
 
121
124
  # Create a scope that can be accessed from the class level or chained to
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Mongoid
3
- VERSION = "5.0.2"
3
+ VERSION = "5.1.0"
4
4
  end
@@ -44,6 +44,8 @@ class Address
44
44
  belongs_to :band
45
45
 
46
46
  scope :without_postcode, ->{ where(postcode: nil) }
47
+ scope :ordered, ->{ order_by(state: 1) }
48
+ scope :without_postcode_ordered, ->{ without_postcode.ordered }
47
49
  scope :rodeo, ->{ where(street: "Rodeo Dr") } do
48
50
  def mansion?
49
51
  all? { |address| address.street == "Rodeo Dr" }
@@ -5,8 +5,10 @@ class Agent
5
5
  field :number, type: String
6
6
  field :dob, type: Time
7
7
  embeds_many :names, as: :namable
8
+ embeds_one :address
8
9
  belongs_to :game
9
10
  belongs_to :agency, touch: true, autobuild: true
10
11
 
11
12
  has_and_belongs_to_many :accounts
13
+ has_and_belongs_to_many :basics
12
14
  end
@@ -2,4 +2,6 @@ class Alert
2
2
  include Mongoid::Document
3
3
  field :message, type: String
4
4
  belongs_to :account
5
+ has_many :items
6
+ belongs_to :post
5
7
  end
@@ -14,6 +14,7 @@ class Post
14
14
  has_and_belongs_to_many :tags, before_add: :before_add_tag, after_add: :after_add_tag, before_remove: :before_remove_tag, after_remove: :after_remove_tag
15
15
  has_many :videos, validate: false
16
16
  has_many :roles, validate: false
17
+ has_many :alerts
17
18
 
18
19
  belongs_to :posteable, polymorphic: true
19
20
  accepts_nested_attributes_for :posteable, autosave: true
@@ -21,3 +21,4 @@ test:
21
21
  raise_not_found_error: true
22
22
  use_activesupport_time_zone: true
23
23
  use_utc: false
24
+ log_level: :warn
@@ -940,7 +940,7 @@ describe Mongoid::Changeable do
940
940
  end
941
941
 
942
942
  it "returns a hash with indifferent access" do
943
- expect(person.changes["title"]).to eq(
943
+ expect(person.changes[:title]).to eq(
944
944
  [ nil, "Captain Obvious" ]
945
945
  )
946
946
  end
@@ -28,6 +28,41 @@ describe Mongoid::Clients::Options do
28
28
  expect(Band.new.persistence_options).to be_nil
29
29
  end
30
30
 
31
+ context 'when passed a block', if: testing_locally? do
32
+
33
+ let!(:connections_before) do
34
+ Band.mongo_client.database.command(serverStatus: 1).first['connections']['current']
35
+ end
36
+
37
+ before do
38
+ Band.with(options) do |klass|
39
+ klass.where(name: 'emily').to_a
40
+ end
41
+ end
42
+
43
+ let(:connections_after) do
44
+ Band.mongo_client.database.command(serverStatus: 1).first['connections']['current']
45
+ end
46
+
47
+ context 'when a new cluster is created by the driver' do
48
+
49
+ let(:options) { { connect_timeout: 2 } }
50
+
51
+ it 'disconnects the new cluster' do
52
+ expect(connections_after).to eq(connections_before)
53
+ end
54
+ end
55
+
56
+ context 'when the same cluster is used by the new client' do
57
+
58
+ let(:options) { { database: 'same-cluster' } }
59
+
60
+ it 'does not disconnect the original cluster' do
61
+ expect(connections_after).to eq(connections_before)
62
+ end
63
+ end
64
+ end
65
+
31
66
  context "when calling .collection method" do
32
67
 
33
68
  before do
@@ -82,6 +117,28 @@ describe Mongoid::Clients::Options do
82
117
  it "passes down the options to collection" do
83
118
  expect(instance.collection.database.name).to eq('test')
84
119
  end
120
+
121
+ context "when the object is shared between threads" do
122
+
123
+ before do
124
+ threads = []
125
+ doc = Band.create(name: "Beatles")
126
+ 100.times do |i|
127
+ threads << Thread.new do
128
+ if i % 2 == 0
129
+ doc.with(nil).set(name: "Rolling Stones")
130
+ else
131
+ doc.with(options).set(name: "Beatles")
132
+ end
133
+ end
134
+ end
135
+ threads.join
136
+ end
137
+
138
+ it "does not share the persistence options" do
139
+ expect(Band.persistence_options).to eq(nil)
140
+ end
141
+ end
85
142
  end
86
143
 
87
144
  describe "#persistence_options" do
@@ -60,6 +60,23 @@ describe Mongoid::Config do
60
60
  end
61
61
  end
62
62
 
63
+ context "when the log level is not set in the configuration" do
64
+
65
+ before do
66
+ Mongoid.configure do |config|
67
+ config.load_configuration(CONFIG)
68
+ end
69
+ end
70
+
71
+ it "sets the Mongoid logger level to the default" do
72
+ expect(Mongoid.logger.level).to eq(Logger::INFO)
73
+ end
74
+
75
+ it "sets the Mongo driver logger level to the default" do
76
+ expect(Mongo::Logger.logger.level).to eq(Logger::INFO)
77
+ end
78
+ end
79
+
63
80
  describe "#load!" do
64
81
 
65
82
  before(:all) do
@@ -89,6 +106,27 @@ describe Mongoid::Config do
89
106
  end
90
107
  end
91
108
 
109
+ context "when the log level is set in the configuration" do
110
+
111
+ before do
112
+ described_class.load!(file, :test)
113
+ end
114
+
115
+ after do
116
+ Mongoid.configure do |config|
117
+ config.load_configuration(CONFIG)
118
+ end
119
+ end
120
+
121
+ it "sets the Mongoid logger level" do
122
+ expect(Mongoid.logger.level).to eq(Logger::WARN)
123
+ end
124
+
125
+ it "sets the Mongo driver logger level" do
126
+ expect(Mongo::Logger.logger.level).to eq(Logger::WARN)
127
+ end
128
+ end
129
+
92
130
  context "when provided an environment" do
93
131
 
94
132
  before do