rails_audit_log 0.8.0 → 1.0.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +294 -55
  3. data/Rakefile +5 -0
  4. data/app/assets/stylesheets/rails_audit_log/_01_base.css +32 -0
  5. data/app/assets/stylesheets/rails_audit_log/_02_layout.css +34 -0
  6. data/app/assets/stylesheets/rails_audit_log/_03_table.css +39 -0
  7. data/app/assets/stylesheets/rails_audit_log/_04_badges.css +13 -0
  8. data/app/assets/stylesheets/rails_audit_log/_05_diff.css +94 -0
  9. data/app/assets/stylesheets/rails_audit_log/_06_timeline.css +21 -0
  10. data/app/assets/stylesheets/rails_audit_log/_07_detail.css +15 -0
  11. data/app/assets/stylesheets/rails_audit_log/_08_pagination.css +56 -0
  12. data/app/assets/stylesheets/rails_audit_log/_09_filters.css +54 -0
  13. data/app/assets/stylesheets/rails_audit_log/application.css +1 -15
  14. data/app/concerns/rails_audit_log/auditable.rb +57 -0
  15. data/app/concerns/rails_audit_log/controller.rb +29 -0
  16. data/app/controllers/rails_audit_log/application_controller.rb +13 -0
  17. data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +32 -0
  18. data/app/controllers/rails_audit_log/resources_controller.rb +14 -0
  19. data/app/helpers/rails_audit_log/application_helper.rb +14 -0
  20. data/app/javascript/rails_audit_log/application.js +8 -0
  21. data/app/javascript/rails_audit_log/diff_controller.js +20 -0
  22. data/app/javascript/rails_audit_log/search_controller.js +12 -0
  23. data/app/jobs/rails_audit_log/application_job.rb +1 -0
  24. data/app/models/rails_audit_log/application_record.rb +1 -0
  25. data/app/models/rails_audit_log/audit_log_entry.rb +127 -25
  26. data/app/views/layouts/rails_audit_log/application.html.erb +16 -10
  27. data/app/views/rails_audit_log/audit_log_entries/_diff.html.erb +55 -0
  28. data/app/views/rails_audit_log/audit_log_entries/index.html.erb +76 -0
  29. data/app/views/rails_audit_log/audit_log_entries/show.html.erb +50 -0
  30. data/app/views/rails_audit_log/resources/show.html.erb +35 -0
  31. data/config/importmap.rb +5 -0
  32. data/config/routes.rb +3 -0
  33. data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +10 -0
  34. data/lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb +21 -0
  35. data/lib/generators/rails_audit_log/migrate_from_paper_trail/templates/migrate_from_paper_trail.rb +99 -0
  36. data/lib/rails_audit_log/engine.rb +29 -0
  37. data/lib/rails_audit_log/matchers.rb +56 -0
  38. data/lib/rails_audit_log/minitest_assertions.rb +36 -0
  39. data/lib/rails_audit_log/paper_trail_compat.rb +76 -0
  40. data/lib/rails_audit_log/test_helpers.rb +20 -0
  41. data/lib/rails_audit_log/version.rb +1 -1
  42. data/lib/rails_audit_log.rb +173 -5
  43. metadata +70 -5
@@ -1,24 +1,67 @@
1
1
  module RailsAuditLog
2
+ # RSpec matchers for asserting audit log behaviour.
3
+ #
4
+ # == Setup
5
+ #
6
+ # # spec/rails_helper.rb
7
+ # require "rails_audit_log/matchers"
8
+ #
9
+ # RSpec.configure do |config|
10
+ # config.include RailsAuditLog::Matchers
11
+ # end
12
+ #
13
+ # == Usage
14
+ #
15
+ # # Assert a record already has an entry
16
+ # expect(post).to have_audit_log_entry(:update).touching(:title)
17
+ #
18
+ # # Assert a block creates a new entry
19
+ # expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update, touching: :title)
2
20
  module Matchers
21
+ # Returns a matcher that asserts the record has at least one matching
22
+ # {AuditLogEntry}.
23
+ #
24
+ # @param event [Symbol, String, nil] optional event filter
25
+ # (<tt>:create</tt>, <tt>:update</tt>, or <tt>:destroy</tt>)
26
+ # @return [HaveAuditLogEntry]
27
+ # @example
28
+ # expect(post).to have_audit_log_entry
29
+ # expect(post).to have_audit_log_entry(:update)
30
+ # expect(post).to have_audit_log_entry(:update).touching(:title)
3
31
  def have_audit_log_entry(event = nil)
4
32
  HaveAuditLogEntry.new(event)
5
33
  end
6
34
 
35
+ # Returns a matcher that asserts the block creates at least one new
36
+ # {AuditLogEntry} matching the given filters.
37
+ #
38
+ # @param event [Symbol, String, nil] optional event filter
39
+ # @param touching [Symbol, String, nil] optional attribute filter
40
+ # @return [CreateAuditLogEntry]
41
+ # @example
42
+ # expect { post.update!(title: "x") }.to create_audit_log_entry(event: :update)
43
+ # expect { post.update!(title: "x") }.to create_audit_log_entry(touching: :title)
7
44
  def create_audit_log_entry(event: nil, touching: nil)
8
45
  CreateAuditLogEntry.new(event: event, touching: touching)
9
46
  end
10
47
 
48
+ # RSpec matcher — asserts that a record already has a matching entry.
11
49
  class HaveAuditLogEntry
12
50
  def initialize(event)
13
51
  @event = event
14
52
  @touching = nil
15
53
  end
16
54
 
55
+ # Chains an attribute filter onto the matcher.
56
+ #
57
+ # @param attribute [Symbol, String]
58
+ # @return [self]
17
59
  def touching(attribute)
18
60
  @touching = attribute
19
61
  self
20
62
  end
21
63
 
64
+ # @api private
22
65
  def matches?(record)
23
66
  @record = record
24
67
  scope = record.audit_log_entries
@@ -27,14 +70,17 @@ module RailsAuditLog
27
70
  scope.exists?
28
71
  end
29
72
 
73
+ # @api private
30
74
  def failure_message
31
75
  "expected #{@record.class}##{@record.id} to have an audit log entry#{qualifier}"
32
76
  end
33
77
 
78
+ # @api private
34
79
  def failure_message_when_negated
35
80
  "expected #{@record.class}##{@record.id} not to have an audit log entry#{qualifier}"
36
81
  end
37
82
 
83
+ # @api private
38
84
  def description
39
85
  "have an audit log entry#{qualifier}"
40
86
  end
@@ -49,21 +95,28 @@ module RailsAuditLog
49
95
  end
50
96
  end
51
97
 
98
+ # RSpec matcher — asserts that a block creates a new matching entry.
52
99
  class CreateAuditLogEntry
53
100
  def initialize(event:, touching:)
54
101
  @event = event
55
102
  @touching = touching
56
103
  end
57
104
 
105
+ # Chains an attribute filter onto the matcher.
106
+ #
107
+ # @param attribute [Symbol, String]
108
+ # @return [self]
58
109
  def touching(attribute)
59
110
  @touching = attribute
60
111
  self
61
112
  end
62
113
 
114
+ # @api private
63
115
  def supports_block_expectations?
64
116
  true
65
117
  end
66
118
 
119
+ # @api private
67
120
  def matches?(block)
68
121
  @before = matching_scope.count
69
122
  block.call
@@ -71,14 +124,17 @@ module RailsAuditLog
71
124
  @after > @before
72
125
  end
73
126
 
127
+ # @api private
74
128
  def failure_message
75
129
  "expected block to create an audit log entry#{qualifier}, but none was created"
76
130
  end
77
131
 
132
+ # @api private
78
133
  def failure_message_when_negated
79
134
  "expected block not to create an audit log entry#{qualifier}, but one was created"
80
135
  end
81
136
 
137
+ # @api private
82
138
  def description
83
139
  "create an audit log entry#{qualifier}"
84
140
  end
@@ -1,11 +1,47 @@
1
1
  module RailsAuditLog
2
+ # Opt-in Minitest assertions for audit log expectations.
3
+ #
4
+ # == Setup
5
+ #
6
+ # # test/test_helper.rb
7
+ # require "rails_audit_log/minitest_assertions"
8
+ #
9
+ # class ActiveSupport::TestCase
10
+ # include RailsAuditLog::MinitestAssertions
11
+ # end
12
+ #
13
+ # == Usage
14
+ #
15
+ # assert_audit_log_entry(post, event: :update, touching: :title)
16
+ # refute_audit_log_entry(post, event: :create)
2
17
  module MinitestAssertions
18
+ # Asserts that +record+ has at least one {AuditLogEntry} matching the
19
+ # given filters. Fails with a descriptive message when no entry is found.
20
+ #
21
+ # @param record [ActiveRecord::Base] the audited record to check
22
+ # @param event [Symbol, String, nil] optional event filter
23
+ # (<tt>:create</tt>, <tt>:update</tt>, or <tt>:destroy</tt>)
24
+ # @param touching [Symbol, String, nil] optional attribute name filter
25
+ # @param message [String, nil] custom failure message; auto-generated when nil
26
+ # @return [void]
27
+ # @example
28
+ # assert_audit_log_entry(post, event: :update, touching: :title)
3
29
  def assert_audit_log_entry(record, event: nil, touching: nil, message: nil)
4
30
  scope = build_scope(record, event, touching)
5
31
  msg = message || default_message("to have", record, event, touching)
6
32
  assert scope.exists?, msg
7
33
  end
8
34
 
35
+ # Asserts that +record+ has *no* {AuditLogEntry} matching the given filters.
36
+ # Fails with a descriptive message when a matching entry is found.
37
+ #
38
+ # @param record [ActiveRecord::Base] the audited record to check
39
+ # @param event [Symbol, String, nil] optional event filter
40
+ # @param touching [Symbol, String, nil] optional attribute name filter
41
+ # @param message [String, nil] custom failure message; auto-generated when nil
42
+ # @return [void]
43
+ # @example
44
+ # refute_audit_log_entry(post, event: :destroy)
9
45
  def refute_audit_log_entry(record, event: nil, touching: nil, message: nil)
10
46
  scope = build_scope(record, event, touching)
11
47
  msg = message || default_message("not to have", record, event, touching)
@@ -0,0 +1,76 @@
1
+ module RailsAuditLog
2
+ # Opt-in compatibility shim for gradual migration from PaperTrail.
3
+ # Include alongside RailsAuditLog::Auditable to keep PaperTrail's
4
+ # familiar API while your codebase migrates.
5
+ #
6
+ # @example
7
+ # class Article < ApplicationRecord
8
+ # include RailsAuditLog::Auditable
9
+ # include RailsAuditLog::PaperTrailCompat
10
+ # end
11
+ #
12
+ # article.versions # audit_log_entries, oldest-first
13
+ # article.paper_trail.version # most recent AuditLogEntry
14
+ # article.paper_trail.previous_version # reconstructed previous state
15
+ # article.paper_trail.originator # whodunnit_snapshot string
16
+ # article.paper_trail.version_at(1.week.ago) # time-travel reconstruction
17
+ module PaperTrailCompat
18
+ extend ActiveSupport::Concern
19
+
20
+ included do
21
+ has_many :versions,
22
+ -> { order(created_at: :asc, id: :asc) },
23
+ class_name: "RailsAuditLog::AuditLogEntry",
24
+ as: :item,
25
+ dependent: :destroy
26
+ end
27
+
28
+ # Returns a proxy object exposing PaperTrail's instance-level API.
29
+ #
30
+ # @return [Proxy]
31
+ def paper_trail
32
+ @paper_trail_proxy ||= Proxy.new(self)
33
+ end
34
+
35
+ # Proxy providing the PaperTrail instance API surface backed by
36
+ # {AuditLogEntry} records.
37
+ class Proxy
38
+ # @param record [ActiveRecord::Base] the record this proxy wraps
39
+ def initialize(record)
40
+ @record = record
41
+ end
42
+
43
+ # The most recent {AuditLogEntry} for the record.
44
+ #
45
+ # @return [AuditLogEntry, nil]
46
+ def version
47
+ @record.audit_log_entries.order(id: :desc).first
48
+ end
49
+
50
+ # Reconstructs the record's state before the most recent change.
51
+ # Returns +nil+ for newly created records (no prior state exists).
52
+ #
53
+ # @return [ActiveRecord::Base, nil] an unpersisted instance; or +nil+
54
+ def previous_version
55
+ version&.reify
56
+ end
57
+
58
+ # Display name of the actor who made the most recent change, as stored
59
+ # in +whodunnit_snapshot+.
60
+ #
61
+ # @return [String, nil]
62
+ def originator
63
+ version&.whodunnit_snapshot
64
+ end
65
+
66
+ # Reconstructs the record's state as it was at +timestamp+.
67
+ # Delegates to {RailsAuditLog.version_at}.
68
+ #
69
+ # @param timestamp [Time]
70
+ # @return [ActiveRecord::Base, nil] an unpersisted instance; or +nil+
71
+ def version_at(timestamp)
72
+ RailsAuditLog.version_at(@record, timestamp)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,5 +1,25 @@
1
1
  module RailsAuditLog
2
+ # Opt-in test helper for suppressing audit writes in test setup code.
3
+ #
4
+ # == Setup
5
+ #
6
+ # # spec/rails_helper.rb
7
+ # require "rails_audit_log/test_helpers"
8
+ #
9
+ # RSpec.configure do |config|
10
+ # config.include RailsAuditLog::TestHelpers
11
+ # end
12
+ #
13
+ # == Usage
14
+ #
15
+ # let(:post) { without_audit_log { Post.create!(title: "fixture") } }
2
16
  module TestHelpers
17
+ # Executes the block with audit logging disabled. A prefix-free wrapper
18
+ # around {RailsAuditLog.disable} intended for use in FactoryBot factories,
19
+ # +let+ blocks, and other test setup where audit noise is unwanted.
20
+ #
21
+ # @yield executes the block without recording any audit entries
22
+ # @return [Object] the return value of the block
3
23
  def without_audit_log(&block)
4
24
  RailsAuditLog.disable(&block)
5
25
  end
@@ -1,3 +1,3 @@
1
1
  module RailsAuditLog
2
- VERSION = "0.8.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,39 +1,153 @@
1
1
  require "rails_audit_log/version"
2
2
  require "rails_audit_log/engine"
3
3
 
4
+ # RailsAuditLog is a Rails engine that tracks ActiveRecord +create+, +update+,
5
+ # and +destroy+ events as {AuditLogEntry} records with JSON-first storage and
6
+ # thread-local actor context.
7
+ #
8
+ # == Quick start
9
+ #
10
+ # # config/initializers/rails_audit_log.rb
11
+ # RailsAuditLog.configure do |config|
12
+ # config.ignored_attributes = %w[updated_at cached_at]
13
+ # config.store_snapshot = true
14
+ # config.async = false
15
+ # end
16
+ #
17
+ # # app/models/article.rb
18
+ # class Article < ApplicationRecord
19
+ # include RailsAuditLog::Auditable
20
+ # audit_log only: %i[title body]
21
+ # end
22
+ #
23
+ # # app/controllers/application_controller.rb
24
+ # class ApplicationController < ActionController::Base
25
+ # include RailsAuditLog::Controller
26
+ # audit_log_actor { current_user }
27
+ # end
4
28
  module RailsAuditLog
5
- # Global default columns to ignore across all audited models.
6
- # Override in an initializer: RailsAuditLog.ignored_attributes = %w[updated_at cached_at]
29
+ # Columns ignored on every audited model unless overridden with +only:+ or
30
+ # +ignore:+ on {Auditable.audit_log}.
31
+ #
32
+ # @return [Array<String>]
7
33
  mattr_accessor :ignored_attributes, default: %w[updated_at]
8
34
 
9
- def self.configure
10
- yield self
11
- end
35
+ # Whether to store a full snapshot of the record's attributes in the +object+
36
+ # column alongside +object_changes+. Disable to reduce storage at the cost of
37
+ # losing {AuditLogEntry#reify} fidelity for pre-snapshot entries.
38
+ #
39
+ # @return [Boolean]
12
40
  mattr_accessor :store_snapshot, default: true
41
+
42
+ # When +true+, captures +remote_ip+ and +user_agent+ from the current request
43
+ # and merges them into every entry's +metadata+ column.
44
+ # Requires {Controller} to be included in your base controller.
45
+ #
46
+ # @return [Boolean]
13
47
  mattr_accessor :capture_request_metadata, default: false
48
+
49
+ # Global cap on the number of {AuditLogEntry} records kept per tracked object.
50
+ # Oldest entries are pruned after each write once the limit is exceeded.
51
+ # Override per-model with <tt>audit_log version_limit: N</tt>.
52
+ #
53
+ # @return [Integer, nil]
14
54
  mattr_accessor :version_limit, default: nil
55
+
56
+ # When +true+, all audit writes are dispatched via +WriteAuditLogJob+ instead
57
+ # of being written inline. Override per-model with <tt>audit_log async: true</tt>.
58
+ #
59
+ # @return [Boolean]
15
60
  mattr_accessor :async, default: false
61
+
62
+ # Passes +connects_to+ options directly to {AuditLogEntry} so audit entries
63
+ # can be stored on a separate database.
64
+ #
65
+ # @return [Hash, nil]
66
+ # @example
67
+ # RailsAuditLog.connects_to = { database: { writing: :audit_primary } }
16
68
  mattr_accessor :connects_to, default: nil
69
+
70
+ # Number of entries per page in the web dashboard.
71
+ #
72
+ # @return [Integer]
73
+ mattr_accessor :page_size, default: 25
74
+
75
+ # Controls how an actor object is serialised into the +whodunnit_snapshot+
76
+ # string column. Defaults to +actor.name+ when available, otherwise +to_s+.
77
+ #
78
+ # @return [Proc]
79
+ # @example Store email instead of name
80
+ # RailsAuditLog.whodunnit_display = ->(actor) { actor.email }
17
81
  mattr_accessor :whodunnit_display, default: ->(actor) {
18
82
  actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
19
83
  }
20
84
 
85
+ # Yields the module so every +mattr_accessor+ setter is reachable as
86
+ # <tt>config.setting = value</tt>.
87
+ #
88
+ # @yield [RailsAuditLog] the module itself
89
+ # @return [void]
90
+ # @example
91
+ # RailsAuditLog.configure do |config|
92
+ # config.ignored_attributes = %w[updated_at]
93
+ # config.async = true
94
+ # end
95
+ def self.configure
96
+ yield self
97
+ end
98
+
99
+ # Sets or returns the authentication block used to gate the web dashboard.
100
+ # The block is evaluated in controller context, so controller helpers
101
+ # (e.g. +current_user+) are available directly.
102
+ # When the block returns falsy, the engine falls back to HTTP Basic auth.
103
+ #
104
+ # @yield block evaluated in controller context; return truthy to allow access
105
+ # @return [Proc, nil] the stored block, or +nil+ when not configured
106
+ # @example Require admin access
107
+ # RailsAuditLog.authenticate { current_user&.admin? }
108
+ def self.authenticate(&block)
109
+ @authenticate = block if block_given?
110
+ @authenticate
111
+ end
112
+
113
+ # Returns the request metadata hash attached to the current thread.
114
+ # Populated by {Controller} when {.capture_request_metadata} is +true+.
115
+ #
116
+ # @return [Hash, nil]
21
117
  def self.request_metadata
22
118
  Thread.current[:rails_audit_log_request_metadata]
23
119
  end
24
120
 
121
+ # @param value [Hash, nil] metadata hash to store on the current thread
122
+ # @return [Hash, nil]
25
123
  def self.request_metadata=(value)
26
124
  Thread.current[:rails_audit_log_request_metadata] = value
27
125
  end
28
126
 
127
+ # Returns the actor set on the current thread (e.g. the signed-in user).
128
+ #
129
+ # @return [Object, nil]
29
130
  def self.actor
30
131
  Thread.current[:rails_audit_log_actor]
31
132
  end
32
133
 
134
+ # Sets the actor on the current thread. Prefer {.with_actor} for scoped
135
+ # assignment so the value is always restored.
136
+ #
137
+ # @param actor [Object, nil]
138
+ # @return [Object, nil]
33
139
  def self.actor=(actor)
34
140
  Thread.current[:rails_audit_log_actor] = actor
35
141
  end
36
142
 
143
+ # Sets the actor for the duration of the block, then restores the previous
144
+ # value. Use this in background jobs and rake tasks.
145
+ #
146
+ # @param actor [Object] the actor to set (e.g. a +User+ record)
147
+ # @yield executes the block with +actor+ as the current actor
148
+ # @return [Object] the return value of the block
149
+ # @example
150
+ # RailsAuditLog.with_actor(robot_user) { DataImporter.new.run }
37
151
  def self.with_actor(actor)
38
152
  previous = self.actor
39
153
  self.actor = actor
@@ -42,10 +156,20 @@ module RailsAuditLog
42
156
  self.actor = previous
43
157
  end
44
158
 
159
+ # Returns +true+ when audit logging is active on the current thread.
160
+ #
161
+ # @return [Boolean]
45
162
  def self.enabled?
46
163
  !Thread.current[:rails_audit_log_disabled]
47
164
  end
48
165
 
166
+ # Suspends audit logging for the duration of the block on the current thread.
167
+ # Useful in seeds, factories, and test setup where audit noise is unwanted.
168
+ #
169
+ # @yield executes the block with audit logging disabled
170
+ # @return [Object] the return value of the block
171
+ # @example
172
+ # RailsAuditLog.disable { Post.create!(title: "seed post") }
49
173
  def self.disable
50
174
  previous = Thread.current[:rails_audit_log_disabled]
51
175
  Thread.current[:rails_audit_log_disabled] = true
@@ -54,14 +178,27 @@ module RailsAuditLog
54
178
  Thread.current[:rails_audit_log_disabled] = previous
55
179
  end
56
180
 
181
+ # Returns the reason string set on the current thread.
182
+ #
183
+ # @return [String, nil]
57
184
  def self.reason
58
185
  Thread.current[:rails_audit_log_reason]
59
186
  end
60
187
 
188
+ # @param value [String, nil]
189
+ # @return [String, nil]
61
190
  def self.reason=(value)
62
191
  Thread.current[:rails_audit_log_reason] = value
63
192
  end
64
193
 
194
+ # Sets a human-readable reason for the changes made within the block.
195
+ # The reason is stored in each {AuditLogEntry#reason} and restored afterwards.
196
+ #
197
+ # @param value [String] reason to attach to every entry written in the block
198
+ # @yield executes the block with +value+ as the current reason
199
+ # @return [Object] the return value of the block
200
+ # @example
201
+ # RailsAuditLog.audit_log_reason("bulk import") { records.each(&:save!) }
65
202
  def self.audit_log_reason(value)
66
203
  previous = self.reason
67
204
  self.reason = value
@@ -70,6 +207,18 @@ module RailsAuditLog
70
207
  self.reason = previous
71
208
  end
72
209
 
210
+ # Collects all {AuditLogEntry} records created within the block and inserts
211
+ # them with a single <tt>INSERT ... VALUES (…), (…)</tt> via +insert_all!+
212
+ # instead of one INSERT per record.
213
+ #
214
+ # Calls are idempotent: if a batch is already in progress on the current
215
+ # thread (i.e. a nested call), the inner block joins the outer batch.
216
+ #
217
+ # @yield executes the block; any audit entries created are buffered
218
+ # @return [Object] the return value of the block
219
+ # @raise [ActiveRecord::RecordInvalid] if any entry fails the +insert_all!+
220
+ # @example
221
+ # RailsAuditLog.batch_audit { 500.times { |i| Post.create!(title: "Post #{i}") } }
73
222
  def self.batch_audit
74
223
  return yield if Thread.current[:rails_audit_log_batch]
75
224
 
@@ -84,10 +233,29 @@ module RailsAuditLog
84
233
  end
85
234
  end
86
235
 
236
+ # Returns the in-progress batch buffer for the current thread, or +nil+ when
237
+ # no batch is active.
238
+ #
239
+ # @api private
240
+ # @return [Array<Hash>, nil]
87
241
  def self.batch_audit_buffer
88
242
  Thread.current[:rails_audit_log_batch]
89
243
  end
90
244
 
245
+ # Reconstructs the state of +record+ as it was at +time+ by replaying audit
246
+ # entries up to that timestamp.
247
+ #
248
+ # Returns an unsaved, non-persisted instance of +record.class+ whose
249
+ # attributes match the record's state at +time+, or +nil+ when no audit
250
+ # entry exists before +time+ or the record was destroyed at or before +time+.
251
+ #
252
+ # @param record [ActiveRecord::Base] the record to reconstruct
253
+ # @param time [Time] the point in time to reconstruct at
254
+ # @return [ActiveRecord::Base, nil] a new, unpersisted instance; or +nil+
255
+ # @example
256
+ # post = Post.find(42)
257
+ # snapshot = RailsAuditLog.version_at(post, 1.week.ago)
258
+ # snapshot.title # => title as it was a week ago
91
259
  def self.version_at(record, time)
92
260
  entry = AuditLogEntry
93
261
  .where(item_type: record.class.name, item_id: record.id)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_audit_log
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -23,9 +23,52 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '7.2'
26
- description: A modern Rails engine that tracks ActiveRecord create, update, and destroy
27
- events with JSON-first storage, whodunnit actor context, and a clean query API.
28
- Drop-in replacement for PaperTrail with no legacy baggage.
26
+ - !ruby/object:Gem::Dependency
27
+ name: turbo-rails
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: importmap-rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: pagy
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '43.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '43.0'
68
+ description: Rails engine that tracks ActiveRecord create, update, and destroy events
69
+ as structured JSON records. Ships a mountable web dashboard, whodunnit actor context,
70
+ batch inserts via insert_all!, async writes via ActiveJob, time-travel reconstruction,
71
+ RSpec matchers, Minitest assertions, and a migration path from PaperTrail.
29
72
  email:
30
73
  - chuck@eclecticcoding.com
31
74
  executables: []
@@ -35,25 +78,47 @@ files:
35
78
  - MIT-LICENSE
36
79
  - README.md
37
80
  - Rakefile
81
+ - app/assets/stylesheets/rails_audit_log/_01_base.css
82
+ - app/assets/stylesheets/rails_audit_log/_02_layout.css
83
+ - app/assets/stylesheets/rails_audit_log/_03_table.css
84
+ - app/assets/stylesheets/rails_audit_log/_04_badges.css
85
+ - app/assets/stylesheets/rails_audit_log/_05_diff.css
86
+ - app/assets/stylesheets/rails_audit_log/_06_timeline.css
87
+ - app/assets/stylesheets/rails_audit_log/_07_detail.css
88
+ - app/assets/stylesheets/rails_audit_log/_08_pagination.css
89
+ - app/assets/stylesheets/rails_audit_log/_09_filters.css
38
90
  - app/assets/stylesheets/rails_audit_log/application.css
39
91
  - app/concerns/rails_audit_log/auditable.rb
40
92
  - app/concerns/rails_audit_log/controller.rb
41
93
  - app/controllers/rails_audit_log/application_controller.rb
94
+ - app/controllers/rails_audit_log/audit_log_entries_controller.rb
95
+ - app/controllers/rails_audit_log/resources_controller.rb
42
96
  - app/helpers/rails_audit_log/application_helper.rb
97
+ - app/javascript/rails_audit_log/application.js
98
+ - app/javascript/rails_audit_log/diff_controller.js
99
+ - app/javascript/rails_audit_log/search_controller.js
43
100
  - app/jobs/rails_audit_log/application_job.rb
44
101
  - app/jobs/rails_audit_log/write_audit_log_job.rb
45
102
  - app/models/rails_audit_log/application_record.rb
46
103
  - app/models/rails_audit_log/audit_log_entry.rb
47
104
  - app/views/layouts/rails_audit_log/application.html.erb
105
+ - app/views/rails_audit_log/audit_log_entries/_diff.html.erb
106
+ - app/views/rails_audit_log/audit_log_entries/index.html.erb
107
+ - app/views/rails_audit_log/audit_log_entries/show.html.erb
108
+ - app/views/rails_audit_log/resources/show.html.erb
109
+ - config/importmap.rb
48
110
  - config/routes.rb
49
111
  - lib/generators/rails_audit_log/initializer/initializer_generator.rb
50
112
  - lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb
51
113
  - lib/generators/rails_audit_log/install/install_generator.rb
52
114
  - lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb
115
+ - lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb
116
+ - lib/generators/rails_audit_log/migrate_from_paper_trail/templates/migrate_from_paper_trail.rb
53
117
  - lib/rails_audit_log.rb
54
118
  - lib/rails_audit_log/engine.rb
55
119
  - lib/rails_audit_log/matchers.rb
56
120
  - lib/rails_audit_log/minitest_assertions.rb
121
+ - lib/rails_audit_log/paper_trail_compat.rb
57
122
  - lib/rails_audit_log/test_helpers.rb
58
123
  - lib/rails_audit_log/version.rb
59
124
  - lib/tasks/rails_audit_log_tasks.rake
@@ -81,5 +146,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
146
  requirements: []
82
147
  rubygems_version: 3.6.9
83
148
  specification_version: 4
84
- summary: Zeitwerk-native audit logging for ActiveRecord
149
+ summary: Audit logging for Rails with a web dashboard and JSON-first storage
85
150
  test_files: []