deleted_at 0.4.0rc1 → 0.5.0.pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
[](https://rubygems.org/gems/deleted_at)
|
2
2
|
[](https://travis-ci.org/TwilightCoders/deleted_at)
|
3
|
-
[](https://codeclimate.com/github/TwilightCoders/deleted_at)
|
3
|
+
[](https://codeclimate.com/github/TwilightCoders/deleted_at/maintainability)
|
4
4
|
[](https://codeclimate.com/github/TwilightCoders/deleted_at/coverage)
|
5
|
+
[](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
|