-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[F] Add soft deletion to projects and texts
* This adds logic to background the deletion of models that can take a while to actually delete because of the complexity of their association hierarchies. * When soft-deleting a project, its texts will also be soft-deleted while the project is being deleted in the background, so that any lookups on the text will 404 as we'd like. * Update controllers, filterable concern to use soft-deletion scopes when applicable * Add TimestampScopes helper * Add some missing inverse associations, minor idiomatic cleanup Resolves MNFLD-937
- Loading branch information
1 parent
7db05e5
commit a662c24
Showing
19 changed files
with
445 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# frozen_string_literal: true | ||
|
||
module SoftDeletions | ||
# @see SoftDeletable#async_destroy | ||
class PurgeJob < ApplicationJob | ||
queue_as :deletions | ||
|
||
discard_on ActiveJob::DeserializationError, ActiveRecord::RecordNotFound, ActiveRecord::RecordNotDestroyed | ||
|
||
# @param [SoftDeletable] record | ||
# @return [void] | ||
def perform(record) | ||
record.destroy! if record.marked_for_purge? | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
# frozen_string_literal: true | ||
|
||
module SoftDeletable | ||
extend ActiveSupport::Concern | ||
|
||
include Filterable | ||
|
||
included do | ||
define_model_callbacks :mark_for_purge, :soft_delete | ||
|
||
scope :existing, -> { sans_deleted } | ||
scope :only_deleted, -> { with_deleted.where.not(deleted_at: nil) } | ||
scope :with_deleted, -> { unscope(where: :deleted_at) } | ||
scope :sans_deleted, -> { with_deleted.where(deleted_at: nil) } | ||
|
||
delegate :soft_deletable_associations, to: :class | ||
|
||
before_mark_for_purge :validate_mark_for_purge! | ||
after_mark_for_purge :enqueue_background_purge! | ||
before_soft_delete :prevent_duplicate_soft_deletion! | ||
after_soft_delete :scramble_deleted_slug!, if: :should_scramble_deleted_slug? | ||
end | ||
|
||
# @see SoftDeletions::PurgeJob | ||
# @return [void] | ||
def async_destroy | ||
return if marked_for_purge? | ||
|
||
soft_delete! unless soft_deleted? | ||
mark_for_purge! | ||
end | ||
|
||
# @return [SoftDeletable, false] | ||
def mark_for_purge | ||
callbacks_result = with_lock do | ||
run_callbacks :mark_for_purge do | ||
mark_record_for_purge! | ||
end | ||
end | ||
|
||
callbacks_result ? self : false | ||
end | ||
|
||
# @raise [ActiveRecord::RecordNotDestroyed] | ||
# @return [SoftDeletable] | ||
def mark_for_purge! | ||
mark_for_purge or raise ActiveRecord::RecordNotDestroyed.new("Failed to mark the record for background purge", self) | ||
end | ||
|
||
def marked_for_purge? | ||
marked_for_purge_at? | ||
end | ||
|
||
def should_scramble_deleted_slug? | ||
has_attribute?(:slug) && slug.present? | ||
end | ||
|
||
def soft_deleted? | ||
deleted_at? | ||
end | ||
|
||
# @return [SoftDeletable, false] | ||
def soft_delete | ||
callbacks_result = with_lock do | ||
run_callbacks :soft_delete do | ||
soft_destroy_record! | ||
soft_destroy_dependent_records! | ||
end | ||
end | ||
|
||
callbacks_result ? self : false | ||
end | ||
|
||
# @raise [ActiveRecord::RecordNotDestroyed] | ||
# @return [SoftDeletable] | ||
def soft_delete! | ||
soft_delete or raise ActiveRecord::RecordNotDestroyed.new("Failed to destroy the record", self) | ||
end | ||
|
||
private | ||
|
||
# @return [void] | ||
def enqueue_background_purge! | ||
AfterCommitEverywhere.after_commit do | ||
SoftDeletions::PurgeJob.set(wait: 5.seconds).perform_later self | ||
end | ||
end | ||
|
||
# @return [void] | ||
def mark_record_for_purge! | ||
update_column :marked_for_purge_at, Time.current | ||
end | ||
|
||
# @return [void] | ||
def prevent_duplicate_soft_deletion! | ||
throw :abort if soft_deleted? | ||
end | ||
|
||
# @return [void] | ||
def scramble_deleted_slug! | ||
new_slug = "#{slug}-deleted-#{SecureRandom.hex(6)}" | ||
|
||
update_column :slug, new_slug | ||
end | ||
|
||
# @return [void] | ||
def soft_destroy_dependent_records! | ||
soft_deletable_associations.each do |association| | ||
associated_records = public_send(association.name) | ||
|
||
case association.options[:dependent] | ||
when :destroy | ||
associated_records.find_each(&:soft_destroy) | ||
when :delete_all | ||
associated_records.sans_deleted.update_all(deleted_at: Time.current) | ||
end | ||
end | ||
end | ||
|
||
# @return [void] | ||
def soft_destroy_record! | ||
update_column :deleted_at, Time.current | ||
end | ||
|
||
# @return [void] | ||
def validate_mark_for_purge! | ||
throw :abort if !soft_deleted? || marked_for_purge? | ||
end | ||
|
||
module ClassMethods | ||
# @return [ActiveRecord::Relation] | ||
def apply_filtering_loads | ||
existing | ||
end | ||
|
||
def soft_deletable? | ||
self < SoftDeletable | ||
end | ||
|
||
# @return [<ActiveRecord::Reflection::AssociationReflection>] | ||
def soft_deletable_associations | ||
reflect_on_all_associations.select do |assoc| | ||
assoc.options[:dependent].present? && assoc.klass.try(:soft_deletable?) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# frozen_string_literal: true | ||
|
||
module TimestampScopes | ||
extend ActiveSupport::Concern | ||
|
||
included do | ||
scope :by_recently_created, -> { order(created_at: :desc) } | ||
scope :by_recently_updated, -> { order(updated_at: :desc) } | ||
|
||
scope :in_recent_order, -> { by_recently_created } | ||
|
||
scope :created_more_than, ->(time) { where(arel_table[:created_at].lteq(time)) } | ||
scope :updated_more_than, ->(time) { where(arel_table[:updated_at].lteq(time)) } | ||
|
||
scope :created_since, ->(time) { where(arel_table[:created_at].gteq(time)) } | ||
scope :updated_since, ->(time) { where(arel_table[:updated_at].gteq(time)) } | ||
|
||
scope :created_in_the_last, ->(duration) { created_since(duration.ago) } | ||
scope :updated_in_the_last, ->(duration) { updated_since(duration.ago) } | ||
end | ||
|
||
module ClassMethods | ||
# @return [ApplicationRecord, nil] | ||
def latest | ||
in_recent_order.first | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.