diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f78821..a0a9d95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ +#### 0.2.5 + +* Improvements to `:touch` callback support on `:embedded_in`. +* Backport fix to an infinite loop issue related to `:touch` callbacks from Mongoid 6. + #### 0.2.4 * Support aliases for index options. diff --git a/README.md b/README.md index 0e63dde..f2c75d2 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ If you would only like some of the patches, please copy and paste the code to yo | `instrument.rb` | Backport instrumentation change to Moped 1. | ● | ○ | ○ | | `reorder.rb` | Backport `Criteria#reorder` method from Mongoid 4. | ● | ○ | ○ | | `only_pluck_localized.rb` | Backport [PR #4299](https://github.com/mongodb/mongoid/pull/4299) from Mongoid 6 which fixes `#only`, `#without`, and `#pluck` with localized fields. | ● | × | × | -| `embedded_touch.rb` | Backport [Issue #3310](https://github.com/mongodb/mongoid/commit/a94c2f43573e58f973913c881ad9d11d62bf857c) from Mongoid 4 to add `:touch` option to `embedded_in`. | ● | ○ | ○ | +| `embedded_touch.rb` (1) | Backport [Issue #3310](https://github.com/mongodb/mongoid/commit/a94c2f43573e58f973913c881ad9d11d62bf857c) from Mongoid 4 to add `:touch` option to `embedded_in`. | ● | ○ | ○ | +| `embedded_touch.rb` (2) | Backport [PR #4392](https://github.com/mongodb/mongoid/pull/4392) from Mongoid 6 to fix an infinite loop issue related to `:touch` callbacks. | ● | ● | ● | | `index_options.rb` | Backport latest index valid index options from Mongoid 6. | ● | ● | ○ | ### License diff --git a/lib/patches/embedded_touch.rb b/lib/patches/embedded_touch.rb index 6577446..578350b 100644 --- a/lib/patches/embedded_touch.rb +++ b/lib/patches/embedded_touch.rb @@ -1,37 +1,96 @@ -# Backport Mongoid 4 :touch option for #embedded_in to Mongoid 3. +# Backport support #embedded_in :touch option from Mongoid 4 to Mongoid 3. +# Also support touch callback on update, and fix infinite loop issue. if Mongoid::VERSION =~ /\A3\./ module Mongoid -module Relations - module Embedded - class In < Relations::One - class << self - def valid_options - [ :autobuild, :cyclic, :polymorphic, :touch ] + module Relations + module Embedded + class In < Relations::One + class << self + def valid_options + [ :autobuild, :cyclic, :polymorphic, :touch ] + end + end + end + end + + module Macros + module ClassMethods + + def embedded_in(name, options = {}, &block) + if ancestors.include?(Mongoid::Versioning) + raise Errors::VersioningNotOnRoot.new(self) + end + meta = characterize(name, Embedded::In, options, &block) + self.embedded = true + relate(name, meta) + builder(name, meta).creator(name, meta) + touchable(meta) + add_counter_cache_callbacks(meta) if meta.counter_cached? + meta end end end - end - module Macros - module ClassMethods + module Touchable + module ClassMethods - def embedded_in(name, options = {}, &block) - if ancestors.include?(Mongoid::Versioning) - raise Errors::VersioningNotOnRoot.new(self) + def touchable(metadata) + if metadata.touchable? + name = metadata.name + method_name = define_relation_touch_method(name) + after_save method_name + after_destroy method_name + after_touch method_name + end + self end - meta = characterize(name, Embedded::In, options, &block) - self.embedded = true - relate(name, meta) - builder(name, meta).creator(name, meta) - touchable(meta) - add_counter_cache_callbacks(meta) if meta.counter_cached? - meta end end end + + module Callbacks + def cascadable_children(kind, children = Set.new) + embedded_relations.each_pair do |name, metadata| + next unless metadata.cascading_callbacks? + without_autobuild do + delayed_pulls = delayed_atomic_pulls[name] + delayed_unsets = delayed_atomic_unsets[name] + children.merge(delayed_pulls) if delayed_pulls + children.merge(delayed_unsets) if delayed_unsets + relation = send(name) + Array.wrap(relation).each do |child| + next if children.include?(child) + children.add(child) if cascadable_child?(kind, child, metadata) + child.send(:cascadable_children, kind, children) + end + end + end + children.to_a + end + + def cascadable_child?(kind, child, metadata) + return false if kind == :initialize || kind == :find || kind == :touch + return false if kind == :validate && metadata.validate? + child.callback_executable?(kind) ? child.in_callback_state?(kind) : false + end + end end + +end + +if Mongoid::VERSION =~ /\A[45]\./ + +module Mongoid + + module Interceptable + def cascadable_child?(kind, child, metadata) + return false if kind == :initialize || kind == :find || kind == :touch + return false if kind == :validate && metadata.validate? + child.callback_executable?(kind) ? child.in_callback_state?(kind) : false + end + end end end diff --git a/lib/version.rb b/lib/version.rb index 8aec0b2..0e7093f 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -1,3 +1,3 @@ module MongoidMonkey - VERSION = '0.2.4' + VERSION = '0.2.5' end diff --git a/spec/app/models/book.rb b/spec/app/models/book.rb new file mode 100644 index 0000000..9fbd147 --- /dev/null +++ b/spec/app/models/book.rb @@ -0,0 +1,6 @@ +class Book + include Mongoid::Document + include Mongoid::Timestamps + + embeds_many :pages, cascade_callbacks: true +end diff --git a/spec/app/models/edit.rb b/spec/app/models/edit.rb new file mode 100644 index 0000000..ebef4c3 --- /dev/null +++ b/spec/app/models/edit.rb @@ -0,0 +1,5 @@ +class Edit + include Mongoid::Document + include Mongoid::Timestamps::Updated + embedded_in :wiki_page, touch: true +end diff --git a/spec/app/models/page.rb b/spec/app/models/page.rb new file mode 100644 index 0000000..3d6b9da --- /dev/null +++ b/spec/app/models/page.rb @@ -0,0 +1,6 @@ +class Page + include Mongoid::Document + + embedded_in :book, touch: true + field :content, :type => String +end diff --git a/spec/app/models/wiki_page.rb b/spec/app/models/wiki_page.rb new file mode 100644 index 0000000..8e8885d --- /dev/null +++ b/spec/app/models/wiki_page.rb @@ -0,0 +1,8 @@ +class WikiPage + include Mongoid::Document + include Mongoid::Timestamps + + field :title, type: String + + embeds_many :edits, validate: false +end diff --git a/spec/unit/embedded_touch_spec.rb b/spec/unit/embedded_touch_spec.rb index fd4d439..839e75b 100644 --- a/spec/unit/embedded_touch_spec.rb +++ b/spec/unit/embedded_touch_spec.rb @@ -1,21 +1,6 @@ require "spec_helper" -if Mongoid::VERSION =~ /\A3\./ - - class Edit - include Mongoid::Document - include Mongoid::Timestamps::Updated - embedded_in :wiki_page, touch: true - end - - class WikiPage - include Mongoid::Document - include Mongoid::Timestamps - - field :title, type: String - - embeds_many :edits, validate: false - end +if Mongoid::VERSION =~ /\A[345]\./ describe Mongoid::Relations::Embedded::In do @@ -48,5 +33,43 @@ class WikiPage expect(page.updated_at).to be_within(5).of(Time.now) end end + + context "when the parent of embedded doc has cascade callbacks" do + + let!(:book) do + Book.new + end + + before do + book.pages.new + book.save + book.unset(:updated_at) + book.pages.first.touch + end + + it "touches the parent document" do + expect(book.updated_at).to be_within(5).of(Time.now) + end + end + + context "when multiple embedded docs with cascade callbacks" do + + let!(:book) do + Book.new + end + + before do + 2.times { book.pages.new } + book.save + book.unset(:updated_at) + book.pages.first.content = "foo" + book.pages.second.content = "bar" + book.pages.first.touch + end + + it "touches the parent document" do + expect(book.updated_at).to be_within(5).of(Time.now) + end + end end end