whodunit 0.1.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.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whodunit
4
+ # Thread-safe current user context using Rails CurrentAttributes.
5
+ #
6
+ # This class provides a thread-safe way to store the current user for auditing purposes.
7
+ # It leverages Rails' CurrentAttributes functionality to ensure proper isolation
8
+ # across threads and requests.
9
+ #
10
+ # @example Setting a user
11
+ # Whodunit::Current.user = User.find(123)
12
+ # Whodunit::Current.user = 123 # Can also set by ID directly
13
+ #
14
+ # @example Getting the user ID
15
+ # Whodunit::Current.user_id # => 123
16
+ #
17
+ # @example In a controller
18
+ # class ApplicationController < ActionController::Base
19
+ # before_action :set_current_user
20
+ #
21
+ # private
22
+ #
23
+ # def set_current_user
24
+ # Whodunit::Current.user = current_user
25
+ # end
26
+ # end
27
+ #
28
+ # @since 0.1.0
29
+ class Current < ActiveSupport::CurrentAttributes
30
+ # @!attribute [rw] user
31
+ # @return [Integer, nil] the current user ID
32
+ attribute :user
33
+
34
+ class << self
35
+ # Store the original user= method before we override it
36
+ alias original_user_assignment user=
37
+
38
+ # Set the current user by object or ID.
39
+ #
40
+ # Accepts either a user object (responds to #id) or a user ID directly.
41
+ # The user ID is stored for database operations.
42
+ #
43
+ # @param user [Object, Integer, nil] user object or user ID
44
+ # @return [Integer, nil] the stored user ID
45
+ # @example
46
+ # Whodunit::Current.user = User.find(123)
47
+ # Whodunit::Current.user = 123
48
+ # Whodunit::Current.user = nil # Clear current user
49
+ def user=(user)
50
+ value = user.respond_to?(:id) ? user.id : user
51
+ original_user_assignment(value)
52
+ end
53
+
54
+ # Override reset to ensure our custom setter is preserved
55
+ def reset
56
+ super
57
+ # Redefine our custom setter after reset
58
+ _redefine_user_setter
59
+ end
60
+
61
+ private
62
+
63
+ def _redefine_user_setter
64
+ # Nothing needed here since we're using original_user_assignment
65
+ end
66
+ end
67
+
68
+ # Get the current user ID for database storage.
69
+ #
70
+ # @return [Integer, nil] the current user ID
71
+ # @example
72
+ # Whodunit::Current.user = User.find(123)
73
+ # Whodunit::Current.user_id # => 123
74
+ def self.user_id
75
+ user
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whodunit
4
+ # Database migration helpers for adding whodunit stamp columns.
5
+ #
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.
9
+ #
10
+ # @example Add stamps to existing table
11
+ # class AddWhodunitStampsToPosts < ActiveRecord::Migration[7.0]
12
+ # def change
13
+ # add_whodunit_stamps :posts
14
+ # # Adds creator_id, updater_id columns
15
+ # # Adds deleter_id if soft-delete detected
16
+ # end
17
+ # end
18
+ #
19
+ # @example Add stamps to new table
20
+ # class CreatePosts < ActiveRecord::Migration[7.0]
21
+ # def change
22
+ # create_table :posts do |t|
23
+ # t.string :title
24
+ # t.text :body
25
+ # t.whodunit_stamps
26
+ # t.timestamps
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # @example Custom data types
32
+ # add_whodunit_stamps :posts,
33
+ # creator_type: :string,
34
+ # updater_type: :uuid,
35
+ # include_deleter: true
36
+ #
37
+ # @since 0.1.0
38
+ module MigrationHelpers
39
+ # Add creator/updater stamp columns to an existing table.
40
+ #
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.
43
+ # Indexes are automatically added for performance.
44
+ #
45
+ # @param table_name [Symbol] the table to add stamps to
46
+ # @param include_deleter [Symbol, Boolean] :auto to auto-detect soft-delete,
47
+ # true to force inclusion, false to exclude
48
+ # @param creator_type [Symbol, nil] data type for creator column (defaults to configured type)
49
+ # @param updater_type [Symbol, nil] data type for updater column (defaults to configured type)
50
+ # @param deleter_type [Symbol, nil] data type for deleter column (defaults to configured type)
51
+ # @return [void]
52
+ # @example Basic usage
53
+ # add_whodunit_stamps :posts
54
+ # @example With custom types
55
+ # add_whodunit_stamps :posts, creator_type: :string, updater_type: :uuid
56
+ # @example Force deleter column
57
+ # add_whodunit_stamps :posts, include_deleter: true
58
+ 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
61
+
62
+ if should_include_deleter?(table_name, include_deleter)
63
+ add_column table_name, Whodunit.deleter_column, deleter_type || Whodunit.deleter_data_type, null: true
64
+ end
65
+
66
+ add_whodunit_indexes(table_name, include_deleter)
67
+ end
68
+
69
+ # Remove stamp columns from an existing table.
70
+ #
71
+ # This method removes the configured creator, updater, and optionally deleter
72
+ # columns from an existing table. Only removes columns that actually exist.
73
+ #
74
+ # @param table_name [Symbol] the table to remove stamps from
75
+ # @param include_deleter [Symbol, Boolean] :auto to auto-detect soft-delete,
76
+ # true to force removal, false to exclude
77
+ # @param _options [Hash] additional options (reserved for future use)
78
+ # @return [void]
79
+ # @example Basic usage
80
+ # remove_whodunit_stamps :posts
81
+ # @example Force deleter removal
82
+ # remove_whodunit_stamps :posts, include_deleter: true
83
+ 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)
86
+
87
+ if should_include_deleter?(table_name, include_deleter) &&
88
+ column_exists?(table_name, Whodunit.deleter_column)
89
+ remove_column table_name, Whodunit.deleter_column
90
+ end
91
+ end
92
+
93
+ # Add stamp columns to a table definition or existing table.
94
+ #
95
+ # This method can be used in two ways:
96
+ # 1. Inside a create_table block (pass table definition as first argument)
97
+ # 2. As a standalone method in migrations (attempts to infer table name)
98
+ #
99
+ # @param table_def [ActiveRecord::ConnectionAdapters::TableDefinition, nil]
100
+ # the table definition (when used in create_table block) or nil
101
+ # @param include_deleter [Symbol, Boolean] :auto to auto-detect soft-delete,
102
+ # true to force inclusion, false to exclude
103
+ # @param creator_type [Symbol, nil] data type for creator column (defaults to configured type)
104
+ # @param updater_type [Symbol, nil] data type for updater column (defaults to configured type)
105
+ # @param deleter_type [Symbol, nil] data type for deleter column (defaults to configured type)
106
+ # @return [void]
107
+ # @example In create_table block
108
+ # create_table :posts do |t|
109
+ # t.string :title
110
+ # t.whodunit_stamps
111
+ # end
112
+ # @example Standalone (infers table from migration name)
113
+ # def change
114
+ # whodunit_stamps # Adds to inferred table
115
+ # end
116
+ def whodunit_stamps(table_def = nil, include_deleter: :auto, creator_type: nil, updater_type: nil,
117
+ deleter_type: nil)
118
+ if table_def.nil?
119
+ handle_migration_stamps(include_deleter, creator_type, updater_type, deleter_type)
120
+ else
121
+ handle_table_definition_stamps(table_def, include_deleter, creator_type, updater_type, deleter_type)
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ # Handle stamps when called as migration method
128
+ def handle_migration_stamps(include_deleter, creator_type, updater_type, deleter_type)
129
+ table_name = infer_table_name_from_migration
130
+ return unless table_name
131
+
132
+ add_whodunit_stamps(table_name, include_deleter: include_deleter, creator_type: creator_type,
133
+ updater_type: updater_type, deleter_type: deleter_type)
134
+ end
135
+
136
+ # Handle stamps when called within create_table block
137
+ 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
140
+
141
+ if should_include_deleter_for_new_table?(include_deleter)
142
+ table_def.column Whodunit.deleter_column, deleter_type || Whodunit.deleter_data_type, null: true
143
+ end
144
+
145
+ add_whodunit_indexes_for_create_table(table_def, include_deleter)
146
+ end
147
+
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
158
+ end
159
+
160
+ # For new tables, be more conservative with auto-detection
161
+ 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
168
+ end
169
+
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) }
180
+ end
181
+
182
+ # Add indexes for performance
183
+ 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"
186
+
187
+ return unless should_include_deleter?(table_name, include_deleter)
188
+
189
+ add_index table_name, Whodunit.deleter_column, name: "index_#{table_name}_on_deleter"
190
+ end
191
+
192
+ # Add indexes within create_table block
193
+ def add_whodunit_indexes_for_create_table(table_def, include_deleter)
194
+ # Skip indexes for table definitions that don't support them
195
+ return unless table_def.respond_to?(:index)
196
+
197
+ 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"
200
+
201
+ return unless should_include_deleter_for_new_table?(include_deleter)
202
+
203
+ table_def.index Whodunit.deleter_column, name: "index_#{table_name}_on_deleter"
204
+ end
205
+
206
+ # Attempt to infer table name from migration class name
207
+ def infer_table_name_from_migration
208
+ return nil unless respond_to?(:migration_name)
209
+
210
+ # Extract table name from migration names like CreatePosts, AddStampsToPosts
211
+ case migration_name
212
+ when /^Create(\w+)$/
213
+ table_name = ::Regexp.last_match(1)
214
+ table_name.respond_to?(:underscore) ? table_name.underscore.pluralize : "#{table_name.downcase}s"
215
+ when /^Add\w*To(\w+)$/
216
+ table_name = ::Regexp.last_match(1)
217
+ table_name.respond_to?(:underscore) ? table_name.underscore : table_name.downcase
218
+ end
219
+ end
220
+
221
+ # Get migration name from class
222
+ def migration_name
223
+ self.class.name.demodulize
224
+ end
225
+ end
226
+ end
227
+
228
+ # Extend ActiveRecord::Migration when available
229
+ ActiveRecord::Migration.include(Whodunit::MigrationHelpers) if defined?(ActiveRecord::Migration)
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whodunit
4
+ # Rails integration for automatic setup of Whodunit functionality.
5
+ #
6
+ # This Railtie automatically extends ActiveRecord migrations with MigrationHelpers
7
+ # and includes ControllerMethods in ActionController::Base when Rails is available.
8
+ # It provides seamless integration without requiring manual setup.
9
+ #
10
+ # @example Automatic integration (no setup required)
11
+ # # In a Rails app, this automatically provides:
12
+ # # - Migration helpers (add_whodunit_stamps, etc.)
13
+ # # - Controller methods (set_whodunit_user, etc.)
14
+ #
15
+ # @note When Rails is not available, this class safely inherits from Object
16
+ # and does not cause any errors.
17
+ #
18
+ # @since 0.1.0
19
+ class Railtie < (defined?(Rails::Railtie) ? Rails::Railtie : Object)
20
+ if defined?(Rails::Railtie)
21
+ railtie_name :whodunit
22
+
23
+ # Extend ActiveRecord with migration helpers.
24
+ #
25
+ # This initializer adds the MigrationHelpers module to ActiveRecord,
26
+ # making methods like add_whodunit_stamps available in migrations.
27
+ #
28
+ # @api private
29
+ initializer "whodunit.extend_active_record" do |_app|
30
+ ActiveSupport.on_load(:active_record) do
31
+ extend Whodunit::MigrationHelpers
32
+ end
33
+ end
34
+
35
+ # Include controller methods in ActionController::Base.
36
+ #
37
+ # This initializer adds the ControllerMethods module to ActionController::Base,
38
+ # making methods like set_whodunit_user and current_whodunit_user available
39
+ # in all controllers.
40
+ #
41
+ # @api private
42
+ initializer "whodunit.extend_action_controller" do |_app|
43
+ ActiveSupport.on_load(:action_controller_base) do
44
+ include Whodunit::ControllerMethods
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,119 @@
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
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whodunit
4
+ # Main concern for adding creator/updater/deleter tracking to ActiveRecord models.
5
+ #
6
+ # This module provides automatic tracking of who created, updated, and deleted records.
7
+ # It intelligently sets up callbacks and associations based on available columns and
8
+ # soft-delete detection.
9
+ #
10
+ # @example Basic usage
11
+ # class Post < ApplicationRecord
12
+ # include Whodunit::Stampable
13
+ # end
14
+ #
15
+ # # Requires creator_id and/or updater_id columns
16
+ # # Automatically adds deleter_id tracking if soft-delete is detected
17
+ #
18
+ # @example Migration
19
+ # class CreatePosts < ActiveRecord::Migration[7.0]
20
+ # def change
21
+ # create_table :posts do |t|
22
+ # t.string :title
23
+ # t.text :body
24
+ # t.whodunit_stamps # Adds creator_id, updater_id columns
25
+ # t.timestamps
26
+ # end
27
+ # end
28
+ # end
29
+ #
30
+ # @example Manual deleter tracking
31
+ # class Post < ApplicationRecord
32
+ # include Whodunit::Stampable
33
+ #
34
+ # # Force enable deleter tracking even without soft-delete
35
+ # enable_whodunit_deleter!
36
+ # end
37
+ #
38
+ # @since 0.1.0
39
+ module Stampable
40
+ extend ActiveSupport::Concern
41
+
42
+ included do
43
+ # Set up callbacks
44
+ before_create :set_whodunit_creator, if: :creator_column?
45
+ before_update :set_whodunit_updater, if: :updater_column?
46
+
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
52
+
53
+ # Set up associations - call on the class
54
+ setup_whodunit_associations
55
+ end
56
+
57
+ class_methods do # rubocop:disable Metrics/BlockLength
58
+ def soft_delete_enabled?
59
+ @soft_delete_enabled ||= Whodunit::SoftDeleteDetector.enabled_for?(self)
60
+ end
61
+
62
+ def enable_whodunit_deleter!
63
+ before_destroy :set_whodunit_deleter, if: :deleter_column?
64
+ setup_deleter_association
65
+ @soft_delete_enabled = true
66
+ end
67
+
68
+ def disable_whodunit_deleter!
69
+ skip_callback :destroy, :before, :set_whodunit_deleter
70
+ @soft_delete_enabled = false
71
+ end
72
+
73
+ private
74
+
75
+ 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?
79
+ end
80
+
81
+ def creator_column_exists?
82
+ column_names.include?(Whodunit.creator_column.to_s)
83
+ end
84
+
85
+ def updater_column_exists?
86
+ column_names.include?(Whodunit.updater_column.to_s)
87
+ end
88
+
89
+ def deleter_column_exists?
90
+ column_names.include?(Whodunit.deleter_column.to_s)
91
+ end
92
+
93
+ def setup_creator_association
94
+ belongs_to :creator,
95
+ class_name: Whodunit.user_class_name,
96
+ foreign_key: Whodunit.creator_column,
97
+ optional: true
98
+ end
99
+
100
+ def setup_updater_association
101
+ belongs_to :updater,
102
+ class_name: Whodunit.user_class_name,
103
+ foreign_key: Whodunit.updater_column,
104
+ optional: true
105
+ end
106
+
107
+ def setup_deleter_association
108
+ belongs_to :deleter,
109
+ class_name: Whodunit.user_class_name,
110
+ foreign_key: Whodunit.deleter_column,
111
+ optional: true
112
+ end
113
+ end
114
+
115
+ # @!group Callback Methods
116
+
117
+ # Set the creator ID when a record is created.
118
+ #
119
+ # This method is automatically called before_create if the model has a creator column.
120
+ # It sets the creator_id to the current user from Whodunit::Current.
121
+ #
122
+ # @return [void]
123
+ # @api private
124
+ def set_whodunit_creator
125
+ return unless Whodunit::Current.user_id
126
+
127
+ self[Whodunit.creator_column] = Whodunit::Current.user_id
128
+ end
129
+
130
+ # Set the updater ID when a record is updated.
131
+ #
132
+ # This method is automatically called before_update if the model has an updater column.
133
+ # It sets the updater_id to the current user from Whodunit::Current.
134
+ # Does not run on new records (creation).
135
+ #
136
+ # @return [void]
137
+ # @api private
138
+ def set_whodunit_updater
139
+ return unless Whodunit::Current.user_id
140
+ return if new_record? # Don't set updater on creation
141
+
142
+ self[Whodunit.updater_column] = Whodunit::Current.user_id
143
+ end
144
+
145
+ # Set the deleter ID when a record is destroyed.
146
+ #
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.
149
+ #
150
+ # @return [void]
151
+ # @api private
152
+ def set_whodunit_deleter
153
+ return unless Whodunit::Current.user_id
154
+
155
+ self[Whodunit.deleter_column] = Whodunit::Current.user_id
156
+ end
157
+
158
+ # @!group Column Presence Checks
159
+
160
+ # Check if the model has a creator column.
161
+ #
162
+ # @return [Boolean] true if the creator column exists
163
+ # @api private
164
+ def creator_column?
165
+ self.class.column_names.include?(Whodunit.creator_column.to_s)
166
+ end
167
+
168
+ # Check if the model has an updater column.
169
+ #
170
+ # @return [Boolean] true if the updater column exists
171
+ # @api private
172
+ def updater_column?
173
+ self.class.column_names.include?(Whodunit.updater_column.to_s)
174
+ end
175
+
176
+ # Check if the model has a deleter column.
177
+ #
178
+ # @return [Boolean] true if the deleter column exists
179
+ # @api private
180
+ def deleter_column?
181
+ self.class.column_names.include?(Whodunit.deleter_column.to_s)
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whodunit
4
+ VERSION = "0.1.0"
5
+ end