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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/{spec/support/rails/public/favicon.ico → CHANGELOG.md} +0 -0
  3. data/{LICENSE.txt → LICENSE} +0 -0
  4. data/README.md +29 -15
  5. data/lib/deleted_at.rb +29 -29
  6. data/lib/deleted_at/active_record.rb +37 -0
  7. data/lib/deleted_at/core.rb +51 -0
  8. data/lib/deleted_at/{views.rb → legacy.rb} +23 -14
  9. data/lib/deleted_at/railtie.rb +5 -4
  10. data/lib/deleted_at/relation.rb +72 -0
  11. data/lib/deleted_at/table_definition.rb +10 -0
  12. data/lib/deleted_at/version.rb +1 -1
  13. metadata +30 -67
  14. data/.gitignore +0 -78
  15. data/.rspec +0 -2
  16. data/.travis.yml +0 -43
  17. data/CODE_OF_CONDUCT.md +0 -74
  18. data/Gemfile +0 -19
  19. data/Rakefile +0 -6
  20. data/deleted_at.gemspec +0 -42
  21. data/gemfiles/activerecord-4.0.Gemfile +0 -3
  22. data/gemfiles/activerecord-4.1.Gemfile +0 -3
  23. data/gemfiles/activerecord-4.2.Gemfile +0 -3
  24. data/gemfiles/activerecord-5.0.Gemfile +0 -3
  25. data/gemfiles/activerecord-5.1.Gemfile +0 -3
  26. data/lib/deleted_at/active_record/base.rb +0 -102
  27. data/lib/deleted_at/active_record/connection_adapters/abstract/schema_definition.rb +0 -17
  28. data/lib/deleted_at/active_record/relation.rb +0 -43
  29. data/spec/deleted_at/active_record/base_spec.rb +0 -21
  30. data/spec/deleted_at/active_record/relation_spec.rb +0 -166
  31. data/spec/deleted_at/views_spec.rb +0 -76
  32. data/spec/spec_helper.rb +0 -28
  33. data/spec/support/rails/app/models/animals/dog.rb +0 -5
  34. data/spec/support/rails/app/models/comment.rb +0 -6
  35. data/spec/support/rails/app/models/post.rb +0 -7
  36. data/spec/support/rails/app/models/user.rb +0 -7
  37. data/spec/support/rails/config/database.yml +0 -4
  38. data/spec/support/rails/config/routes.rb +0 -3
  39. data/spec/support/rails/db/schema.rb +0 -27
  40. data/spec/support/rails/log/.gitignore +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c164168eb496804835ab5f48676d72a12fb549dccb649b0249355403db9b5b7e
4
- data.tar.gz: 6e684b4ef6a4146f40b6a0684b5aa18df975894df8da62ec62907d34bece3ec9
3
+ metadata.gz: df0de7db0fb5c4de89e941d080ac07fc08265ae29c2ae550e9e11b1057699763
4
+ data.tar.gz: e79f565b01952c39277e5c49f100441c3b19cfd6245056192b8261c4bd33541c
5
5
  SHA512:
6
- metadata.gz: f16daa013ef9ba2c06a42dea4ed2cd1d3e4008d419daa987aa01224408c1a11dd2d38ad93c5685119acfc28b46db4de240ff66ab93a629a7feec2dc632ddf905
7
- data.tar.gz: 916c6f5c3e8cf4ebabcb628b23e4015fb16f3c83eb57ef5c393bdfafdb16973149dbedafcf3cfba593bd68f7da6f6dd39200c527ce3c9555fec88d303081bcd2
6
+ metadata.gz: 0a18356dbbecaa30e77709349b85797ca3a50edadfde5474f62a3db841b52c28a14285ca4498318d17f4a3bf5b5b1e246936d69087b218e68592e6d99a0affe0
7
+ data.tar.gz: e16f8f3c9b010aa85a0922fe2673d14866eb0451a14988edc1bd6be3efe6d83e75993562046c1ea93ec2b188923d0b4e5596031fadd4439ac989369440a838d9
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
- Deleting data is never good. A common solution is to use `default_scope`, but conventional wisdom (and for good reason) deams this a bad practice. So how do we achieve the same effect with minimal intervention. What we're looking for is the cliche "clean" solution.
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
- `DeletedAt` requires PostgreSQL 9.1+ and Ruby 2.0.0+ (as the `pg` gem requires Ruby 2.0.0).
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
- Using `DeletedAt` is very simple. It follows a familiar pattern seen throughout the rest of the Ruby/Rails community.
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
- You'll (probably) need to migrate your database for `deleted_at` to work properly.
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 `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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/[USERNAME]/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.
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
 
@@ -1,15 +1,18 @@
1
- require "deleted_at/version"
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
- def self.load
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
- # Now that the views have been installed, initialize the new class extensions
35
- # e.g. User -> User::All and User::Deleted
36
- model.with_deleted_at
27
+ def self.disabled?
28
+ @disabled == true
37
29
  end
38
30
 
39
- def self.uninstall(model)
40
- return false unless model.has_deleted_at_column?
31
+ def self.disable
32
+ @disabled = true
33
+ end
41
34
 
42
- DeletedAt::Views.uninstall_deleted_view(model)
43
- DeletedAt::Views.uninstall_present_view(model)
35
+ def self.enable
36
+ @disabled = false
37
+ end
44
38
 
45
- # We've removed the database views, now remove the class extensions
46
- DeletedAt::ActiveRecord::Base.remove_class_views(model)
39
+ def self.gemspec
40
+ @gemspec ||= eval(`gem spec deleted_at --ruby`).freeze
47
41
  end
48
42
 
49
- def self.testify(value)
50
- value == true || value == 't' || value == 1 || value == '1'
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
- private
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 Views
2
+ module Legacy
3
+ def self.uninstall(model)
4
+ return false unless model.has_deleted_at_column?
3
5
 
4
- def self.install_present_view(model)
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 1
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
- DeletedAt.testify(query.first['exists'])
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 1
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
- DeletedAt.testify(query.first['exists'])
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
@@ -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.load
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