acts_as_paranoid 0.6.3 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -16
- data/README.md +130 -68
- data/lib/acts_as_paranoid.rb +31 -30
- data/lib/acts_as_paranoid/associations.rb +21 -17
- data/lib/acts_as_paranoid/core.rb +87 -77
- data/lib/acts_as_paranoid/relation.rb +2 -0
- data/lib/acts_as_paranoid/validations.rb +8 -74
- data/lib/acts_as_paranoid/version.rb +3 -1
- data/test/test_associations.rb +119 -43
- data/test/test_core.rb +90 -66
- data/test/test_default_scopes.rb +7 -5
- data/test/test_helper.rb +87 -65
- data/test/test_inheritance.rb +3 -1
- data/test/test_relations.rb +18 -10
- data/test/test_validations.rb +9 -7
- metadata +57 -26
- data/lib/acts_as_paranoid/preloader_association.rb +0 -16
- data/test/test_preloader_association.rb +0 -27
data/lib/acts_as_paranoid.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
|
2
|
-
require 'acts_as_paranoid/associations'
|
3
|
-
require 'acts_as_paranoid/validations'
|
4
|
-
require 'acts_as_paranoid/relation'
|
5
|
-
require 'acts_as_paranoid/preloader_association'
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
3
|
+
require "active_record"
|
4
|
+
require "acts_as_paranoid/core"
|
5
|
+
require "acts_as_paranoid/associations"
|
6
|
+
require "acts_as_paranoid/validations"
|
7
|
+
require "acts_as_paranoid/relation"
|
8
8
|
|
9
|
+
module ActsAsParanoid
|
9
10
|
def paranoid?
|
10
|
-
|
11
|
+
included_modules.include?(ActsAsParanoid::Core)
|
11
12
|
end
|
12
13
|
|
13
14
|
def validates_as_paranoid
|
@@ -15,19 +16,29 @@ module ActsAsParanoid
|
|
15
16
|
end
|
16
17
|
|
17
18
|
def acts_as_paranoid(options = {})
|
18
|
-
|
19
|
+
if !options.is_a?(Hash) && !options.empty?
|
20
|
+
raise ArgumentError, "Hash expected, got #{options.class.name}"
|
21
|
+
end
|
19
22
|
|
20
23
|
class_attribute :paranoid_configuration
|
21
24
|
|
22
|
-
self.paranoid_configuration = {
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
self.paranoid_configuration = {
|
26
|
+
column: "deleted_at",
|
27
|
+
column_type: "time",
|
28
|
+
recover_dependent_associations: true,
|
29
|
+
dependent_recovery_window: 2.minutes,
|
30
|
+
recovery_value: nil,
|
31
|
+
double_tap_destroys_fully: true
|
32
|
+
}
|
33
|
+
if options[:column_type] == "string"
|
34
|
+
paranoid_configuration.merge!(deleted_value: "deleted")
|
35
|
+
end
|
36
|
+
paranoid_configuration.merge!(allow_nulls: true) if options[:column_type] == "boolean"
|
37
|
+
paranoid_configuration.merge!(options) # user options
|
28
38
|
|
29
|
-
|
30
|
-
"
|
39
|
+
unless %w[time boolean string].include? paranoid_configuration[:column_type]
|
40
|
+
raise ArgumentError, "'time', 'boolean' or 'string' expected" \
|
41
|
+
" for :column_type option, got #{paranoid_configuration[:column_type]}"
|
31
42
|
end
|
32
43
|
|
33
44
|
return if paranoid?
|
@@ -37,28 +48,18 @@ module ActsAsParanoid
|
|
37
48
|
# Magic!
|
38
49
|
default_scope { where(paranoid_default_scope) }
|
39
50
|
|
40
|
-
if
|
41
|
-
scope :deleted_inside_time_window, lambda {|time, window|
|
42
|
-
deleted_after_time((time - window)).deleted_before_time((time + window))
|
43
|
-
}
|
44
|
-
|
45
|
-
scope :deleted_after_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} > ?", time) }
|
46
|
-
scope :deleted_before_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} < ?", time) }
|
47
|
-
end
|
51
|
+
define_deleted_time_scopes if paranoid_column_type == :time
|
48
52
|
end
|
49
53
|
end
|
50
54
|
|
51
55
|
# Extend ActiveRecord's functionality
|
52
|
-
ActiveRecord::Base.
|
56
|
+
ActiveRecord::Base.extend ActsAsParanoid
|
53
57
|
|
54
58
|
# Extend ActiveRecord::Base with paranoid associations
|
55
|
-
ActiveRecord::Base.
|
59
|
+
ActiveRecord::Base.include ActsAsParanoid::Associations
|
56
60
|
|
57
61
|
# Override ActiveRecord::Relation's behavior
|
58
|
-
ActiveRecord::Relation.
|
62
|
+
ActiveRecord::Relation.include ActsAsParanoid::Relation
|
59
63
|
|
60
64
|
# Push the recover callback onto the activerecord callback list
|
61
65
|
ActiveRecord::Callbacks::CALLBACKS.push(:before_recover, :after_recover)
|
62
|
-
|
63
|
-
# Use with_deleted in preloader build_scope
|
64
|
-
ActiveRecord::Associations::Preloader::Association.send :include, ActsAsParanoid::PreloaderAssociation
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsParanoid
|
2
4
|
module Associations
|
3
5
|
def self.included(base)
|
@@ -16,29 +18,31 @@ module ActsAsParanoid
|
|
16
18
|
end
|
17
19
|
|
18
20
|
with_deleted = options.delete(:with_deleted)
|
19
|
-
result = belongs_to_without_deleted(target, scope, options)
|
20
|
-
|
21
21
|
if with_deleted
|
22
|
-
if
|
23
|
-
|
22
|
+
if scope
|
23
|
+
old_scope = scope
|
24
|
+
scope = proc do |*args|
|
25
|
+
if old_scope.arity == 0
|
26
|
+
instance_exec(&old_scope).with_deleted
|
27
|
+
else
|
28
|
+
old_scope.call(*args).with_deleted
|
29
|
+
end
|
30
|
+
end
|
24
31
|
else
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def #{target}_with_unscoped(*args)
|
31
|
-
association = association(:#{target})
|
32
|
-
return nil if association.options[:polymorphic] && association.klass.nil?
|
33
|
-
return #{target}_without_unscoped(*args) unless association.klass.paranoid?
|
34
|
-
association.klass.with_deleted.scoping { association.klass.unscoped { #{target}_without_unscoped(*args) } }
|
32
|
+
scope = proc do
|
33
|
+
if respond_to? :with_deleted
|
34
|
+
self.with_deleted
|
35
|
+
else
|
36
|
+
all
|
35
37
|
end
|
36
|
-
|
37
|
-
alias_method :#{target}, :#{target}_with_unscoped
|
38
|
-
RUBY
|
38
|
+
end
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
result = belongs_to_without_deleted(target, scope, **options)
|
43
|
+
|
44
|
+
result.values.last.options[:with_deleted] = with_deleted if with_deleted
|
45
|
+
|
42
46
|
result
|
43
47
|
end
|
44
48
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsParanoid
|
2
4
|
module Core
|
3
5
|
def self.included(base)
|
@@ -23,7 +25,8 @@ module ActsAsParanoid
|
|
23
25
|
|
24
26
|
def only_deleted
|
25
27
|
if string_type_with_deleted_value?
|
26
|
-
without_paranoid_default_scope
|
28
|
+
without_paranoid_default_scope
|
29
|
+
.where(paranoid_column_reference => paranoid_configuration[:deleted_value])
|
27
30
|
elsif boolean_type_not_nullable?
|
28
31
|
without_paranoid_default_scope.where(paranoid_column_reference => true)
|
29
32
|
else
|
@@ -36,17 +39,18 @@ module ActsAsParanoid
|
|
36
39
|
end
|
37
40
|
|
38
41
|
def delete_all(conditions = nil)
|
39
|
-
where(conditions)
|
42
|
+
where(conditions)
|
43
|
+
.update_all(["#{paranoid_configuration[:column]} = ?", delete_now_value])
|
40
44
|
end
|
41
45
|
|
42
46
|
def paranoid_default_scope
|
43
47
|
if string_type_with_deleted_value?
|
44
|
-
|
45
|
-
or(
|
48
|
+
all.table[paranoid_column].eq(nil)
|
49
|
+
.or(all.table[paranoid_column].not_eq(paranoid_configuration[:deleted_value]))
|
46
50
|
elsif boolean_type_not_nullable?
|
47
|
-
|
51
|
+
all.table[paranoid_column].eq(false)
|
48
52
|
else
|
49
|
-
|
53
|
+
all.table[paranoid_column].eq(nil)
|
50
54
|
end
|
51
55
|
end
|
52
56
|
|
@@ -66,8 +70,14 @@ module ActsAsParanoid
|
|
66
70
|
paranoid_configuration[:column_type].to_sym
|
67
71
|
end
|
68
72
|
|
73
|
+
def paranoid_column_reference
|
74
|
+
"#{table_name}.#{paranoid_column}"
|
75
|
+
end
|
76
|
+
|
69
77
|
def dependent_associations
|
70
|
-
|
78
|
+
reflect_on_all_associations.select do |a|
|
79
|
+
[:destroy, :delete_all].include?(a.options[:dependent])
|
80
|
+
end
|
71
81
|
end
|
72
82
|
|
73
83
|
def delete_now_value
|
@@ -78,20 +88,27 @@ module ActsAsParanoid
|
|
78
88
|
end
|
79
89
|
end
|
80
90
|
|
81
|
-
|
91
|
+
protected
|
92
|
+
|
93
|
+
def define_deleted_time_scopes
|
94
|
+
scope :deleted_inside_time_window, lambda { |time, window|
|
95
|
+
deleted_after_time((time - window)).deleted_before_time((time + window))
|
96
|
+
}
|
97
|
+
|
98
|
+
scope :deleted_after_time, lambda { |time|
|
99
|
+
where("#{table_name}.#{paranoid_column} > ?", time)
|
100
|
+
}
|
101
|
+
scope :deleted_before_time, lambda { |time|
|
102
|
+
where("#{table_name}.#{paranoid_column} < ?", time)
|
103
|
+
}
|
104
|
+
end
|
82
105
|
|
83
106
|
def without_paranoid_default_scope
|
84
|
-
scope =
|
107
|
+
scope = all
|
85
108
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
scope.where_values.delete(paranoid_default_scope)
|
90
|
-
else
|
91
|
-
scope = scope.unscope(where: paranoid_default_scope)
|
92
|
-
# Fix problems with unscope group chain
|
93
|
-
scope = scope.unscoped if scope.to_sql.include? paranoid_default_scope.to_sql
|
94
|
-
end
|
109
|
+
scope = scope.unscope(where: paranoid_column)
|
110
|
+
# Fix problems with unscope group chain
|
111
|
+
scope = scope.unscoped if scope.to_sql.include? paranoid_default_scope.to_sql
|
95
112
|
|
96
113
|
scope
|
97
114
|
end
|
@@ -102,7 +119,7 @@ module ActsAsParanoid
|
|
102
119
|
end
|
103
120
|
|
104
121
|
def paranoid_value
|
105
|
-
|
122
|
+
send(self.class.paranoid_column)
|
106
123
|
end
|
107
124
|
|
108
125
|
# Straight from ActiveRecord 5.1!
|
@@ -118,9 +135,10 @@ module ActsAsParanoid
|
|
118
135
|
destroy_dependent_associations!
|
119
136
|
|
120
137
|
if persisted?
|
121
|
-
# Handle composite keys, otherwise we would just use
|
122
|
-
self.class.
|
123
|
-
|
138
|
+
# Handle composite keys, otherwise we would just use
|
139
|
+
# `self.class.primary_key.to_sym => self.id`.
|
140
|
+
self.class
|
141
|
+
.delete_all!(Hash[[Array(self.class.primary_key), Array(id)].transpose])
|
124
142
|
decrement_counters_on_associations
|
125
143
|
end
|
126
144
|
|
@@ -135,11 +153,11 @@ module ActsAsParanoid
|
|
135
153
|
if !deleted?
|
136
154
|
with_transaction_returning_status do
|
137
155
|
run_callbacks :destroy do
|
138
|
-
|
139
156
|
if persisted?
|
140
|
-
# Handle composite keys, otherwise we would just use
|
141
|
-
self.class.
|
142
|
-
|
157
|
+
# Handle composite keys, otherwise we would just use
|
158
|
+
# `self.class.primary_key.to_sym => self.id`.
|
159
|
+
self.class
|
160
|
+
.delete_all(Hash[[Array(self.class.primary_key), Array(id)].transpose])
|
143
161
|
decrement_counters_on_associations
|
144
162
|
end
|
145
163
|
|
@@ -149,38 +167,39 @@ module ActsAsParanoid
|
|
149
167
|
self
|
150
168
|
end
|
151
169
|
end
|
152
|
-
|
153
|
-
|
154
|
-
destroy_fully!
|
155
|
-
end
|
170
|
+
elsif paranoid_configuration[:double_tap_destroys_fully]
|
171
|
+
destroy_fully!
|
156
172
|
end
|
157
173
|
end
|
158
174
|
|
159
|
-
|
175
|
+
alias destroy destroy!
|
176
|
+
|
177
|
+
def recover(options = {})
|
178
|
+
return if !deleted?
|
160
179
|
|
161
|
-
def recover(options={})
|
162
|
-
return if !self.deleted?
|
163
180
|
options = {
|
164
|
-
:
|
165
|
-
:
|
166
|
-
:
|
181
|
+
recursive: self.class.paranoid_configuration[:recover_dependent_associations],
|
182
|
+
recovery_window: self.class.paranoid_configuration[:dependent_recovery_window],
|
183
|
+
raise_error: false
|
167
184
|
}.merge(options)
|
168
185
|
|
169
186
|
self.class.transaction do
|
170
187
|
run_callbacks :recover do
|
171
|
-
|
188
|
+
if options[:recursive]
|
189
|
+
recover_dependent_associations(options[:recovery_window], options)
|
190
|
+
end
|
172
191
|
increment_counters_on_associations
|
173
192
|
self.paranoid_value = self.class.paranoid_configuration[:recovery_value]
|
174
193
|
if options[:raise_error]
|
175
|
-
|
194
|
+
save!
|
176
195
|
else
|
177
|
-
|
196
|
+
save
|
178
197
|
end
|
179
198
|
end
|
180
199
|
end
|
181
200
|
end
|
182
201
|
|
183
|
-
def recover!(options={})
|
202
|
+
def recover!(options = {})
|
184
203
|
options[:raise_error] = true
|
185
204
|
|
186
205
|
recover(options)
|
@@ -190,14 +209,7 @@ module ActsAsParanoid
|
|
190
209
|
self.class.dependent_associations.each do |reflection|
|
191
210
|
next unless (klass = get_reflection_class(reflection)).paranoid?
|
192
211
|
|
193
|
-
scope = klass.only_deleted
|
194
|
-
|
195
|
-
# Merge in the association's scope
|
196
|
-
scope = if ActiveRecord::VERSION::MAJOR >= 6
|
197
|
-
scope.merge(ActiveRecord::Associations::AssociationScope.scope(association(reflection.name)))
|
198
|
-
else
|
199
|
-
scope.merge(association(reflection.name).association_scope)
|
200
|
-
end
|
212
|
+
scope = klass.only_deleted.merge(get_association_scope(reflection: reflection))
|
201
213
|
|
202
214
|
# We can only recover by window if both parent and dependant have a
|
203
215
|
# paranoid column type of :time.
|
@@ -215,77 +227,75 @@ module ActsAsParanoid
|
|
215
227
|
self.class.dependent_associations.each do |reflection|
|
216
228
|
next unless (klass = get_reflection_class(reflection)).paranoid?
|
217
229
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
scope = if ActiveRecord::VERSION::MAJOR >= 6
|
222
|
-
scope.merge(ActiveRecord::Associations::AssociationScope.scope(association(reflection.name)))
|
223
|
-
else
|
224
|
-
scope.merge(association(reflection.name).association_scope)
|
225
|
-
end
|
226
|
-
|
227
|
-
scope.each do |object|
|
228
|
-
object.destroy!
|
229
|
-
end
|
230
|
+
klass
|
231
|
+
.only_deleted.merge(get_association_scope(reflection: reflection))
|
232
|
+
.each(&:destroy!)
|
230
233
|
end
|
231
234
|
end
|
232
235
|
|
233
236
|
def deleted?
|
234
|
-
|
235
|
-
|
237
|
+
return true if @destroyed
|
238
|
+
|
239
|
+
if self.class.string_type_with_deleted_value?
|
240
|
+
paranoid_value == paranoid_configuration[:deleted_value]
|
236
241
|
elsif self.class.boolean_type_not_nullable?
|
237
|
-
paranoid_value ==
|
242
|
+
paranoid_value == true
|
238
243
|
else
|
239
|
-
paranoid_value.nil?
|
244
|
+
!paranoid_value.nil?
|
240
245
|
end
|
241
246
|
end
|
242
247
|
|
243
|
-
|
248
|
+
alias destroyed? deleted?
|
244
249
|
|
245
250
|
def deleted_fully?
|
246
251
|
@destroyed
|
247
252
|
end
|
248
253
|
|
249
|
-
|
254
|
+
alias destroyed_fully? deleted_fully?
|
250
255
|
|
251
256
|
private
|
252
257
|
|
258
|
+
def get_association_scope(reflection:)
|
259
|
+
ActiveRecord::Associations::AssociationScope.scope(association(reflection.name))
|
260
|
+
end
|
261
|
+
|
253
262
|
def get_reflection_class(reflection)
|
254
263
|
if reflection.macro == :belongs_to && reflection.options.include?(:polymorphic)
|
255
|
-
|
264
|
+
send(reflection.foreign_type).constantize
|
256
265
|
else
|
257
266
|
reflection.klass
|
258
267
|
end
|
259
268
|
end
|
260
269
|
|
261
270
|
def paranoid_value=(value)
|
262
|
-
|
271
|
+
write_attribute(self.class.paranoid_column, value)
|
263
272
|
end
|
264
273
|
|
265
|
-
def update_counters_on_associations
|
266
|
-
|
274
|
+
def update_counters_on_associations(method_sym)
|
267
275
|
return unless [:decrement_counter, :increment_counter].include? method_sym
|
268
276
|
|
269
277
|
each_counter_cached_association_reflection do |assoc_reflection|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
278
|
+
associated_object = send(assoc_reflection.name)
|
279
|
+
next unless associated_object
|
280
|
+
|
281
|
+
counter_cache_column = assoc_reflection.counter_cache_column
|
282
|
+
associated_object.class.send(method_sym, counter_cache_column,
|
283
|
+
associated_object.id)
|
274
284
|
end
|
275
285
|
end
|
276
286
|
|
277
287
|
def each_counter_cached_association_reflection
|
278
|
-
_reflections.each do |
|
288
|
+
_reflections.each do |_name, reflection|
|
279
289
|
yield reflection if reflection.belongs_to? && reflection.counter_cache_column
|
280
290
|
end
|
281
291
|
end
|
282
292
|
|
283
293
|
def increment_counters_on_associations
|
284
|
-
|
294
|
+
update_counters_on_associations :increment_counter
|
285
295
|
end
|
286
296
|
|
287
297
|
def decrement_counters_on_associations
|
288
|
-
|
298
|
+
update_counters_on_associations :decrement_counter
|
289
299
|
end
|
290
300
|
|
291
301
|
def stale_paranoid_value
|