Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Trigger an after_commit callback when restoring a record #559

Merged
merged 4 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,19 @@ Client.restore(id, :recursive => true, :recovery_window => 2.minutes)
client.restore(:recursive => true, :recovery_window => 2.minutes)
```

If you want to trigger an after_commit callback when restoring a record:

``` ruby
class Client < ActiveRecord::Base
acts_as_paranoid after_restore_commit: true

after_commit :commit_called, on: :restore
# or
after_restore_commit :commit_called
...
end
```

Note that by default paranoia will not prevent that a soft destroyed object can't be associated with another object of a different model.
A Rails validator is provided should you require this functionality:
``` ruby
Expand Down
43 changes: 41 additions & 2 deletions lib/paranoia.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,16 @@ def paranoia_destroy!
end

def trigger_transactional_callbacks?
super || @_trigger_destroy_callback && paranoia_destroyed?
super || @_trigger_destroy_callback && paranoia_destroyed? ||
@_trigger_restore_callback && !paranoia_destroyed?
end

def transaction_include_any_action?(actions)
super || actions.any? do |action|
if action == :restore
paranoia_after_restore_commit && @_trigger_restore_callback
end
end
end

def paranoia_delete
Expand All @@ -115,6 +124,10 @@ def restore!(opts = {})
if within_recovery_window?(recovery_window_range) && ((noop_if_frozen && [email protected]?) || !noop_if_frozen)
@_disable_counter_cache = !paranoia_destroyed?
write_attribute paranoia_column, paranoia_sentinel_value
if paranoia_after_restore_commit
@_trigger_restore_callback = true
add_to_transaction
end
update_columns(paranoia_restore_attributes)
each_counter_cached_associations do |association|
if send(association.reflection.name)
Expand All @@ -128,6 +141,10 @@ def restore!(opts = {})
end

self
ensure
if paranoia_after_restore_commit
@_trigger_restore_callback = false
end
end
alias :restore :restore!

Expand Down Expand Up @@ -262,6 +279,23 @@ def restore_associated_records(recovery_window_range = nil)
end
end

module ActiveRecord
module Transactions
module RestoreSupport
def self.included(base)
base::ACTIONS << :restore unless base::ACTIONS.include?(:restore)
end
end

module ClassMethods
def after_restore_commit(*args, &block)
set_options_for_callbacks!(args, on: :restore)
set_callback(:commit, :after, *args, &block)
end
end
end
end

ActiveSupport.on_load(:active_record) do
class ActiveRecord::Base
def self.acts_as_paranoid(options={})
Expand All @@ -278,10 +312,11 @@ def self.acts_as_paranoid(options={})
alias_method :destroy_without_paranoia, :destroy

include Paranoia
class_attribute :paranoia_column, :paranoia_sentinel_value
class_attribute :paranoia_column, :paranoia_sentinel_value, :paranoia_after_restore_commit

self.paranoia_column = (options[:column] || :deleted_at).to_s
self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value }
self.paranoia_after_restore_commit = options.fetch(:after_restore_commit) { false }
def self.paranoia_scope
where(paranoia_column => paranoia_sentinel_value)
end
Expand All @@ -297,6 +332,10 @@ class << self; alias_method :without_deleted, :paranoia_scope end
after_restore {
self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers)
}

if paranoia_after_restore_commit
ActiveRecord::Transactions.send(:include, ActiveRecord::Transactions::RestoreSupport)
end
end

# Please do not use this method in production.
Expand Down
139 changes: 139 additions & 0 deletions test/paranoia_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def setup!
'featureful_models' => 'deleted_at DATETIME, name VARCHAR(32)',
'plain_models' => 'deleted_at DATETIME',
'callback_models' => 'deleted_at DATETIME',
'after_commit_on_restore_callback_models' => 'deleted_at DATETIME',
'after_restore_commit_callback_models' => 'deleted_at DATETIME',
'after_commit_callback_restore_enabled_models' => 'deleted_at DATETIME',
'after_other_commit_callback_restore_enabled_models' => 'deleted_at DATETIME',
'after_commit_callback_models' => 'deleted_at DATETIME',
'fail_callback_models' => 'deleted_at DATETIME',
'association_with_abort_models' => 'deleted_at DATETIME',
Expand Down Expand Up @@ -626,6 +630,100 @@ def test_restore_behavior_for_callbacks
model.reload

assert model.instance_variable_get(:@restore_callback_called)
assert_nil model.instance_variable_get(:@after_commit_callback_called)
end

def test_after_commit_on_restore
model = AfterCommitOnRestoreCallbackModel.new
model.save
id = model.id
model.destroy

assert model.paranoia_destroyed?

model = AfterCommitOnRestoreCallbackModel.only_deleted.find(id)
model.restore!
model.reload

assert model.instance_variable_get(:@restore_callback_called)
assert model.instance_variable_get(:@after_restore_callback_called)
assert model.instance_variable_get(:@after_restore_commit_callback_called)
end

def test_after_restore_commit
model = AfterRestoreCommitCallbackModel.new
model.save
id = model.id
model.destroy

assert model.paranoia_destroyed?

model = AfterRestoreCommitCallbackModel.only_deleted.find(id)
model.restore!
model.reload

assert model.instance_variable_get(:@restore_callback_called)
assert model.instance_variable_get(:@after_restore_callback_called)
assert model.instance_variable_get(:@after_restore_commit_callback_called)
end

def test_after_restore_commit_once
model = AfterRestoreCommitCallbackModel.new
model.save
id = model.id
model.destroy

assert model.paranoia_destroyed?
assert model.instance_variable_get(:@after_destroy_commit_callback_called)

model.remove_called_variables
model = AfterRestoreCommitCallbackModel.only_deleted.find(id)
model.restore!
model.reload

assert model.instance_variable_get(:@restore_callback_called)
assert model.instance_variable_get(:@after_restore_callback_called)
assert model.instance_variable_get(:@after_restore_commit_callback_called)
assert_nil model.instance_variable_get(:@after_destroy_commit_callback_called)

model.remove_called_variables
model.destroy
assert model.instance_variable_get(:@after_destroy_commit_callback_called)
assert_nil model.instance_variable_get(:@after_restore_commit_callback_called)
end

def test_after_commit_restore_enabled
model = AfterCommitCallbackRestoreEnabledModel.new
model.save
id = model.id
model.destroy

assert model.paranoia_destroyed?

model = AfterCommitCallbackRestoreEnabledModel.only_deleted.find(id)
model.restore!
model.reload

assert model.instance_variable_get(:@restore_callback_called)
assert model.instance_variable_get(:@after_restore_callback_called)
assert model.instance_variable_get(:@after_commit_callback_called)
end

def test_not_call_after_other_commit_restore_enabled
model = AfterOtherCommitCallbackRestoreEnabledModel.new
model.save
id = model.id
model.destroy

assert model.paranoia_destroyed?

model = AfterOtherCommitCallbackRestoreEnabledModel.only_deleted.find(id)
model.restore!
model.reload

assert model.instance_variable_get(:@restore_callback_called)
assert model.instance_variable_get(:@after_restore_callback_called)
assert_nil model.instance_variable_get(:@after_other_commit_callback_called)
end

def test_really_destroy
Expand Down Expand Up @@ -1329,6 +1427,47 @@ def remove_called_variables
end
end

class AfterCommitOnRestoreCallbackModel < ActiveRecord::Base
acts_as_paranoid after_restore_commit: true
before_restore { |model| model.instance_variable_set :@restore_callback_called, true }
after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true }
after_commit :set_after_restore_commit_called, on: :restore

def set_after_restore_commit_called
@after_restore_commit_callback_called = true
end
end

class AfterRestoreCommitCallbackModel < ActiveRecord::Base
acts_as_paranoid after_restore_commit: true
before_restore { |model| model.instance_variable_set :@restore_callback_called, true }
after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true }
after_restore_commit { |model| model.instance_variable_set :@after_restore_commit_callback_called, true }
after_destroy_commit { |model| model.instance_variable_set :@after_destroy_commit_callback_called, true }

def remove_called_variables
instance_variables.each {|name| (name.to_s.end_with?('_called')) ? remove_instance_variable(name) : nil}
end
end

class AfterCommitCallbackRestoreEnabledModel < ActiveRecord::Base
acts_as_paranoid after_restore_commit: true
before_restore { |model| model.instance_variable_set :@restore_callback_called, true }
after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true }
after_commit { |model| model.instance_variable_set :@after_commit_callback_called, true }
end

class AfterOtherCommitCallbackRestoreEnabledModel < ActiveRecord::Base
acts_as_paranoid after_restore_commit: true
before_restore { |model| model.instance_variable_set :@restore_callback_called, true }
after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true }
after_commit :set_after_other_commit_called, on: [:create, :destroy, :update]

def set_after_other_commit_called
@after_other_commit_callback_called = true
end
end

class AssociationWithAbortModel < ActiveRecord::Base
acts_as_paranoid
has_many :related_models, class_name: 'RelatedModel', foreign_key: :parent_model_id, dependent: :destroy
Expand Down
Loading