acts_as_paranoid 0.5.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +117 -0
- data/LICENSE +1 -1
- data/README.md +175 -50
- data/lib/acts_as_paranoid.rb +34 -31
- data/lib/acts_as_paranoid/associations.rb +28 -17
- data/lib/acts_as_paranoid/core.rb +144 -53
- data/lib/acts_as_paranoid/relation.rb +2 -0
- data/lib/acts_as_paranoid/validations.rb +8 -65
- data/lib/acts_as_paranoid/version.rb +3 -1
- data/test/test_associations.rb +161 -39
- data/test/test_core.rb +252 -55
- data/test/test_default_scopes.rb +38 -37
- data/test/test_helper.rb +136 -67
- data/test/test_inheritance.rb +5 -3
- data/test/test_relations.rb +29 -21
- data/test/test_validations.rb +10 -7
- metadata +80 -30
- data/lib/acts_as_paranoid/preloader_association.rb +0 -15
- 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 }
|
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,37 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsParanoid
|
2
4
|
module Associations
|
3
5
|
def self.included(base)
|
4
6
|
base.extend ClassMethods
|
5
7
|
class << base
|
6
|
-
|
8
|
+
alias_method :belongs_to_without_deleted, :belongs_to
|
9
|
+
alias_method :belongs_to, :belongs_to_with_deleted
|
7
10
|
end
|
8
11
|
end
|
9
12
|
|
10
13
|
module ClassMethods
|
11
14
|
def belongs_to_with_deleted(target, scope = nil, options = {})
|
12
|
-
|
13
|
-
|
15
|
+
if scope.is_a?(Hash)
|
16
|
+
options = scope
|
17
|
+
scope = nil
|
18
|
+
end
|
14
19
|
|
20
|
+
with_deleted = options.delete(:with_deleted)
|
15
21
|
if with_deleted
|
16
|
-
if
|
17
|
-
|
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
|
18
31
|
else
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
def #{target}_with_unscoped(*args)
|
25
|
-
association = association(:#{target})
|
26
|
-
return nil if association.options[:polymorphic] && association.klass.nil?
|
27
|
-
return #{target}_without_unscoped(*args) unless association.klass.paranoid?
|
28
|
-
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
|
29
37
|
end
|
30
|
-
|
31
|
-
RUBY
|
38
|
+
end
|
32
39
|
end
|
33
40
|
end
|
34
41
|
|
42
|
+
result = belongs_to_without_deleted(target, scope, **options)
|
43
|
+
|
44
|
+
result.values.last.options[:with_deleted] = with_deleted if with_deleted
|
45
|
+
|
35
46
|
result
|
36
47
|
end
|
37
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,16 +119,31 @@ module ActsAsParanoid
|
|
102
119
|
end
|
103
120
|
|
104
121
|
def paranoid_value
|
105
|
-
|
122
|
+
send(self.class.paranoid_column)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Straight from ActiveRecord 5.1!
|
126
|
+
def delete
|
127
|
+
self.class.delete(id) if persisted?
|
128
|
+
stale_paranoid_value
|
129
|
+
freeze
|
106
130
|
end
|
107
131
|
|
108
132
|
def destroy_fully!
|
109
133
|
with_transaction_returning_status do
|
110
134
|
run_callbacks :destroy do
|
111
135
|
destroy_dependent_associations!
|
112
|
-
|
113
|
-
|
114
|
-
|
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!(Hash[[Array(self.class.primary_key), Array(id)].transpose])
|
142
|
+
decrement_counters_on_associations
|
143
|
+
end
|
144
|
+
|
145
|
+
stale_paranoid_value
|
146
|
+
@destroyed = true
|
115
147
|
freeze
|
116
148
|
end
|
117
149
|
end
|
@@ -121,43 +153,63 @@ module ActsAsParanoid
|
|
121
153
|
if !deleted?
|
122
154
|
with_transaction_returning_status do
|
123
155
|
run_callbacks :destroy do
|
124
|
-
|
125
|
-
|
126
|
-
|
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(Hash[[Array(self.class.primary_key), Array(id)].transpose])
|
161
|
+
decrement_counters_on_associations
|
162
|
+
end
|
163
|
+
|
164
|
+
@_trigger_destroy_callback = true
|
165
|
+
|
166
|
+
stale_paranoid_value
|
127
167
|
self
|
128
168
|
end
|
129
169
|
end
|
130
|
-
|
170
|
+
elsif paranoid_configuration[:double_tap_destroys_fully]
|
131
171
|
destroy_fully!
|
132
172
|
end
|
133
173
|
end
|
134
174
|
|
135
|
-
|
175
|
+
alias destroy destroy!
|
176
|
+
|
177
|
+
def recover(options = {})
|
178
|
+
return if !deleted?
|
136
179
|
|
137
|
-
def recover(options={})
|
138
180
|
options = {
|
139
|
-
:
|
140
|
-
:
|
181
|
+
recursive: self.class.paranoid_configuration[:recover_dependent_associations],
|
182
|
+
recovery_window: self.class.paranoid_configuration[:dependent_recovery_window],
|
183
|
+
raise_error: false
|
141
184
|
}.merge(options)
|
142
185
|
|
143
186
|
self.class.transaction do
|
144
187
|
run_callbacks :recover do
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
188
|
+
if options[:recursive]
|
189
|
+
recover_dependent_associations(options[:recovery_window], options)
|
190
|
+
end
|
191
|
+
increment_counters_on_associations
|
192
|
+
self.paranoid_value = self.class.paranoid_configuration[:recovery_value]
|
193
|
+
if options[:raise_error]
|
194
|
+
save!
|
195
|
+
else
|
196
|
+
save
|
197
|
+
end
|
149
198
|
end
|
150
199
|
end
|
151
200
|
end
|
152
201
|
|
202
|
+
def recover!(options = {})
|
203
|
+
options[:raise_error] = true
|
204
|
+
|
205
|
+
recover(options)
|
206
|
+
end
|
207
|
+
|
153
208
|
def recover_dependent_associations(window, options)
|
154
209
|
self.class.dependent_associations.each do |reflection|
|
155
210
|
next unless (klass = get_reflection_class(reflection)).paranoid?
|
156
211
|
|
157
|
-
scope = klass.only_deleted
|
158
|
-
|
159
|
-
# Merge in the association's scope
|
160
|
-
scope = scope.merge(association(reflection.name).association_scope)
|
212
|
+
scope = klass.only_deleted.merge(get_association_scope(reflection: reflection))
|
161
213
|
|
162
214
|
# We can only recover by window if both parent and dependant have a
|
163
215
|
# paranoid column type of :time.
|
@@ -175,41 +227,80 @@ module ActsAsParanoid
|
|
175
227
|
self.class.dependent_associations.each do |reflection|
|
176
228
|
next unless (klass = get_reflection_class(reflection)).paranoid?
|
177
229
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
scope = scope.merge(association(reflection.name).association_scope)
|
182
|
-
|
183
|
-
scope.each do |object|
|
184
|
-
object.destroy!
|
185
|
-
end
|
230
|
+
klass
|
231
|
+
.only_deleted.merge(get_association_scope(reflection: reflection))
|
232
|
+
.each(&:destroy!)
|
186
233
|
end
|
187
234
|
end
|
188
235
|
|
189
236
|
def deleted?
|
190
|
-
|
191
|
-
|
237
|
+
return true if @destroyed
|
238
|
+
|
239
|
+
if self.class.string_type_with_deleted_value?
|
240
|
+
paranoid_value == paranoid_configuration[:deleted_value]
|
192
241
|
elsif self.class.boolean_type_not_nullable?
|
193
|
-
paranoid_value ==
|
242
|
+
paranoid_value == true
|
194
243
|
else
|
195
|
-
paranoid_value.nil?
|
244
|
+
!paranoid_value.nil?
|
196
245
|
end
|
197
246
|
end
|
198
247
|
|
199
|
-
|
248
|
+
alias destroyed? deleted?
|
249
|
+
|
250
|
+
def deleted_fully?
|
251
|
+
@destroyed
|
252
|
+
end
|
253
|
+
|
254
|
+
alias destroyed_fully? deleted_fully?
|
200
255
|
|
201
256
|
private
|
202
257
|
|
258
|
+
def get_association_scope(reflection:)
|
259
|
+
ActiveRecord::Associations::AssociationScope.scope(association(reflection.name))
|
260
|
+
end
|
261
|
+
|
203
262
|
def get_reflection_class(reflection)
|
204
263
|
if reflection.macro == :belongs_to && reflection.options.include?(:polymorphic)
|
205
|
-
|
264
|
+
send(reflection.foreign_type).constantize
|
206
265
|
else
|
207
266
|
reflection.klass
|
208
267
|
end
|
209
268
|
end
|
210
269
|
|
211
270
|
def paranoid_value=(value)
|
212
|
-
|
271
|
+
write_attribute(self.class.paranoid_column, value)
|
272
|
+
end
|
273
|
+
|
274
|
+
def update_counters_on_associations(method_sym)
|
275
|
+
return unless [:decrement_counter, :increment_counter].include? method_sym
|
276
|
+
|
277
|
+
each_counter_cached_association_reflection do |assoc_reflection|
|
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)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def each_counter_cached_association_reflection
|
288
|
+
_reflections.each do |_name, reflection|
|
289
|
+
yield reflection if reflection.belongs_to? && reflection.counter_cache_column
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def increment_counters_on_associations
|
294
|
+
update_counters_on_associations :increment_counter
|
295
|
+
end
|
296
|
+
|
297
|
+
def decrement_counters_on_associations
|
298
|
+
update_counters_on_associations :decrement_counter
|
299
|
+
end
|
300
|
+
|
301
|
+
def stale_paranoid_value
|
302
|
+
self.paranoid_value = self.class.delete_now_value
|
303
|
+
clear_attribute_changes([self.class.paranoid_column])
|
213
304
|
end
|
214
305
|
end
|
215
306
|
end
|