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

Ensures destroy and restore operations are wrapped in a transaction that is rolled back when the callback chain is halted #71

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
37 changes: 23 additions & 14 deletions lib/destroyed_at.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require 'destroyed_at/belongs_to_association'
require 'destroyed_at/has_many_association'
require 'destroyed_at/has_one_association'
require 'destroyed_at/record_not_restored'
require 'destroyed_at/mapper'

module DestroyedAt
Expand Down Expand Up @@ -39,12 +40,14 @@ def destroyed(time = nil)

# Set an object's destroyed_at time.
def destroy(timestamp = nil)
timestamp ||= @marked_for_destruction_at || current_time_from_proper_timezone
raw_write_attribute(:destroyed_at, timestamp)
run_callbacks(:destroy) do
destroy_associations
self.class.unscoped.where(self.class.primary_key => id).update_all(destroyed_at: timestamp)
@destroyed = true
with_transaction_returning_status do
timestamp ||= @marked_for_destruction_at || current_time_from_proper_timezone
raw_write_attribute(:destroyed_at, timestamp)
run_callbacks(:destroy) do
destroy_associations
self.class.unscoped.where(self.class.primary_key => id).update_all(destroyed_at: timestamp)
@destroyed = true
end
end
end

Expand All @@ -56,16 +59,22 @@ def mark_for_destruction(timestamp = nil)

# Set an object's destroyed_at time to nil.
def restore
state = nil
run_callbacks(:restore) do
if state = (self.class.unscoped.where(self.class.primary_key => id).update_all(destroyed_at: nil) == 1)
_restore_associations
raw_write_attribute(:destroyed_at, nil)
@destroyed = false
true
with_transaction_returning_status do
state = nil
run_callbacks(:restore) do
if state = (self.class.unscoped.where(self.class.primary_key => id).update_all(destroyed_at: nil) == 1)
_restore_associations
raw_write_attribute(:destroyed_at, nil)
@destroyed = false
true
end
end
state
end
state
end

def restore!
restore || raise(RecordNotRestored, self)
end

def persisted?
Expand Down
7 changes: 7 additions & 0 deletions lib/destroyed_at/record_not_restored.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class DestroyedAt::RecordNotRestored < ActiveRecord::ActiveRecordError
attr_reader :record
def initialize(record)
@record = record
super()
end
end
71 changes: 71 additions & 0 deletions test/destroyed_at_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,53 @@
comment.destroyed_at.must_equal datetime
end

it 'rollsback the transaction when destroying fails' do
pet_type = PetType.create(name: 'Cat')
pet = Pet.create(pet_type: pet_type)

Pet.count.must_equal 1
PetType.count.must_equal 1

pet_type.destroy

Pet.count.must_equal 1
PetType.count.must_equal 1
end

it 'runs the after_commit callbacks when successful' do
pet_type = PetType.create(name: 'Cat')

Pet.count.must_equal 0
PetType.count.must_equal 1

pet_type.reset_transaction_state_callback_observers

pet_type.after_commit_called?.must_equal false
pet_type.after_rollback_called?.must_equal false

pet_type.destroy

pet_type.after_commit_called?.must_equal true
pet_type.after_rollback_called?.must_equal false
end

it 'runs the after_rollback callbacks when failed' do
pet_type = PetType.create(name: 'Cat')
pet = Pet.create(pet_type: pet_type)

Pet.count.must_equal 1
PetType.count.must_equal 1

pet_type.reset_transaction_state_callback_observers

pet_type.after_commit_called?.must_equal false
pet_type.after_rollback_called?.must_equal false

pet_type.destroy

pet_type.after_commit_called?.must_equal false
pet_type.after_rollback_called?.must_equal true
end
end

describe 'restoring an activerecord instance' do
Expand Down Expand Up @@ -192,6 +239,30 @@
post.comments.wont_include comment_1
post.comments.must_include comment_2
end

it 'rollsback the transaction when restoring fails' do
pet_type = PetType.create(name: 'Cat')

PetType.count.must_equal 1
pet_type.destroy

Pet.count.must_equal 0
PetType.count.must_equal 0
pet_type.restore

Pet.count.must_equal 0
PetType.count.must_equal 0
end

it 'raises DestroyedAt::RecordNotRestored if the record cannot be restored using #restore!' do
pet_type = PetType.create(name: 'Cat')

PetType.count.must_equal 1
pet_type.destroy

PetType.count.must_equal 0
assert_raises(DestroyedAt::RecordNotRestored) { pet_type.restore! }
end
end

describe 'deleting a record' do
Expand Down
37 changes: 36 additions & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class << self
ActiveRecord::Base.connection.execute(%{CREATE TABLE images (id INTEGER PRIMARY KEY, post_id INTEGER);})
ActiveRecord::Base.connection.execute(%{CREATE TABLE posts (id INTEGER PRIMARY KEY, author_id INTEGER, destroyed_at DATETIME);})
ActiveRecord::Base.connection.execute(%{CREATE TABLE people (id INTEGER PRIMARY KEY);})
ActiveRecord::Base.connection.execute(%{CREATE TABLE pets (id INTEGER PRIMARY KEY, person_id INTEGER);})
ActiveRecord::Base.connection.execute(%{CREATE TABLE pet_types (id INTEGER PRIMARY KEY, name TEXT, destroyed_at DATETIME);})
ActiveRecord::Base.connection.execute(%{CREATE TABLE pets (id INTEGER PRIMARY KEY, person_id INTEGER, pet_type_id INTEGER);})
ActiveRecord::Base.connection.execute(%{CREATE TABLE likes (id INTEGER PRIMARY KEY, likeable_id INTEGER, likeable_type TEXT, destroyed_at DATETIME);})

class Author < ActiveRecord::Base
Expand All @@ -52,6 +53,40 @@ class Person < ActiveRecord::Base

class Pet < ActiveRecord::Base
belongs_to :person
belongs_to :pet_type
end

class PetType < ActiveRecord::Base
include DestroyedAt
has_many :pets, dependent: :restrict_with_error

after_restore do
Pet.create!(pet_type: self) # mutate the database

raise ActiveRecord::Rollback # halt the callback chain
end

# Observe the transaction state callbacks
def reset_transaction_state_callback_observers
@after_commit_called = false
@after_rollback_called = false
end

after_commit do
@after_commit_called = true
end

def after_commit_called?
!!@after_commit_called
end

after_rollback do
@after_rollback_called = true
end

def after_rollback_called?
!!@after_rollback_called
end
end

class Avatar < ActiveRecord::Base
Expand Down