acts_as_paranoid 0.6.3 → 0.7.0
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/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
|