deleted_at 0.4.0rc1 → 0.5.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/{spec/support/rails/public/favicon.ico → CHANGELOG.md} +0 -0
- data/{LICENSE.txt → LICENSE} +0 -0
- data/README.md +29 -15
- data/lib/deleted_at.rb +29 -29
- data/lib/deleted_at/active_record.rb +37 -0
- data/lib/deleted_at/core.rb +51 -0
- data/lib/deleted_at/{views.rb → legacy.rb} +23 -14
- data/lib/deleted_at/railtie.rb +5 -4
- data/lib/deleted_at/relation.rb +72 -0
- data/lib/deleted_at/table_definition.rb +10 -0
- data/lib/deleted_at/version.rb +1 -1
- metadata +30 -67
- data/.gitignore +0 -78
- data/.rspec +0 -2
- data/.travis.yml +0 -43
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -19
- data/Rakefile +0 -6
- data/deleted_at.gemspec +0 -42
- data/gemfiles/activerecord-4.0.Gemfile +0 -3
- data/gemfiles/activerecord-4.1.Gemfile +0 -3
- data/gemfiles/activerecord-4.2.Gemfile +0 -3
- data/gemfiles/activerecord-5.0.Gemfile +0 -3
- data/gemfiles/activerecord-5.1.Gemfile +0 -3
- data/lib/deleted_at/active_record/base.rb +0 -102
- data/lib/deleted_at/active_record/connection_adapters/abstract/schema_definition.rb +0 -17
- data/lib/deleted_at/active_record/relation.rb +0 -43
- data/spec/deleted_at/active_record/base_spec.rb +0 -21
- data/spec/deleted_at/active_record/relation_spec.rb +0 -166
- data/spec/deleted_at/views_spec.rb +0 -76
- data/spec/spec_helper.rb +0 -28
- data/spec/support/rails/app/models/animals/dog.rb +0 -5
- data/spec/support/rails/app/models/comment.rb +0 -6
- data/spec/support/rails/app/models/post.rb +0 -7
- data/spec/support/rails/app/models/user.rb +0 -7
- data/spec/support/rails/config/database.yml +0 -4
- data/spec/support/rails/config/routes.rb +0 -3
- data/spec/support/rails/db/schema.rb +0 -27
- data/spec/support/rails/log/.gitignore +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df0de7db0fb5c4de89e941d080ac07fc08265ae29c2ae550e9e11b1057699763
|
4
|
+
data.tar.gz: e79f565b01952c39277e5c49f100441c3b19cfd6245056192b8261c4bd33541c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a18356dbbecaa30e77709349b85797ca3a50edadfde5474f62a3db841b52c28a14285ca4498318d17f4a3bf5b5b1e246936d69087b218e68592e6d99a0affe0
|
7
|
+
data.tar.gz: e16f8f3c9b010aa85a0922fe2673d14866eb0451a14988edc1bd6be3efe6d83e75993562046c1ea93ec2b188923d0b4e5596031fadd4439ac989369440a838d9
|
File without changes
|
data/{LICENSE.txt → LICENSE}
RENAMED
File without changes
|
data/README.md
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
[![Version ](https://img.shields.io/gem/v/deleted_at.svg?maxAge=2592000)](https://rubygems.org/gems/deleted_at)
|
2
2
|
[![Build Status ](https://travis-ci.org/TwilightCoders/deleted_at.svg)](https://travis-ci.org/TwilightCoders/deleted_at)
|
3
|
-
[![Code Climate ](https://api.codeclimate.com/v1/badges/762cdcd63990efa768b0/maintainability)](https://codeclimate.com/github/TwilightCoders/deleted_at)
|
3
|
+
[![Code Climate ](https://api.codeclimate.com/v1/badges/762cdcd63990efa768b0/maintainability)](https://codeclimate.com/github/TwilightCoders/deleted_at/maintainability)
|
4
4
|
[![Test Coverage](https://codeclimate.com/github/TwilightCoders/deleted_at/badges/coverage.svg)](https://codeclimate.com/github/TwilightCoders/deleted_at/coverage)
|
5
|
+
[![Dependencies ](https://gemnasium.com/badges/github.com/TwilightCoders/deleted_at.svg)](https://gemnasium.com/github.com/TwilightCoders/deleted_at)
|
5
6
|
|
6
7
|
# DeletedAt
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
DeletedAt leverages the power of SQL views to achieve the same effect. It also takes advantage of Ruby's flexibility.
|
9
|
+
Hide your "deleted" data (unless specifically asked for) without resorting to `default_scope` by leveraging in-line sub-selects.
|
11
10
|
|
12
11
|
## Requirements
|
13
12
|
|
14
|
-
|
13
|
+
- Ruby 2.3+
|
14
|
+
- ActiveRecord 4.2+
|
15
15
|
|
16
16
|
## Installation
|
17
17
|
|
@@ -31,45 +31,59 @@ Or install it yourself as:
|
|
31
31
|
|
32
32
|
## Usage
|
33
33
|
|
34
|
-
|
34
|
+
Invoking `with_deleted_at` sets the class up to use the `deleted_at` functionality.
|
35
35
|
|
36
36
|
```ruby
|
37
37
|
class User < ActiveRecord::Base
|
38
|
-
# Feel free to include/extend other modules before or after, as you see fit...
|
39
|
-
|
40
38
|
with_deleted_at
|
41
39
|
|
42
40
|
# the rest of your model code...
|
43
41
|
end
|
44
42
|
```
|
45
43
|
|
46
|
-
|
44
|
+
To work properly, the tables that back these models must have a `deleted_at` timestamp column.
|
47
45
|
|
48
46
|
```ruby
|
49
47
|
class AddDeletedAtColumnToUsers < ActiveRecord::Migration
|
50
48
|
|
51
49
|
def up
|
52
50
|
add_column :users, :deleted_at, 'timestamp with time zone'
|
53
|
-
|
54
|
-
DeletedAt.install(User)
|
55
51
|
end
|
56
52
|
|
57
53
|
def down
|
58
|
-
DeletedAt.uninstall(User)
|
59
|
-
|
60
54
|
remove_column :users, :deleted_at, 'timestamp with time zone'
|
61
55
|
end
|
62
56
|
|
63
57
|
end
|
64
58
|
```
|
65
59
|
|
60
|
+
If you're starting with a brand-new table, the existing `timestamps` DSL has been extended to accept `deleted_at: true` as an option, for convenience. Or you can do it seperately as shown above.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class CreatCommentsTable < ActiveRecord::Migration
|
64
|
+
|
65
|
+
def up
|
66
|
+
create_table :comments do |t|
|
67
|
+
# ...
|
68
|
+
# to the `timestamps` DSL
|
69
|
+
t.timestamps null: false, deleted_at: true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def down
|
74
|
+
drop_table :comments
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
66
80
|
## Development
|
67
81
|
|
68
|
-
After checking out the repo, run `
|
82
|
+
After checking out the repo, run `bundle` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
69
83
|
|
70
84
|
## Contributing
|
71
85
|
|
72
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
86
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/TwilightCoders/deleted_at. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
73
87
|
|
74
88
|
## License
|
75
89
|
|
data/lib/deleted_at.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
-
require
|
2
|
-
require 'deleted_at/views'
|
3
|
-
require 'deleted_at/active_record/base'
|
4
|
-
require 'deleted_at/active_record/relation'
|
5
|
-
require 'deleted_at/active_record/connection_adapters/abstract/schema_definition'
|
6
|
-
|
1
|
+
require 'deleted_at/version'
|
7
2
|
require 'deleted_at/railtie' if defined?(Rails::Railtie)
|
8
3
|
|
9
4
|
module DeletedAt
|
10
5
|
|
6
|
+
MissingColumn = Class.new(StandardError)
|
7
|
+
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
column: :deleted_at,
|
10
|
+
proc: -> { Time.now.utc }
|
11
|
+
}
|
12
|
+
|
11
13
|
class << self
|
12
14
|
attr_writer :logger
|
15
|
+
attr_reader :disabled
|
13
16
|
|
14
17
|
def logger
|
15
18
|
@logger ||= Logger.new($stdout).tap do |log|
|
@@ -19,37 +22,34 @@ module DeletedAt
|
|
19
22
|
end
|
20
23
|
end
|
21
24
|
|
22
|
-
|
23
|
-
::ActiveRecord::Relation.send :prepend, DeletedAt::ActiveRecord::Relation
|
24
|
-
::ActiveRecord::Base.send :include, DeletedAt::ActiveRecord::Base
|
25
|
-
::ActiveRecord::ConnectionAdapters::TableDefinition.send :prepend, DeletedAt::ActiveRecord::ConnectionAdapters::TableDefinition
|
26
|
-
end
|
27
|
-
|
28
|
-
def self.install(model)
|
29
|
-
return false unless model.has_deleted_at_column?
|
30
|
-
|
31
|
-
DeletedAt::Views.install_present_view(model)
|
32
|
-
DeletedAt::Views.install_deleted_view(model)
|
25
|
+
@disabled = false
|
33
26
|
|
34
|
-
|
35
|
-
|
36
|
-
model.with_deleted_at
|
27
|
+
def self.disabled?
|
28
|
+
@disabled == true
|
37
29
|
end
|
38
30
|
|
39
|
-
def self.
|
40
|
-
|
31
|
+
def self.disable
|
32
|
+
@disabled = true
|
33
|
+
end
|
41
34
|
|
42
|
-
|
43
|
-
|
35
|
+
def self.enable
|
36
|
+
@disabled = false
|
37
|
+
end
|
44
38
|
|
45
|
-
|
46
|
-
|
39
|
+
def self.gemspec
|
40
|
+
@gemspec ||= eval(`gem spec deleted_at --ruby`).freeze
|
47
41
|
end
|
48
42
|
|
49
|
-
def self.
|
50
|
-
|
43
|
+
def self.install(model)
|
44
|
+
logger.warn <<-STR
|
45
|
+
Great news! You're using the new and improved version of DeletedAt. No more table renaming.
|
46
|
+
You'll want to migrate your old models to use the new (non-view based) functionality.
|
47
|
+
Follow the instructions at #{gemspec.homepage}.
|
48
|
+
STR
|
51
49
|
end
|
52
50
|
|
53
|
-
|
51
|
+
def self.uninstall(model)
|
52
|
+
|
53
|
+
end
|
54
54
|
|
55
55
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'deleted_at/relation'
|
3
|
+
|
4
|
+
module DeletedAt
|
5
|
+
module ActiveRecord
|
6
|
+
|
7
|
+
def self.prepended(subclass)
|
8
|
+
subclass.const_get(:ActiveRecord_Relation).prepend(DeletedAt::Relation)
|
9
|
+
subclass.const_get(:ActiveRecord_AssociationRelation).prepend(DeletedAt::Relation)
|
10
|
+
subclass.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
def inherited(subclass)
|
16
|
+
super
|
17
|
+
subclass.with_deleted_at self.deleted_at
|
18
|
+
end
|
19
|
+
|
20
|
+
def all
|
21
|
+
const_get(:Present)
|
22
|
+
end
|
23
|
+
|
24
|
+
def const_missing(const)
|
25
|
+
case const
|
26
|
+
when :All, :Deleted, :Present
|
27
|
+
all_without_deleted_at.tap do |rel|
|
28
|
+
rel.deleted_at_scope = const
|
29
|
+
end
|
30
|
+
else super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'deleted_at/active_record'
|
2
|
+
|
3
|
+
module DeletedAt
|
4
|
+
|
5
|
+
module Core
|
6
|
+
|
7
|
+
def self.prepended(subclass)
|
8
|
+
class << subclass
|
9
|
+
cattr_accessor :deleted_at do
|
10
|
+
DeletedAt::DEFAULT_OPTIONS
|
11
|
+
end
|
12
|
+
alias all_without_deleted_at all
|
13
|
+
end
|
14
|
+
|
15
|
+
subclass.extend(ClassMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.raise_missing(klass)
|
19
|
+
message = "Missing `#{klass.deleted_at[:column]}` in `#{klass.name}` when trying to employ `deleted_at`"
|
20
|
+
raise(DeletedAt::MissingColumn, message)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.has_deleted_at_column?(klass)
|
24
|
+
klass.columns.map(&:name).include?(klass.deleted_at[:column].to_s)
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
|
29
|
+
def with_deleted_at(options={}, &block)
|
30
|
+
return if ::DeletedAt.disabled?
|
31
|
+
|
32
|
+
self.deleted_at.merge(options)
|
33
|
+
self.deleted_at[:proc] = block if block_given?
|
34
|
+
|
35
|
+
DeletedAt::Core.raise_missing(self) unless Core.has_deleted_at_column?(self)
|
36
|
+
|
37
|
+
self.prepend(DeletedAt::ActiveRecord)
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
def deleted_at_attributes
|
42
|
+
attributes = {
|
43
|
+
deleted_at[:column] => deleted_at[:proc].call
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
end # End ClassMethods
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -1,15 +1,30 @@
|
|
1
1
|
module DeletedAt
|
2
|
-
module
|
2
|
+
module Legacy
|
3
|
+
def self.uninstall(model)
|
4
|
+
return false unless model.has_deleted_at_column?
|
3
5
|
|
4
|
-
|
6
|
+
uninstall_deleted_view(model)
|
5
7
|
uninstall_present_view(model)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.install(model)
|
11
|
+
return false unless model.has_deleted_at_column?
|
12
|
+
|
13
|
+
install_present_view(model)
|
14
|
+
install_deleted_view(model)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def self.install_present_view(model)
|
20
|
+
# uninstall_present_view(model)
|
6
21
|
present_table_name = present_view(model)
|
7
22
|
|
8
23
|
while_spoofing_table_name(model, all_table(model)) do
|
9
24
|
model.connection.execute("ALTER TABLE \"#{present_table_name}\" RENAME TO \"#{model.table_name}\"")
|
10
25
|
model.connection.execute <<-SQL
|
11
26
|
CREATE OR REPLACE VIEW "#{present_table_name}"
|
12
|
-
AS #{ model.where(model.deleted_at_column => nil).to_sql }
|
27
|
+
AS #{ model.select('*').where(model.deleted_at_column => nil).to_sql }
|
13
28
|
SQL
|
14
29
|
end
|
15
30
|
end
|
@@ -21,7 +36,7 @@ module DeletedAt
|
|
21
36
|
while_spoofing_table_name(model, all_table(model)) do
|
22
37
|
model.connection.execute <<-SQL
|
23
38
|
CREATE OR REPLACE VIEW "#{table_name}"
|
24
|
-
AS #{ model.where.not(model.deleted_at_column => nil).to_sql }
|
39
|
+
AS #{ model.select('*').where.not(model.deleted_at_column => nil).to_sql }
|
25
40
|
SQL
|
26
41
|
end
|
27
42
|
end
|
@@ -29,23 +44,23 @@ module DeletedAt
|
|
29
44
|
def self.all_table_exists?(model)
|
30
45
|
query = model.connection.execute <<-SQL
|
31
46
|
SELECT EXISTS (
|
32
|
-
SELECT
|
47
|
+
SELECT true
|
33
48
|
FROM information_schema.tables
|
34
49
|
WHERE table_name = '#{all_table(model)}'
|
35
50
|
) AS exists;
|
36
51
|
SQL
|
37
|
-
|
52
|
+
query.first['exists']
|
38
53
|
end
|
39
54
|
|
40
55
|
def self.deleted_view_exists?(model)
|
41
56
|
query = model.connection.execute <<-SQL
|
42
57
|
SELECT EXISTS (
|
43
|
-
SELECT
|
58
|
+
SELECT true
|
44
59
|
FROM information_schema.tables
|
45
60
|
WHERE table_name = '#{deleted_view(model)}'
|
46
61
|
) AS exists;
|
47
62
|
SQL
|
48
|
-
|
63
|
+
query.first['exists']
|
49
64
|
end
|
50
65
|
|
51
66
|
def self.present_view(model)
|
@@ -61,9 +76,6 @@ module DeletedAt
|
|
61
76
|
end
|
62
77
|
|
63
78
|
def self.uninstall_present_view(model)
|
64
|
-
# Legacy
|
65
|
-
model.connection.execute("DROP VIEW IF EXISTS \"#{model.table_name}/present\"")
|
66
|
-
# New
|
67
79
|
return unless all_table_exists?(model)
|
68
80
|
model.connection.execute("DROP VIEW IF EXISTS \"#{present_view(model)}\"")
|
69
81
|
model.connection.execute("ALTER TABLE \"#{all_table(model)}\" RENAME TO \"#{present_view(model)}\"")
|
@@ -73,14 +85,11 @@ module DeletedAt
|
|
73
85
|
model.connection.execute("DROP VIEW IF EXISTS \"#{deleted_view(model)}\"")
|
74
86
|
end
|
75
87
|
|
76
|
-
private
|
77
|
-
|
78
88
|
def self.while_spoofing_table_name(model, new_name, &block)
|
79
89
|
old_name = model.table_name
|
80
90
|
model.table_name = new_name
|
81
91
|
yield
|
82
92
|
model.table_name = old_name
|
83
93
|
end
|
84
|
-
|
85
94
|
end
|
86
95
|
end
|
data/lib/deleted_at/railtie.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'rails/railtie'
|
2
|
+
require 'deleted_at/core'
|
3
|
+
require 'deleted_at/table_definition'
|
2
4
|
|
3
5
|
module DeletedAt
|
4
6
|
class Railtie < Rails::Railtie
|
5
|
-
|
6
|
-
initializer 'deleted_at.load' do
|
7
|
+
initializer 'deleted_at.load' do |_app|
|
7
8
|
ActiveSupport.on_load(:active_record) do
|
8
|
-
DeletedAt
|
9
|
+
::ActiveRecord::Base.prepend(DeletedAt::Core)
|
10
|
+
::ActiveRecord::ConnectionAdapters::TableDefinition.prepend(DeletedAt::TableDefinition)
|
9
11
|
end
|
10
12
|
end
|
11
|
-
|
12
13
|
end
|
13
14
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module DeletedAt
|
2
|
+
|
3
|
+
module Relation
|
4
|
+
|
5
|
+
def self.prepended(subclass)
|
6
|
+
subclass.class_eval do
|
7
|
+
attr_writer :deleted_at_scope
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def deleted_at_scope
|
12
|
+
@deleted_at_scope ||= :Present
|
13
|
+
end
|
14
|
+
|
15
|
+
def deleted_at_select
|
16
|
+
scoped_arel = case deleted_at_scope
|
17
|
+
when :Deleted
|
18
|
+
table.where(table[deleted_at[:column]].not_eq(nil))
|
19
|
+
when :Present
|
20
|
+
table.where(table[deleted_at[:column]].eq(nil))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def deleted_at_subselect(arel)
|
26
|
+
if (subselect = deleted_at_select)
|
27
|
+
subselect.project(arel_columns(columns.map(&:name)))
|
28
|
+
Arel::Nodes::TableAlias.new(Arel::Nodes::Grouping.new(subselect.ast), table_name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_arel
|
33
|
+
super.tap do |arel|
|
34
|
+
if (subselect = deleted_at_subselect(arel)) && !arel.froms.include?(subselect)
|
35
|
+
DeletedAt.logger.debug("DeletedAt sub-selecting from #{subselect.to_sql}")
|
36
|
+
arel.from(subselect)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Deletes the records matching +conditions+ without instantiating the records
|
42
|
+
# first, and hence not calling the +destroy+ method nor invoking callbacks. This
|
43
|
+
# is a single SQL DELETE statement that goes straight to the database, much more
|
44
|
+
# efficient than +destroy_all+. Be careful with relations though, in particular
|
45
|
+
# <tt>:dependent</tt> rules defined on associations are not honored. Returns the
|
46
|
+
# number of rows affected.
|
47
|
+
#
|
48
|
+
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
|
49
|
+
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
|
50
|
+
# Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
|
51
|
+
#
|
52
|
+
# Both calls delete the affected posts all at once with a single DELETE statement.
|
53
|
+
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
|
54
|
+
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
|
55
|
+
#
|
56
|
+
# If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
|
57
|
+
#
|
58
|
+
# Post.limit(100).delete_all
|
59
|
+
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
|
60
|
+
def delete_all(*args)
|
61
|
+
conditions = args.pop
|
62
|
+
if conditions
|
63
|
+
ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
|
64
|
+
Passing conditions to delete_all is not supported in DeletedAt
|
65
|
+
To achieve the same use where(conditions).delete_all.
|
66
|
+
MESSAGE
|
67
|
+
end
|
68
|
+
update_all(klass.deleted_at_attributes)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|