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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c68bbab9b951c3ed257a22769b665e0525f8b41ecb23c3f7375488cc099f9f65
4
- data.tar.gz: 3e23380c8ba1502e359c768fe4a99fe3af499598801de896cc030075b8db7de5
3
+ metadata.gz: 0b45b1b2f4cfb91e98e2c4656c67305403108af39c5069127d91f0521599bc94
4
+ data.tar.gz: 56472d61e9f422643eefe7dca84b7f050fa8d91bb4274bb2354a4e01f04d5ee4
5
5
  SHA512:
6
- metadata.gz: 14e70828520db567bb75dbc472c961fc292281183f630d66d2543e47680947668cd17fc03db2491fa1b0cd52fc423941ca0e73b83e2bca65cbf811a5fdbfc55c
7
- data.tar.gz: 6fe6fb76793405dac3ff6f7ad799aa0736e4a5cdfdd4c878b435210606570ce130a73eb570498efb8fc41e1ff1790c44496aae307dba0af689f5ad3fb416d7e2
6
+ metadata.gz: c959a0b51f441b28a855b95a537e5983592ed5eb7eef4cbf5eb42c1d4c120849a27ac4113c784f27defa6703939d6cb02a0c2ccc8bd24f0a0508673351407b97
7
+ data.tar.gz: a5ca68eebccf5794c9b65521a0c7a232f3178d7aacb6af89c9de3b3bc1365588a6e62a39c52c9821987aaa8735231d289fbc1f28fc7a95be391171d40afbfe31
data/.rubocop.yml CHANGED
@@ -6,7 +6,7 @@ plugins:
6
6
 
7
7
  AllCops:
8
8
  NewCops: enable
9
- TargetRubyVersion: 3.0
9
+ TargetRubyVersion: 3.1
10
10
  Exclude:
11
11
  - 'vendor/**/*'
12
12
  - 'tmp/**/*'
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
- ## Smart Soft-Delete Detection
109
+ ## Soft-Delete Support
110
110
 
111
- Whodunit automatically detects popular soft-delete solutions:
111
+ Whodunit automatically tracks who deleted records when using soft-delete. Simply configure your soft-delete column:
112
112
 
113
- - **Discard** (`gem 'discard'`)
114
- - **Paranoia** (`gem 'paranoia'`)
115
- - **ActsAsParanoid** (`gem 'acts_as_paranoid'`)
116
- - **Custom implementations** with timestamp columns like `deleted_at`, `discarded_at`, etc.
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.auto_detect_soft_delete = false # Default: true
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(&block)
151
- with_whodunit_user(nil, &block)
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. It intelligently detects soft-delete
8
- # implementations and adds appropriate indexes for performance.
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 columns
15
- # # Adds deleter_id if soft-delete detected
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 creator and updater columns to an existing table.
42
- # It optionally adds a deleter column based on soft-delete detection or explicit configuration.
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 auto-detect soft-delete,
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
- add_column table_name, Whodunit.creator_column, creator_type || Whodunit.creator_data_type, null: true
60
- add_column table_name, Whodunit.updater_column, updater_type || Whodunit.updater_data_type, null: true
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?(table_name, 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 actually exist.
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 auto-detect soft-delete,
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
- remove_column table_name, Whodunit.creator_column if column_exists?(table_name, Whodunit.creator_column)
85
- remove_column table_name, Whodunit.updater_column if column_exists?(table_name, Whodunit.updater_column)
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?(table_name, 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 auto-detect soft-delete,
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
- table_def.column Whodunit.creator_column, creator_type || Whodunit.creator_data_type, null: true
139
- table_def.column Whodunit.updater_column, updater_type || Whodunit.updater_data_type, null: true
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
- def should_include_deleter?(table_name, include_deleter)
150
- case include_deleter
151
- when :auto
152
- soft_delete_detected_for_table?(table_name)
153
- when true
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 with auto-detection
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
- case include_deleter
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
- # Detect soft-delete patterns in existing table
171
- def soft_delete_detected_for_table?(table_name)
172
- return false unless table_exists?(table_name)
173
-
174
- soft_delete_columns = %w[
175
- deleted_at destroyed_at discarded_at archived_at
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
- return unless should_include_deleter?(table_name, include_deleter)
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
- table_def.index Whodunit.creator_column, name: "index_#{table_name}_on_creator"
199
- table_def.index Whodunit.updater_column, name: "index_#{table_name}_on_updater"
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
 
@@ -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
 
@@ -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
- # Only add destroy callback if soft-delete is detected
48
- if Whodunit.auto_detect_soft_delete &&
49
- Whodunit::SoftDeleteDetector.enabled_for?(self)
50
- before_destroy :set_whodunit_deleter, if: :deleter_column?
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 ||= Whodunit::SoftDeleteDetector.enabled_for?(self)
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?(Whodunit.creator_column.to_s)
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?(Whodunit.updater_column.to_s)
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?(Whodunit.deleter_column.to_s)
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: Whodunit.user_class_name,
96
- foreign_key: Whodunit.creator_column,
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: Whodunit.user_class_name,
103
- foreign_key: Whodunit.updater_column,
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: Whodunit.user_class_name,
110
- foreign_key: Whodunit.deleter_column,
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[Whodunit.creator_column] = Whodunit::Current.user_id
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
- self[Whodunit.updater_column] = Whodunit::Current.user_id
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
- # This method is automatically called before_destroy if the model has a deleter column
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[Whodunit.deleter_column] = Whodunit::Current.user_id
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.column_names.include?(Whodunit.creator_column.to_s)
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.column_names.include?(Whodunit.updater_column.to_s)
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.column_names.include?(Whodunit.deleter_column.to_s)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Whodunit
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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
- # Whether to automatically detect soft-delete implementations (default: true)
59
- # @return [Boolean] auto-detection setting
60
- mattr_accessor :auto_detect_soft_delete, default: true
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.1.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. Features smart soft-delete detection
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.com/kanutocd/whodunit/blob/main/README.md
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.0.0
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