whodunit 0.1.0 → 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/.rubocop.yml +1 -1
- data/CHANGELOG.md +34 -6
- data/README.md +59 -7
- data/exe/whodunit +18 -0
- data/lib/whodunit/controller_methods.rb +2 -2
- data/lib/whodunit/generator.rb +99 -0
- data/lib/whodunit/migration_helpers.rb +94 -56
- data/lib/whodunit/railtie.rb +6 -0
- data/lib/whodunit/stampable.rb +115 -35
- data/lib/whodunit/table_definition_extension.rb +57 -0
- data/lib/whodunit/version.rb +1 -1
- data/lib/whodunit.rb +46 -4
- metadata +10 -8
- data/lib/whodunit/soft_delete_detector.rb +0 -119
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b45b1b2f4cfb91e98e2c4656c67305403108af39c5069127d91f0521599bc94
|
4
|
+
data.tar.gz: 56472d61e9f422643eefe7dca84b7f050fa8d91bb4274bb2354a4e01f04d5ee4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c959a0b51f441b28a855b95a537e5983592ed5eb7eef4cbf5eb42c1d4c120849a27ac4113c784f27defa6703939d6cb02a0c2ccc8bd24f0a0508673351407b97
|
7
|
+
data.tar.gz: a5ca68eebccf5794c9b65521a0c7a232f3178d7aacb6af89c9de3b3bc1365588a6e62a39c52c9821987aaa8735231d289fbc1f28fc7a95be391171d40afbfe31
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.2.0] - 2025-01-20
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- New `whodunit install` CLI command to generate configuration initializer
|
15
|
+
- Per-model configuration override capability via `whodunit_config` block
|
16
|
+
- Column enabling/disabling - set individual columns to `nil` to disable them
|
17
|
+
- Configuration validation to prevent disabling both creator and updater columns
|
18
|
+
- Comprehensive generator with Rails app detection and safety prompts
|
19
|
+
- Enhanced YARD documentation reflecting simplified architecture
|
20
|
+
|
21
|
+
### Changed
|
22
|
+
|
23
|
+
- **BREAKING**: `soft_delete_column` now defaults to `nil` instead of `:deleted_at`
|
24
|
+
- **BREAKING**: Removed automatic soft-delete detection - now purely configuration-based
|
25
|
+
- **BREAKING**: Simplified `being_soft_deleted?` logic to check only configured column
|
26
|
+
- Migration helpers now respect column enabling/disabling configuration
|
27
|
+
- Updated all documentation to reflect simplified, configuration-based approach
|
28
|
+
- Improved test infrastructure with better configuration isolation
|
29
|
+
|
30
|
+
### Removed
|
31
|
+
|
32
|
+
- **BREAKING**: `SoftDeleteDetector` class and all auto-detection logic
|
33
|
+
- **BREAKING**: `SOFT_DELETE_COLUMNS` constant and pattern-matching detection
|
34
|
+
- Complex database schema introspection for soft-delete detection
|
35
|
+
|
36
|
+
### Performance
|
37
|
+
|
38
|
+
- Eliminated expensive auto-detection queries during model initialization
|
39
|
+
- Reduced computational overhead by trusting user configuration
|
40
|
+
- Simplified callback and association setup based on explicit configuration
|
41
|
+
|
42
|
+
## [0.1.0] - 2025-01-15
|
43
|
+
|
10
44
|
### Added
|
11
45
|
|
12
46
|
- Initial release of Whodunit gem
|
@@ -36,9 +70,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
36
70
|
|
37
71
|
- Rails 7.2+
|
38
72
|
- Ruby 3.1+
|
39
|
-
|
40
|
-
## [0.1.0] - TBD
|
41
|
-
|
42
|
-
### Added
|
43
|
-
|
44
|
-
- Initial gem structure and basic functionality
|
data/README.md
CHANGED
@@ -106,14 +106,25 @@ user.updater # => User who last updated this record
|
|
106
106
|
user.deleter # => User who deleted this record (if soft-delete enabled)
|
107
107
|
```
|
108
108
|
|
109
|
-
##
|
109
|
+
## Soft-Delete Support
|
110
110
|
|
111
|
-
Whodunit automatically
|
111
|
+
Whodunit automatically tracks who deleted records when using soft-delete. Simply configure your soft-delete column:
|
112
112
|
|
113
|
-
|
114
|
-
-
|
115
|
-
|
116
|
-
|
113
|
+
```ruby
|
114
|
+
# Most common soft-delete column (default)
|
115
|
+
config.soft_delete_column = :deleted_at
|
116
|
+
|
117
|
+
# For Discard gem users
|
118
|
+
config.soft_delete_column = :discarded_at
|
119
|
+
|
120
|
+
# For custom implementations
|
121
|
+
config.soft_delete_column = :archived_at
|
122
|
+
|
123
|
+
# Disable soft-delete support
|
124
|
+
config.soft_delete_column = nil
|
125
|
+
```
|
126
|
+
|
127
|
+
When configured, Whodunit will automatically add the `deleter_id` column to migrations when the soft-delete column is detected in your table.
|
117
128
|
|
118
129
|
## Configuration
|
119
130
|
|
@@ -124,7 +135,8 @@ Whodunit.configure do |config|
|
|
124
135
|
config.creator_column = :created_by_id # Default: :creator_id
|
125
136
|
config.updater_column = :updated_by_id # Default: :updater_id
|
126
137
|
config.deleter_column = :deleted_by_id # Default: :deleter_id
|
127
|
-
config.
|
138
|
+
config.soft_delete_column = :discarded_at # Default: nil
|
139
|
+
config.auto_inject_whodunit_stamps = false # Default: true
|
128
140
|
|
129
141
|
# Column data type configuration
|
130
142
|
config.column_data_type = :integer # Default: :bigint (applies to all columns)
|
@@ -142,6 +154,46 @@ By default, all stamp columns use `:bigint` data type. You can customize this in
|
|
142
154
|
- **Individual**: Set specific column types to override the global default
|
143
155
|
- **Per-migration**: Override types on a per-migration basis (see Migration Helpers)
|
144
156
|
|
157
|
+
### Automatic Injection (Rails Integration)
|
158
|
+
|
159
|
+
By default, Whodunit automatically adds stamp columns to your migrations, just like how Rails automatically handles `timestamps`:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
# Automatic injection is enabled by default!
|
163
|
+
# Your migrations automatically get whodunit stamps:
|
164
|
+
class CreatePosts < ActiveRecord::Migration[8.0]
|
165
|
+
def change
|
166
|
+
create_table :posts do |t|
|
167
|
+
t.string :title
|
168
|
+
t.text :body
|
169
|
+
t.timestamps
|
170
|
+
# t.whodunit_stamps automatically added after t.timestamps!
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Disable automatic injection globally:
|
176
|
+
Whodunit.configure do |config|
|
177
|
+
config.auto_inject_whodunit_stamps = false
|
178
|
+
end
|
179
|
+
|
180
|
+
# Skip auto-injection for specific tables:
|
181
|
+
create_table :system_logs do |t|
|
182
|
+
t.string :message
|
183
|
+
t.timestamps skip_whodunit_stamps: true
|
184
|
+
end
|
185
|
+
|
186
|
+
# Or add manually if you want specific options:
|
187
|
+
create_table :posts do |t|
|
188
|
+
t.string :title
|
189
|
+
t.whodunit_stamps include_deleter: true # Manual override
|
190
|
+
t.timestamps
|
191
|
+
# No auto-injection since already added manually
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
195
|
+
This feature respects soft-delete auto-detection and includes the deleter column when appropriate.
|
196
|
+
|
145
197
|
## Manual User Setting
|
146
198
|
|
147
199
|
For background jobs, tests, or special scenarios:
|
data/exe/whodunit
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "../lib/whodunit/generator"
|
5
|
+
|
6
|
+
# Parse command line arguments
|
7
|
+
command = ARGV[0]
|
8
|
+
|
9
|
+
case command
|
10
|
+
when "install"
|
11
|
+
Whodunit::Generator.install_initializer
|
12
|
+
when "help", "--help", "-h", nil
|
13
|
+
puts Whodunit::Generator.help_message
|
14
|
+
else
|
15
|
+
puts "Unknown command: #{command}"
|
16
|
+
puts Whodunit::Generator.help_message
|
17
|
+
exit 1
|
18
|
+
end
|
@@ -147,8 +147,8 @@ module Whodunit
|
|
147
147
|
# without_whodunit_user do
|
148
148
|
# Post.bulk_import(data) # No creator_id will be set
|
149
149
|
# end
|
150
|
-
def without_whodunit_user(&
|
151
|
-
with_whodunit_user(nil, &
|
150
|
+
def without_whodunit_user(&)
|
151
|
+
with_whodunit_user(nil, &)
|
152
152
|
end
|
153
153
|
|
154
154
|
class_methods do
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module Whodunit
|
6
|
+
# Generator for creating Whodunit configuration files
|
7
|
+
class Generator
|
8
|
+
INITIALIZER_CONTENT = <<~RUBY
|
9
|
+
# frozen_string_literal: true
|
10
|
+
|
11
|
+
# Whodunit configuration
|
12
|
+
# This file was generated by `whodunit install` command.
|
13
|
+
# Uncomment and modify the options you want to customize.
|
14
|
+
|
15
|
+
# Whodunit.configure do |config|
|
16
|
+
# config.user_class = 'Account' # Default: 'User'
|
17
|
+
# config.creator_column = :created_by_id # Default: :creator_id
|
18
|
+
# config.updater_column = :updated_by_id # Default: :updater_id
|
19
|
+
# config.deleter_column = :deleted_by_id # Default: :deleter_id
|
20
|
+
# config.soft_delete_column = :discarded_at # Default: nil
|
21
|
+
# config.auto_inject_whodunit_stamps = false # Default: true
|
22
|
+
#
|
23
|
+
# # Column data type configuration
|
24
|
+
# config.column_data_type = :integer # Default: :bigint (applies to all columns)
|
25
|
+
# config.creator_column_type = :string # Default: nil (uses column_data_type)
|
26
|
+
# config.updater_column_type = :uuid # Default: nil (uses column_data_type)
|
27
|
+
# config.deleter_column_type = :integer # Default: nil (uses column_data_type)
|
28
|
+
# end
|
29
|
+
RUBY
|
30
|
+
|
31
|
+
def self.install_initializer
|
32
|
+
config_dir = "config/initializers"
|
33
|
+
config_file = File.join(config_dir, "whodunit.rb")
|
34
|
+
|
35
|
+
validate_rails_application!
|
36
|
+
ensure_config_directory_exists!(config_dir)
|
37
|
+
handle_existing_file!(config_file)
|
38
|
+
create_initializer_file!(config_file)
|
39
|
+
show_success_message(config_file)
|
40
|
+
end
|
41
|
+
|
42
|
+
private_class_method def self.validate_rails_application!
|
43
|
+
return if File.exist?("config/application.rb")
|
44
|
+
|
45
|
+
puts "❌ Error: This doesn't appear to be a Rails application."
|
46
|
+
puts " Make sure you're in the root directory of your Rails app."
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
|
50
|
+
private_class_method def self.ensure_config_directory_exists!(config_dir)
|
51
|
+
return if Dir.exist?(config_dir)
|
52
|
+
|
53
|
+
puts "📁 Creating #{config_dir} directory..."
|
54
|
+
FileUtils.mkdir_p(config_dir)
|
55
|
+
end
|
56
|
+
|
57
|
+
private_class_method def self.handle_existing_file!(config_file)
|
58
|
+
return unless File.exist?(config_file)
|
59
|
+
|
60
|
+
puts "⚠️ #{config_file} already exists!"
|
61
|
+
print " Do you want to overwrite it? (y/N): "
|
62
|
+
response = $stdin.gets.chomp.downcase
|
63
|
+
return if %w[y yes].include?(response)
|
64
|
+
|
65
|
+
puts " Cancelled."
|
66
|
+
exit 0
|
67
|
+
end
|
68
|
+
|
69
|
+
private_class_method def self.create_initializer_file!(config_file)
|
70
|
+
File.write(config_file, INITIALIZER_CONTENT)
|
71
|
+
puts "✅ Generated #{config_file}"
|
72
|
+
end
|
73
|
+
|
74
|
+
private_class_method def self.show_success_message(config_file)
|
75
|
+
puts ""
|
76
|
+
puts "📝 Next steps:"
|
77
|
+
puts " 1. Edit #{config_file} to customize your configuration"
|
78
|
+
puts " 2. Uncomment the options you want to change"
|
79
|
+
puts " 3. Restart your Rails server to apply changes"
|
80
|
+
puts ""
|
81
|
+
puts "📖 For more information, see: https://github.com/kanutocd/whodunit?tab=readme-ov-file#configuration"
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.help_message
|
85
|
+
<<~HELP
|
86
|
+
Whodunit - Lightweight creator/updater/deleter tracking for ActiveRecord
|
87
|
+
|
88
|
+
Usage:
|
89
|
+
whodunit install Generate config/initializers/whodunit.rb
|
90
|
+
whodunit help Show this help message
|
91
|
+
|
92
|
+
Examples:
|
93
|
+
whodunit install # Creates config/initializers/whodunit.rb with sample configuration
|
94
|
+
|
95
|
+
For more information, visit: https://github.com/kanutocd/whodunit
|
96
|
+
HELP
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -4,15 +4,20 @@ module Whodunit
|
|
4
4
|
# Database migration helpers for adding whodunit stamp columns.
|
5
5
|
#
|
6
6
|
# This module provides convenient methods for adding creator/updater/deleter
|
7
|
-
# tracking columns to database tables.
|
8
|
-
#
|
7
|
+
# tracking columns to database tables based on configuration. Uses the configured
|
8
|
+
# column names and data types, and adds appropriate indexes for performance.
|
9
|
+
#
|
10
|
+
# The helpers respect column enabling/disabling configuration:
|
11
|
+
# - Only adds columns that are enabled (not nil) in configuration
|
12
|
+
# - Uses configured column names and data types
|
13
|
+
# - Deleter column inclusion based on soft_delete_column configuration
|
9
14
|
#
|
10
15
|
# @example Add stamps to existing table
|
11
16
|
# class AddWhodunitStampsToPosts < ActiveRecord::Migration[7.0]
|
12
17
|
# def change
|
13
18
|
# add_whodunit_stamps :posts
|
14
|
-
# # Adds creator_id, updater_id
|
15
|
-
# # Adds deleter_id if
|
19
|
+
# # Adds enabled columns: creator_id, updater_id
|
20
|
+
# # Adds deleter_id if soft_delete_column is configured
|
16
21
|
# end
|
17
22
|
# end
|
18
23
|
#
|
@@ -28,38 +33,57 @@ module Whodunit
|
|
28
33
|
# end
|
29
34
|
# end
|
30
35
|
#
|
31
|
-
# @example Custom data types
|
36
|
+
# @example Custom data types and explicit deleter inclusion
|
32
37
|
# add_whodunit_stamps :posts,
|
33
38
|
# creator_type: :string,
|
34
39
|
# updater_type: :uuid,
|
35
40
|
# include_deleter: true
|
36
41
|
#
|
42
|
+
# @example With custom column configuration
|
43
|
+
# # In config/initializers/whodunit.rb:
|
44
|
+
# Whodunit.configure do |config|
|
45
|
+
# config.creator_column = :created_by_id
|
46
|
+
# config.deleter_column = nil # Disable deleter column
|
47
|
+
# config.soft_delete_column = :archived_at
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# # Migration will use custom column names and only add enabled columns
|
51
|
+
#
|
37
52
|
# @since 0.1.0
|
38
53
|
module MigrationHelpers
|
39
|
-
# Add creator/updater stamp columns to an existing table.
|
54
|
+
# Add creator/updater/deleter stamp columns to an existing table.
|
40
55
|
#
|
41
|
-
# This method adds the configured
|
42
|
-
#
|
56
|
+
# This method adds the configured whodunit columns to an existing table based on
|
57
|
+
# the current configuration. Only adds columns that are enabled (not nil).
|
58
|
+
# The deleter column is included based on soft_delete_column configuration when
|
59
|
+
# include_deleter is :auto, or explicitly when include_deleter is true.
|
43
60
|
# Indexes are automatically added for performance.
|
44
61
|
#
|
45
62
|
# @param table_name [Symbol] the table to add stamps to
|
46
|
-
# @param include_deleter [Symbol, Boolean] :auto to
|
63
|
+
# @param include_deleter [Symbol, Boolean] :auto to check soft_delete_column configuration,
|
47
64
|
# true to force inclusion, false to exclude
|
48
65
|
# @param creator_type [Symbol, nil] data type for creator column (defaults to configured type)
|
49
66
|
# @param updater_type [Symbol, nil] data type for updater column (defaults to configured type)
|
50
67
|
# @param deleter_type [Symbol, nil] data type for deleter column (defaults to configured type)
|
51
68
|
# @return [void]
|
52
|
-
# @example Basic usage
|
69
|
+
# @example Basic usage (uses configuration)
|
53
70
|
# add_whodunit_stamps :posts
|
54
71
|
# @example With custom types
|
55
72
|
# add_whodunit_stamps :posts, creator_type: :string, updater_type: :uuid
|
56
|
-
# @example Force deleter column
|
73
|
+
# @example Force deleter column inclusion
|
57
74
|
# add_whodunit_stamps :posts, include_deleter: true
|
75
|
+
# @example Exclude deleter column
|
76
|
+
# add_whodunit_stamps :posts, include_deleter: false
|
58
77
|
def add_whodunit_stamps(table_name, include_deleter: :auto, creator_type: nil, updater_type: nil, deleter_type: nil)
|
59
|
-
|
60
|
-
|
78
|
+
if Whodunit.creator_enabled?
|
79
|
+
add_column table_name, Whodunit.creator_column, creator_type || Whodunit.creator_data_type, null: true
|
80
|
+
end
|
81
|
+
|
82
|
+
if Whodunit.updater_enabled?
|
83
|
+
add_column table_name, Whodunit.updater_column, updater_type || Whodunit.updater_data_type, null: true
|
84
|
+
end
|
61
85
|
|
62
|
-
if should_include_deleter?(
|
86
|
+
if should_include_deleter?(include_deleter)
|
63
87
|
add_column table_name, Whodunit.deleter_column, deleter_type || Whodunit.deleter_data_type, null: true
|
64
88
|
end
|
65
89
|
|
@@ -69,22 +93,30 @@ module Whodunit
|
|
69
93
|
# Remove stamp columns from an existing table.
|
70
94
|
#
|
71
95
|
# This method removes the configured creator, updater, and optionally deleter
|
72
|
-
# columns from an existing table. Only removes columns that
|
96
|
+
# columns from an existing table. Only removes columns that are enabled in
|
97
|
+
# configuration and actually exist in the database.
|
73
98
|
#
|
74
99
|
# @param table_name [Symbol] the table to remove stamps from
|
75
|
-
# @param include_deleter [Symbol, Boolean] :auto to
|
100
|
+
# @param include_deleter [Symbol, Boolean] :auto to check soft_delete_column configuration,
|
76
101
|
# true to force removal, false to exclude
|
77
102
|
# @param _options [Hash] additional options (reserved for future use)
|
78
103
|
# @return [void]
|
79
|
-
# @example Basic usage
|
104
|
+
# @example Basic usage (uses configuration)
|
80
105
|
# remove_whodunit_stamps :posts
|
81
106
|
# @example Force deleter removal
|
82
107
|
# remove_whodunit_stamps :posts, include_deleter: true
|
108
|
+
# @example Exclude deleter removal
|
109
|
+
# remove_whodunit_stamps :posts, include_deleter: false
|
83
110
|
def remove_whodunit_stamps(table_name, include_deleter: :auto, **_options)
|
84
|
-
|
85
|
-
|
111
|
+
if Whodunit.creator_enabled? && column_exists?(table_name, Whodunit.creator_column)
|
112
|
+
remove_column table_name, Whodunit.creator_column
|
113
|
+
end
|
114
|
+
|
115
|
+
if Whodunit.updater_enabled? && column_exists?(table_name, Whodunit.updater_column)
|
116
|
+
remove_column table_name, Whodunit.updater_column
|
117
|
+
end
|
86
118
|
|
87
|
-
if should_include_deleter?(
|
119
|
+
if should_include_deleter?(include_deleter) &&
|
88
120
|
column_exists?(table_name, Whodunit.deleter_column)
|
89
121
|
remove_column table_name, Whodunit.deleter_column
|
90
122
|
end
|
@@ -96,15 +128,19 @@ module Whodunit
|
|
96
128
|
# 1. Inside a create_table block (pass table definition as first argument)
|
97
129
|
# 2. As a standalone method in migrations (attempts to infer table name)
|
98
130
|
#
|
131
|
+
# Only adds columns that are enabled in configuration. For new tables,
|
132
|
+
# deleter column is only added when explicitly requested (include_deleter: true)
|
133
|
+
# to be conservative.
|
134
|
+
#
|
99
135
|
# @param table_def [ActiveRecord::ConnectionAdapters::TableDefinition, nil]
|
100
136
|
# the table definition (when used in create_table block) or nil
|
101
|
-
# @param include_deleter [Symbol, Boolean] :auto to
|
137
|
+
# @param include_deleter [Symbol, Boolean] :auto to check soft_delete_column configuration,
|
102
138
|
# true to force inclusion, false to exclude
|
103
139
|
# @param creator_type [Symbol, nil] data type for creator column (defaults to configured type)
|
104
140
|
# @param updater_type [Symbol, nil] data type for updater column (defaults to configured type)
|
105
141
|
# @param deleter_type [Symbol, nil] data type for deleter column (defaults to configured type)
|
106
142
|
# @return [void]
|
107
|
-
# @example In create_table block
|
143
|
+
# @example In create_table block (uses configuration)
|
108
144
|
# create_table :posts do |t|
|
109
145
|
# t.string :title
|
110
146
|
# t.whodunit_stamps
|
@@ -113,6 +149,10 @@ module Whodunit
|
|
113
149
|
# def change
|
114
150
|
# whodunit_stamps # Adds to inferred table
|
115
151
|
# end
|
152
|
+
# @example Force deleter column in new table
|
153
|
+
# create_table :posts do |t|
|
154
|
+
# t.whodunit_stamps include_deleter: true
|
155
|
+
# end
|
116
156
|
def whodunit_stamps(table_def = nil, include_deleter: :auto, creator_type: nil, updater_type: nil,
|
117
157
|
deleter_type: nil)
|
118
158
|
if table_def.nil?
|
@@ -135,8 +175,13 @@ module Whodunit
|
|
135
175
|
|
136
176
|
# Handle stamps when called within create_table block
|
137
177
|
def handle_table_definition_stamps(table_def, include_deleter, creator_type, updater_type, deleter_type)
|
138
|
-
|
139
|
-
|
178
|
+
if Whodunit.creator_enabled?
|
179
|
+
table_def.column Whodunit.creator_column, creator_type || Whodunit.creator_data_type, null: true
|
180
|
+
end
|
181
|
+
|
182
|
+
if Whodunit.updater_enabled?
|
183
|
+
table_def.column Whodunit.updater_column, updater_type || Whodunit.updater_data_type, null: true
|
184
|
+
end
|
140
185
|
|
141
186
|
if should_include_deleter_for_new_table?(include_deleter)
|
142
187
|
table_def.column Whodunit.deleter_column, deleter_type || Whodunit.deleter_data_type, null: true
|
@@ -145,46 +190,37 @@ module Whodunit
|
|
145
190
|
add_whodunit_indexes_for_create_table(table_def, include_deleter)
|
146
191
|
end
|
147
192
|
|
148
|
-
# Determine if deleter column should be included
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
true
|
155
|
-
else
|
156
|
-
false
|
157
|
-
end
|
193
|
+
# Determine if deleter column should be included based on configuration.
|
194
|
+
# When :auto, checks if soft_delete_column is configured (not nil).
|
195
|
+
# @param include_deleter [Symbol, Boolean] the inclusion preference
|
196
|
+
# @return [Boolean] true if deleter column should be included
|
197
|
+
def should_include_deleter?(include_deleter)
|
198
|
+
include_deleter == :auto ? soft_delete_enabled? : include_deleter.eql?(true)
|
158
199
|
end
|
159
200
|
|
160
|
-
# For new tables, be more conservative
|
201
|
+
# For new tables, be more conservative - only include deleter when explicitly requested.
|
202
|
+
# This prevents adding deleter columns to tables that may not need them.
|
203
|
+
# @param include_deleter [Symbol, Boolean] the inclusion preference
|
204
|
+
# @return [Boolean] true only when explicitly requested (true)
|
161
205
|
def should_include_deleter_for_new_table?(include_deleter)
|
162
|
-
|
163
|
-
when true
|
164
|
-
true
|
165
|
-
else
|
166
|
-
false # Don't auto-add for new tables, let user be explicit
|
167
|
-
end
|
206
|
+
include_deleter.eql?(true)
|
168
207
|
end
|
169
208
|
|
170
|
-
#
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
soft_deleted_at soft_destroyed_at removed_at
|
177
|
-
]
|
178
|
-
|
179
|
-
soft_delete_columns.any? { |col| column_exists?(table_name, col) || column_exists?(table_name, col.to_sym) }
|
209
|
+
# Check if soft-delete is enabled based on configuration.
|
210
|
+
# Uses the configured soft_delete_column - if it's set (not nil), soft delete is enabled.
|
211
|
+
# @return [Boolean] true if soft_delete_column is configured
|
212
|
+
def soft_delete_enabled?
|
213
|
+
# Simple configuration-based check - trust the user's configuration
|
214
|
+
Whodunit.soft_delete_enabled?
|
180
215
|
end
|
181
216
|
|
182
217
|
# Add indexes for performance
|
183
218
|
def add_whodunit_indexes(table_name, include_deleter)
|
184
|
-
add_index table_name, Whodunit.creator_column, name: "index_#{table_name}_on_creator"
|
185
|
-
add_index table_name, Whodunit.updater_column, name: "index_#{table_name}_on_updater"
|
219
|
+
add_index table_name, Whodunit.creator_column, name: "index_#{table_name}_on_creator" if Whodunit.creator_enabled?
|
186
220
|
|
187
|
-
|
221
|
+
add_index table_name, Whodunit.updater_column, name: "index_#{table_name}_on_updater" if Whodunit.updater_enabled?
|
222
|
+
|
223
|
+
return unless should_include_deleter?(include_deleter)
|
188
224
|
|
189
225
|
add_index table_name, Whodunit.deleter_column, name: "index_#{table_name}_on_deleter"
|
190
226
|
end
|
@@ -195,8 +231,10 @@ module Whodunit
|
|
195
231
|
return unless table_def.respond_to?(:index)
|
196
232
|
|
197
233
|
table_name = table_def.respond_to?(:name) ? table_def.name : "table"
|
198
|
-
|
199
|
-
table_def.index Whodunit.
|
234
|
+
|
235
|
+
table_def.index Whodunit.creator_column, name: "index_#{table_name}_on_creator" if Whodunit.creator_enabled?
|
236
|
+
|
237
|
+
table_def.index Whodunit.updater_column, name: "index_#{table_name}_on_updater" if Whodunit.updater_enabled?
|
200
238
|
|
201
239
|
return unless should_include_deleter_for_new_table?(include_deleter)
|
202
240
|
|
data/lib/whodunit/railtie.rb
CHANGED
@@ -24,11 +24,17 @@ module Whodunit
|
|
24
24
|
#
|
25
25
|
# This initializer adds the MigrationHelpers module to ActiveRecord,
|
26
26
|
# making methods like add_whodunit_stamps available in migrations.
|
27
|
+
# It also extends TableDefinition for automatic whodunit_stamps injection.
|
27
28
|
#
|
28
29
|
# @api private
|
29
30
|
initializer "whodunit.extend_active_record" do |_app|
|
30
31
|
ActiveSupport.on_load(:active_record) do
|
31
32
|
extend Whodunit::MigrationHelpers
|
33
|
+
|
34
|
+
# Extend TableDefinition for automatic whodunit_stamps injection
|
35
|
+
ActiveRecord::ConnectionAdapters::TableDefinition.include(
|
36
|
+
Whodunit::TableDefinitionExtension
|
37
|
+
)
|
32
38
|
end
|
33
39
|
end
|
34
40
|
|
data/lib/whodunit/stampable.rb
CHANGED
@@ -36,78 +36,121 @@ module Whodunit
|
|
36
36
|
# end
|
37
37
|
#
|
38
38
|
# @since 0.1.0
|
39
|
+
# rubocop:disable Metrics/ModuleLength
|
39
40
|
module Stampable
|
40
41
|
extend ActiveSupport::Concern
|
41
42
|
|
42
43
|
included do
|
43
|
-
# Set up callbacks
|
44
|
+
# Set up callbacks - the if conditions will check per-model settings at runtime
|
44
45
|
before_create :set_whodunit_creator, if: :creator_column?
|
45
46
|
before_update :set_whodunit_updater, if: :updater_column?
|
46
47
|
|
47
|
-
#
|
48
|
-
if
|
49
|
-
|
50
|
-
|
51
|
-
end
|
48
|
+
# Add deleter tracking for both hard and soft deletes
|
49
|
+
# The if conditions will check per-model settings at runtime
|
50
|
+
before_destroy :set_whodunit_deleter, if: :deleter_column?
|
51
|
+
before_update :set_whodunit_deleter, if: :being_soft_deleted?
|
52
52
|
|
53
53
|
# Set up associations - call on the class
|
54
54
|
setup_whodunit_associations
|
55
55
|
end
|
56
56
|
|
57
57
|
class_methods do # rubocop:disable Metrics/BlockLength
|
58
|
+
# Per-model configuration storage
|
59
|
+
attr_accessor :whodunit_config_overrides
|
60
|
+
|
61
|
+
# Configure per-model settings
|
62
|
+
def whodunit_config
|
63
|
+
@whodunit_config_overrides ||= {}
|
64
|
+
yield @whodunit_config_overrides if block_given?
|
65
|
+
@whodunit_config_overrides
|
66
|
+
end
|
67
|
+
|
58
68
|
def soft_delete_enabled?
|
59
|
-
@soft_delete_enabled ||=
|
69
|
+
@soft_delete_enabled ||= model_soft_delete_enabled?
|
60
70
|
end
|
61
71
|
|
62
72
|
def enable_whodunit_deleter!
|
63
73
|
before_destroy :set_whodunit_deleter, if: :deleter_column?
|
74
|
+
before_update :set_whodunit_deleter, if: :being_soft_deleted?
|
64
75
|
setup_deleter_association
|
65
76
|
@soft_delete_enabled = true
|
66
77
|
end
|
67
78
|
|
68
79
|
def disable_whodunit_deleter!
|
69
80
|
skip_callback :destroy, :before, :set_whodunit_deleter
|
81
|
+
skip_callback :update, :before, :set_whodunit_deleter
|
70
82
|
@soft_delete_enabled = false
|
71
83
|
end
|
72
84
|
|
85
|
+
# Get effective configuration value (model override or global default)
|
86
|
+
def whodunit_setting(key)
|
87
|
+
return @whodunit_config_overrides[key] if @whodunit_config_overrides&.key?(key)
|
88
|
+
|
89
|
+
Whodunit.public_send(key)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Check if creator column is enabled for this model
|
93
|
+
def model_creator_enabled?
|
94
|
+
creator_column = whodunit_setting(:creator_column)
|
95
|
+
!creator_column.nil?
|
96
|
+
end
|
97
|
+
|
98
|
+
# Check if updater column is enabled for this model
|
99
|
+
def model_updater_enabled?
|
100
|
+
updater_column = whodunit_setting(:updater_column)
|
101
|
+
!updater_column.nil?
|
102
|
+
end
|
103
|
+
|
104
|
+
# Check if deleter column is enabled for this model
|
105
|
+
def model_deleter_enabled?
|
106
|
+
deleter_column = whodunit_setting(:deleter_column)
|
107
|
+
!deleter_column.nil?
|
108
|
+
end
|
109
|
+
|
110
|
+
# Check if soft delete is enabled for this model
|
111
|
+
def model_soft_delete_enabled?
|
112
|
+
soft_delete_column = whodunit_setting(:soft_delete_column)
|
113
|
+
!soft_delete_column.nil?
|
114
|
+
end
|
115
|
+
|
73
116
|
private
|
74
117
|
|
75
118
|
def setup_whodunit_associations
|
76
|
-
setup_creator_association if creator_column_exists?
|
77
|
-
setup_updater_association if updater_column_exists?
|
78
|
-
setup_deleter_association if deleter_column_exists? && soft_delete_enabled?
|
119
|
+
setup_creator_association if creator_column_exists? && model_creator_enabled?
|
120
|
+
setup_updater_association if updater_column_exists? && model_updater_enabled?
|
121
|
+
setup_deleter_association if deleter_column_exists? && model_deleter_enabled? && soft_delete_enabled?
|
79
122
|
end
|
80
123
|
|
81
124
|
def creator_column_exists?
|
82
|
-
column_names.include?(
|
125
|
+
model_creator_enabled? && column_names.include?(whodunit_setting(:creator_column).to_s)
|
83
126
|
end
|
84
127
|
|
85
128
|
def updater_column_exists?
|
86
|
-
column_names.include?(
|
129
|
+
model_updater_enabled? && column_names.include?(whodunit_setting(:updater_column).to_s)
|
87
130
|
end
|
88
131
|
|
89
132
|
def deleter_column_exists?
|
90
|
-
column_names.include?(
|
133
|
+
model_deleter_enabled? && column_names.include?(whodunit_setting(:deleter_column).to_s)
|
91
134
|
end
|
92
135
|
|
93
136
|
def setup_creator_association
|
94
137
|
belongs_to :creator,
|
95
|
-
class_name:
|
96
|
-
foreign_key:
|
138
|
+
class_name: whodunit_setting(:user_class).to_s,
|
139
|
+
foreign_key: whodunit_setting(:creator_column),
|
97
140
|
optional: true
|
98
141
|
end
|
99
142
|
|
100
143
|
def setup_updater_association
|
101
144
|
belongs_to :updater,
|
102
|
-
class_name:
|
103
|
-
foreign_key:
|
145
|
+
class_name: whodunit_setting(:user_class).to_s,
|
146
|
+
foreign_key: whodunit_setting(:updater_column),
|
104
147
|
optional: true
|
105
148
|
end
|
106
149
|
|
107
150
|
def setup_deleter_association
|
108
151
|
belongs_to :deleter,
|
109
|
-
class_name:
|
110
|
-
foreign_key:
|
152
|
+
class_name: whodunit_setting(:user_class).to_s,
|
153
|
+
foreign_key: whodunit_setting(:deleter_column),
|
111
154
|
optional: true
|
112
155
|
end
|
113
156
|
end
|
@@ -124,61 +167,98 @@ module Whodunit
|
|
124
167
|
def set_whodunit_creator
|
125
168
|
return unless Whodunit::Current.user_id
|
126
169
|
|
127
|
-
self[
|
170
|
+
self[self.class.whodunit_setting(:creator_column)] = Whodunit::Current.user_id
|
128
171
|
end
|
129
172
|
|
130
173
|
# Set the updater ID when a record is updated.
|
131
174
|
#
|
132
175
|
# This method is automatically called before_update if the model has an updater column.
|
133
176
|
# It sets the updater_id to the current user from Whodunit::Current.
|
134
|
-
# Does not run on new records (creation).
|
177
|
+
# Does not run on new records (creation) or during soft-delete operations.
|
135
178
|
#
|
136
179
|
# @return [void]
|
137
180
|
# @api private
|
138
181
|
def set_whodunit_updater
|
139
182
|
return unless Whodunit::Current.user_id
|
183
|
+
|
140
184
|
return if new_record? # Don't set updater on creation
|
141
185
|
|
142
|
-
|
186
|
+
return if being_soft_deleted? # Don't set updater during soft-delete
|
187
|
+
|
188
|
+
self[self.class.whodunit_setting(:updater_column)] = Whodunit::Current.user_id
|
143
189
|
end
|
144
190
|
|
145
|
-
# Set the deleter ID when a record is destroyed.
|
191
|
+
# Set the deleter ID when a record is destroyed or soft-deleted.
|
192
|
+
#
|
193
|
+
# This method is automatically called in two scenarios:
|
194
|
+
# 1. before_destroy for hard deletes (if deleter column exists)
|
195
|
+
# 2. before_update for soft-deletes (when being_soft_deleted? returns true)
|
146
196
|
#
|
147
|
-
#
|
148
|
-
# and soft-delete is enabled. It sets the deleter_id to the current user from Whodunit::Current.
|
197
|
+
# It sets the deleter_id to the current user from Whodunit::Current.
|
149
198
|
#
|
150
199
|
# @return [void]
|
151
200
|
# @api private
|
152
201
|
def set_whodunit_deleter
|
153
202
|
return unless Whodunit::Current.user_id
|
154
203
|
|
155
|
-
self[
|
204
|
+
self[self.class.whodunit_setting(:deleter_column)] = Whodunit::Current.user_id
|
156
205
|
end
|
157
206
|
|
158
207
|
# @!group Column Presence Checks
|
159
208
|
|
160
|
-
# Check if the model has a creator column.
|
209
|
+
# Check if the model has a creator column and it's enabled.
|
161
210
|
#
|
162
|
-
# @return [Boolean] true if the creator column exists
|
211
|
+
# @return [Boolean] true if the creator column exists and is enabled
|
163
212
|
# @api private
|
164
213
|
def creator_column?
|
165
|
-
self.class.
|
214
|
+
return false unless self.class.model_creator_enabled?
|
215
|
+
|
216
|
+
column_name = self.class.whodunit_setting(:creator_column).to_s
|
217
|
+
self.class.column_names.include?(column_name)
|
166
218
|
end
|
167
219
|
|
168
|
-
# Check if the model has an updater column.
|
220
|
+
# Check if the model has an updater column and it's enabled.
|
169
221
|
#
|
170
|
-
# @return [Boolean] true if the updater column exists
|
222
|
+
# @return [Boolean] true if the updater column exists and is enabled
|
171
223
|
# @api private
|
172
224
|
def updater_column?
|
173
|
-
self.class.
|
225
|
+
return false unless self.class.model_updater_enabled?
|
226
|
+
|
227
|
+
column_name = self.class.whodunit_setting(:updater_column).to_s
|
228
|
+
self.class.column_names.include?(column_name)
|
174
229
|
end
|
175
230
|
|
176
|
-
# Check if the model has a deleter column.
|
231
|
+
# Check if the model has a deleter column and it's enabled.
|
177
232
|
#
|
178
|
-
# @return [Boolean] true if the deleter column exists
|
233
|
+
# @return [Boolean] true if the deleter column exists and is enabled
|
179
234
|
# @api private
|
180
235
|
def deleter_column?
|
181
|
-
self.class.
|
236
|
+
return false unless self.class.model_deleter_enabled?
|
237
|
+
|
238
|
+
column_name = self.class.whodunit_setting(:deleter_column).to_s
|
239
|
+
self.class.column_names.include?(column_name)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Check if the current update operation is a soft-delete.
|
243
|
+
#
|
244
|
+
# Uses ActiveRecord's dirty tracking to detect if any soft-delete columns
|
245
|
+
# are being changed from nil to a timestamp, which indicates a soft-delete operation.
|
246
|
+
#
|
247
|
+
# @return [Boolean] true if this update is setting a soft-delete timestamp
|
248
|
+
# @api private
|
249
|
+
def being_soft_deleted?
|
250
|
+
return false unless deleter_column?
|
251
|
+
return false unless Whodunit::Current.user_id
|
252
|
+
|
253
|
+
soft_delete_column = self.class.whodunit_setting(:soft_delete_column)
|
254
|
+
return false unless soft_delete_column
|
255
|
+
|
256
|
+
# Simple: just check the configured soft-delete column
|
257
|
+
column_name = soft_delete_column.to_s
|
258
|
+
attribute_changed?(column_name) &&
|
259
|
+
attribute_was(column_name).nil? &&
|
260
|
+
!send(column_name).nil?
|
182
261
|
end
|
183
262
|
end
|
263
|
+
# rubocop:enable Metrics/ModuleLength
|
184
264
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Whodunit
|
4
|
+
# Extension for ActiveRecord::ConnectionAdapters::TableDefinition to automatically
|
5
|
+
# inject whodunit_stamps when creating tables.
|
6
|
+
#
|
7
|
+
# This module monkey-patches the TableDefinition's column creation methods to
|
8
|
+
# automatically add whodunit stamp columns when auto-injection is enabled.
|
9
|
+
#
|
10
|
+
# @example Enabling auto-injection
|
11
|
+
# Whodunit.configure do |config|
|
12
|
+
# config.auto_inject_whodunit_stamps = true
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# # Now this migration will automatically include whodunit stamps:
|
16
|
+
# class CreatePosts < ActiveRecord::Migration[8.0]
|
17
|
+
# def change
|
18
|
+
# create_table :posts do |t|
|
19
|
+
# t.string :title
|
20
|
+
# t.text :body
|
21
|
+
# t.timestamps
|
22
|
+
# # t.whodunit_stamps automatically added at the end!
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @since 0.1.0
|
28
|
+
module TableDefinitionExtension
|
29
|
+
extend ActiveSupport::Concern
|
30
|
+
|
31
|
+
included do
|
32
|
+
# Track whether whodunit_stamps have been automatically added
|
33
|
+
attr_accessor :_whodunit_stamps_added
|
34
|
+
end
|
35
|
+
|
36
|
+
# Override timestamps to trigger automatic whodunit_stamps injection
|
37
|
+
def timestamps(**options)
|
38
|
+
result = super
|
39
|
+
|
40
|
+
# Auto-inject whodunit_stamps after timestamps if enabled and not already added
|
41
|
+
if Whodunit.auto_inject_whodunit_stamps &&
|
42
|
+
!@_whodunit_stamps_added &&
|
43
|
+
!options[:skip_whodunit_stamps]
|
44
|
+
whodunit_stamps(include_deleter: :auto)
|
45
|
+
@_whodunit_stamps_added = true
|
46
|
+
end
|
47
|
+
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
# Also override whodunit_stamps to track that they've been added
|
52
|
+
def whodunit_stamps(**options)
|
53
|
+
@_whodunit_stamps_added = true
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/whodunit/version.rb
CHANGED
data/lib/whodunit.rb
CHANGED
@@ -3,10 +3,10 @@
|
|
3
3
|
require "active_support/all"
|
4
4
|
require_relative "whodunit/version"
|
5
5
|
require_relative "whodunit/current"
|
6
|
-
require_relative "whodunit/soft_delete_detector"
|
7
6
|
require_relative "whodunit/stampable"
|
8
7
|
require_relative "whodunit/migration_helpers"
|
9
8
|
require_relative "whodunit/controller_methods"
|
9
|
+
require_relative "whodunit/table_definition_extension"
|
10
10
|
require_relative "whodunit/railtie"
|
11
11
|
|
12
12
|
# Lightweight creator/updater/deleter tracking for ActiveRecord models.
|
@@ -55,9 +55,15 @@ module Whodunit
|
|
55
55
|
# @return [Symbol] the deleter column name
|
56
56
|
mattr_accessor :deleter_column, default: :deleter_id
|
57
57
|
|
58
|
-
#
|
59
|
-
#
|
60
|
-
|
58
|
+
# The column name used for soft-delete timestamps (default: nil)
|
59
|
+
# Set to a column name to enable soft-delete support (e.g., :deleted_at, :discarded_at)
|
60
|
+
# Set to nil to disable soft-delete support entirely
|
61
|
+
# @return [Symbol, nil] the soft-delete column name
|
62
|
+
mattr_accessor :soft_delete_column, default: nil
|
63
|
+
|
64
|
+
# Whether to automatically add whodunit_stamps to create_table migrations (default: true)
|
65
|
+
# @return [Boolean] auto-injection setting
|
66
|
+
mattr_accessor :auto_inject_whodunit_stamps, default: true
|
61
67
|
|
62
68
|
# @!group Data Type Configuration
|
63
69
|
|
@@ -88,8 +94,10 @@ module Whodunit
|
|
88
94
|
#
|
89
95
|
# @yield [self] configuration block
|
90
96
|
# @return [void]
|
97
|
+
# @raise [Whodunit::Error] if both creator_column and updater_column are set to nil
|
91
98
|
def self.configure
|
92
99
|
yield self
|
100
|
+
validate_column_configuration!
|
93
101
|
end
|
94
102
|
|
95
103
|
# Get the user class name as a string
|
@@ -118,4 +126,38 @@ module Whodunit
|
|
118
126
|
def self.deleter_data_type
|
119
127
|
deleter_column_type || column_data_type
|
120
128
|
end
|
129
|
+
|
130
|
+
# Check if soft-delete is enabled
|
131
|
+
# @return [Boolean] true if soft-delete is configured (soft_delete_column is not nil)
|
132
|
+
def self.soft_delete_enabled?
|
133
|
+
!soft_delete_column.nil?
|
134
|
+
end
|
135
|
+
|
136
|
+
# Check if creator column is enabled
|
137
|
+
# @return [Boolean] true if creator_column is not nil
|
138
|
+
def self.creator_enabled?
|
139
|
+
!creator_column.nil?
|
140
|
+
end
|
141
|
+
|
142
|
+
# Check if updater column is enabled
|
143
|
+
# @return [Boolean] true if updater_column is not nil
|
144
|
+
def self.updater_enabled?
|
145
|
+
!updater_column.nil?
|
146
|
+
end
|
147
|
+
|
148
|
+
# Check if deleter column is enabled
|
149
|
+
# @return [Boolean] true if deleter_column is not nil
|
150
|
+
def self.deleter_enabled?
|
151
|
+
!deleter_column.nil?
|
152
|
+
end
|
153
|
+
|
154
|
+
# Validate that column configuration is valid
|
155
|
+
# @raise [Whodunit::Error] if both creator_column and updater_column are nil
|
156
|
+
def self.validate_column_configuration!
|
157
|
+
return if creator_enabled? || updater_enabled?
|
158
|
+
|
159
|
+
raise Whodunit::Error,
|
160
|
+
"At least one of creator_column or updater_column must be configured (not nil). " \
|
161
|
+
"Setting both to nil would disable all stamping functionality."
|
162
|
+
end
|
121
163
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: whodunit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ken C. Demanawa
|
@@ -193,11 +193,11 @@ dependencies:
|
|
193
193
|
- !ruby/object:Gem::Version
|
194
194
|
version: 0.9.37
|
195
195
|
description: A lightweight Rails gem that provides simple auditing by tracking who
|
196
|
-
created, updated, and deleted ActiveRecord models.
|
197
|
-
and zero performance overhead.
|
196
|
+
created, updated, and deleted ActiveRecord models.
|
198
197
|
email:
|
199
198
|
- kenneth.c.demanawa@gmail.com
|
200
|
-
executables:
|
199
|
+
executables:
|
200
|
+
- whodunit
|
201
201
|
extensions: []
|
202
202
|
extra_rdoc_files: []
|
203
203
|
files:
|
@@ -209,16 +209,18 @@ files:
|
|
209
209
|
- LICENSE
|
210
210
|
- README.md
|
211
211
|
- Rakefile
|
212
|
+
- exe/whodunit
|
212
213
|
- gemfiles/rails_7_2.gemfile
|
213
214
|
- gemfiles/rails_8_2.gemfile
|
214
215
|
- gemfiles/rails_edge.gemfile
|
215
216
|
- lib/whodunit.rb
|
216
217
|
- lib/whodunit/controller_methods.rb
|
217
218
|
- lib/whodunit/current.rb
|
219
|
+
- lib/whodunit/generator.rb
|
218
220
|
- lib/whodunit/migration_helpers.rb
|
219
221
|
- lib/whodunit/railtie.rb
|
220
|
-
- lib/whodunit/soft_delete_detector.rb
|
221
222
|
- lib/whodunit/stampable.rb
|
223
|
+
- lib/whodunit/table_definition_extension.rb
|
222
224
|
- lib/whodunit/version.rb
|
223
225
|
- sig/whodunit.rbs
|
224
226
|
homepage: https://github.com/kanutocd/whodunit
|
@@ -230,7 +232,7 @@ metadata:
|
|
230
232
|
source_code_uri: https://github.com/kanutocd/whodunit.git
|
231
233
|
changelog_uri: https://github.com/kanutocd/whodunit/blob/main/CHANGELOG.md
|
232
234
|
bug_tracker_uri: https://github.com/kanutocd/whodunit/issues
|
233
|
-
documentation_uri: https://github.
|
235
|
+
documentation_uri: https://kanutocd.github.io/whodunit
|
234
236
|
rubygems_mfa_required: 'true'
|
235
237
|
post_install_message:
|
236
238
|
rdoc_options: []
|
@@ -238,9 +240,9 @@ require_paths:
|
|
238
240
|
- lib
|
239
241
|
required_ruby_version: !ruby/object:Gem::Requirement
|
240
242
|
requirements:
|
241
|
-
- - "
|
243
|
+
- - ">"
|
242
244
|
- !ruby/object:Gem::Version
|
243
|
-
version: 3.
|
245
|
+
version: '3.1'
|
244
246
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
245
247
|
requirements:
|
246
248
|
- - ">="
|
@@ -1,119 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Whodunit
|
4
|
-
# Smart detection of soft-delete implementations.
|
5
|
-
#
|
6
|
-
# This class provides multi-layered detection for various soft-delete implementations
|
7
|
-
# including popular gems like Discard and Paranoia, as well as custom implementations.
|
8
|
-
#
|
9
|
-
# @example Check if a model has soft-delete enabled
|
10
|
-
# class Post < ApplicationRecord
|
11
|
-
# # has deleted_at column
|
12
|
-
# end
|
13
|
-
#
|
14
|
-
# Whodunit::SoftDeleteDetector.enabled_for?(Post) # => true
|
15
|
-
#
|
16
|
-
# @example With Discard gem
|
17
|
-
# class Post < ApplicationRecord
|
18
|
-
# include Discard::Model
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# Whodunit::SoftDeleteDetector.enabled_for?(Post) # => true
|
22
|
-
#
|
23
|
-
# @since 0.1.0
|
24
|
-
class SoftDeleteDetector
|
25
|
-
# Main detection method that checks if soft-delete is enabled for a model.
|
26
|
-
#
|
27
|
-
# Uses multiple detection strategies:
|
28
|
-
# 1. Gem-based detection (Discard, Paranoia, etc.)
|
29
|
-
# 2. Column pattern detection (deleted_at, discarded_at, etc.)
|
30
|
-
# 3. Method-based detection (respond_to? soft-delete methods)
|
31
|
-
#
|
32
|
-
# @param model [Class] the ActiveRecord model class to check
|
33
|
-
# @return [Boolean] true if soft-delete is detected, false otherwise
|
34
|
-
# @example
|
35
|
-
# Whodunit::SoftDeleteDetector.enabled_for?(Post) # => true
|
36
|
-
# Whodunit::SoftDeleteDetector.enabled_for?(User) # => false
|
37
|
-
def self.enabled_for?(model)
|
38
|
-
return false unless model.respond_to?(:columns)
|
39
|
-
|
40
|
-
gem_based_detection?(model) ||
|
41
|
-
column_pattern_detection(model) ||
|
42
|
-
method_based_detection(model)
|
43
|
-
end
|
44
|
-
|
45
|
-
# Detect popular soft-delete gems.
|
46
|
-
#
|
47
|
-
# Detects the following gems:
|
48
|
-
# - Discard (checks for Discard module inclusion)
|
49
|
-
# - Paranoia (checks for paranoid? method and Paranoid ancestors)
|
50
|
-
# - ActsAsParanoid (checks for acts_as_paranoid method)
|
51
|
-
#
|
52
|
-
# @param model [Class] the ActiveRecord model class to check
|
53
|
-
# @return [Boolean] true if a gem-based soft-delete is detected
|
54
|
-
# @api private
|
55
|
-
def self.gem_based_detection?(model)
|
56
|
-
# Discard gem
|
57
|
-
return true if model.included_modules.any? { |mod| mod.to_s.include?("Discard") }
|
58
|
-
|
59
|
-
# Paranoia gem
|
60
|
-
return true if model.respond_to?(:paranoid?) ||
|
61
|
-
model.respond_to?(:acts_as_paranoid) ||
|
62
|
-
model.ancestors.any? { |a| a.to_s.include?("Paranoid") }
|
63
|
-
|
64
|
-
# ActsAsParanoid (older version)
|
65
|
-
return true if model.respond_to?(:acts_as_paranoid)
|
66
|
-
|
67
|
-
false
|
68
|
-
end
|
69
|
-
|
70
|
-
# Detect soft-delete by column patterns.
|
71
|
-
#
|
72
|
-
# Looks for common soft-delete column names:
|
73
|
-
# - deleted_at, destroyed_at, discarded_at, archived_at
|
74
|
-
# - soft_deleted_at, soft_destroyed_at, removed_at
|
75
|
-
#
|
76
|
-
# Only considers timestamp columns (datetime, timestamp, date).
|
77
|
-
#
|
78
|
-
# @param model [Class] the ActiveRecord model class to check
|
79
|
-
# @return [Boolean] true if soft-delete columns are found
|
80
|
-
# @api private
|
81
|
-
def self.column_pattern_detection(model)
|
82
|
-
soft_delete_columns = %w[
|
83
|
-
deleted_at destroyed_at discarded_at archived_at
|
84
|
-
soft_deleted_at soft_destroyed_at removed_at
|
85
|
-
]
|
86
|
-
|
87
|
-
model.columns.any? do |column|
|
88
|
-
soft_delete_columns.include?(column.name) &&
|
89
|
-
timestamp_column?(column)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
# Detect soft-delete by method presence.
|
94
|
-
#
|
95
|
-
# Checks if the model responds to common soft-delete method names.
|
96
|
-
# This catches custom implementations that follow standard naming conventions.
|
97
|
-
#
|
98
|
-
# @param model [Class] the ActiveRecord model class to check
|
99
|
-
# @return [Boolean] true if soft-delete methods are found
|
100
|
-
# @api private
|
101
|
-
def self.method_based_detection(model)
|
102
|
-
soft_delete_methods = %w[
|
103
|
-
deleted_at destroyed_at discarded_at archived_at
|
104
|
-
soft_deleted_at soft_destroyed_at removed_at
|
105
|
-
]
|
106
|
-
|
107
|
-
soft_delete_methods.any? { |method| model.respond_to?(method) }
|
108
|
-
end
|
109
|
-
|
110
|
-
# Check if a column is a timestamp type.
|
111
|
-
#
|
112
|
-
# @param column [Object] the column object (responds to #type)
|
113
|
-
# @return [Boolean] true if the column is a timestamp type
|
114
|
-
# @api private
|
115
|
-
def self.timestamp_column?(column)
|
116
|
-
%i[datetime timestamp date].include?(column.type)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|