├── lib ├── kamikakushi │ ├── version.rb │ ├── active_record │ │ └── base.rb │ ├── kamikakushi.rb │ └── kaonashi.rb └── kamikakushi.rb ├── Gemfile ├── spec ├── spec_helper.rb ├── support │ └── active_record_setting.rb └── kamikakushi │ ├── kaonashi_spec.rb │ └── kamikakushi_spec.rb ├── Rakefile ├── README.md ├── kamikakushi.gemspec ├── MIT-LICENSE ├── .gitignore └── Gemfile.lock /lib/kamikakushi/version.rb: -------------------------------------------------------------------------------- 1 | module Kamikakushi 2 | VERSION = "0.0.5" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development, :test do 6 | gem 'rspec-rails' 7 | end 8 | -------------------------------------------------------------------------------- /lib/kamikakushi.rb: -------------------------------------------------------------------------------- 1 | require 'kamikakushi/kamikakushi' 2 | require 'kamikakushi/kaonashi' 3 | require 'kamikakushi/active_record/base' 4 | -------------------------------------------------------------------------------- /lib/kamikakushi/active_record/base.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | class Base 3 | include Kamikakushi::Kamikakushi 4 | include Kamikakushi::Kaonashi 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | require 'bundler/setup' 3 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 4 | 5 | Bundler.require 6 | 7 | RSpec.configure do |config| 8 | end 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'Kamikakushi' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | 18 | 19 | 20 | Bundler::GemHelper.install_tasks 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kamikakushi 2 | 3 | very very simple soft deletion gem:) 4 | 5 | ``` 6 | class Post < ActiveRecord::Base 7 | kamikakushi 8 | end 9 | ``` 10 | 11 | ``` 12 | class Comment < ActiveRecord::Base 13 | kaonashi parent: :post 14 | belongs_to :post 15 | end 16 | ``` 17 | 18 | ## usage 19 | 20 | ``` 21 | post = Post.create(content: 'demo') 22 | post.destroy 23 | post.destroyed? # true 24 | post.restore 25 | post.destroyed? # false 26 | post.purge # real destroy 27 | post.reload # raise ActiveRecord::RecordNotFound 28 | ``` 29 | 30 | ``` 31 | post = Post.create(content: 'demo') 32 | comment = post.comments.create(content: 'hoge') 33 | post.destroy 34 | comments.destroyed? # true 35 | ``` 36 | 37 | ## scope 38 | 39 | `with_deleted`, `without_deleted`, `only_deleted` 40 | -------------------------------------------------------------------------------- /kamikakushi.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "kamikakushi/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "kamikakushi" 9 | s.version = Kamikakushi::VERSION 10 | s.authors = ["ppworks"] 11 | s.email = ["koshikawa@ppworks.jp"] 12 | s.homepage = "https://github.com/ppworks/kamikakushi" 13 | s.summary = "Hide away record as soft delete" 14 | s.description = "Hide away record as soft delete" 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] 18 | 19 | s.add_dependency "rails", "~> 4.1" 20 | s.add_development_dependency "sqlite3" 21 | end 22 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 YOURNAME 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/support/active_record_setting.rb: -------------------------------------------------------------------------------- 1 | require 'rails/all' 2 | require 'kamikakushi' 3 | ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ':memory:'}} 4 | ActiveRecord::Base.establish_connection :test 5 | 6 | class Post < ActiveRecord::Base 7 | kamikakushi 8 | attr_accessor :comment_after_destroy 9 | has_many :comments 10 | before_destroy :set_before_destroy 11 | after_destroy :set_after_destroy 12 | 13 | def set_before_destroy 14 | return false if self.protected 15 | end 16 | 17 | def set_after_destroy 18 | self.comment_after_destroy = "KAMIKAKUSHI" 19 | end 20 | end 21 | 22 | class Comment < ActiveRecord::Base 23 | kaonashi parent: :post 24 | belongs_to :post 25 | end 26 | 27 | class CreateAllTables < ActiveRecord::Migration 28 | def self.up 29 | create_table(:posts) do |t| 30 | t.text :content 31 | t.boolean :protected 32 | t.datetime :deleted_at 33 | end 34 | 35 | create_table(:comments) do |t| 36 | t.integer :post_id 37 | t.text :content 38 | end 39 | end 40 | end 41 | ActiveRecord::Migration.verbose = false 42 | CreateAllTables.up 43 | -------------------------------------------------------------------------------- /lib/kamikakushi/kamikakushi.rb: -------------------------------------------------------------------------------- 1 | module Kamikakushi 2 | module Kamikakushi 3 | extend ActiveSupport::Concern 4 | 5 | module ClassMethods 6 | def kamikakushi(options = {}) 7 | options.reverse_merge!(column_name: :deleted_at) 8 | define_singleton_method(:kamikakushi_column_name) { options[:column_name] } 9 | class_eval do 10 | include InstanceMethods 11 | default_scope { without_deleted } 12 | alias_method_chain :destroy, :kamikakushi 13 | alias_method_chain :destroyed?, :kamikakushi 14 | 15 | scope :with_deleted, -> { 16 | unscope(where: kamikakushi_column_name.to_sym) 17 | } 18 | 19 | scope :without_deleted, -> { 20 | where(kamikakushi_column_name => nil) 21 | } 22 | 23 | scope :only_deleted, -> { 24 | with_deleted.where.not(kamikakushi_column_name => nil) 25 | } 26 | end 27 | end 28 | end 29 | 30 | module InstanceMethods 31 | def destroy_with_kamikakushi 32 | run_callbacks(:destroy) do 33 | touch(self.class.kamikakushi_column_name) 34 | end 35 | end 36 | 37 | def destroyed_with_kamikakushi? 38 | self.deleted_at? || destroyed_without_kamikakushi? 39 | end 40 | 41 | def restore 42 | update_column(self.class.kamikakushi_column_name.to_sym, write_attribute(self.class.kamikakushi_column_name, nil)) 43 | end 44 | 45 | def purge 46 | destroy_without_kamikakushi 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/kamikakushi/kaonashi_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Kamikakushi::Kaonashi do 4 | let!(:post) { Post.create(content: 'demo') } 5 | let!(:comment) { post.comments.create(post_id: post.id, content: 'comment xxxx') } 6 | after { Post.with_deleted.delete_all } 7 | after { Comment.with_deleted.delete_all } 8 | 9 | describe 'select record' do 10 | subject { Comment.all.to_sql } 11 | it { is_expected.to include 'WHERE "posts"."deleted_at" IS NULL' } 12 | end 13 | 14 | describe '#destroyed?' do 15 | it do 16 | expect { 17 | post.destroy 18 | }.to change(comment, :destroyed?).from(false).to(true) 19 | end 20 | end 21 | 22 | describe 'scope' do 23 | let!(:deleted_post) { Post.create(content: 'deleted') } 24 | let!(:deleted_comment) { deleted_post.comments.create(post_id: deleted_post.id, content: 'deleted') } 25 | 26 | before do 27 | deleted_post.destroy 28 | end 29 | 30 | describe '.with_deleted' do 31 | subject { Comment.with_deleted.all.to_a } 32 | it { is_expected.to include comment } 33 | it { is_expected.to include deleted_comment } 34 | end 35 | 36 | describe '.without_deleted' do 37 | subject { Comment.without_deleted.all.to_a } 38 | it { is_expected.to include comment } 39 | it { is_expected.not_to include deleted_comment } 40 | end 41 | 42 | describe '.only_deleted' do 43 | subject { Comment.only_deleted.all.to_a } 44 | it { is_expected.not_to include comment } 45 | it { is_expected.to include deleted_comment } 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/kamikakushi/kaonashi.rb: -------------------------------------------------------------------------------- 1 | module Kamikakushi 2 | module Kaonashi 3 | extend ActiveSupport::Concern 4 | 5 | module ClassMethods 6 | def kaonashi(options = {}) 7 | define_singleton_method(:kaonashi_parent_name) { options[:parent] } 8 | return unless kaonashi_parent_name 9 | 10 | class_eval do 11 | include InstanceMethods 12 | default_scope { without_deleted } 13 | alias_method_chain :destroyed?, :kaonashi 14 | 15 | scope :with_deleted, -> { 16 | join_with_dependent_parent(kaonashi_parent_name, :with_deleted) 17 | } 18 | 19 | scope :without_deleted, -> { 20 | join_with_dependent_parent(kaonashi_parent_name, :without_deleted) 21 | } 22 | 23 | scope :only_deleted, -> { 24 | join_with_dependent_parent(kaonashi_parent_name, :only_deleted) 25 | } 26 | end 27 | end 28 | 29 | private 30 | 31 | def join_with_dependent_parent(kaonashi_parent_name, scope_name) 32 | association = reflect_on_all_associations.find { |a| a.name == kaonashi_parent_name } 33 | 34 | parent_arel = association.klass.arel_table 35 | joins_conditions = arel_table.join(parent_arel) 36 | .on(parent_arel[association.klass.primary_key.to_sym].eq arel_table[association.foreign_key]) 37 | .join_sources 38 | joins(joins_conditions).merge(association.klass.__send__(scope_name)) 39 | end 40 | end 41 | 42 | module InstanceMethods 43 | def destroyed_with_kaonashi? 44 | association = self.class.reflect_on_all_associations.find { |a| a.name == self.class.kaonashi_parent_name } 45 | association.klass.with_deleted.find(__send__(association.foreign_key)).destroyed? 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/kamikakushi/kamikakushi_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Kamikakushi::Kamikakushi do 4 | let!(:post) { Post.create(content: 'demo') } 5 | after { Post.with_deleted.delete_all } 6 | 7 | describe 'select record' do 8 | subject { Post.all.to_sql } 9 | it { is_expected.to include 'WHERE "posts"."deleted_at" IS NULL' } 10 | end 11 | 12 | describe 'logical delete' do 13 | it do 14 | expect { 15 | post.destroy 16 | post.reload 17 | }.to change(post, :deleted_at).from(nil) 18 | end 19 | 20 | it 'should call before_destroy callback ' do 21 | expect { 22 | post.protected = true 23 | post.destroy 24 | }.not_to change(post, :deleted_at).from(nil) 25 | end 26 | 27 | it 'should call after_destroy callback' do 28 | expect { 29 | post.destroy 30 | }.to change(post, :comment_after_destroy).from(nil).to('KAMIKAKUSHI') 31 | end 32 | end 33 | 34 | describe 'real delete' do 35 | it do 36 | expect { 37 | post.purge 38 | post.reload 39 | }.to raise_error(ActiveRecord::RecordNotFound) 40 | end 41 | end 42 | 43 | describe '#destroyed?' do 44 | it do 45 | expect { 46 | post.destroy 47 | }.to change(post, :destroyed?).from(false).to(true) 48 | end 49 | end 50 | 51 | describe '#restore' do 52 | before { post.destroy } 53 | 54 | it do 55 | expect { 56 | post.restore 57 | post.reload 58 | }.to change(post, :destroyed?).from(true).to(false) 59 | end 60 | end 61 | 62 | describe 'scope' do 63 | let!(:deleted_post) { Post.create(content: 'deleted', deleted_at: Time.current) } 64 | 65 | describe '.with_deleted' do 66 | subject { Post.with_deleted.all.to_a } 67 | it { is_expected.to include post } 68 | it { is_expected.to include deleted_post } 69 | end 70 | 71 | describe '.without_deleted' do 72 | subject { Post.without_deleted.all.to_a } 73 | it { is_expected.to include post } 74 | it { is_expected.not_to include deleted_post } 75 | end 76 | 77 | describe '.only_deleted' do 78 | subject { Post.only_deleted.all.to_a } 79 | it { is_expected.not_to include post } 80 | it { is_expected.to include deleted_post } 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Added by gemignore. Snippet 'Global/OSX' 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear on external disk 13 | .Spotlight-V100 14 | .Trashes 15 | 16 | # Directories potentially created on remote AFP share 17 | .AppleDB 18 | .AppleDesktop 19 | Network Trash Folder 20 | Temporary Items 21 | .apdisk 22 | 23 | 24 | # Added by gemignore. Snippet 'Global/Linux' 25 | *~ 26 | 27 | # KDE directory preferences 28 | .directory 29 | 30 | 31 | # Added by gemignore. Snippet 'Global/vim' 32 | [._]*.s[a-w][a-z] 33 | [._]s[a-w][a-z] 34 | *.un~ 35 | Session.vim 36 | .netrwhist 37 | *~ 38 | 39 | 40 | # Added by gemignore. Snippet 'Ruby' 41 | *.gem 42 | *.rbc 43 | /.config 44 | /coverage/ 45 | /InstalledFiles 46 | /pkg/ 47 | /spec/reports/ 48 | /test/tmp/ 49 | /test/version_tmp/ 50 | /tmp/ 51 | 52 | ## Specific to RubyMotion: 53 | .dat* 54 | .repl_history 55 | build/ 56 | 57 | ## Documentation cache and generated files: 58 | /.yardoc/ 59 | /_yardoc/ 60 | /doc/ 61 | /rdoc/ 62 | 63 | ## Environment normalisation: 64 | /.bundle/ 65 | /lib/bundler/man/ 66 | 67 | # for a library or gem, you might want to ignore these files since the code is 68 | # intended to run in multiple environments; otherwise, check them in: 69 | # Gemfile.lock 70 | # .ruby-version 71 | # .ruby-gemset 72 | 73 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 74 | .rvmrc 75 | 76 | 77 | # Added by gemignore. Snippet 'Rails' 78 | *.rbc 79 | capybara-*.html 80 | .rspec 81 | /log 82 | /tmp 83 | /db/*.sqlite3 84 | /public/system 85 | /coverage/ 86 | /spec/tmp 87 | **.orig 88 | rerun.txt 89 | pickle-email-*.html 90 | 91 | # TODO Comment out these rules if you are OK with secrets being uploaded to the repo 92 | config/initializers/secret_token.rb 93 | config/secrets.yml 94 | 95 | ## Environment normalisation: 96 | /.bundle 97 | /vendor/bundle 98 | 99 | # these should all be checked in to normalise the environment: 100 | # Gemfile.lock, .ruby-version, .ruby-gemset 101 | 102 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 103 | .rvmrc 104 | 105 | # if using bower-rails ignore default bower_components path bower.json files 106 | /vendor/assets/bower_components 107 | *.bowerrc 108 | bower.json 109 | 110 | ## Rails engine 111 | spec/dummy/log 112 | spec/dummy/tmp 113 | spec/dummy/db/*.sqlite3 114 | spec/dummy/public/system 115 | spec/dummy/coverage/ 116 | spec/dummy/spec/tmp 117 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | kamikakushi (0.0.5) 5 | rails (~> 4.1) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actionmailer (4.1.9) 11 | actionpack (= 4.1.9) 12 | actionview (= 4.1.9) 13 | mail (~> 2.5, >= 2.5.4) 14 | actionpack (4.1.9) 15 | actionview (= 4.1.9) 16 | activesupport (= 4.1.9) 17 | rack (~> 1.5.2) 18 | rack-test (~> 0.6.2) 19 | actionview (4.1.9) 20 | activesupport (= 4.1.9) 21 | builder (~> 3.1) 22 | erubis (~> 2.7.0) 23 | activemodel (4.1.9) 24 | activesupport (= 4.1.9) 25 | builder (~> 3.1) 26 | activerecord (4.1.9) 27 | activemodel (= 4.1.9) 28 | activesupport (= 4.1.9) 29 | arel (~> 5.0.0) 30 | activesupport (4.1.9) 31 | i18n (~> 0.6, >= 0.6.9) 32 | json (~> 1.7, >= 1.7.7) 33 | minitest (~> 5.1) 34 | thread_safe (~> 0.1) 35 | tzinfo (~> 1.1) 36 | arel (5.0.1.20140414130214) 37 | builder (3.2.2) 38 | diff-lcs (1.2.5) 39 | erubis (2.7.0) 40 | hike (1.2.3) 41 | i18n (0.7.0) 42 | json (1.8.2) 43 | mail (2.6.3) 44 | mime-types (>= 1.16, < 3) 45 | mime-types (2.4.3) 46 | minitest (5.5.0) 47 | multi_json (1.10.1) 48 | rack (1.5.2) 49 | rack-test (0.6.2) 50 | rack (>= 1.0) 51 | rails (4.1.9) 52 | actionmailer (= 4.1.9) 53 | actionpack (= 4.1.9) 54 | actionview (= 4.1.9) 55 | activemodel (= 4.1.9) 56 | activerecord (= 4.1.9) 57 | activesupport (= 4.1.9) 58 | bundler (>= 1.3.0, < 2.0) 59 | railties (= 4.1.9) 60 | sprockets-rails (~> 2.0) 61 | railties (4.1.9) 62 | actionpack (= 4.1.9) 63 | activesupport (= 4.1.9) 64 | rake (>= 0.8.7) 65 | thor (>= 0.18.1, < 2.0) 66 | rake (10.4.2) 67 | rspec-core (3.1.7) 68 | rspec-support (~> 3.1.0) 69 | rspec-expectations (3.1.2) 70 | diff-lcs (>= 1.2.0, < 2.0) 71 | rspec-support (~> 3.1.0) 72 | rspec-mocks (3.1.3) 73 | rspec-support (~> 3.1.0) 74 | rspec-rails (3.1.0) 75 | actionpack (>= 3.0) 76 | activesupport (>= 3.0) 77 | railties (>= 3.0) 78 | rspec-core (~> 3.1.0) 79 | rspec-expectations (~> 3.1.0) 80 | rspec-mocks (~> 3.1.0) 81 | rspec-support (~> 3.1.0) 82 | rspec-support (3.1.2) 83 | sprockets (2.12.3) 84 | hike (~> 1.2) 85 | multi_json (~> 1.0) 86 | rack (~> 1.0) 87 | tilt (~> 1.1, != 1.3.0) 88 | sprockets-rails (2.2.4) 89 | actionpack (>= 3.0) 90 | activesupport (>= 3.0) 91 | sprockets (>= 2.8, < 4.0) 92 | sqlite3 (1.3.10) 93 | thor (0.19.1) 94 | thread_safe (0.3.4) 95 | tilt (1.4.1) 96 | tzinfo (1.2.2) 97 | thread_safe (~> 0.1) 98 | 99 | PLATFORMS 100 | ruby 101 | 102 | DEPENDENCIES 103 | kamikakushi! 104 | rspec-rails 105 | sqlite3 106 | --------------------------------------------------------------------------------