deleted_at 0.3.0 → 0.4.0rc1

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.
@@ -0,0 +1,17 @@
1
+ module DeletedAt
2
+ module ActiveRecord
3
+ module ConnectionAdapters #:nodoc:
4
+ module TableDefinition
5
+
6
+ def timestamps(**options)
7
+ options[:null] = false if options[:null].nil?
8
+
9
+ column(:created_at, :datetime, options)
10
+ column(:updated_at, :datetime, options)
11
+ column(:deleted_at, :datetime, options.merge(null: true)) if options[:deleted_at]
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,11 +4,6 @@ module DeletedAt
4
4
  # = Active Record Relation
5
5
  module Relation
6
6
 
7
- def deleted_at_attributes
8
- # We _do_ have klass at this point
9
- { klass.deleted_at_column => Time.now.utc }
10
- end
11
-
12
7
  # Deletes the records matching +conditions+ without instantiating the records
13
8
  # first, and hence not calling the +destroy+ method nor invoking callbacks. This
14
9
  # is a single SQL DELETE statement that goes straight to the database, much more
@@ -28,63 +23,21 @@ module DeletedAt
28
23
  #
29
24
  # Post.limit(100).delete_all
30
25
  # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
31
- def delete_all(conditions = nil)
26
+ def delete_all(*args)
27
+ conditions = args.pop
32
28
  if archive_with_deleted_at?
33
- where(conditions).update_all(deleted_at_attributes)
29
+ if conditions
30
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
31
+ Passing conditions to delete_all is not supported in DeletedAt
32
+ To achieve the same use where(conditions).delete_all.
33
+ MESSAGE
34
+ end
35
+ update_all(klass.deleted_at_attributes)
34
36
  else
35
- super
37
+ super() # Specifically drop args
36
38
  end
37
39
  end
38
40
 
39
- # Deletes the row with a primary key matching the +id+ argument, using a
40
- # SQL +DELETE+ statement, and returns the number of rows deleted. Active
41
- # Record objects are not instantiated, so the object's callbacks are not
42
- # executed, including any <tt>:dependent</tt> association options.
43
- #
44
- # You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
45
- #
46
- # Note: Although it is often much faster than the alternative,
47
- # <tt>#destroy</tt>, skipping callbacks might bypass business logic in
48
- # your application that ensures referential integrity or performs other
49
- # essential jobs.
50
- #
51
- # ==== Examples
52
- #
53
- # # Delete a single row
54
- # Todo.delete(1)
55
- #
56
- # # Delete multiple rows
57
- # Todo.delete([2,3,4])
58
- def delete(id_or_array)
59
- where(primary_key => id_or_array).delete_all
60
- end
61
-
62
- # Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
63
- # therefore all callbacks and filters are fired off before the object is deleted. This method is
64
- # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
65
- #
66
- # This essentially finds the object (or multiple objects) with the given id, creates a new object
67
- # from the attributes, and then calls destroy on it.
68
- #
69
- # ==== Parameters
70
- #
71
- # * +id+ - Can be either an Integer or an Array of Integers.
72
- #
73
- # ==== Examples
74
- #
75
- # # Destroy a single object
76
- # Todo.destroy(1)
77
- #
78
- # # Destroy multiple objects
79
- # todos = [1,2,3]
80
- # Todo.destroy(todos)
81
- def destroy(id)
82
- if id.is_a?(Array)
83
- id.map { |one_id| destroy(one_id) }
84
- else
85
- find(id).destroy
86
- end
87
- end
88
41
  end
89
42
  end
90
43
  end
@@ -1,3 +1,3 @@
1
1
  module DeletedAt
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0rc1"
3
3
  end
@@ -3,23 +3,27 @@ module DeletedAt
3
3
 
4
4
  def self.install_present_view(model)
5
5
  uninstall_present_view(model)
6
- all_table_name = all_table(model)
7
6
  present_table_name = present_view(model)
8
7
 
9
- model.connection.execute("ALTER TABLE \"#{present_table_name}\" RENAME TO \"#{all_table_name}\"")
10
- model.connection.execute <<-SQL
11
- CREATE OR REPLACE VIEW "#{present_table_name}"
12
- AS SELECT * FROM "#{all_table_name}" WHERE #{model.deleted_at_column} IS NULL;
13
- SQL
8
+ while_spoofing_table_name(model, all_table(model)) do
9
+ model.connection.execute("ALTER TABLE \"#{present_table_name}\" RENAME TO \"#{model.table_name}\"")
10
+ model.connection.execute <<-SQL
11
+ CREATE OR REPLACE VIEW "#{present_table_name}"
12
+ AS #{ model.where(model.deleted_at_column => nil).to_sql }
13
+ SQL
14
+ end
14
15
  end
15
16
 
16
17
  def self.install_deleted_view(model)
17
- return warn("You must install the all/present tables/views first!") unless all_table_exists?(model)
18
+ return DeletedAt.logger.warn("You must install the all/present tables/views first!") unless all_table_exists?(model)
18
19
  table_name = deleted_view(model)
19
- model.connection.execute <<-SQL
20
- CREATE OR REPLACE VIEW "#{table_name}"
21
- AS SELECT * FROM "#{all_table(model)}" WHERE #{model.deleted_at_column} IS NOT NULL;
22
- SQL
20
+
21
+ while_spoofing_table_name(model, all_table(model)) do
22
+ model.connection.execute <<-SQL
23
+ CREATE OR REPLACE VIEW "#{table_name}"
24
+ AS #{ model.where.not(model.deleted_at_column => nil).to_sql }
25
+ SQL
26
+ end
23
27
  end
24
28
 
25
29
  def self.all_table_exists?(model)
@@ -28,9 +32,9 @@ module DeletedAt
28
32
  SELECT 1
29
33
  FROM information_schema.tables
30
34
  WHERE table_name = '#{all_table(model)}'
31
- );
35
+ ) AS exists;
32
36
  SQL
33
- get_truthy_value_from_psql(query)
37
+ DeletedAt.testify(query.first['exists'])
34
38
  end
35
39
 
36
40
  def self.deleted_view_exists?(model)
@@ -39,9 +43,9 @@ module DeletedAt
39
43
  SELECT 1
40
44
  FROM information_schema.tables
41
45
  WHERE table_name = '#{deleted_view(model)}'
42
- );
46
+ ) AS exists;
43
47
  SQL
44
- get_truthy_value_from_psql(query)
48
+ DeletedAt.testify(query.first['exists'])
45
49
  end
46
50
 
47
51
  def self.present_view(model)
@@ -71,11 +75,11 @@ module DeletedAt
71
75
 
72
76
  private
73
77
 
74
- def self.get_truthy_value_from_psql(result)
75
- # Some versions of PSQL return {"?column?"=>"t"}
76
- # instead of {"first"=>"t"}, so we're saying screw it,
77
- # just give me the first value of whatever is returned
78
- result.try(:first).try(:values).try(:first) == 't'
78
+ def self.while_spoofing_table_name(model, new_name, &block)
79
+ old_name = model.table_name
80
+ model.table_name = new_name
81
+ yield
82
+ model.table_name = old_name
79
83
  end
80
84
 
81
85
  end
@@ -0,0 +1,21 @@
1
+ require "spec_helper"
2
+
3
+ describe DeletedAt::ActiveRecord::Base do
4
+
5
+ context "model missing deleted_at column" do
6
+
7
+ it "fails when trying to install" do
8
+ expect(DeletedAt.install(Comment)).to eq(false)
9
+ expect(DeletedAt.uninstall(Comment)).to eq(false)
10
+ end
11
+
12
+ it "warns when using with_deleted_at" do
13
+ expected_stderr = "Missing `deleted_at` in `Comment` when trying to employ `deleted_at`"
14
+ allow(Comment).to receive(:has_deleted_at_views?).and_return(true)
15
+ expect(DeletedAt.logger).to receive(:warn).with(expected_stderr)
16
+ Comment.with_deleted_at
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,166 @@
1
+ require "spec_helper"
2
+
3
+ describe DeletedAt::ActiveRecord::Relation do
4
+
5
+ context "models using deleted_at" do
6
+
7
+ it "#destroy should set deleted_at" do
8
+ DeletedAt.install(User)
9
+ User.create(name: 'bob')
10
+ User.create(name: 'john')
11
+ User.create(name: 'sally')
12
+
13
+ User.first.destroy
14
+
15
+ expect(User.count).to eq(2)
16
+ expect(User::All.count).to eq(3)
17
+ expect(User::Deleted.count).to eq(1)
18
+ DeletedAt.uninstall(User)
19
+ end
20
+
21
+ it "#delete should set deleted_at" do
22
+ DeletedAt.install(User)
23
+ User.create(name: 'bob')
24
+ User.create(name: 'john')
25
+ User.create(name: 'sally')
26
+
27
+ User.first.delete
28
+
29
+ expect(User.count).to eq(2)
30
+ expect(User::All.count).to eq(3)
31
+ expect(User::Deleted.count).to eq(1)
32
+ DeletedAt.uninstall(User)
33
+ end
34
+
35
+ context '#destroy_all' do
36
+ it "should set deleted_at" do
37
+ DeletedAt.install(User)
38
+ User.create(name: 'bob')
39
+ User.create(name: 'john')
40
+ User.create(name: 'sally')
41
+
42
+ User.all.destroy_all
43
+
44
+ expect(User.count).to eq(0)
45
+ expect(User::All.count).to eq(3)
46
+ expect(User::Deleted.count).to eq(3)
47
+ DeletedAt.uninstall(User)
48
+ end
49
+
50
+ it "with conditions should set deleted_at" do
51
+ DeletedAt.install(User)
52
+ User.create(name: 'bob')
53
+ User.create(name: 'john')
54
+ User.create(name: 'sally')
55
+
56
+ User.where(name: 'bob').destroy_all
57
+
58
+ expect(User.count).to eq(2)
59
+ expect(User::All.count).to eq(3)
60
+ expect(User::Deleted.count).to eq(1)
61
+ DeletedAt.uninstall(User)
62
+ end
63
+ end
64
+
65
+ context '#delete_all' do
66
+ it "should set deleted_at" do
67
+ DeletedAt.install(Animals::Dog)
68
+ Animals::Dog.create(name: 'bob')
69
+ Animals::Dog.create(name: 'john')
70
+ Animals::Dog.create(name: 'sally')
71
+
72
+ # conditions should not matter
73
+ Animals::Dog.all.delete_all(name: 'bob')
74
+
75
+ expect(Animals::Dog.count).to eq(0)
76
+ expect(Animals::Dog::All.count).to eq(3)
77
+ expect(Animals::Dog::Deleted.count).to eq(3)
78
+ DeletedAt.uninstall(Animals::Dog)
79
+ end
80
+
81
+ it "with conditions should set deleted_at" do
82
+ DeletedAt.install(Animals::Dog)
83
+ Animals::Dog.create(name: 'bob')
84
+ Animals::Dog.create(name: 'john')
85
+ Animals::Dog.create(name: 'sally')
86
+
87
+ Animals::Dog.where(name: 'bob').delete_all
88
+
89
+ expect(Animals::Dog.count).to eq(2)
90
+ expect(Animals::Dog::All.count).to eq(3)
91
+ expect(Animals::Dog::Deleted.count).to eq(1)
92
+ DeletedAt.uninstall(Animals::Dog)
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ context "models not using deleted_at" do
99
+
100
+ it "#destroy should actually delete the record" do
101
+ Comment.create(title: 'Agree')
102
+ Comment.create(title: 'Disagree')
103
+ Comment.create(title: 'Defer')
104
+
105
+ Comment.first.destroy
106
+
107
+ expect(Comment.count).to eq(2)
108
+ end
109
+
110
+ it "#delete should actually delete the record" do
111
+ Comment.create(title: 'Agree')
112
+ Comment.create(title: 'Disagree')
113
+ Comment.create(title: 'Defer')
114
+
115
+ Comment.first.delete
116
+
117
+ expect(Comment.count).to eq(2)
118
+ end
119
+
120
+ context '#destroy_all' do
121
+ it "should actually delete records" do
122
+ Comment.create(title: 'Agree')
123
+ Comment.create(title: 'Disagree')
124
+ Comment.create(title: 'Defer')
125
+
126
+ Comment.all.destroy_all
127
+
128
+ expect(Comment.count).to eq(0)
129
+ end
130
+
131
+ it "with conditions should actually delete records" do
132
+ Comment.create(title: 'Agree')
133
+ Comment.create(title: 'Disagree')
134
+ Comment.create(title: 'Defer')
135
+
136
+ Comment.where(title: 'Disagree').destroy_all
137
+
138
+ expect(Comment.count).to eq(2)
139
+ end
140
+ end
141
+
142
+ context '#delete_all' do
143
+ it "should actually delete records" do
144
+ Comment.create(title: 'Agree')
145
+ Comment.create(title: 'Disagree')
146
+ Comment.create(title: 'Defer')
147
+
148
+ Comment.all.delete_all
149
+
150
+ expect(Comment.count).to eq(0)
151
+ end
152
+
153
+ it "with conditions should actually delete records" do
154
+ Comment.create(title: 'Agree')
155
+ Comment.create(title: 'Disagree')
156
+ Comment.create(title: 'Defer')
157
+
158
+ Comment.where(title: 'Agree').delete_all
159
+
160
+ expect(Comment.count).to eq(2)
161
+ end
162
+ end
163
+
164
+ end
165
+
166
+ end
@@ -0,0 +1,76 @@
1
+ require "spec_helper"
2
+
3
+ describe DeletedAt::Views do
4
+ {
5
+ User => "simple model",
6
+ Post => "model with customized table_name",
7
+ Animals::Dog => "namespaced model"
8
+ }.each do |model, description|
9
+
10
+ plural = model.name.pluralize
11
+ table_name = model.table_name
12
+
13
+ describe "#install for #{description}" do
14
+ after(:each) do
15
+ DeletedAt.uninstall(model)
16
+ end
17
+
18
+ it 'should not raise error' do
19
+ expect{ DeletedAt.install(model) }.to_not raise_error()
20
+ end
21
+
22
+ it 'should rename the models table' do
23
+ DeletedAt.install(model)
24
+ expect(ActiveRecord::Base.connection.table_exists?("#{table_name}/all")).to be_truthy
25
+ end
26
+
27
+ it "should have a view for all non-deleted #{plural}" do
28
+ DeletedAt.install(model)
29
+ if Gem::Version.new(Rails.version) < Gem::Version.new("5.0")
30
+ expect(ActiveRecord::Base.connection.table_exists?(table_name)).to be_truthy
31
+ else
32
+ expect(ActiveRecord::Base.connection.view_exists?(table_name)).to be_truthy
33
+ end
34
+ end
35
+
36
+ it "should have a view for all deleted #{plural}" do
37
+ DeletedAt.install(model)
38
+ if Gem::Version.new(Rails.version) < Gem::Version.new("5.0")
39
+ expect(ActiveRecord::Base.connection.table_exists?("#{table_name}/deleted")).to be_truthy
40
+ else
41
+ expect(ActiveRecord::Base.connection.view_exists?("#{table_name}/deleted")).to be_truthy
42
+ end
43
+ end
44
+
45
+ it 'creates the DeletedAt class extensions' do
46
+ DeletedAt.install(model)
47
+ expect(model.const_defined?(:All)).to be_truthy
48
+ expect(model.const_defined?(:Deleted)).to be_truthy
49
+ end
50
+
51
+ it 'sets the correct table name for modified class' do
52
+ DeletedAt.install(model)
53
+ expect(model.table_name).to eql(table_name)
54
+ expect(model::All.table_name).to eql("#{table_name}/all")
55
+ expect(model::Deleted.table_name).to eql("#{table_name}/deleted")
56
+ end
57
+ end
58
+
59
+ describe "#uninstall for #{description}" do
60
+ before(:each) do
61
+ DeletedAt.install(model)
62
+ end
63
+
64
+ it 'should not raise error' do
65
+ expect{ DeletedAt.uninstall(model) }.to_not raise_error()
66
+ end
67
+
68
+ it 'should remove model extensions' do
69
+ DeletedAt.uninstall(model)
70
+ expect(model.const_defined?(:All)).to be_falsy
71
+ expect(model.const_defined?(:Deleted)).to be_falsy
72
+ end
73
+ end
74
+ end
75
+
76
+ end