rails_audit_log 0.3.0 → 0.4.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: adf2bbd729f0c0a53c0016389d8b8af4ca66cedede3343bb3376cf880edc447c
4
- data.tar.gz: 640764fdbe148a2f8d0309350ae74358256b7cc9b530f0c35d0bc045ef736b4c
3
+ metadata.gz: cb82307e35624fe226336c4fb33559b5432a6290c4978076aadec80e6939afa0
4
+ data.tar.gz: 47e700f38e3d1fbe2a0953ba687285389721b31e8a6531dcbc5e336c6d9af438
5
5
  SHA512:
6
- metadata.gz: 938374f49a4385c038124abb6aeb95eaa8369dccc20d8eb125e96071954c3b8858cb22dc94781bdfa31e96d006645df5bb07f989c83d4ca35d1792451cb82f07
7
- data.tar.gz: 82f6ca3524c11b954705d62b576d11c8ae5008f49b26c973789d43466a6d1ab3d4462fd012885a0b83177220c8ceda83c381af5db07e0b371deafca1db14badc
6
+ metadata.gz: 5dc66b780e8d1875260ef21c143b3ec823832ffa7e90c58f8392e002515d4cbe30c797936f4766bd15791a9b1b37e2f8918973f33fec58e396ca117a3623a563
7
+ data.tar.gz: d7fbf0c15e39d95a57270203b296ce5bac2835cc15f69d44cd6a7b000a884bc8d639cee91de5c1dd84aeaec64795fa6325397a1926a67f9a8317cc1128c18808
data/README.md CHANGED
@@ -39,7 +39,7 @@ Every `create`, `update`, and `destroy` is now recorded automatically:
39
39
 
40
40
  ```ruby
41
41
  article = Article.create!(title: "Hello")
42
- article.audit_log_entries.count # => 1
42
+ article.audit_log_entries.count # => 1
43
43
  article.audit_log_entries.first.event # => "create"
44
44
 
45
45
  article.update!(title: "World")
@@ -77,6 +77,123 @@ RailsAuditLog.with_actor(current_user) do
77
77
  end
78
78
  ```
79
79
 
80
+ ### Querying the audit log
81
+
82
+ ```ruby
83
+ # Event scopes
84
+ article.audit_log_entries.created_events
85
+ article.audit_log_entries.updated_events
86
+ article.audit_log_entries.destroyed_events
87
+
88
+ # Filter by actor, resource, or time
89
+ RailsAuditLog::AuditLogEntry.by_actor(current_user)
90
+ RailsAuditLog::AuditLogEntry.for_resource(Article)
91
+ RailsAuditLog::AuditLogEntry.for_resource(article)
92
+ RailsAuditLog::AuditLogEntry.since(1.week.ago)
93
+ RailsAuditLog::AuditLogEntry.until(Date.yesterday)
94
+
95
+ # Find entries that touched a specific attribute
96
+ RailsAuditLog::AuditLogEntry.touching(:title)
97
+
98
+ # Inspect what changed
99
+ entry = article.audit_log_entries.last
100
+ entry.changed_attributes # => ["title"]
101
+ entry.diff
102
+ # => { "title" => { from: "Hello", to: "World" } }
103
+ ```
104
+
105
+ ### Selective tracking
106
+
107
+ Track only specific attributes, or exclude noisy ones:
108
+
109
+ ```ruby
110
+ class Article < ApplicationRecord
111
+ include RailsAuditLog::Auditable
112
+
113
+ # Track only these columns
114
+ audit_log only: [:title, :status]
115
+
116
+ # Or exclude specific columns
117
+ audit_log ignore: [:cached_at, :views_count]
118
+ end
119
+ ```
120
+
121
+ Configure global defaults in an initializer:
122
+
123
+ ```ruby
124
+ # config/initializers/rails_audit_log.rb
125
+ RailsAuditLog.ignored_attributes = %w[updated_at cached_at]
126
+ ```
127
+
128
+ Updates that produce no tracked changes after filtering are silently skipped.
129
+
130
+ ### Disabling auditing
131
+
132
+ Suppress all audit writes inside a block — thread-safe, works in jobs and imports:
133
+
134
+ ```ruby
135
+ RailsAuditLog.disable do
136
+ Article.insert_all!(bulk_rows)
137
+ end
138
+
139
+ RailsAuditLog.enabled? # => true (restored after the block)
140
+ ```
141
+
142
+ Disable on a specific record instance:
143
+
144
+ ```ruby
145
+ article.skip_audit_log { article.update!(cached_at: Time.current) }
146
+ ```
147
+
148
+ ### Object reconstruction
149
+
150
+ #### Reify a single entry
151
+
152
+ `AuditLogEntry#reify` returns an unsaved ActiveRecord instance reflecting the record's state **before** the entry was recorded:
153
+
154
+ ```ruby
155
+ article.update!(title: "v2")
156
+ entry = article.audit_log_entries.updated_events.last
157
+
158
+ previous = entry.reify
159
+ previous.title # => "v1" (the pre-update state)
160
+ previous.persisted? # => false
161
+ ```
162
+
163
+ Returns `nil` for `create` entries (nothing existed before).
164
+
165
+ #### Reconstruct state at any point in time
166
+
167
+ ```ruby
168
+ snapshot = RailsAuditLog.version_at(article, 1.week.ago)
169
+ snapshot.title # => whatever the title was a week ago
170
+ ```
171
+
172
+ Returns `nil` if the record had no history at that time or was already destroyed.
173
+
174
+ #### Navigate the version chain
175
+
176
+ ```ruby
177
+ entry = article.audit_log_entries.updated_events.last
178
+ entry.previous # => the entry before this one
179
+ entry.next # => the entry after this one (nil if last)
180
+ ```
181
+
182
+ ### Object snapshot storage
183
+
184
+ By default every entry stores a full `object` snapshot of the pre-change state alongside `object_changes`. This makes `reify` and `version_at` reliable without any database lookups:
185
+
186
+ ```ruby
187
+ entry.object # => { "id" => 1, "title" => "v1", ... }
188
+ entry.object_changes # => { "title" => ["v1", "v2"] }
189
+ ```
190
+
191
+ To save storage at the cost of reduced reification accuracy, switch to diff-only mode:
192
+
193
+ ```ruby
194
+ RailsAuditLog.store_snapshot = false
195
+ ```
196
+
80
197
  ## Requirements
81
198
 
82
199
  - Ruby >= 3.3
@@ -30,19 +30,21 @@ module RailsAuditLog
30
30
  private
31
31
 
32
32
  def record_audit_create
33
- record_audit_entry("create", saved_changes)
33
+ record_audit_entry("create", saved_changes, nil)
34
34
  end
35
35
 
36
36
  def record_audit_update
37
- record_audit_entry("update", saved_changes)
37
+ snapshot = attributes.merge(saved_changes.transform_values { |v| v[0] }) if RailsAuditLog.store_snapshot
38
+ record_audit_entry("update", saved_changes, snapshot)
38
39
  end
39
40
 
40
41
  def record_audit_destroy
42
+ snapshot = attributes.dup if RailsAuditLog.store_snapshot
41
43
  changes = attributes.transform_values { |v| [v, nil] }
42
- record_audit_entry("destroy", changes)
44
+ record_audit_entry("destroy", changes, snapshot)
43
45
  end
44
46
 
45
- def record_audit_entry(event, changes)
47
+ def record_audit_entry(event, changes, snapshot = nil)
46
48
  return unless RailsAuditLog.enabled?
47
49
 
48
50
  filtered = filter_changes(changes)
@@ -54,6 +56,7 @@ module RailsAuditLog
54
56
  item_type: self.class.name,
55
57
  item_id: id,
56
58
  object_changes: filtered,
59
+ object: snapshot,
57
60
  actor_type: actor&.class&.name,
58
61
  actor_id: actor.respond_to?(:id) ? actor.id : nil
59
62
  )
@@ -38,6 +38,40 @@ module RailsAuditLog
38
38
 
39
39
  # Instance methods
40
40
 
41
+ def reify
42
+ return nil if event == "create"
43
+
44
+ klass = item_type.constantize
45
+
46
+ if object.present?
47
+ instance = klass.new
48
+ instance.assign_attributes(object.except("id"))
49
+ instance.id = object.fetch("id") { item_id }
50
+ return instance
51
+ end
52
+
53
+ # Fallback: diff-only mode or entries recorded before snapshot support
54
+ from_attrs = (object_changes || {}).transform_values { |from_to| from_to[0] }
55
+
56
+ if event == "update"
57
+ record = klass.find_by(id: item_id)
58
+ from_attrs = record.attributes.merge(from_attrs) if record
59
+ end
60
+
61
+ instance = klass.new
62
+ instance.assign_attributes(from_attrs.except("id"))
63
+ instance.id = from_attrs.fetch("id") { item_id }
64
+ instance
65
+ end
66
+
67
+ def previous
68
+ self.class.where(item_type: item_type, item_id: item_id).where("id < ?", id).order(id: :desc).first
69
+ end
70
+
71
+ def next
72
+ self.class.where(item_type: item_type, item_id: item_id).where("id > ?", id).order(id: :asc).first
73
+ end
74
+
41
75
  def changed_attributes
42
76
  object_changes&.keys || []
43
77
  end
@@ -48,11 +82,11 @@ module RailsAuditLog
48
82
  object_changes.transform_values { |from_to| { from: from_to[0], to: from_to[1] } }
49
83
  end
50
84
 
51
- # Attribute scope — uses json_extract (SQLite/MySQL) or json ? key (PostgreSQL)
85
+ # Attribute scope — uses json_extract (SQLite/MySQL) or ->> (PostgreSQL)
52
86
  scope :touching, ->(attribute) {
53
87
  if connection.adapter_name =~ /PostgreSQL/i
54
88
  # :nocov:
55
- where("object_changes ? :key", key: attribute.to_s)
89
+ where("object_changes->>? IS NOT NULL", attribute.to_s)
56
90
  # :nocov:
57
91
  else
58
92
  where("json_extract(object_changes, ?) IS NOT NULL", "$.#{attribute}")
@@ -5,6 +5,7 @@ class CreateAuditLogEntries < ActiveRecord::Migration[<%= ActiveRecord::Migratio
5
5
  t.string :item_type, null: false
6
6
  t.bigint :item_id, null: false
7
7
  t.json :object_changes
8
+ t.json :object
8
9
  t.string :actor_type
9
10
  t.bigint :actor_id
10
11
  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
@@ -1,3 +1,3 @@
1
1
  module RailsAuditLog
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -5,6 +5,7 @@ module RailsAuditLog
5
5
  # Global default columns to ignore across all audited models.
6
6
  # Override in an initializer: RailsAuditLog.ignored_attributes = %w[updated_at cached_at]
7
7
  mattr_accessor :ignored_attributes, default: %w[updated_at]
8
+ mattr_accessor :store_snapshot, default: true
8
9
 
9
10
  def self.actor
10
11
  Thread.current[:rails_audit_log_actor]
@@ -33,4 +34,23 @@ module RailsAuditLog
33
34
  ensure
34
35
  Thread.current[:rails_audit_log_disabled] = previous
35
36
  end
37
+
38
+ def self.version_at(record, time)
39
+ entry = AuditLogEntry
40
+ .where(item_type: record.class.name, item_id: record.id)
41
+ .where(created_at: ..time)
42
+ .order(created_at: :desc, id: :desc)
43
+ .first
44
+
45
+ return nil if entry.nil? || entry.event == "destroy"
46
+
47
+ klass = record.class
48
+ to_attrs = (entry.object_changes || {}).transform_values { |v| v[1] }
49
+ attrs = entry.object.present? ? entry.object.merge(to_attrs) : to_attrs
50
+
51
+ instance = klass.new
52
+ instance.assign_attributes(attrs.except("id"))
53
+ instance.id = attrs.fetch("id") { entry.item_id }
54
+ instance
55
+ end
36
56
  end
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith