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