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 +4 -4
- data/README.md +118 -1
- data/app/concerns/rails_audit_log/auditable.rb +7 -4
- data/app/models/rails_audit_log/audit_log_entry.rb +36 -2
- data/lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb +1 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +20 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb82307e35624fe226336c4fb33559b5432a6290c4978076aadec80e6939afa0
|
|
4
|
+
data.tar.gz: 47e700f38e3d1fbe2a0953ba687285389721b31e8a6531dcbc5e336c6d9af438
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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" }
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -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
|