acts_as_paranoid 0.6.1 → 0.7.2
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 +113 -8
- data/CONTRIBUTING.md +59 -0
- data/README.md +151 -57
- data/lib/acts_as_paranoid.rb +37 -30
- data/lib/acts_as_paranoid/associations.rb +21 -17
- data/lib/acts_as_paranoid/core.rb +112 -75
- 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 +253 -57
- data/test/test_default_scopes.rb +7 -5
- data/test/test_helper.rb +134 -64
- data/test/test_inheritance.rb +3 -1
- data/test/test_relations.rb +18 -10
- data/test/test_validations.rb +9 -7
- metadata +72 -32
- 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,18 +16,34 @@ module ActsAsParanoid
|
|
15
16
|
end
|
16
17
|
|
17
18
|
def acts_as_paranoid(options = {})
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
if !options.is_a?(Hash) && !options.empty?
|
20
|
+
raise ArgumentError, "Hash expected, got #{options.class.name}"
|
21
|
+
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
self.paranoid_configuration
|
25
|
-
|
23
|
+
class_attribute :paranoid_configuration
|
24
|
+
|
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
|
+
elsif options[:column_type] == "boolean" && !options[:allow_nulls]
|
36
|
+
paranoid_configuration.merge!(recovery_value: false)
|
37
|
+
elsif options[:column_type] == "boolean"
|
38
|
+
paranoid_configuration.merge!(allow_nulls: true)
|
39
|
+
end
|
26
40
|
|
27
|
-
|
41
|
+
paranoid_configuration.merge!(options) # user options
|
28
42
|
|
29
|
-
|
43
|
+
unless %w[time boolean string].include? paranoid_configuration[:column_type]
|
44
|
+
raise ArgumentError, "'time', 'boolean' or 'string' expected" \
|
45
|
+
" for :column_type option, got #{paranoid_configuration[:column_type]}"
|
46
|
+
end
|
30
47
|
|
31
48
|
return if paranoid?
|
32
49
|
|
@@ -35,28 +52,18 @@ module ActsAsParanoid
|
|
35
52
|
# Magic!
|
36
53
|
default_scope { where(paranoid_default_scope) }
|
37
54
|
|
38
|
-
if
|
39
|
-
scope :deleted_inside_time_window, lambda {|time, window|
|
40
|
-
deleted_after_time((time - window)).deleted_before_time((time + window))
|
41
|
-
}
|
42
|
-
|
43
|
-
scope :deleted_after_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} > ?", time) }
|
44
|
-
scope :deleted_before_time, lambda { |time| where("#{self.table_name}.#{paranoid_column} < ?", time) }
|
45
|
-
end
|
55
|
+
define_deleted_time_scopes if paranoid_column_type == :time
|
46
56
|
end
|
47
57
|
end
|
48
58
|
|
49
59
|
# Extend ActiveRecord's functionality
|
50
|
-
ActiveRecord::Base.
|
60
|
+
ActiveRecord::Base.extend ActsAsParanoid
|
51
61
|
|
52
62
|
# Extend ActiveRecord::Base with paranoid associations
|
53
|
-
ActiveRecord::Base.
|
63
|
+
ActiveRecord::Base.include ActsAsParanoid::Associations
|
54
64
|
|
55
65
|
# Override ActiveRecord::Relation's behavior
|
56
|
-
ActiveRecord::Relation.
|
66
|
+
ActiveRecord::Relation.include ActsAsParanoid::Relation
|
57
67
|
|
58
68
|
# Push the recover callback onto the activerecord callback list
|
59
69
|
ActiveRecord::Callbacks::CALLBACKS.push(:before_recover, :after_recover)
|
60
|
-
|
61
|
-
# Use with_deleted in preloader build_scope
|
62
|
-
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,13 +119,13 @@ 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!
|
109
126
|
def delete
|
110
127
|
self.class.delete(id) if persisted?
|
111
|
-
|
128
|
+
stale_paranoid_value
|
112
129
|
freeze
|
113
130
|
end
|
114
131
|
|
@@ -118,13 +135,15 @@ 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!([Array(self.class.primary_key), Array(id)].transpose.to_h)
|
124
142
|
decrement_counters_on_associations
|
125
143
|
end
|
126
144
|
|
127
|
-
|
145
|
+
stale_paranoid_value
|
146
|
+
@destroyed = true
|
128
147
|
freeze
|
129
148
|
end
|
130
149
|
end
|
@@ -134,57 +153,63 @@ module ActsAsParanoid
|
|
134
153
|
if !deleted?
|
135
154
|
with_transaction_returning_status do
|
136
155
|
run_callbacks :destroy do
|
137
|
-
|
138
156
|
if persisted?
|
139
|
-
# Handle composite keys, otherwise we would just use
|
140
|
-
self.class.
|
141
|
-
|
157
|
+
# Handle composite keys, otherwise we would just use
|
158
|
+
# `self.class.primary_key.to_sym => self.id`.
|
159
|
+
self.class
|
160
|
+
.delete_all([Array(self.class.primary_key), Array(id)].transpose.to_h)
|
142
161
|
decrement_counters_on_associations
|
143
162
|
end
|
144
163
|
|
145
164
|
@_trigger_destroy_callback = true
|
146
165
|
|
147
|
-
|
166
|
+
stale_paranoid_value
|
148
167
|
self
|
149
168
|
end
|
150
169
|
end
|
151
|
-
|
152
|
-
|
153
|
-
destroy_fully!
|
154
|
-
end
|
170
|
+
elsif paranoid_configuration[:double_tap_destroys_fully]
|
171
|
+
destroy_fully!
|
155
172
|
end
|
156
173
|
end
|
157
174
|
|
158
|
-
|
175
|
+
alias destroy destroy!
|
176
|
+
|
177
|
+
def recover(options = {})
|
178
|
+
return if !deleted?
|
159
179
|
|
160
|
-
def recover(options={})
|
161
180
|
options = {
|
162
|
-
:
|
163
|
-
:
|
181
|
+
recursive: self.class.paranoid_configuration[:recover_dependent_associations],
|
182
|
+
recovery_window: self.class.paranoid_configuration[:dependent_recovery_window],
|
183
|
+
raise_error: false
|
164
184
|
}.merge(options)
|
165
185
|
|
166
186
|
self.class.transaction do
|
167
187
|
run_callbacks :recover do
|
168
|
-
|
188
|
+
if options[:recursive]
|
189
|
+
recover_dependent_associations(options[:recovery_window], options)
|
190
|
+
end
|
169
191
|
increment_counters_on_associations
|
170
192
|
self.paranoid_value = self.class.paranoid_configuration[:recovery_value]
|
171
|
-
|
193
|
+
if options[:raise_error]
|
194
|
+
save!
|
195
|
+
else
|
196
|
+
save
|
197
|
+
end
|
172
198
|
end
|
173
199
|
end
|
174
200
|
end
|
175
201
|
|
202
|
+
def recover!(options = {})
|
203
|
+
options[:raise_error] = true
|
204
|
+
|
205
|
+
recover(options)
|
206
|
+
end
|
207
|
+
|
176
208
|
def recover_dependent_associations(window, options)
|
177
209
|
self.class.dependent_associations.each do |reflection|
|
178
210
|
next unless (klass = get_reflection_class(reflection)).paranoid?
|
179
211
|
|
180
|
-
scope = klass.only_deleted
|
181
|
-
|
182
|
-
# Merge in the association's scope
|
183
|
-
scope = if ActiveRecord::VERSION::MAJOR >= 6
|
184
|
-
scope.merge(ActiveRecord::Associations::AssociationScope.scope(association(reflection.name)))
|
185
|
-
else
|
186
|
-
scope.merge(association(reflection.name).association_scope)
|
187
|
-
end
|
212
|
+
scope = klass.only_deleted.merge(get_association_scope(reflection: reflection))
|
188
213
|
|
189
214
|
# We can only recover by window if both parent and dependant have a
|
190
215
|
# paranoid column type of :time.
|
@@ -202,70 +227,82 @@ module ActsAsParanoid
|
|
202
227
|
self.class.dependent_associations.each do |reflection|
|
203
228
|
next unless (klass = get_reflection_class(reflection)).paranoid?
|
204
229
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
scope = if ActiveRecord::VERSION::MAJOR >= 6
|
209
|
-
scope.merge(ActiveRecord::Associations::AssociationScope.scope(association(reflection.name)))
|
210
|
-
else
|
211
|
-
scope.merge(association(reflection.name).association_scope)
|
212
|
-
end
|
213
|
-
|
214
|
-
scope.each do |object|
|
215
|
-
object.destroy!
|
216
|
-
end
|
230
|
+
klass
|
231
|
+
.only_deleted.merge(get_association_scope(reflection: reflection))
|
232
|
+
.each(&:destroy!)
|
217
233
|
end
|
218
234
|
end
|
219
235
|
|
220
236
|
def deleted?
|
221
|
-
|
222
|
-
|
237
|
+
return true if @destroyed
|
238
|
+
|
239
|
+
if self.class.string_type_with_deleted_value?
|
240
|
+
paranoid_value == paranoid_configuration[:deleted_value]
|
223
241
|
elsif self.class.boolean_type_not_nullable?
|
224
|
-
paranoid_value ==
|
242
|
+
paranoid_value == true
|
225
243
|
else
|
226
|
-
paranoid_value.nil?
|
244
|
+
!paranoid_value.nil?
|
227
245
|
end
|
228
246
|
end
|
229
247
|
|
230
|
-
|
248
|
+
alias destroyed? deleted?
|
249
|
+
|
250
|
+
def deleted_fully?
|
251
|
+
@destroyed
|
252
|
+
end
|
253
|
+
|
254
|
+
alias destroyed_fully? deleted_fully?
|
231
255
|
|
232
256
|
private
|
233
257
|
|
258
|
+
def get_association_scope(reflection:)
|
259
|
+
ActiveRecord::Associations::AssociationScope.scope(association(reflection.name))
|
260
|
+
end
|
261
|
+
|
234
262
|
def get_reflection_class(reflection)
|
235
263
|
if reflection.macro == :belongs_to && reflection.options.include?(:polymorphic)
|
236
|
-
|
264
|
+
send(reflection.foreign_type).constantize
|
237
265
|
else
|
238
266
|
reflection.klass
|
239
267
|
end
|
240
268
|
end
|
241
269
|
|
242
270
|
def paranoid_value=(value)
|
243
|
-
|
271
|
+
write_attribute(self.class.paranoid_column, value)
|
244
272
|
end
|
245
273
|
|
246
|
-
def update_counters_on_associations
|
247
|
-
|
248
|
-
return unless [:decrement_counter, :increment_counter].include? method_sym
|
249
|
-
|
274
|
+
def update_counters_on_associations(method_sym)
|
250
275
|
each_counter_cached_association_reflection do |assoc_reflection|
|
276
|
+
reflection_options = assoc_reflection.options
|
277
|
+
next unless reflection_options[:counter_cache]
|
278
|
+
|
251
279
|
associated_object = send(assoc_reflection.name)
|
280
|
+
next unless associated_object
|
281
|
+
|
252
282
|
counter_cache_column = assoc_reflection.counter_cache_column
|
253
|
-
associated_object.class.send(method_sym, counter_cache_column,
|
283
|
+
associated_object.class.send(method_sym, counter_cache_column,
|
284
|
+
associated_object.id)
|
285
|
+
associated_object.touch if reflection_options[:touch]
|
254
286
|
end
|
255
287
|
end
|
256
288
|
|
257
289
|
def each_counter_cached_association_reflection
|
258
|
-
_reflections.each do |
|
259
|
-
yield reflection if reflection.belongs_to? && reflection.counter_cache_column
|
290
|
+
_reflections.each do |_name, reflection|
|
291
|
+
yield reflection if reflection.belongs_to? && reflection.counter_cache_column
|
260
292
|
end
|
261
293
|
end
|
262
294
|
|
263
295
|
def increment_counters_on_associations
|
264
|
-
|
296
|
+
update_counters_on_associations :increment_counter
|
265
297
|
end
|
266
298
|
|
267
299
|
def decrement_counters_on_associations
|
268
|
-
|
300
|
+
update_counters_on_associations :decrement_counter
|
301
|
+
end
|
302
|
+
|
303
|
+
def stale_paranoid_value
|
304
|
+
self.paranoid_value = self.class.delete_now_value
|
305
|
+
clear_attribute_changes([self.class.paranoid_column])
|
269
306
|
end
|
270
307
|
end
|
271
308
|
end
|