active_record-undo 0.1.5 → 0.3.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 +78 -2
- data/DOCUMENTATION.md +340 -133
- data/README.md +473 -190
- data/app/controllers/active_record/undo/application_controller.rb +100 -0
- data/app/controllers/active_record/undo/logs_controller/async_handler.rb +50 -0
- data/app/controllers/active_record/undo/logs_controller.rb +116 -0
- data/app/controllers/active_record/undo/restores_controller.rb +11 -0
- data/app/jobs/active_record/undo/restore_job.rb +72 -0
- data/app/jobs/active_record/undo/soft_delete_job.rb +75 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20260920000000_add_status_and_error_message_to_undo_logs.rb +15 -0
- data/lib/active_record/undo/cascade_handler.rb +66 -0
- data/lib/active_record/undo/configuration.rb +9 -1
- data/lib/active_record/undo/engine.rb +14 -1
- data/lib/active_record/undo/model_extension/async_methods.rb +68 -0
- data/lib/active_record/undo/model_extension/attribution_helper.rb +8 -0
- data/lib/active_record/undo/model_extension/tenant_verification.rb +1 -1
- data/lib/active_record/undo/model_extension.rb +22 -6
- data/lib/active_record/undo/safe_redirect.rb +60 -0
- data/lib/active_record/undo/status_predicates.rb +39 -0
- data/lib/active_record/undo/undo_log/token_verifier.rb +48 -0
- data/lib/active_record/undo/undo_log.rb +77 -2
- data/lib/active_record/undo/version.rb +1 -1
- data/lib/active_record/undo/view_helpers/status_badge_helper.rb +84 -0
- data/lib/active_record/undo/view_helpers.rb +105 -0
- data/lib/active_record/undo.rb +37 -1
- data/sig/active_record/undo.rbs +50 -1
- metadata +29 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require_relative 'cascade_handler'
|
|
5
5
|
require_relative 'model_extension/attribution_helper'
|
|
6
6
|
require_relative 'model_extension/tenant_verification'
|
|
7
|
+
require_relative 'model_extension/async_methods'
|
|
7
8
|
|
|
8
9
|
module ActiveRecord
|
|
9
10
|
module Undo
|
|
@@ -53,6 +54,8 @@ module ActiveRecord
|
|
|
53
54
|
module InstanceMethods
|
|
54
55
|
include AttributionHelper
|
|
55
56
|
include TenantVerification
|
|
57
|
+
include AsyncMethods
|
|
58
|
+
include StatusPredicates
|
|
56
59
|
|
|
57
60
|
def soft_deleted?
|
|
58
61
|
public_send(self.class.undoable_column).present?
|
|
@@ -69,13 +72,25 @@ module ActiveRecord
|
|
|
69
72
|
end
|
|
70
73
|
|
|
71
74
|
def undoable?
|
|
72
|
-
return false unless soft_deleted?
|
|
75
|
+
return false unless persisted? && soft_deleted?
|
|
76
|
+
|
|
77
|
+
log = undo_log
|
|
78
|
+
return false unless log
|
|
79
|
+
|
|
80
|
+
log.respond_to?(:completed?) ? log.completed? : true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def undo_log
|
|
84
|
+
return nil unless persisted? && soft_deleted?
|
|
85
|
+
|
|
86
|
+
find_latest_undo_log_item&.undo_log
|
|
87
|
+
end
|
|
88
|
+
alias latest_undo_log undo_log
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
item_id: id
|
|
77
|
-
)
|
|
90
|
+
def signed_token(expires_in: ActiveRecord::Undo.config.token_expires_in, purpose: :restore)
|
|
91
|
+
undo_log&.signed_token(expires_in: expires_in, purpose: purpose)
|
|
78
92
|
end
|
|
93
|
+
alias to_signed_token signed_token
|
|
79
94
|
|
|
80
95
|
def soft_delete!(whodunnit: nil, tenant: nil)
|
|
81
96
|
ensure_undoable_column_exists!
|
|
@@ -113,8 +128,9 @@ module ActiveRecord
|
|
|
113
128
|
|
|
114
129
|
def find_latest_undo_log_item
|
|
115
130
|
ActiveRecord::Undo::UndoLogItem
|
|
131
|
+
.joins(:undo_log)
|
|
116
132
|
.where(item: self)
|
|
117
|
-
.order(created_at: :desc)
|
|
133
|
+
.order(created_at: :desc, id: :desc)
|
|
118
134
|
.first
|
|
119
135
|
end
|
|
120
136
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
module Undo
|
|
7
|
+
module SafeRedirect
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def determine_redirect_path
|
|
11
|
+
safe_redirect_path(params[:redirect_to]) ||
|
|
12
|
+
safe_redirect_path(request.referer) ||
|
|
13
|
+
resolve_fallback_path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def safe_redirect_path(path)
|
|
17
|
+
return nil if invalid_path_string?(path)
|
|
18
|
+
|
|
19
|
+
uri = URI.parse(path)
|
|
20
|
+
return path if valid_relative_path?(path, uri)
|
|
21
|
+
return path if valid_same_host_path?(uri)
|
|
22
|
+
|
|
23
|
+
nil
|
|
24
|
+
rescue URI::InvalidURIError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def invalid_path_string?(path)
|
|
29
|
+
path.blank? || !path.is_a?(String) || path.match?(/[\r\n]/)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def valid_relative_path?(path, uri)
|
|
33
|
+
uri.relative? && uri.scheme.nil? && path.start_with?('/') && !path.start_with?('//') && !path.start_with?('/\\')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def valid_same_host_path?(uri)
|
|
37
|
+
%w[http https].include?(uri.scheme) &&
|
|
38
|
+
uri.host == request.host &&
|
|
39
|
+
(uri.port.nil? || uri.port == request.port)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def resolve_fallback_path
|
|
43
|
+
cfg = ActiveRecord::Undo.config.default_redirect_path
|
|
44
|
+
if cfg.respond_to?(:call)
|
|
45
|
+
cfg.arity.zero? ? cfg.call : cfg.call(main_app)
|
|
46
|
+
elsif cfg.present?
|
|
47
|
+
cfg.to_s
|
|
48
|
+
else
|
|
49
|
+
default_root_path
|
|
50
|
+
end
|
|
51
|
+
rescue StandardError
|
|
52
|
+
'/'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def default_root_path
|
|
56
|
+
main_app.respond_to?(:root_path) ? main_app.root_path : '/'
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Undo
|
|
5
|
+
module StatusPredicates
|
|
6
|
+
def soft_delete_pending?
|
|
7
|
+
soft_delete_status == 'pending'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def soft_delete_processing?
|
|
11
|
+
soft_delete_status == 'processing'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def soft_delete_completed?
|
|
15
|
+
soft_delete_status == 'completed'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def soft_delete_failed?
|
|
19
|
+
soft_delete_status == 'failed'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def restore_pending?
|
|
23
|
+
restore_status == 'pending'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def restore_processing?
|
|
27
|
+
restore_status == 'processing'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def restore_completed?
|
|
31
|
+
restore_status == 'completed'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def restore_failed?
|
|
35
|
+
restore_status == 'failed'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/message_verifier'
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
module Undo
|
|
7
|
+
class UndoLog < ActiveRecord::Base
|
|
8
|
+
module TokenVerifier
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
def find_by_signed_token(token, purpose: :restore)
|
|
13
|
+
return nil unless token.is_a?(String) && token.present?
|
|
14
|
+
|
|
15
|
+
verified_id = verify_signed_token(token, purpose: purpose)
|
|
16
|
+
return nil unless verified_id
|
|
17
|
+
|
|
18
|
+
find_by(id: verified_id)
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def verify_signed_token(token, purpose: :restore)
|
|
24
|
+
return nil unless token.is_a?(String) && token.present?
|
|
25
|
+
|
|
26
|
+
token_verifier.verified(token, purpose: purpose)
|
|
27
|
+
rescue StandardError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def token_verifier
|
|
32
|
+
if defined?(Rails.application) && Rails.application.respond_to?(:message_verifier)
|
|
33
|
+
Rails.application.message_verifier(:active_record_undo)
|
|
34
|
+
else
|
|
35
|
+
key = ActiveRecord::Undo.config.token_secret_key || 'active_record_undo_default_secret_key_32_bytes'
|
|
36
|
+
@token_verifier ||= ActiveSupport::MessageVerifier.new(key, digest: 'SHA256')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def signed_token(expires_in: ActiveRecord::Undo.config.token_expires_in, purpose: :restore)
|
|
42
|
+
self.class.token_verifier.generate(id, purpose: purpose, expires_in: expires_in)
|
|
43
|
+
end
|
|
44
|
+
alias to_signed_token signed_token
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
# lib/active_record/undo/undo_log.rb
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
3
|
+
require_relative 'undo_log/token_verifier'
|
|
4
|
+
|
|
4
5
|
module ActiveRecord
|
|
5
6
|
module Undo
|
|
6
7
|
class UndoLog < ActiveRecord::Base
|
|
7
8
|
self.table_name = 'undo_logs'
|
|
8
9
|
|
|
10
|
+
include ModelExtension::AttributionHelper
|
|
11
|
+
include ModelExtension::TenantVerification
|
|
12
|
+
include TokenVerifier
|
|
13
|
+
include StatusPredicates
|
|
14
|
+
|
|
9
15
|
has_many :undo_log_items, class_name: 'ActiveRecord::Undo::UndoLogItem', dependent: :destroy
|
|
10
16
|
|
|
11
17
|
belongs_to :whodunnit, polymorphic: true, optional: true
|
|
12
18
|
belongs_to :tenant, polymorphic: true, optional: true
|
|
13
19
|
|
|
20
|
+
enum :status, { pending: 0, processing: 1, completed: 2, failed: 3 }, default: :completed
|
|
21
|
+
enum :action, { soft_delete: 0, restore: 1 }, default: :soft_delete
|
|
22
|
+
|
|
14
23
|
scope :for_whodunnit, lambda { |user|
|
|
15
24
|
if user.is_a?(ActiveRecord::Base)
|
|
16
25
|
where(whodunnit: user)
|
|
@@ -36,10 +45,76 @@ module ActiveRecord
|
|
|
36
45
|
end
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
def expired?
|
|
49
|
+
period = ActiveRecord::Undo.config.retention_period
|
|
50
|
+
return false unless period && created_at
|
|
51
|
+
|
|
52
|
+
created_at < Time.current - period
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def soft_delete_status
|
|
56
|
+
return status if action == 'soft_delete' || action.nil?
|
|
57
|
+
|
|
58
|
+
'completed'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def restore_status
|
|
62
|
+
return status if action == 'restore'
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def restore_later!(whodunnit: nil, queue: nil)
|
|
68
|
+
verify_restore_preconditions!
|
|
69
|
+
with_lock do
|
|
70
|
+
validate_restore_status_eligibility!
|
|
71
|
+
update!(action: :restore, status: :pending)
|
|
72
|
+
end
|
|
73
|
+
enqueue_restore_job!(whodunnit, queue)
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
39
77
|
# Restores all records associated with this deletion batch
|
|
40
|
-
def restore!
|
|
78
|
+
def restore!(whodunnit: nil)
|
|
79
|
+
verify_restore_preconditions!
|
|
80
|
+
validate_restore_status_eligibility!
|
|
81
|
+
|
|
82
|
+
actor = resolve_whodunnit(whodunnit)
|
|
83
|
+
with_whodunnit_context(actor) do
|
|
84
|
+
execute_restore_transaction!
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def verify_restore_preconditions!
|
|
41
91
|
ActiveRecord::Undo.verify_configured_context!
|
|
92
|
+
verify_tenant_match!(self) if tenant_present?
|
|
93
|
+
raise ActiveRecord::Undo::Error, 'Undo action has expired.' if expired?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validate_restore_status_eligibility!
|
|
97
|
+
raise ActiveRecord::Undo::Error, 'Cannot restore: UndoLog is currently processing.' if processing?
|
|
98
|
+
|
|
99
|
+
if pending? && action == 'restore'
|
|
100
|
+
raise ActiveRecord::Undo::Error, 'Cannot restore: UndoLog is already scheduled for restore.'
|
|
101
|
+
end
|
|
102
|
+
return if completed? || (action == 'restore' && failed?)
|
|
103
|
+
|
|
104
|
+
raise ActiveRecord::Undo::Error, "Cannot restore: UndoLog is not completed (current status: #{status})."
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def enqueue_restore_job!(whodunnit, queue)
|
|
108
|
+
job_class = ActiveRecord::Undo::RestoreJob
|
|
109
|
+
job = queue.present? ? job_class.set(queue: queue) : job_class
|
|
110
|
+
job.perform_later(id, whodunnit: whodunnit)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def tenant_present?
|
|
114
|
+
tenant_id.present? || tenant_type.present?
|
|
115
|
+
end
|
|
42
116
|
|
|
117
|
+
def execute_restore_transaction!
|
|
43
118
|
transaction do
|
|
44
119
|
# Reverse order ensures child records are restored before or after parents as needed
|
|
45
120
|
undo_log_items.reverse_each(&:restore_item!)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Undo
|
|
5
|
+
module ViewHelpers
|
|
6
|
+
module StatusBadgeHelper
|
|
7
|
+
def undo_status_badge(target, action: :auto, **html_options)
|
|
8
|
+
return nil if target.nil?
|
|
9
|
+
|
|
10
|
+
act, status = resolve_target_action_and_status(target, action)
|
|
11
|
+
return nil unless status
|
|
12
|
+
|
|
13
|
+
classes = build_badge_classes(act, status, html_options.delete(:class))
|
|
14
|
+
label = html_options.delete(:text) || status.to_s.humanize
|
|
15
|
+
render_badge_tag(label, classes, html_options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def soft_delete_status_badge(target, **html_options)
|
|
19
|
+
undo_status_badge(target, action: :soft_delete, **html_options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def restore_status_badge(target, **html_options)
|
|
23
|
+
undo_status_badge(target, action: :restore, **html_options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def resolve_target_action_and_status(target, action)
|
|
29
|
+
return nil if target.nil?
|
|
30
|
+
|
|
31
|
+
case action
|
|
32
|
+
when :soft_delete
|
|
33
|
+
['soft_delete', extract_target_status(target, :soft_delete_status)]
|
|
34
|
+
when :restore
|
|
35
|
+
['restore', extract_target_status(target, :restore_status)]
|
|
36
|
+
else
|
|
37
|
+
resolve_auto_action_and_status(target)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_target_status(target, status_method)
|
|
42
|
+
if target.respond_to?(status_method)
|
|
43
|
+
target.public_send(status_method)
|
|
44
|
+
elsif target.respond_to?(:status)
|
|
45
|
+
target.status
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resolve_auto_action_and_status(target)
|
|
50
|
+
return ['restore', target.restore_status] if auto_restore_status?(target)
|
|
51
|
+
return ['soft_delete', target.soft_delete_status] if auto_soft_delete_status?(target)
|
|
52
|
+
return [log_action(target), target.status] if target.respond_to?(:status)
|
|
53
|
+
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def auto_restore_status?(target)
|
|
58
|
+
target.respond_to?(:restore_status) && target.restore_status.present?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def auto_soft_delete_status?(target)
|
|
62
|
+
target.respond_to?(:soft_delete_status) && target.soft_delete_status.present?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def log_action(target)
|
|
66
|
+
target.respond_to?(:action) ? target.action : nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_badge_classes(action, status, custom_class)
|
|
70
|
+
action_class = action ? "undo-#{action.to_s.tr('_', '-')}-badge" : nil
|
|
71
|
+
['undo-status-badge', action_class, "undo-status-#{status}", custom_class].compact.join(' ')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_badge_tag(label, classes, html_options)
|
|
75
|
+
if respond_to?(:content_tag)
|
|
76
|
+
content_tag(:span, label, html_options.merge(class: classes))
|
|
77
|
+
else
|
|
78
|
+
"<span class=\"#{classes}\">#{label}</span>"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'view_helpers/status_badge_helper'
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
module Undo
|
|
7
|
+
module ViewHelpers
|
|
8
|
+
include StatusBadgeHelper
|
|
9
|
+
|
|
10
|
+
def undo_button_to(target, text = nil, signed: false, **html_options)
|
|
11
|
+
btn_text = extract_undo_text(text, html_options)
|
|
12
|
+
url = resolve_undo_path(target, signed: signed)
|
|
13
|
+
button_to(btn_text, url, method: :post, **html_options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def undo_link_to(target, text = nil, signed: false, **html_options)
|
|
17
|
+
link_text = extract_undo_text(text, html_options)
|
|
18
|
+
url = resolve_undo_path(target, signed: signed)
|
|
19
|
+
data = { turbo_method: :post }.merge(html_options.delete(:data) || {})
|
|
20
|
+
link_to(link_text, url, method: :post, data: data, **html_options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def extract_undo_text(text, html_options)
|
|
26
|
+
text || html_options.delete(:text) || 'Undo'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve_undo_path(target, signed: false)
|
|
30
|
+
router = undo_engine_router
|
|
31
|
+
if signed
|
|
32
|
+
token = extract_signed_token(target)
|
|
33
|
+
router.signed_restore_path(token: token)
|
|
34
|
+
else
|
|
35
|
+
router.restore_log_path(extract_log_target(target))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extract_signed_token(target)
|
|
40
|
+
return target if target.is_a?(String)
|
|
41
|
+
|
|
42
|
+
token = token_from_target(target)
|
|
43
|
+
token || (raise ArgumentError, "Cannot generate signed token for #{target.inspect}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def token_from_target(target)
|
|
47
|
+
if target.respond_to?(:signed_token)
|
|
48
|
+
target.signed_token
|
|
49
|
+
elsif target.respond_to?(:undo_log)
|
|
50
|
+
target.undo_log&.signed_token
|
|
51
|
+
else
|
|
52
|
+
find_log_for_model(target)&.signed_token
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def extract_log_target(target)
|
|
57
|
+
return target if target.is_a?(ActiveRecord::Undo::UndoLog)
|
|
58
|
+
|
|
59
|
+
if model_target?(target)
|
|
60
|
+
log = log_for_target(target)
|
|
61
|
+
return log if log
|
|
62
|
+
|
|
63
|
+
raise ArgumentError, "Cannot resolve undo log for #{target.inspect}. " \
|
|
64
|
+
'Ensure the record is soft-deleted and undoable.'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
target
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def model_target?(target)
|
|
71
|
+
target.respond_to?(:undoable?) || target.respond_to?(:undo_log)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def log_for_target(target)
|
|
75
|
+
if target.respond_to?(:undo_log)
|
|
76
|
+
target.undo_log
|
|
77
|
+
else
|
|
78
|
+
find_log_for_model(target)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def find_log_for_model(model)
|
|
83
|
+
return nil unless model.respond_to?(:undoable?)
|
|
84
|
+
|
|
85
|
+
ActiveRecord::Undo::UndoLogItem
|
|
86
|
+
.joins(:undo_log)
|
|
87
|
+
.where(item: model)
|
|
88
|
+
.order(created_at: :desc, id: :desc)
|
|
89
|
+
.first&.undo_log
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def undo_engine_router
|
|
93
|
+
if respond_to?(:active_record_undo)
|
|
94
|
+
active_record_undo
|
|
95
|
+
elsif defined?(main_app) && main_app.respond_to?(:active_record_undo)
|
|
96
|
+
main_app.active_record_undo
|
|
97
|
+
elsif respond_to?(:restore_log_path)
|
|
98
|
+
self
|
|
99
|
+
else
|
|
100
|
+
ActiveRecord::Undo::Engine.routes.url_helpers
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/active_record/undo.rb
CHANGED
|
@@ -12,12 +12,14 @@ end
|
|
|
12
12
|
require 'set'
|
|
13
13
|
require_relative 'undo/version'
|
|
14
14
|
require_relative 'undo/engine' if defined?(Rails::Engine)
|
|
15
|
+
require_relative 'undo/view_helpers' if defined?(ActionView)
|
|
15
16
|
require_relative 'undo/configuration'
|
|
17
|
+
require_relative 'undo/safe_redirect'
|
|
18
|
+
require_relative 'undo/status_predicates'
|
|
16
19
|
require_relative 'undo/model_extension'
|
|
17
20
|
require_relative 'undo/undo_log'
|
|
18
21
|
require_relative 'undo/undo_log_item'
|
|
19
22
|
require_relative 'undo/purger'
|
|
20
|
-
require_relative 'undo/purge_job'
|
|
21
23
|
|
|
22
24
|
module ActiveRecord
|
|
23
25
|
module Undo
|
|
@@ -85,6 +87,28 @@ module ActiveRecord
|
|
|
85
87
|
raise ActiveRecord::Undo::SecurityError, msg
|
|
86
88
|
end
|
|
87
89
|
|
|
90
|
+
def base_controller_class
|
|
91
|
+
configured = config.base_controller
|
|
92
|
+
return configured if configured.is_a?(Class)
|
|
93
|
+
|
|
94
|
+
(configured.presence && configured.to_s.safe_constantize) || default_base_controller
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def default_base_controller
|
|
98
|
+
defined?(ActionController::Base) ? ActionController::Base : Object
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def base_job_class
|
|
102
|
+
configured = config.base_job
|
|
103
|
+
return configured if configured.is_a?(Class)
|
|
104
|
+
|
|
105
|
+
(configured.presence && configured.to_s.safe_constantize) || default_base_job
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def default_base_job
|
|
109
|
+
defined?(ActiveJob::Base) ? ActiveJob::Base : Object
|
|
110
|
+
end
|
|
111
|
+
|
|
88
112
|
private
|
|
89
113
|
|
|
90
114
|
def eager_load_rails!
|
|
@@ -98,6 +122,18 @@ module ActiveRecord
|
|
|
98
122
|
end
|
|
99
123
|
end
|
|
100
124
|
|
|
125
|
+
begin
|
|
126
|
+
require 'active_job'
|
|
127
|
+
rescue LoadError
|
|
128
|
+
# ActiveJob is optional
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if defined?(ActiveJob::Base)
|
|
132
|
+
require_relative 'undo/purge_job'
|
|
133
|
+
require_relative '../../app/jobs/active_record/undo/soft_delete_job'
|
|
134
|
+
require_relative '../../app/jobs/active_record/undo/restore_job'
|
|
135
|
+
end
|
|
136
|
+
|
|
101
137
|
if defined?(ActiveSupport)
|
|
102
138
|
ActiveSupport.on_load(:active_record) do
|
|
103
139
|
include ActiveRecord::Undo::ModelExtension
|
data/sig/active_record/undo.rbs
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
module ActiveRecord
|
|
2
2
|
module Undo
|
|
3
3
|
VERSION: String
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class SecurityError < Error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.whodunnit: () -> untyped
|
|
12
|
+
def self.whodunnit=: (untyped) -> untyped
|
|
13
|
+
def self.current_tenant: () -> untyped
|
|
14
|
+
def self.current_tenant=: (untyped) -> untyped
|
|
15
|
+
def self.config: () -> Configuration
|
|
16
|
+
def self.configure: () { (Configuration) -> void } -> void
|
|
17
|
+
def self.registered_models: () -> Set[untyped]
|
|
18
|
+
def self.undoable_models: () -> Array[untyped]
|
|
19
|
+
def self.base_controller_class: () -> untyped
|
|
20
|
+
def self.base_job_class: () -> untyped
|
|
21
|
+
|
|
22
|
+
class Configuration
|
|
23
|
+
attr_accessor retention_period: untyped
|
|
24
|
+
attr_accessor current_user_method: untyped
|
|
25
|
+
attr_accessor current_tenant_method: untyped
|
|
26
|
+
attr_accessor base_controller: untyped
|
|
27
|
+
attr_accessor base_job: untyped
|
|
28
|
+
attr_accessor default_redirect_path: untyped
|
|
29
|
+
attr_accessor token_secret_key: String?
|
|
30
|
+
attr_accessor token_expires_in: untyped
|
|
31
|
+
attr_accessor error_handling: Symbol
|
|
32
|
+
|
|
33
|
+
def initialize: () -> void
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module StatusPredicates
|
|
37
|
+
def soft_delete_pending?: () -> bool
|
|
38
|
+
def soft_delete_processing?: () -> bool
|
|
39
|
+
def soft_delete_completed?: () -> bool
|
|
40
|
+
def soft_delete_failed?: () -> bool
|
|
41
|
+
def restore_pending?: () -> bool
|
|
42
|
+
def restore_processing?: () -> bool
|
|
43
|
+
def restore_completed?: () -> bool
|
|
44
|
+
def restore_failed?: () -> bool
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module ViewHelpers
|
|
48
|
+
def undo_button_to: (untyped target, ?String? text, ?signed: bool, **untyped html_options) -> untyped
|
|
49
|
+
def undo_link_to: (untyped target, ?String? text, ?signed: bool, **untyped html_options) -> untyped
|
|
50
|
+
def undo_status_badge: (untyped target, ?action: Symbol, **untyped html_options) -> untyped
|
|
51
|
+
def soft_delete_status_badge: (untyped target, **untyped html_options) -> untyped
|
|
52
|
+
def restore_status_badge: (untyped target, **untyped html_options) -> untyped
|
|
53
|
+
end
|
|
5
54
|
end
|
|
6
55
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_record-undo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Saurabh Sharma
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activejob
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 7.0.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 7.0.0
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: activerecord
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -136,8 +150,16 @@ files:
|
|
|
136
150
|
- LICENSE.txt
|
|
137
151
|
- README.md
|
|
138
152
|
- Rakefile
|
|
153
|
+
- app/controllers/active_record/undo/application_controller.rb
|
|
154
|
+
- app/controllers/active_record/undo/logs_controller.rb
|
|
155
|
+
- app/controllers/active_record/undo/logs_controller/async_handler.rb
|
|
156
|
+
- app/controllers/active_record/undo/restores_controller.rb
|
|
157
|
+
- app/jobs/active_record/undo/restore_job.rb
|
|
158
|
+
- app/jobs/active_record/undo/soft_delete_job.rb
|
|
159
|
+
- config/routes.rb
|
|
139
160
|
- db/migrate/20260808000000_create_active_record_undo_tables.rb
|
|
140
161
|
- db/migrate/20260821000000_add_tenant_and_user_attribution_to_undo_logs.rb
|
|
162
|
+
- db/migrate/20260920000000_add_status_and_error_message_to_undo_logs.rb
|
|
141
163
|
- lib/active_record/undo.rb
|
|
142
164
|
- lib/active_record/undo/cascade_handler.rb
|
|
143
165
|
- lib/active_record/undo/cascade_handler/association_finder.rb
|
|
@@ -145,15 +167,21 @@ files:
|
|
|
145
167
|
- lib/active_record/undo/configuration.rb
|
|
146
168
|
- lib/active_record/undo/engine.rb
|
|
147
169
|
- lib/active_record/undo/model_extension.rb
|
|
170
|
+
- lib/active_record/undo/model_extension/async_methods.rb
|
|
148
171
|
- lib/active_record/undo/model_extension/attribution_helper.rb
|
|
149
172
|
- lib/active_record/undo/model_extension/tenant_verification.rb
|
|
150
173
|
- lib/active_record/undo/purge_job.rb
|
|
151
174
|
- lib/active_record/undo/purger.rb
|
|
152
175
|
- lib/active_record/undo/purger/reflection_helper.rb
|
|
176
|
+
- lib/active_record/undo/safe_redirect.rb
|
|
177
|
+
- lib/active_record/undo/status_predicates.rb
|
|
153
178
|
- lib/active_record/undo/tasks/purge.rake
|
|
154
179
|
- lib/active_record/undo/undo_log.rb
|
|
180
|
+
- lib/active_record/undo/undo_log/token_verifier.rb
|
|
155
181
|
- lib/active_record/undo/undo_log_item.rb
|
|
156
182
|
- lib/active_record/undo/version.rb
|
|
183
|
+
- lib/active_record/undo/view_helpers.rb
|
|
184
|
+
- lib/active_record/undo/view_helpers/status_badge_helper.rb
|
|
157
185
|
- lib/tasks/active_record_undo_tasks.rake
|
|
158
186
|
- sig/active_record/undo.rbs
|
|
159
187
|
homepage: https://github.com/saurabh-activecode/active_record-undo
|