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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf93211ea95d97c01b541195ade4517d221d39035896e06a6432c512e5f284c0
4
- data.tar.gz: f1bce85319393eaca9db2e078a0087e1f0818c1025fdd23e472fb8381a322b91
3
+ metadata.gz: 6cc56d9335d26c5f43d52de380aa354a268cbb266696799cf1775f051162c2d8
4
+ data.tar.gz: '027792cdf8815ec2822177492b674d05534a3d96163d463dd603a415d471ace2'
5
5
  SHA512:
6
- metadata.gz: f0f9def2049dc5ca95e13fad9746bc84a79c7e8081d8e84dd0f4fbfd3915b57b9dd35aaa600687970475f49b395e15c6725d3c558b1272f7afda6ba3b18054c0
7
- data.tar.gz: 1699b3f01dbfd55377144207ff36c987a83419adf318671a16676e5513fd31fc98f908e5b2f83cef3a9f7b597bdf7ac8fe2cf4da2c275f569bdcd4837a3a1727
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, default: nil
7
- class_attribute :_audit_log_ignore, default: nil
8
- class_attribute :_audit_log_meta, default: nil
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
- from_attrs = (object_changes || {}).transform_values { |from_to| from_to[0] }
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)
@@ -1,3 +1,3 @@
1
1
  module RailsAuditLog
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -73,7 +73,10 @@ module RailsAuditLog
73
73
  return nil if entry.nil? || entry.event == "destroy"
74
74
 
75
75
  klass = record.class
76
- to_attrs = (entry.object_changes || {}).transform_values { |v| v[1] }
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
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith