rails_audit_log 0.5.0 → 0.6.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 +49 -0
- data/app/concerns/rails_audit_log/auditable.rb +55 -4
- data/app/models/rails_audit_log/audit_log_entry.rb +7 -2
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +4 -1
- 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: 6cc56d9335d26c5f43d52de380aa354a268cbb266696799cf1775f051162c2d8
|
|
4
|
+
data.tar.gz: '027792cdf8815ec2822177492b674d05534a3d96163d463dd603a415d471ace2'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 107247da00cc1f7eec9750f9d59eb2b41fa47f4ec9c3a0b00f587882bf6080250fe5af5152a2d13491fdb6ebf6aa7d3f82a6418f89777b9dca3819c024692805
|
|
7
|
+
data.tar.gz: 3715691ec2538d7fca5538af23e9f087899469876dcb98b660d82f18230ee531f45660950c95e4230f15a6a430b4b85999691472b9c61cf1d4e88af6c90849c2
|
data/README.md
CHANGED
|
@@ -102,6 +102,55 @@ entry.diff
|
|
|
102
102
|
# => { "title" => { from: "Hello", to: "World" } }
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
### Association tracking
|
|
106
|
+
|
|
107
|
+
Track `has_many` add and remove events by passing `associations: true` to `audit_log`. Call `audit_log` **before** the `has_many` declarations so the callbacks are wired at class load time:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class Post < ApplicationRecord
|
|
111
|
+
include RailsAuditLog::Auditable
|
|
112
|
+
audit_log associations: true
|
|
113
|
+
has_many :tags
|
|
114
|
+
has_many :comments, dependent: :destroy
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each add or remove creates an `update` entry on the parent with the associated record's identity in `object_changes`:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
post = Post.create!(title: "Hello")
|
|
122
|
+
tag = post.tags.create!(name: "Ruby")
|
|
123
|
+
|
|
124
|
+
entry = post.audit_log_entries.updated_events.last
|
|
125
|
+
entry.object_changes
|
|
126
|
+
# => { "tags" => [nil, { "id" => 1, "type" => "Tag" }] }
|
|
127
|
+
|
|
128
|
+
post.tags.delete(tag)
|
|
129
|
+
entry = post.audit_log_entries.updated_events.last
|
|
130
|
+
entry.object_changes
|
|
131
|
+
# => { "tags" => [{ "id" => 1, "type" => "Tag" }, nil] }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Track only a named subset of associations:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
audit_log associations: [:tags] # comments changes are not recorded
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`has_many :through` and `has_and_belongs_to_many` work the same way — no extra configuration:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
class Post < ApplicationRecord
|
|
144
|
+
include RailsAuditLog::Auditable
|
|
145
|
+
audit_log associations: true
|
|
146
|
+
has_many :taggings
|
|
147
|
+
has_many :tags, through: :taggings # tracked automatically
|
|
148
|
+
has_and_belongs_to_many :categories # tracked automatically
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`belongs_to` foreign-key changes are already tracked as regular column updates and require no extra configuration.
|
|
153
|
+
|
|
105
154
|
### Selective tracking
|
|
106
155
|
|
|
107
156
|
Track only specific attributes, or exclude noisy ones:
|
|
@@ -3,9 +3,10 @@ module RailsAuditLog
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
-
class_attribute :_audit_log_only,
|
|
7
|
-
class_attribute :_audit_log_ignore,
|
|
8
|
-
class_attribute :_audit_log_meta,
|
|
6
|
+
class_attribute :_audit_log_only, default: nil
|
|
7
|
+
class_attribute :_audit_log_ignore, default: nil
|
|
8
|
+
class_attribute :_audit_log_meta, default: nil
|
|
9
|
+
class_attribute :_audit_log_associations, default: nil
|
|
9
10
|
|
|
10
11
|
has_many :audit_log_entries,
|
|
11
12
|
class_name: "RailsAuditLog::AuditLogEntry",
|
|
@@ -15,13 +16,45 @@ module RailsAuditLog
|
|
|
15
16
|
after_create :record_audit_create
|
|
16
17
|
after_update :record_audit_update
|
|
17
18
|
after_destroy :record_audit_destroy
|
|
19
|
+
|
|
20
|
+
# Intercept has_many (including :through) and has_and_belongs_to_many to
|
|
21
|
+
# inject after_add/after_remove callbacks when association tracking is
|
|
22
|
+
# enabled. Must be defined after has_many :audit_log_entries so that the
|
|
23
|
+
# internal association is not affected.
|
|
24
|
+
def self.has_many(name, scope = nil, **options, &extension)
|
|
25
|
+
if _audit_log_associations && name.to_s != "audit_log_entries"
|
|
26
|
+
options = _build_audit_association_options(name.to_s, options)
|
|
27
|
+
end
|
|
28
|
+
scope ? super(name, scope, **options, &extension) : super(name, **options, &extension)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.has_and_belongs_to_many(name, scope = nil, **options, &extension)
|
|
32
|
+
if _audit_log_associations
|
|
33
|
+
options = _build_audit_association_options(name.to_s, options)
|
|
34
|
+
end
|
|
35
|
+
scope ? super(name, scope, **options, &extension) : super(name, **options, &extension)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self._build_audit_association_options(assoc_name, options)
|
|
39
|
+
tracked = _audit_log_associations == true ||
|
|
40
|
+
Array(_audit_log_associations).map(&:to_s).include?(assoc_name)
|
|
41
|
+
return options unless tracked
|
|
42
|
+
|
|
43
|
+
add_cb = ->(owner, rec) { owner.send(:record_audit_association_change, assoc_name, nil, { "id" => rec.id, "type" => rec.class.name }) }
|
|
44
|
+
remove_cb = ->(owner, rec) { owner.send(:record_audit_association_change, assoc_name, { "id" => rec.id, "type" => rec.class.name }, nil) }
|
|
45
|
+
options.merge(
|
|
46
|
+
after_add: [*options[:after_add]] + [add_cb],
|
|
47
|
+
after_remove: [*options[:after_remove]] + [remove_cb]
|
|
48
|
+
)
|
|
49
|
+
end
|
|
18
50
|
end
|
|
19
51
|
|
|
20
52
|
class_methods do
|
|
21
|
-
def audit_log(only: nil, ignore: nil, meta: nil)
|
|
53
|
+
def audit_log(only: nil, ignore: nil, meta: nil, associations: nil)
|
|
22
54
|
self._audit_log_only = only.map(&:to_s) if only
|
|
23
55
|
self._audit_log_ignore = ignore.map(&:to_s) if ignore
|
|
24
56
|
self._audit_log_meta = meta if meta
|
|
57
|
+
self._audit_log_associations = associations unless associations.nil?
|
|
25
58
|
end
|
|
26
59
|
end
|
|
27
60
|
|
|
@@ -46,6 +79,24 @@ module RailsAuditLog
|
|
|
46
79
|
record_audit_entry("destroy", changes, snapshot)
|
|
47
80
|
end
|
|
48
81
|
|
|
82
|
+
def record_audit_association_change(assoc_name, before, after)
|
|
83
|
+
return unless RailsAuditLog.enabled?
|
|
84
|
+
|
|
85
|
+
actor = RailsAuditLog.actor
|
|
86
|
+
meta = build_audit_metadata
|
|
87
|
+
RailsAuditLog::AuditLogEntry.create!(
|
|
88
|
+
event: "update",
|
|
89
|
+
item_type: self.class.name,
|
|
90
|
+
item_id: id,
|
|
91
|
+
object_changes: { assoc_name => [before, after] },
|
|
92
|
+
reason: RailsAuditLog.reason,
|
|
93
|
+
metadata: meta.presence,
|
|
94
|
+
whodunnit_snapshot: actor ? RailsAuditLog.whodunnit_display.call(actor) : nil,
|
|
95
|
+
actor_type: actor&.class&.name,
|
|
96
|
+
actor_id: actor.respond_to?(:id) ? actor.id : nil
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
49
100
|
def record_audit_entry(event, changes, snapshot = nil)
|
|
50
101
|
return unless RailsAuditLog.enabled?
|
|
51
102
|
|
|
@@ -51,8 +51,13 @@ module RailsAuditLog
|
|
|
51
51
|
return instance
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
# Fallback: diff-only mode or entries recorded before snapshot support
|
|
55
|
-
|
|
54
|
+
# Fallback: diff-only mode or entries recorded before snapshot support.
|
|
55
|
+
# Filter to column names so association-change entries (e.g. tags, comments)
|
|
56
|
+
# don't get assigned to the record as if they were scalar attributes.
|
|
57
|
+
column_names = klass.column_names.map(&:to_s)
|
|
58
|
+
from_attrs = (object_changes || {})
|
|
59
|
+
.select { |k, _| column_names.include?(k) }
|
|
60
|
+
.transform_values { |from_to| from_to[0] }
|
|
56
61
|
|
|
57
62
|
if event == "update"
|
|
58
63
|
record = klass.find_by(id: item_id)
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -73,7 +73,10 @@ module RailsAuditLog
|
|
|
73
73
|
return nil if entry.nil? || entry.event == "destroy"
|
|
74
74
|
|
|
75
75
|
klass = record.class
|
|
76
|
-
|
|
76
|
+
column_names = klass.column_names.map(&:to_s)
|
|
77
|
+
to_attrs = (entry.object_changes || {})
|
|
78
|
+
.select { |k, _| column_names.include?(k) }
|
|
79
|
+
.transform_values { |v| v[1] }
|
|
77
80
|
attrs = entry.object.present? ? entry.object.merge(to_attrs) : to_attrs
|
|
78
81
|
|
|
79
82
|
instance = klass.new
|