Skip to content

Commit 7b96793

Browse files
feat: Trigger an after_commit callback when restoring a record (#559)
* Do not use sqlite3 v2.0.0 or later to fix CI * Implement after_restore_commit feature --------- Co-authored-by: Mathieu Jobin <[email protected]>
1 parent ba6dde7 commit 7b96793

File tree

3 files changed

+194
-2
lines changed

3 files changed

+194
-2
lines changed

Diff for: README.md

+13
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,19 @@ Client.restore(id, :recursive => true, :recovery_window => 2.minutes)
206206
client.restore(:recursive => true, :recovery_window => 2.minutes)
207207
```
208208

209+
If you want to trigger an after_commit callback when restoring a record:
210+
211+
``` ruby
212+
class Client < ActiveRecord::Base
213+
acts_as_paranoid after_restore_commit: true
214+
215+
after_commit :commit_called, on: :restore
216+
# or
217+
after_restore_commit :commit_called
218+
...
219+
end
220+
```
221+
209222
Note that by default paranoia will not prevent that a soft destroyed object can't be associated with another object of a different model.
210223
A Rails validator is provided should you require this functionality:
211224
``` ruby

Diff for: lib/paranoia.rb

+42-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,16 @@ def paranoia_destroy!
9494
end
9595

9696
def trigger_transactional_callbacks?
97-
super || @_trigger_destroy_callback && paranoia_destroyed?
97+
super || @_trigger_destroy_callback && paranoia_destroyed? ||
98+
@_trigger_restore_callback && !paranoia_destroyed?
99+
end
100+
101+
def transaction_include_any_action?(actions)
102+
super || actions.any? do |action|
103+
if action == :restore
104+
paranoia_after_restore_commit && @_trigger_restore_callback
105+
end
106+
end
98107
end
99108

100109
def paranoia_delete
@@ -121,6 +130,10 @@ def restore!(opts = {})
121130
if within_recovery_window?(recovery_window_range) && ((noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen)
122131
@_disable_counter_cache = !paranoia_destroyed?
123132
write_attribute paranoia_column, paranoia_sentinel_value
133+
if paranoia_after_restore_commit
134+
@_trigger_restore_callback = true
135+
add_to_transaction
136+
end
124137
update_columns(paranoia_restore_attributes)
125138
each_counter_cached_associations do |association|
126139
if send(association.reflection.name)
@@ -134,6 +147,10 @@ def restore!(opts = {})
134147
end
135148

136149
self
150+
ensure
151+
if paranoia_after_restore_commit
152+
@_trigger_restore_callback = false
153+
end
137154
end
138155
alias :restore :restore!
139156

@@ -260,6 +277,23 @@ def restore_associated_records(recovery_window_range = nil)
260277
end
261278
end
262279

280+
module ActiveRecord
281+
module Transactions
282+
module RestoreSupport
283+
def self.included(base)
284+
base::ACTIONS << :restore unless base::ACTIONS.include?(:restore)
285+
end
286+
end
287+
288+
module ClassMethods
289+
def after_restore_commit(*args, &block)
290+
set_options_for_callbacks!(args, on: :restore)
291+
set_callback(:commit, :after, *args, &block)
292+
end
293+
end
294+
end
295+
end
296+
263297
module Paranoia::Relation
264298
def paranoia_delete_all
265299
update_all(klass.paranoia_destroy_attributes)
@@ -285,10 +319,12 @@ def self.acts_as_paranoid(options={})
285319
class << self; delegate :really_delete_all, to: :all end
286320

287321
include Paranoia
288-
class_attribute :paranoia_column, :paranoia_sentinel_value, :delete_all_enabled
322+
class_attribute :paranoia_column, :paranoia_sentinel_value, :paranoia_after_restore_commit,
323+
:delete_all_enabled
289324

290325
self.paranoia_column = (options[:column] || :deleted_at).to_s
291326
self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value }
327+
self.paranoia_after_restore_commit = options.fetch(:after_restore_commit) { false }
292328
def self.paranoia_scope
293329
where(paranoia_column => paranoia_sentinel_value)
294330
end
@@ -305,6 +341,10 @@ class << self; alias_method :without_deleted, :paranoia_scope end
305341
self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers)
306342
}
307343

344+
if paranoia_after_restore_commit
345+
ActiveRecord::Transactions.send(:include, ActiveRecord::Transactions::RestoreSupport)
346+
end
347+
308348
self.delete_all_enabled = options[:delete_all_enabled] || Paranoia.delete_all_enabled
309349

310350
if self.delete_all_enabled

Diff for: test/paranoia_test.rb

+139
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ def setup!
3030
'featureful_models' => 'deleted_at DATETIME, name VARCHAR(32)',
3131
'plain_models' => 'deleted_at DATETIME',
3232
'callback_models' => 'deleted_at DATETIME',
33+
'after_commit_on_restore_callback_models' => 'deleted_at DATETIME',
34+
'after_restore_commit_callback_models' => 'deleted_at DATETIME',
35+
'after_commit_callback_restore_enabled_models' => 'deleted_at DATETIME',
36+
'after_other_commit_callback_restore_enabled_models' => 'deleted_at DATETIME',
3337
'after_commit_callback_models' => 'deleted_at DATETIME',
3438
'fail_callback_models' => 'deleted_at DATETIME',
3539
'association_with_abort_models' => 'deleted_at DATETIME',
@@ -626,6 +630,100 @@ def test_restore_behavior_for_callbacks
626630
model.reload
627631

628632
assert model.instance_variable_get(:@restore_callback_called)
633+
assert_nil model.instance_variable_get(:@after_commit_callback_called)
634+
end
635+
636+
def test_after_commit_on_restore
637+
model = AfterCommitOnRestoreCallbackModel.new
638+
model.save
639+
id = model.id
640+
model.destroy
641+
642+
assert model.paranoia_destroyed?
643+
644+
model = AfterCommitOnRestoreCallbackModel.only_deleted.find(id)
645+
model.restore!
646+
model.reload
647+
648+
assert model.instance_variable_get(:@restore_callback_called)
649+
assert model.instance_variable_get(:@after_restore_callback_called)
650+
assert model.instance_variable_get(:@after_restore_commit_callback_called)
651+
end
652+
653+
def test_after_restore_commit
654+
model = AfterRestoreCommitCallbackModel.new
655+
model.save
656+
id = model.id
657+
model.destroy
658+
659+
assert model.paranoia_destroyed?
660+
661+
model = AfterRestoreCommitCallbackModel.only_deleted.find(id)
662+
model.restore!
663+
model.reload
664+
665+
assert model.instance_variable_get(:@restore_callback_called)
666+
assert model.instance_variable_get(:@after_restore_callback_called)
667+
assert model.instance_variable_get(:@after_restore_commit_callback_called)
668+
end
669+
670+
def test_after_restore_commit_once
671+
model = AfterRestoreCommitCallbackModel.new
672+
model.save
673+
id = model.id
674+
model.destroy
675+
676+
assert model.paranoia_destroyed?
677+
assert model.instance_variable_get(:@after_destroy_commit_callback_called)
678+
679+
model.remove_called_variables
680+
model = AfterRestoreCommitCallbackModel.only_deleted.find(id)
681+
model.restore!
682+
model.reload
683+
684+
assert model.instance_variable_get(:@restore_callback_called)
685+
assert model.instance_variable_get(:@after_restore_callback_called)
686+
assert model.instance_variable_get(:@after_restore_commit_callback_called)
687+
assert_nil model.instance_variable_get(:@after_destroy_commit_callback_called)
688+
689+
model.remove_called_variables
690+
model.destroy
691+
assert model.instance_variable_get(:@after_destroy_commit_callback_called)
692+
assert_nil model.instance_variable_get(:@after_restore_commit_callback_called)
693+
end
694+
695+
def test_after_commit_restore_enabled
696+
model = AfterCommitCallbackRestoreEnabledModel.new
697+
model.save
698+
id = model.id
699+
model.destroy
700+
701+
assert model.paranoia_destroyed?
702+
703+
model = AfterCommitCallbackRestoreEnabledModel.only_deleted.find(id)
704+
model.restore!
705+
model.reload
706+
707+
assert model.instance_variable_get(:@restore_callback_called)
708+
assert model.instance_variable_get(:@after_restore_callback_called)
709+
assert model.instance_variable_get(:@after_commit_callback_called)
710+
end
711+
712+
def test_not_call_after_other_commit_restore_enabled
713+
model = AfterOtherCommitCallbackRestoreEnabledModel.new
714+
model.save
715+
id = model.id
716+
model.destroy
717+
718+
assert model.paranoia_destroyed?
719+
720+
model = AfterOtherCommitCallbackRestoreEnabledModel.only_deleted.find(id)
721+
model.restore!
722+
model.reload
723+
724+
assert model.instance_variable_get(:@restore_callback_called)
725+
assert model.instance_variable_get(:@after_restore_callback_called)
726+
assert_nil model.instance_variable_get(:@after_other_commit_callback_called)
629727
end
630728

631729
def test_really_destroy
@@ -1394,6 +1492,47 @@ def remove_called_variables
13941492
end
13951493
end
13961494

1495+
class AfterCommitOnRestoreCallbackModel < ActiveRecord::Base
1496+
acts_as_paranoid after_restore_commit: true
1497+
before_restore { |model| model.instance_variable_set :@restore_callback_called, true }
1498+
after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true }
1499+
after_commit :set_after_restore_commit_called, on: :restore
1500+
1501+
def set_after_restore_commit_called
1502+
@after_restore_commit_callback_called = true
1503+
end
1504+
end
1505+
1506+
class AfterRestoreCommitCallbackModel < ActiveRecord::Base
1507+
acts_as_paranoid after_restore_commit: true
1508+
before_restore { |model| model.instance_variable_set :@restore_callback_called, true }
1509+
after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true }
1510+
after_restore_commit { |model| model.instance_variable_set :@after_restore_commit_callback_called, true }
1511+
after_destroy_commit { |model| model.instance_variable_set :@after_destroy_commit_callback_called, true }
1512+
1513+
def remove_called_variables
1514+
instance_variables.each {|name| (name.to_s.end_with?('_called')) ? remove_instance_variable(name) : nil}
1515+
end
1516+
end
1517+
1518+
class AfterCommitCallbackRestoreEnabledModel < ActiveRecord::Base
1519+
acts_as_paranoid after_restore_commit: true
1520+
before_restore { |model| model.instance_variable_set :@restore_callback_called, true }
1521+
after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true }
1522+
after_commit { |model| model.instance_variable_set :@after_commit_callback_called, true }
1523+
end
1524+
1525+
class AfterOtherCommitCallbackRestoreEnabledModel < ActiveRecord::Base
1526+
acts_as_paranoid after_restore_commit: true
1527+
before_restore { |model| model.instance_variable_set :@restore_callback_called, true }
1528+
after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true }
1529+
after_commit :set_after_other_commit_called, on: [:create, :destroy, :update]
1530+
1531+
def set_after_other_commit_called
1532+
@after_other_commit_callback_called = true
1533+
end
1534+
end
1535+
13971536
class AssociationWithAbortModel < ActiveRecord::Base
13981537
acts_as_paranoid
13991538
has_many :related_models, class_name: 'RelatedModel', foreign_key: :parent_model_id, dependent: :destroy

0 commit comments

Comments
 (0)