model_timeline 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,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'model_timeline/version'
4
+ require 'model_timeline/timelineable'
5
+ require 'model_timeline/timeline_entry'
6
+ require 'model_timeline/controller_additions'
7
+ require 'model_timeline/configuration_error'
8
+ require 'model_timeline/generators/install_generator' if defined?(Rails)
9
+ require 'model_timeline/railtie' if defined?(Rails)
10
+ require 'model_timeline/rspec' if defined?(RSpec)
11
+ require 'model_timeline/rspec/matchers' if defined?(RSpec)
12
+
13
+ # A module for tracking and recording changes to ActiveRecord models.
14
+ #
15
+ # ModelTimeline provides functionality to create and maintain a history of model changes,
16
+ # including user attribution, timestamps, and additional contextual metadata.
17
+ #
18
+ # @example Basic configuration
19
+ # ModelTimeline.configure do |config|
20
+ # config.current_user_method = :current_admin
21
+ # config.current_ip_method = :visitor_ip
22
+ # end
23
+ #
24
+ # @example Temporarily disabling timeline tracking
25
+ # ModelTimeline.without_timeline do
26
+ # # Changes made here won't be recorded
27
+ # user.update(name: 'New Name')
28
+ # end
29
+ #
30
+ # @example Using with custom user and metadata
31
+ # ModelTimeline.with_timeline(current_user: admin, metadata: {reason: 'Admin action'}) do
32
+ # user.update(status: 'suspended')
33
+ # end
34
+ #
35
+ module ModelTimeline
36
+ class << self
37
+ # Sets the method name used to retrieve the current user
38
+ # @param value [Symbol, String] The method name to call
39
+ attr_writer :current_user_method
40
+
41
+ # Sets the method name used to retrieve the client IP address
42
+ # @param value [Symbol, String] The method name to call
43
+ attr_writer :current_ip_method
44
+
45
+ # Sets whether timeline recording is enabled
46
+ # @param value [Boolean] true to enable, false to disable
47
+ attr_writer :enabled
48
+
49
+ # Configures the ModelTimeline module
50
+ #
51
+ # @yield [self] Yields the ModelTimeline module for configuration
52
+ # @return [void]
53
+ def configure
54
+ yield self if block_given?
55
+ end
56
+
57
+ # Gets the method name used to retrieve the current user
58
+ #
59
+ # @return [Symbol] The method name, defaults to :current_user
60
+ def current_user_method
61
+ @current_user_method || :current_user
62
+ end
63
+
64
+ # Gets the method name used to retrieve the client IP address
65
+ #
66
+ # @return [Symbol] The method name, defaults to :remote_ip
67
+ def current_ip_method
68
+ @current_ip_method || :remote_ip
69
+ end
70
+
71
+ # Checks if timeline recording is enabled
72
+ #
73
+ # @return [Boolean] true if enabled or not explicitly disabled, false otherwise
74
+ def enabled?
75
+ @enabled.nil? || @enabled
76
+ end
77
+
78
+ # Enables timeline recording
79
+ #
80
+ # @return [true] Always returns true
81
+ def enable!
82
+ self.enabled = true
83
+ end
84
+
85
+ # Disables timeline recording
86
+ #
87
+ # @return [false] Always returns false
88
+ def disable!
89
+ self.enabled = false
90
+ end
91
+
92
+ # Temporarily disables timeline recording for the duration of the block
93
+ #
94
+ # @yield The block to execute with timeline recording disabled
95
+ # @return [Object] Returns the result of the block
96
+ def without_timeline
97
+ previous_state = enabled?
98
+ disable!
99
+ yield
100
+ ensure
101
+ self.enabled = previous_state
102
+ end
103
+
104
+ # Temporarily sets custom user, IP, and metadata for timeline entries
105
+ #
106
+ # @param current_user [Object, nil] The user to associate with timeline entries
107
+ # @param current_ip [String, nil] The IP address to associate with timeline entries
108
+ # @param metadata [Hash] Additional metadata to store with timeline entries
109
+ # @yield The block to execute with the custom context
110
+ # @return [Object] Returns the result of the block
111
+ def with_timeline(current_user: nil, current_ip: nil, metadata: {}, &block)
112
+ previous_user = ModelTimeline.current_user
113
+ previous_ip = ModelTimeline.current_ip
114
+ previous_metadata = ModelTimeline.metadata.dup
115
+
116
+ ModelTimeline.store_user_and_ip(current_user, current_ip) if current_user || current_ip
117
+ ModelTimeline.with_metadata(metadata, &block)
118
+ ensure
119
+ ModelTimeline.store_user_and_ip(previous_user, previous_ip)
120
+ ModelTimeline.metadata = previous_metadata
121
+ end
122
+
123
+ # Gets the thread-local storage hash for the current request
124
+ #
125
+ # @api private
126
+ # @return [Hash] The request store hash
127
+ def request_store
128
+ Thread.current[:model_timeline_request_store] ||= {}
129
+ end
130
+
131
+ # Stores the current user and IP address in thread-local storage
132
+ #
133
+ # @param user [Object] The current user
134
+ # @param ip_address [String] The current IP address
135
+ # @return [void]
136
+ def store_user_and_ip(user, ip_address)
137
+ request_store[:current_user] = user
138
+ request_store[:ip_address] = ip_address
139
+ end
140
+
141
+ # Gets the current user from thread-local storage
142
+ #
143
+ # @return [Object, nil] The current user or nil if not set
144
+ def current_user
145
+ request_store[:current_user]
146
+ end
147
+
148
+ # Gets the current IP address from thread-local storage
149
+ #
150
+ # @return [String, nil] The current IP address or nil if not set
151
+ def current_ip
152
+ request_store[:ip_address]
153
+ end
154
+
155
+ # Clears all data from the request store
156
+ #
157
+ # @return [Hash] The empty request store
158
+ def clear_request_store
159
+ Thread.current[:model_timeline_request_store] = {}
160
+ end
161
+
162
+ # Gets the current metadata hash from thread-local storage
163
+ #
164
+ # @return [Hash] The metadata hash
165
+ def metadata
166
+ Thread.current[:model_timeline_metadata] ||= {}
167
+ end
168
+
169
+ # Sets the metadata hash in thread-local storage
170
+ #
171
+ # @param hash [Hash] The metadata hash to store
172
+ # @return [Hash] The provided metadata hash
173
+ def metadata=(hash)
174
+ Thread.current[:model_timeline_metadata] = hash
175
+ end
176
+
177
+ # Temporarily merges additional metadata for the duration of the block
178
+ #
179
+ # @param hash [Hash] The metadata to merge with the current metadata
180
+ # @yield The block to execute with the merged metadata
181
+ # @return [Object] Returns the result of the block
182
+ def with_metadata(hash)
183
+ previous_metadata = metadata.dup
184
+ self.metadata = metadata.merge(hash)
185
+ yield
186
+ ensure
187
+ self.metadata = previous_metadata
188
+ end
189
+
190
+ # Clears all metadata from thread-local storage
191
+ #
192
+ # @return [Hash] The empty metadata hash
193
+ def clear_metadata!
194
+ Thread.current[:model_timeline_metadata] = {}
195
+ end
196
+ end
197
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: model_timeline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alexandre Stapenhorst
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-06-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pg
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 5.2.0
41
+ description: Track changes to your Rails models with multiple configurable loggers
42
+ using PostgreSQL
43
+ email:
44
+ - eng.alexandreh@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - LICENSE
50
+ - README.md
51
+ - lib/model_timeline.rb
52
+ - lib/model_timeline/configuration_error.rb
53
+ - lib/model_timeline/controller_additions.rb
54
+ - lib/model_timeline/generators/install_generator.rb
55
+ - lib/model_timeline/generators/templates/migration.rb.tt
56
+ - lib/model_timeline/railtie.rb
57
+ - lib/model_timeline/rspec.rb
58
+ - lib/model_timeline/rspec/matchers.rb
59
+ - lib/model_timeline/timeline_entry.rb
60
+ - lib/model_timeline/timelineable.rb
61
+ - lib/model_timeline/version.rb
62
+ homepage: https://github.com/alexandreh92/model_timeline
63
+ licenses:
64
+ - MIT
65
+ metadata:
66
+ homepage_uri: https://github.com/alexandreh92/model_timeline
67
+ source_code_uri: https://github.com/alexandreh92/model_timeline
68
+ changelog_uri: https://github.com/alexandreh92/model_timeline/blob/master/CHANGELOG.md
69
+ rubygems_mfa_required: 'true'
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 2.6.0
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.2.33
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Flexible audit logging for Rails models with PostgreSQL
89
+ test_files: []