acts_as_paranoid 0.5.0 → 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 +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
|