active_record-undo 0.1.5 → 0.2.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/DOCUMENTATION.md +96 -22
- data/README.md +350 -197
- data/app/controllers/active_record/undo/application_controller.rb +91 -0
- data/app/controllers/active_record/undo/logs_controller.rb +87 -0
- data/app/controllers/active_record/undo/restores_controller.rb +11 -0
- data/config/routes.rb +11 -0
- data/lib/active_record/undo/configuration.rb +8 -1
- data/lib/active_record/undo/engine.rb +14 -1
- 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 +15 -2
- data/lib/active_record/undo/safe_redirect.rb +60 -0
- data/lib/active_record/undo/undo_log.rb +60 -2
- data/lib/active_record/undo/version.rb +1 -1
- data/lib/active_record/undo/view_helpers.rb +101 -0
- data/lib/active_record/undo.rb +13 -0
- metadata +7 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
module Undo
|
|
7
|
+
class ApplicationController < ActiveRecord::Undo.base_controller_class
|
|
8
|
+
include SafeRedirect
|
|
9
|
+
|
|
10
|
+
protect_from_forgery with: :exception, unless: -> { request.format.json? }
|
|
11
|
+
|
|
12
|
+
rescue_from ActiveRecord::Undo::SecurityError, with: :handle_security_error
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def resolve_whodunnit_user
|
|
17
|
+
if respond_to?(:current_user, true) && send(:current_user).present?
|
|
18
|
+
send(:current_user)
|
|
19
|
+
elsif ActiveRecord::Undo.config.current_user_method.respond_to?(:call)
|
|
20
|
+
ActiveRecord::Undo.config.current_user_method.call
|
|
21
|
+
else
|
|
22
|
+
ActiveRecord::Undo.whodunnit
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def resolve_current_tenant
|
|
27
|
+
if respond_to?(:current_tenant, true) && send(:current_tenant).present?
|
|
28
|
+
send(:current_tenant)
|
|
29
|
+
elsif ActiveRecord::Undo.config.current_tenant_method.respond_to?(:call)
|
|
30
|
+
ActiveRecord::Undo.config.current_tenant_method.call
|
|
31
|
+
else
|
|
32
|
+
ActiveRecord::Undo.current_tenant
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def handle_security_error(error)
|
|
37
|
+
respond_to do |format|
|
|
38
|
+
format.html { handle_html_error(error.message, :forbidden) }
|
|
39
|
+
format.turbo_stream { render_turbo_stream_error(error.message, :forbidden) }
|
|
40
|
+
format.json { render json: { error: error.message }, status: :forbidden }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def handle_not_found
|
|
45
|
+
msg = 'Undo log not found or already restored.'
|
|
46
|
+
respond_to do |format|
|
|
47
|
+
format.html { handle_html_error(msg, :not_found) }
|
|
48
|
+
format.turbo_stream { render_turbo_stream_error(msg, :not_found) }
|
|
49
|
+
format.json { render json: { error: msg }, status: :not_found }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_expired
|
|
54
|
+
msg = 'Undo action has expired.'
|
|
55
|
+
respond_to do |format|
|
|
56
|
+
format.html { handle_html_error(msg, :unprocessable_entity) }
|
|
57
|
+
format.turbo_stream { render_turbo_stream_error(msg, :unprocessable_entity) }
|
|
58
|
+
format.json { render json: { error: msg }, status: :unprocessable_entity }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def handle_html_error(message, status)
|
|
63
|
+
if should_redirect_on_error?
|
|
64
|
+
redirect_to determine_redirect_path, alert: message, status: :see_other
|
|
65
|
+
else
|
|
66
|
+
render plain: message, status: status
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def should_redirect_on_error?
|
|
71
|
+
return true if ActiveRecord::Undo.config.error_handling == :redirect
|
|
72
|
+
return false if ActiveRecord::Undo.config.error_handling == :render
|
|
73
|
+
|
|
74
|
+
safe_redirect_path(params[:redirect_to]).present? || safe_redirect_path(request.referer).present?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_turbo_stream_error(message, status)
|
|
78
|
+
render inline: turbo_stream_template('alert', message),
|
|
79
|
+
content_type: 'text/vnd.turbo-stream.html',
|
|
80
|
+
status: status
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def turbo_stream_template(type, message)
|
|
84
|
+
escaped = ERB::Util.html_escape(message)
|
|
85
|
+
'<turbo-stream action="append" target="notifications"><template>' \
|
|
86
|
+
"<div class=\"undo-notification undo-#{type}\">#{escaped}</div>" \
|
|
87
|
+
'</template></turbo-stream>'
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Undo
|
|
5
|
+
class LogsController < ApplicationController
|
|
6
|
+
before_action :set_undo_log
|
|
7
|
+
before_action :check_not_found
|
|
8
|
+
before_action :check_expired
|
|
9
|
+
|
|
10
|
+
def restore
|
|
11
|
+
return unless @undo_log
|
|
12
|
+
|
|
13
|
+
count = @undo_log.undo_log_items.count
|
|
14
|
+
execute_restore_action
|
|
15
|
+
respond_with_restored_log(count)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def set_undo_log
|
|
21
|
+
@undo_log = find_undo_log
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def check_not_found
|
|
25
|
+
handle_not_found if @undo_log.nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def check_expired
|
|
29
|
+
handle_expired if @undo_log&.expired?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def find_undo_log
|
|
33
|
+
if params[:token].present?
|
|
34
|
+
ActiveRecord::Undo::UndoLog.find_by_signed_token(params[:token])
|
|
35
|
+
elsif params[:id].present?
|
|
36
|
+
find_by_id_or_token(params[:id])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def find_by_id_or_token(identifier)
|
|
41
|
+
str = identifier.to_s
|
|
42
|
+
return nil if str.blank?
|
|
43
|
+
|
|
44
|
+
if str.match?(/\A\d+\z/) || uuid_identifier?(str)
|
|
45
|
+
ActiveRecord::Undo::UndoLog.find_by(id: identifier)
|
|
46
|
+
else
|
|
47
|
+
ActiveRecord::Undo::UndoLog.find_by_signed_token(str)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def uuid_identifier?(str)
|
|
52
|
+
str.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def execute_restore_action
|
|
56
|
+
tenant = resolve_current_tenant
|
|
57
|
+
with_tenant_context(tenant) do
|
|
58
|
+
@undo_log.restore!(whodunnit: resolve_whodunnit_user)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def with_tenant_context(tenant)
|
|
63
|
+
orig = ActiveRecord::Undo.current_tenant
|
|
64
|
+
ActiveRecord::Undo.current_tenant = tenant if tenant.present?
|
|
65
|
+
yield
|
|
66
|
+
ensure
|
|
67
|
+
ActiveRecord::Undo.current_tenant = orig
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def respond_with_restored_log(count)
|
|
71
|
+
respond_to do |format|
|
|
72
|
+
format.html do
|
|
73
|
+
redirect_to determine_redirect_path, notice: 'Record successfully restored.', status: :see_other
|
|
74
|
+
end
|
|
75
|
+
format.turbo_stream { render_turbo_stream_success }
|
|
76
|
+
format.json { render json: { success: true, restored_items_count: count }, status: :ok }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_turbo_stream_success
|
|
81
|
+
render inline: turbo_stream_template('notice', 'Record successfully restored.'),
|
|
82
|
+
content_type: 'text/vnd.turbo-stream.html',
|
|
83
|
+
status: :ok
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
module ActiveRecord
|
|
4
4
|
module Undo
|
|
5
5
|
class Configuration
|
|
6
|
-
attr_accessor :retention_period, :current_user_method, :current_tenant_method
|
|
6
|
+
attr_accessor :retention_period, :current_user_method, :current_tenant_method,
|
|
7
|
+
:base_controller, :default_redirect_path, :token_secret_key,
|
|
8
|
+
:token_expires_in, :error_handling
|
|
7
9
|
|
|
8
10
|
def initialize
|
|
9
11
|
@retention_period = 30.days
|
|
10
12
|
@current_user_method = nil
|
|
11
13
|
@current_tenant_method = nil
|
|
14
|
+
@base_controller = '::ApplicationController'
|
|
15
|
+
@default_redirect_path = ->(main_app) { main_app.respond_to?(:root_path) ? main_app.root_path : '/' }
|
|
16
|
+
@token_secret_key = nil
|
|
17
|
+
@token_expires_in = 24.hours
|
|
18
|
+
@error_handling = :auto
|
|
12
19
|
end
|
|
13
20
|
end
|
|
14
21
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
# lib/active_record/undo/engine.rb
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
3
|
+
require_relative 'view_helpers'
|
|
4
|
+
|
|
4
5
|
module ActiveRecord
|
|
5
6
|
module Undo
|
|
6
7
|
class Engine < ::Rails::Engine
|
|
@@ -15,6 +16,18 @@ module ActiveRecord
|
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
19
|
+
initializer 'active_record_undo.view_helpers' do
|
|
20
|
+
ActiveSupport.on_load(:action_view) do
|
|
21
|
+
include ActiveRecord::Undo::ViewHelpers
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
initializer 'active_record_undo.turbo_stream_mime_type' do
|
|
26
|
+
if defined?(Mime::Type) && (!defined?(Mime[:turbo_stream]) || Mime[:turbo_stream].nil?)
|
|
27
|
+
Mime::Type.register 'text/vnd.turbo-stream.html', :turbo_stream
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
18
31
|
rake_tasks do
|
|
19
32
|
path = root.join('lib', 'tasks', 'active_record_undo_tasks.rake')
|
|
20
33
|
load path if File.exist?(path)
|
|
@@ -40,6 +40,14 @@ module ActiveRecord
|
|
|
40
40
|
attrs[:tenant_id] = ten
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
|
+
|
|
44
|
+
def with_whodunnit_context(actor)
|
|
45
|
+
orig = ActiveRecord::Undo.whodunnit
|
|
46
|
+
ActiveRecord::Undo.whodunnit = actor
|
|
47
|
+
yield
|
|
48
|
+
ensure
|
|
49
|
+
ActiveRecord::Undo.whodunnit = orig
|
|
50
|
+
end
|
|
43
51
|
end
|
|
44
52
|
end
|
|
45
53
|
end
|
|
@@ -69,7 +69,7 @@ module ActiveRecord
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def undoable?
|
|
72
|
-
return false unless soft_deleted?
|
|
72
|
+
return false unless persisted? && soft_deleted?
|
|
73
73
|
|
|
74
74
|
ActiveRecord::Undo::UndoLogItem.joins(:undo_log).exists?(
|
|
75
75
|
item_type: self.class.name,
|
|
@@ -77,6 +77,18 @@ module ActiveRecord
|
|
|
77
77
|
)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
def undo_log
|
|
81
|
+
return nil unless persisted? && soft_deleted?
|
|
82
|
+
|
|
83
|
+
find_latest_undo_log_item&.undo_log
|
|
84
|
+
end
|
|
85
|
+
alias latest_undo_log undo_log
|
|
86
|
+
|
|
87
|
+
def signed_token(expires_in: ActiveRecord::Undo.config.token_expires_in, purpose: :restore)
|
|
88
|
+
undo_log&.signed_token(expires_in: expires_in, purpose: purpose)
|
|
89
|
+
end
|
|
90
|
+
alias to_signed_token signed_token
|
|
91
|
+
|
|
80
92
|
def soft_delete!(whodunnit: nil, tenant: nil)
|
|
81
93
|
ensure_undoable_column_exists!
|
|
82
94
|
return false if soft_deleted?
|
|
@@ -113,8 +125,9 @@ module ActiveRecord
|
|
|
113
125
|
|
|
114
126
|
def find_latest_undo_log_item
|
|
115
127
|
ActiveRecord::Undo::UndoLogItem
|
|
128
|
+
.joins(:undo_log)
|
|
116
129
|
.where(item: self)
|
|
117
|
-
.order(created_at: :desc)
|
|
130
|
+
.order(created_at: :desc, id: :desc)
|
|
118
131
|
.first
|
|
119
132
|
end
|
|
120
133
|
|
|
@@ -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
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
# lib/active_record/undo/undo_log.rb
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
3
|
+
require 'active_support/message_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
|
+
|
|
9
13
|
has_many :undo_log_items, class_name: 'ActiveRecord::Undo::UndoLogItem', dependent: :destroy
|
|
10
14
|
|
|
11
15
|
belongs_to :whodunnit, polymorphic: true, optional: true
|
|
@@ -36,10 +40,64 @@ module ActiveRecord
|
|
|
36
40
|
end
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
def self.find_by_signed_token(token, purpose: :restore)
|
|
44
|
+
return nil unless token.is_a?(String) && token.present?
|
|
45
|
+
|
|
46
|
+
verified_id = verify_signed_token(token, purpose: purpose)
|
|
47
|
+
return nil unless verified_id
|
|
48
|
+
|
|
49
|
+
find_by(id: verified_id)
|
|
50
|
+
rescue StandardError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.verify_signed_token(token, purpose: :restore)
|
|
55
|
+
return nil unless token.is_a?(String) && token.present?
|
|
56
|
+
|
|
57
|
+
token_verifier.verified(token, purpose: purpose)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.token_verifier
|
|
63
|
+
if defined?(Rails.application) && Rails.application.respond_to?(:message_verifier)
|
|
64
|
+
Rails.application.message_verifier(:active_record_undo)
|
|
65
|
+
else
|
|
66
|
+
key = ActiveRecord::Undo.config.token_secret_key || 'active_record_undo_default_secret_key_32_bytes'
|
|
67
|
+
@token_verifier ||= ActiveSupport::MessageVerifier.new(key, digest: 'SHA256')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def expired?
|
|
72
|
+
period = ActiveRecord::Undo.config.retention_period
|
|
73
|
+
return false unless period && created_at
|
|
74
|
+
|
|
75
|
+
created_at < Time.current - period
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def signed_token(expires_in: ActiveRecord::Undo.config.token_expires_in, purpose: :restore)
|
|
79
|
+
self.class.token_verifier.generate(id, purpose: purpose, expires_in: expires_in)
|
|
80
|
+
end
|
|
81
|
+
alias to_signed_token signed_token
|
|
82
|
+
|
|
39
83
|
# Restores all records associated with this deletion batch
|
|
40
|
-
def restore!
|
|
84
|
+
def restore!(whodunnit: nil)
|
|
41
85
|
ActiveRecord::Undo.verify_configured_context!
|
|
86
|
+
verify_tenant_match!(self) if tenant_present?
|
|
87
|
+
|
|
88
|
+
actor = resolve_whodunnit(whodunnit)
|
|
89
|
+
with_whodunnit_context(actor) do
|
|
90
|
+
execute_restore_transaction!
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def tenant_present?
|
|
97
|
+
tenant_id.present? || tenant_type.present?
|
|
98
|
+
end
|
|
42
99
|
|
|
100
|
+
def execute_restore_transaction!
|
|
43
101
|
transaction do
|
|
44
102
|
# Reverse order ensures child records are restored before or after parents as needed
|
|
45
103
|
undo_log_items.reverse_each(&:restore_item!)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Undo
|
|
5
|
+
module ViewHelpers
|
|
6
|
+
def undo_button_to(target, text = nil, signed: false, **html_options)
|
|
7
|
+
btn_text = extract_undo_text(text, html_options)
|
|
8
|
+
url = resolve_undo_path(target, signed: signed)
|
|
9
|
+
button_to(btn_text, url, method: :post, **html_options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def undo_link_to(target, text = nil, signed: false, **html_options)
|
|
13
|
+
link_text = extract_undo_text(text, html_options)
|
|
14
|
+
url = resolve_undo_path(target, signed: signed)
|
|
15
|
+
data = { turbo_method: :post }.merge(html_options.delete(:data) || {})
|
|
16
|
+
link_to(link_text, url, method: :post, data: data, **html_options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def extract_undo_text(text, html_options)
|
|
22
|
+
text || html_options.delete(:text) || 'Undo'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def resolve_undo_path(target, signed: false)
|
|
26
|
+
router = undo_engine_router
|
|
27
|
+
if signed
|
|
28
|
+
token = extract_signed_token(target)
|
|
29
|
+
router.signed_restore_path(token: token)
|
|
30
|
+
else
|
|
31
|
+
router.restore_log_path(extract_log_target(target))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def extract_signed_token(target)
|
|
36
|
+
return target if target.is_a?(String)
|
|
37
|
+
|
|
38
|
+
token = token_from_target(target)
|
|
39
|
+
token || (raise ArgumentError, "Cannot generate signed token for #{target.inspect}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def token_from_target(target)
|
|
43
|
+
if target.respond_to?(:signed_token)
|
|
44
|
+
target.signed_token
|
|
45
|
+
elsif target.respond_to?(:undo_log)
|
|
46
|
+
target.undo_log&.signed_token
|
|
47
|
+
else
|
|
48
|
+
find_log_for_model(target)&.signed_token
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_log_target(target)
|
|
53
|
+
return target if target.is_a?(ActiveRecord::Undo::UndoLog)
|
|
54
|
+
|
|
55
|
+
if model_target?(target)
|
|
56
|
+
log = log_for_target(target)
|
|
57
|
+
return log if log
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "Cannot resolve undo log for #{target.inspect}. " \
|
|
60
|
+
'Ensure the record is soft-deleted and undoable.'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
target
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def model_target?(target)
|
|
67
|
+
target.respond_to?(:undoable?) || target.respond_to?(:undo_log)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def log_for_target(target)
|
|
71
|
+
if target.respond_to?(:undo_log)
|
|
72
|
+
target.undo_log
|
|
73
|
+
else
|
|
74
|
+
find_log_for_model(target)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def find_log_for_model(model)
|
|
79
|
+
return nil unless model.respond_to?(:undoable?)
|
|
80
|
+
|
|
81
|
+
ActiveRecord::Undo::UndoLogItem
|
|
82
|
+
.joins(:undo_log)
|
|
83
|
+
.where(item: model)
|
|
84
|
+
.order(created_at: :desc, id: :desc)
|
|
85
|
+
.first&.undo_log
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def undo_engine_router
|
|
89
|
+
if respond_to?(:active_record_undo)
|
|
90
|
+
active_record_undo
|
|
91
|
+
elsif defined?(main_app) && main_app.respond_to?(:active_record_undo)
|
|
92
|
+
main_app.active_record_undo
|
|
93
|
+
elsif respond_to?(:restore_log_path)
|
|
94
|
+
self
|
|
95
|
+
else
|
|
96
|
+
ActiveRecord::Undo::Engine.routes.url_helpers
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/active_record/undo.rb
CHANGED
|
@@ -12,7 +12,9 @@ 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'
|
|
16
18
|
require_relative 'undo/model_extension'
|
|
17
19
|
require_relative 'undo/undo_log'
|
|
18
20
|
require_relative 'undo/undo_log_item'
|
|
@@ -85,6 +87,17 @@ 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
|
+
|
|
88
101
|
private
|
|
89
102
|
|
|
90
103
|
def eager_load_rails!
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Saurabh Sharma
|
|
@@ -136,6 +136,10 @@ files:
|
|
|
136
136
|
- LICENSE.txt
|
|
137
137
|
- README.md
|
|
138
138
|
- Rakefile
|
|
139
|
+
- app/controllers/active_record/undo/application_controller.rb
|
|
140
|
+
- app/controllers/active_record/undo/logs_controller.rb
|
|
141
|
+
- app/controllers/active_record/undo/restores_controller.rb
|
|
142
|
+
- config/routes.rb
|
|
139
143
|
- db/migrate/20260808000000_create_active_record_undo_tables.rb
|
|
140
144
|
- db/migrate/20260821000000_add_tenant_and_user_attribution_to_undo_logs.rb
|
|
141
145
|
- lib/active_record/undo.rb
|
|
@@ -150,10 +154,12 @@ files:
|
|
|
150
154
|
- lib/active_record/undo/purge_job.rb
|
|
151
155
|
- lib/active_record/undo/purger.rb
|
|
152
156
|
- lib/active_record/undo/purger/reflection_helper.rb
|
|
157
|
+
- lib/active_record/undo/safe_redirect.rb
|
|
153
158
|
- lib/active_record/undo/tasks/purge.rake
|
|
154
159
|
- lib/active_record/undo/undo_log.rb
|
|
155
160
|
- lib/active_record/undo/undo_log_item.rb
|
|
156
161
|
- lib/active_record/undo/version.rb
|
|
162
|
+
- lib/active_record/undo/view_helpers.rb
|
|
157
163
|
- lib/tasks/active_record_undo_tasks.rake
|
|
158
164
|
- sig/active_record/undo.rbs
|
|
159
165
|
homepage: https://github.com/saurabh-activecode/active_record-undo
|