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

Add soft delete functionality to Avram #323

Merged
merged 1 commit into from
Mar 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions db/migrations/20200316160609_create_soft_deletable_items.cr
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
90 changes: 90 additions & 0 deletions spec/soft_delete_spec.cr
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
9 changes: 9 additions & 0 deletions spec/support/boxes/soft_deletable_item_box.cr
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
11 changes: 11 additions & 0 deletions spec/support/soft_deletable_item.cr
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
1 change: 1 addition & 0 deletions src/avram.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require "dexter"
require "lucky_cli"
require "wordsmith"
require "habitat"
require "blank"
require "./avram/criteria"
require "./avram/type"
require "./avram/table_for"
Expand Down
4 changes: 4 additions & 0 deletions src/avram/save_operation_template.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class Avram::SaveOperationTemplate
::{{ type }}::BaseQuery
end

def save_operation_class : ::{{ type }}::SaveOperation.class
::{{ type }}::SaveOperation
end

class ::{{ type }}::SaveOperation < Avram::SaveOperation({{ type }})
{% if primary_key_type.id == UUID.id %}
before_save set_uuid
Expand Down
53 changes: 53 additions & 0 deletions src/avram/soft_delete/model.cr
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?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wontruefree FIxed!

end

abstract def soft_deleted_at : Time?
end
65 changes: 65 additions & 0 deletions src/avram/soft_delete/query.cr
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