From 2e37a444e1f3ae3bbb3800e2860aa59e2609a522 Mon Sep 17 00:00:00 2001 From: Marco Mastrodonato Date: Thu, 28 Mar 2024 17:45:30 +0100 Subject: [PATCH] Added `with_deleted` option to `has_one` relationship --- README.md | 36 +++++++++++-- lib/acts_as_paranoid/associations.rb | 18 +++++-- test/test_associations.rb | 80 ++++++++++++++++++++++------ 3 files changed, 110 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 19c8d59..e4700bf 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ or in the recover statement ```ruby Paranoiac.only_deleted.where("name = ?", "not dead yet").first - .recover(recovery_window: 30.seconds) + .recover(recovery_window: 30.seconds) ``` ### recover! @@ -245,7 +245,7 @@ p1.recover #=> fails validation! ### Status -A paranoid object could be deleted or destroyed fully. +A paranoid object could be deleted or destroyed fully. You can check if the object is deleted with the `deleted?` helper @@ -311,8 +311,9 @@ Paranoiac.pretty.only_deleted.count #=> 1 Associations are also supported. From the simplest behaviors you'd expect to more nifty things like the ones -mentioned previously or the usage of the `:with_deleted` option with -`belongs_to` +mentioned previously or the usage of the `:with_deleted` option with: + +#### belongs_to ```ruby class Parent < ActiveRecord::Base @@ -336,6 +337,33 @@ child.parent #=> nil child.parent_including_deleted #=> Parent (it works!) ``` +#### has_one + +```ruby +class Parent < ActiveRecord::Base + has_one :child, class_name: "ParanoiacChild" + has_one :child_with_deleted, class_name: "ParanoiacChild", with_deleted: true +end + +class ParanoiacChild < ActiveRecord::Base + acts_as_paranoid + belongs_to :parent +end + +parent = Parent.first + +child = ParanoiacChild.create +parent.child = child + +parent.child #=> ParanoiacChild + +child.destroy +parent.reload + +parent.child #=> nil +parent.child_with_deleted #=> ParanoiacChild +``` + ### Callbacks There are couple of callbacks that you may use when dealing with deletion and diff --git a/lib/acts_as_paranoid/associations.rb b/lib/acts_as_paranoid/associations.rb index a0141dd..6daa1ae 100644 --- a/lib/acts_as_paranoid/associations.rb +++ b/lib/acts_as_paranoid/associations.rb @@ -7,11 +7,25 @@ def self.included(base) class << base alias_method :belongs_to_without_deleted, :belongs_to alias_method :belongs_to, :belongs_to_with_deleted + + alias_method :has_one_without_deleted, :has_one + alias_method :has_one, :has_one_with_deleted end end module ClassMethods def belongs_to_with_deleted(target, scope = nil, options = {}) + relation_with_deleted(target, relation: :belongs_to, scope: scope, options: options) + end + + def has_one_with_deleted(target, scope = nil, options = {}) + relation_with_deleted(target, relation: :has_one, scope: scope, options: options) + end + + private + + # @param relation [String,Symbol] :belongs_to or :has_one + def relation_with_deleted(target, relation:, scope: nil, options: {}) if scope.is_a?(Hash) options = scope scope = nil @@ -23,7 +37,7 @@ def belongs_to_with_deleted(target, scope = nil, options = {}) scope = make_scope_with_deleted(scope) end - result = belongs_to_without_deleted(target, scope, **options) + result = send("#{relation}_without_deleted", target, scope, **options) if with_deleted options = result.values.last.options @@ -34,8 +48,6 @@ def belongs_to_with_deleted(target, scope = nil, options = {}) result end - private - def make_scope_with_deleted(scope) if scope old_scope = scope diff --git a/test/test_associations.rb b/test/test_associations.rb index 4c8fd46..ce77116 100644 --- a/test/test_associations.rb +++ b/test/test_associations.rb @@ -59,16 +59,16 @@ class ParanoidHasManyDependant < ActiveRecord::Base -> { where(name: "hello").includes(:not_paranoid) }, class_name: "ParanoidTime", foreign_key: :paranoid_time_id belongs_to :paranoid_time_with_deleted, class_name: "ParanoidTime", - foreign_key: :paranoid_time_id, - with_deleted: true + foreign_key: :paranoid_time_id, + with_deleted: true belongs_to :paranoid_time_with_scope_with_deleted, -> { where(name: "hello").includes(:not_paranoid) }, class_name: "ParanoidTime", foreign_key: :paranoid_time_id, with_deleted: true belongs_to :paranoid_time_polymorphic_with_deleted, class_name: "ParanoidTime", - foreign_key: :paranoid_time_id, - polymorphic: true, - with_deleted: true + foreign_key: :paranoid_time_id, + polymorphic: true, + with_deleted: true belongs_to :paranoid_belongs_dependant, dependent: :destroy end @@ -77,6 +77,16 @@ class ParanoidBelongsDependant < ActiveRecord::Base acts_as_paranoid has_many :paranoid_has_many_dependants + has_one :paranoid_has_one_dependant + has_one :paranoid_has_one_dependant_with_deleted, + class_name: "ParanoidHasOneDependant", + with_deleted: true + end + + class ParanoidHasOneDependant < ActiveRecord::Base + acts_as_paranoid + + belongs_to :paranoid_belongs_dependant, dependent: :destroy end class ParanoidTime < ActiveRecord::Base @@ -196,6 +206,14 @@ def setup timestamps t end + create_table :paranoid_has_one_dependants do |t| + t.string :name + t.datetime :deleted_at + t.integer :paranoid_belongs_dependant_id + + timestamps t + end + create_table :paranoid_destroy_companies do |t| t.string :name t.datetime :deleted_at @@ -357,7 +375,7 @@ def test_belongs_to_with_scope_option expected_includes_values = ParanoidTime.includes(:not_paranoid).includes_values includes_values = paranoid_has_many_dependant - .association(:paranoid_time_with_scope).scope.includes_values + .association(:paranoid_time_with_scope).scope.includes_values assert_equal expected_includes_values, includes_values @@ -385,7 +403,7 @@ def test_belongs_to_with_scope_and_deleted_option includes_values = ParanoidTime.includes(:not_paranoid).includes_values assert_equal includes_values, paranoid_has_many_dependant - .association(:paranoid_time_with_scope_with_deleted).scope.includes_values + .association(:paranoid_time_with_scope_with_deleted).scope.includes_values paranoid_time = ParanoidTime.create(name: "not-hello") paranoid_has_many_dependant.paranoid_time = paranoid_time @@ -397,19 +415,19 @@ def test_belongs_to_with_scope_and_deleted_option paranoid_has_many_dependant.reload assert_equal paranoid_time, paranoid_has_many_dependant - .paranoid_time_with_scope_with_deleted + .paranoid_time_with_scope_with_deleted paranoid_time.destroy paranoid_has_many_dependant.reload assert_equal paranoid_time, paranoid_has_many_dependant - .paranoid_time_with_scope_with_deleted + .paranoid_time_with_scope_with_deleted end def test_belongs_to_with_deleted paranoid_time = ParanoidTime.create! name: "paranoid" paranoid_has_many_dependant = paranoid_time.paranoid_has_many_dependants - .create(name: "dependant!") + .create(name: "dependant!") assert_equal paranoid_time, paranoid_has_many_dependant.paranoid_time assert_equal paranoid_time, paranoid_has_many_dependant.paranoid_time_with_deleted @@ -470,17 +488,17 @@ def test_belongs_to_with_deleted_as_inverse_of_has_many def test_belongs_to_polymorphic_with_deleted paranoid_time = ParanoidTime.create! name: "paranoid" paranoid_has_many_dependant = ParanoidHasManyDependant - .create!(name: "dependant!", paranoid_time_polymorphic_with_deleted: paranoid_time) + .create!(name: "dependant!", paranoid_time_polymorphic_with_deleted: paranoid_time) assert_equal paranoid_time, paranoid_has_many_dependant.paranoid_time assert_equal paranoid_time, paranoid_has_many_dependant - .paranoid_time_polymorphic_with_deleted + .paranoid_time_polymorphic_with_deleted paranoid_time.destroy assert_nil paranoid_has_many_dependant.reload.paranoid_time assert_equal paranoid_time, paranoid_has_many_dependant - .reload.paranoid_time_polymorphic_with_deleted + .reload.paranoid_time_polymorphic_with_deleted end def test_belongs_to_nil_polymorphic_with_deleted @@ -500,7 +518,7 @@ def test_belongs_to_nil_polymorphic_with_deleted def test_belongs_to_options paranoid_time = ParanoidHasManyDependant.reflections - .with_indifferent_access[:paranoid_time] + .with_indifferent_access[:paranoid_time] assert_equal :belongs_to, paranoid_time.macro assert_nil paranoid_time.options[:with_deleted] @@ -509,7 +527,7 @@ def test_belongs_to_options def test_belongs_to_with_deleted_options paranoid_time_with_deleted = ParanoidHasManyDependant.reflections - .with_indifferent_access[:paranoid_time_with_deleted] + .with_indifferent_access[:paranoid_time_with_deleted] assert_equal :belongs_to, paranoid_time_with_deleted.macro assert paranoid_time_with_deleted.options[:with_deleted] @@ -517,7 +535,7 @@ def test_belongs_to_with_deleted_options def test_belongs_to_polymorphic_with_deleted_options paranoid_time_polymorphic_with_deleted = ParanoidHasManyDependant.reflections - .with_indifferent_access[:paranoid_time_polymorphic_with_deleted] + .with_indifferent_access[:paranoid_time_polymorphic_with_deleted] assert_equal :belongs_to, paranoid_time_polymorphic_with_deleted.macro assert paranoid_time_polymorphic_with_deleted.options[:with_deleted] @@ -542,6 +560,34 @@ def test_only_find_associated_records_when_finding_with_paranoid_deleted assert_equal [child], parent.paranoid_has_many_dependants.with_deleted.to_a end + def test_has_one_with_deleted_options + parent = + ParanoidBelongsDependant.reflections + .with_indifferent_access[:paranoid_has_one_dependant_with_deleted] + + assert_equal :has_one, parent.macro + assert parent.options[:with_deleted] + end + + def test_has_one_associated_records_when_finding_with_paranoid_deleted + parent = ParanoidBelongsDependant.create + child = ParanoidHasOneDependant.create + parent.paranoid_has_one_dependant = child + + unrelated_parent = ParanoidBelongsDependant.create + unrelated_child = ParanoidHasOneDependant.create + unrelated_parent.paranoid_has_one_dependant = unrelated_child + + child.destroy + + assert_paranoid_deletion(child) + + parent.reload + + assert_nil parent.paranoid_has_one_dependant + assert_equal child, parent.paranoid_has_one_dependant_with_deleted + end + def test_join_with_model_with_deleted obj = ParanoidHasManyDependant.create(paranoid_time: ParanoidTime.create) @@ -569,7 +615,7 @@ def test_includes_with_deleted paranoid_time.destroy ParanoidHasManyDependant.with_deleted - .includes(:paranoid_time_with_deleted).each do |hasmany| + .includes(:paranoid_time_with_deleted).each do |hasmany| assert_not_nil hasmany.paranoid_time_with_deleted end end