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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +453 -0
- data/lib/model_timeline/configuration_error.rb +24 -0
- data/lib/model_timeline/controller_additions.rb +64 -0
- data/lib/model_timeline/generators/install_generator.rb +44 -0
- data/lib/model_timeline/generators/templates/migration.rb.tt +27 -0
- data/lib/model_timeline/railtie.rb +31 -0
- data/lib/model_timeline/rspec/matchers.rb +230 -0
- data/lib/model_timeline/rspec.rb +49 -0
- data/lib/model_timeline/timeline_entry.rb +60 -0
- data/lib/model_timeline/timelineable.rb +214 -0
- data/lib/model_timeline/version.rb +9 -0
- data/lib/model_timeline.rb +197 -0
- metadata +89 -0
|
@@ -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: []
|