-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add soft delete functionality to Avram (#323)
- Loading branch information
1 parent
2828306
commit bb57679
Showing
8 changed files
with
247 additions
and
0 deletions.
There are no files selected for viewing
14 changes: 14 additions & 0 deletions
14
db/migrations/20200316160609_create_soft_deletable_items.cr
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,14 @@ | ||
class CreateSoftDeletableItems::V20200316160609 < Avram::Migrator::Migration::V1 | ||
def migrate | ||
create table_for(SoftDeletableItem) do | ||
primary_key id : Int64 | ||
add_timestamps | ||
|
||
add soft_deleted_at : Time? | ||
end | ||
end | ||
|
||
def rollback | ||
drop table_for(SoftDeletableItem) | ||
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,90 @@ | ||
require "./spec_helper" | ||
|
||
class SoftDeletableItemQuery < SoftDeletableItem::BaseQuery | ||
include Avram::SoftDelete::Query | ||
end | ||
|
||
describe "Avram soft delete" do | ||
describe "models" do | ||
it "allows soft deleting a record" do | ||
item = SoftDeletableItemBox.create &.kept | ||
|
||
item = item.soft_delete | ||
|
||
item.soft_deleted_at.should_not be_nil | ||
end | ||
|
||
it "allows restoring a soft deleted record" do | ||
item = SoftDeletableItemBox.create &.soft_deleted | ||
|
||
item = item.restore | ||
|
||
item.soft_deleted_at.should be_nil | ||
end | ||
|
||
it "allows checking if a record is soft deleted" do | ||
item = SoftDeletableItemBox.create &.kept | ||
item.soft_deleted?.should be_false | ||
|
||
item = item.soft_delete | ||
|
||
item.soft_deleted?.should be_true | ||
end | ||
end | ||
|
||
describe "queries" do | ||
it "can get only kept records" do | ||
kept_item = SoftDeletableItemBox.create &.kept | ||
SoftDeletableItemBox.create &.soft_deleted | ||
|
||
SoftDeletableItemQuery.new.only_soft_deleted.only_kept.results.should eq([ | ||
kept_item, | ||
]) | ||
end | ||
|
||
it "can get only soft deleted records" do | ||
SoftDeletableItemBox.create &.kept | ||
soft_deleted_item = SoftDeletableItemBox.create &.soft_deleted | ||
|
||
SoftDeletableItemQuery.new.only_kept.only_soft_deleted.results.should eq([ | ||
soft_deleted_item, | ||
]) | ||
end | ||
|
||
it "can get soft deleted and kept records" do | ||
kept_item = SoftDeletableItemBox.create &.kept | ||
soft_deleted_item = SoftDeletableItemBox.create &.soft_deleted | ||
|
||
SoftDeletableItemQuery.new.only_kept.with_soft_deleted.results.should eq([ | ||
kept_item, | ||
soft_deleted_item, | ||
]) | ||
end | ||
|
||
it "can bulk soft delete" do | ||
kept_item = SoftDeletableItemBox.create &.kept | ||
soft_deleted_item = SoftDeletableItemBox.create &.soft_deleted | ||
|
||
num_restored = SoftDeletableItemQuery.new.soft_delete | ||
|
||
num_restored.should eq(1) | ||
kept_item.reload.soft_deleted?.should be_true | ||
soft_deleted_item.reload.soft_deleted?.should be_true | ||
end | ||
|
||
it "can bulk restore" do | ||
kept_item = SoftDeletableItemBox.create &.kept | ||
soft_deleted_item = SoftDeletableItemBox.create &.soft_deleted | ||
|
||
num_restored = SoftDeletableItemQuery.new.restore | ||
|
||
num_restored.should eq(1) | ||
kept_item.reload.soft_deleted?.should be_false | ||
soft_deleted_item.reload.soft_deleted?.should be_false | ||
end | ||
end | ||
end | ||
|
||
private def reload(item) | ||
SoftDeletableItemQuery.find(item.id) | ||
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,9 @@ | ||
class SoftDeletableItemBox < BaseBox | ||
def kept | ||
soft_deleted_at nil | ||
end | ||
|
||
def soft_deleted | ||
soft_deleted_at Time.utc | ||
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,11 @@ | ||
class SoftDeletableItem < BaseModel | ||
include Avram::SoftDelete::Model | ||
|
||
skip_default_columns | ||
|
||
table do | ||
primary_key id : Int64 | ||
column soft_deleted_at : Time? | ||
timestamps | ||
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,53 @@ | ||
# Add methods for soft deleting and restoring an individual record | ||
# | ||
# Include this module in your model, and make sure to add a `soft_deleted_at` | ||
# column to your model. The column type must be `Time?` | ||
# | ||
# ```crystal | ||
# # In a migration | ||
# add soft_deleted_at : Time? | ||
# | ||
# # In your model | ||
# class Article < BaseModel | ||
# include Avram::SoftDelete::Model | ||
|
||
# table do | ||
# column soft_deleted_at : Time? | ||
# end | ||
# end | ||
# ``` | ||
# | ||
# You should also add the `Avram::SoftDeleteQuery` to your query | ||
# | ||
# ```crystal | ||
# class ArticleQuery < Article::BaseQuery | ||
# include Avram::SoftDelete::Query | ||
# end | ||
# ``` | ||
module Avram::SoftDelete::Model | ||
# Soft delete the record | ||
# | ||
# This will set `soft_deleted_at` to the current time (`Time.utc`) | ||
def soft_delete : self | ||
save_operation_class.update!(self, soft_deleted_at: Time.utc) | ||
end | ||
|
||
# Restore the record | ||
# | ||
# This will set `soft_deleted_at` to `nil` | ||
def restore : self | ||
save_operation_class.update!(self, soft_deleted_at: nil) | ||
end | ||
|
||
abstract def save_operation_class | ||
|
||
# Returns true if soft deleted, otherwise false | ||
# | ||
# If the `soft_deleted_at` has a time value the record is "soft deleted". | ||
# If `soft_deleted_at` is `nil` the record has not been deleted yet. | ||
def soft_deleted? : Bool | ||
soft_deleted_at.present? | ||
end | ||
|
||
abstract def soft_deleted_at : Time? | ||
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,65 @@ | ||
# Add methods for querying/updating soft deleted and kept records. | ||
# | ||
# First include the model module in your model: `Avram::SoftDelete::Model` | ||
# | ||
# Then add this module your query | ||
# | ||
# ```crystal | ||
# class ArticleQuery < Article::BaseQuery | ||
# include Avram::SoftDelete::Query | ||
# end | ||
# ``` | ||
module Avram::SoftDelete::Query | ||
# Only return kept records | ||
# | ||
# Kept records are considered "kept/not soft deleted" if the | ||
# `soft_deleted_at` column is `nil` | ||
def only_kept | ||
reset_where(&.soft_deleted_at).soft_deleted_at.is_nil | ||
end | ||
|
||
# Only return soft deleted records | ||
# | ||
# Soft deleted records are considered "soft deleted" if the | ||
# `soft_deleted_at` column has a non-nil value | ||
def only_soft_deleted | ||
reset_where(&.soft_deleted_at).soft_deleted_at.is_not_nil | ||
end | ||
|
||
# Returns all records | ||
# | ||
# This works be removing where clauses for the `soft_deleted_at` column. | ||
# That means you can do `MyQuery.new.only_kept.with_soft_deleted` and you | ||
# will get all records, not just the kept ones. | ||
def with_soft_deleted | ||
reset_where(&.soft_deleted_at) | ||
end | ||
|
||
# Bulk soft delete records | ||
# | ||
# ## Example | ||
# | ||
# This will soft delete all `Article` record older than 1 year: | ||
# | ||
# ```crystal | ||
# ArticleQuery.new.created_at.lt(1.year.ago).soft_delete | ||
# ``` | ||
def soft_delete | ||
only_kept.update(soft_deleted_at: Time.utc) | ||
end | ||
|
||
# Bulk restore records | ||
# | ||
# ## Example | ||
# | ||
# This will restore `Article` records updated in the last week: | ||
# | ||
# ```crystal | ||
# ArticleQuery.new.updated_at.gt(1.week.ago).restore | ||
# ``` | ||
def restore : Int64 | ||
only_soft_deleted.update(soft_deleted_at: nil) | ||
end | ||
|
||
abstract def soft_deleted_at | ||
end |