rails_audit_log 0.6.0 → 0.8.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 +157 -0
- data/app/concerns/rails_audit_log/auditable.rb +54 -12
- data/app/jobs/rails_audit_log/write_audit_log_job.rb +20 -0
- data/app/models/rails_audit_log/audit_log_entry.rb +10 -0
- data/lib/generators/rails_audit_log/initializer/initializer_generator.rb +15 -0
- data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +31 -0
- data/lib/rails_audit_log/engine.rb +6 -0
- data/lib/rails_audit_log/matchers.rb +103 -0
- data/lib/rails_audit_log/minitest_assertions.rb +31 -0
- data/lib/rails_audit_log/test_helpers.rb +7 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +25 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5cd71899d109aa978910a72ef144aeafb601dc70790e044ef723b49d1d560f65
|
|
4
|
+
data.tar.gz: 8884deec2a0abf63f14639794d6ac614689c9799e09b375ffc0cc7d788943e7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c32ada5189eda30461ea1dbc10a67982fb0ff6c4244da4339aa3376082b38a5272bb3a1b91fe50110c08ae9ee0ae3ede52d2a43701c0f7260a4d5a6581c0f42d
|
|
7
|
+
data.tar.gz: 448becbb80428dd754bca4d1ab43625de5ec85a2c9d712e9e4a28f7c14b0c18ce6865b25dfd9ea6c46f7b1814988d0eea77ae8b6aee7a230927cc37a80f60cf3
|
data/README.md
CHANGED
|
@@ -23,6 +23,14 @@ bin/rails generate rails_audit_log:install
|
|
|
23
23
|
bin/rails db:migrate
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Optionally scaffold a commented initializer with every configuration option:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bin/rails generate rails_audit_log:initializer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This creates `config/initializers/rails_audit_log.rb` with all settings documented as commented examples inside a `RailsAuditLog.configure` block.
|
|
33
|
+
|
|
26
34
|
## Usage
|
|
27
35
|
|
|
28
36
|
### Tracking a model
|
|
@@ -102,6 +110,32 @@ entry.diff
|
|
|
102
110
|
# => { "title" => { from: "Hello", to: "World" } }
|
|
103
111
|
```
|
|
104
112
|
|
|
113
|
+
### Separate audit database
|
|
114
|
+
|
|
115
|
+
Route all audit writes to a dedicated database by setting `connects_to` in an initializer. The engine applies it to `AuditLogEntry` at boot:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# config/initializers/rails_audit_log.rb
|
|
119
|
+
RailsAuditLog.connects_to = {
|
|
120
|
+
database: { writing: :audit_log, reading: :audit_log }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The key (e.g. `:audit_log`) must match a database key in `config/database.yml`. All reads and writes on `AuditLogEntry` — including `batch_audit` inserts and `WriteAuditLogJob` — use that connection automatically.
|
|
125
|
+
|
|
126
|
+
### Lightweight queries
|
|
127
|
+
|
|
128
|
+
Use `.slim` to exclude the three JSON blob columns (`object_changes`, `object`, `metadata`) from the SQL projection. This is useful for index or listing views where you only need the entry header (who, what event, when):
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
entries = AuditLogEntry.slim.for_resource(article).since(1.week.ago)
|
|
132
|
+
entries.first.event # => "update"
|
|
133
|
+
entries.first.actor_type # => "User"
|
|
134
|
+
entries.first.object_changes # => raises ActiveModel::MissingAttributeError
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> **Note:** Use `.count(:id)` instead of `.count` when chaining `.slim` with other scopes — Rails' `COUNT` with a multi-column `SELECT` is not supported by all databases.
|
|
138
|
+
|
|
105
139
|
### Association tracking
|
|
106
140
|
|
|
107
141
|
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:
|
|
@@ -151,6 +185,59 @@ end
|
|
|
151
185
|
|
|
152
186
|
`belongs_to` foreign-key changes are already tracked as regular column updates and require no extra configuration.
|
|
153
187
|
|
|
188
|
+
### Bulk audit writes
|
|
189
|
+
|
|
190
|
+
Wrap a block with `RailsAuditLog.batch_audit` to buffer all audit entries and flush them in a single `insert_all!` call at the end, eliminating N+1 inserts during imports:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
RailsAuditLog.batch_audit do
|
|
194
|
+
records.each { |attrs| Post.create!(attrs) }
|
|
195
|
+
end
|
|
196
|
+
# All audit entries written in one INSERT
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Nested calls accumulate into the outermost batch — only one `insert_all!` fires. If the block raises, the buffer is discarded and no entries are written.
|
|
200
|
+
|
|
201
|
+
> **Note:** Version pruning (`version_limit`) is deferred in batch mode — the next write outside the batch will trigger pruning as usual.
|
|
202
|
+
|
|
203
|
+
### Async audit writes
|
|
204
|
+
|
|
205
|
+
Offload audit writes to a background job by passing `async: true` to `audit_log`. The entry is enqueued via `RailsAuditLog::WriteAuditLogJob` (a subclass of `ActiveJob::Base`) so the request does not block on the database write:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
class Post < ApplicationRecord
|
|
209
|
+
include RailsAuditLog::Auditable
|
|
210
|
+
audit_log async: true
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Enable async globally in an initializer — per-model `async: true` takes precedence:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
# config/initializers/rails_audit_log.rb
|
|
218
|
+
RailsAuditLog.async = true
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Configure the queue adapter and queue name through standard ActiveJob settings. Version pruning also runs inside the job when `version_limit` is set.
|
|
222
|
+
|
|
223
|
+
### Capping history per record
|
|
224
|
+
|
|
225
|
+
Limit how many audit entries are kept per record with `version_limit:`. Oldest entries are pruned automatically after each write once the cap is reached:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class Post < ApplicationRecord
|
|
229
|
+
include RailsAuditLog::Auditable
|
|
230
|
+
audit_log version_limit: 10 # keep only the 10 most recent entries
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Set a global default in an initializer — per-model values take precedence:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# config/initializers/rails_audit_log.rb
|
|
238
|
+
RailsAuditLog.version_limit = 50
|
|
239
|
+
```
|
|
240
|
+
|
|
154
241
|
### Selective tracking
|
|
155
242
|
|
|
156
243
|
Track only specific attributes, or exclude noisy ones:
|
|
@@ -305,6 +392,76 @@ To save storage at the cost of reduced reification accuracy, switch to diff-only
|
|
|
305
392
|
RailsAuditLog.store_snapshot = false
|
|
306
393
|
```
|
|
307
394
|
|
|
395
|
+
### Test helper
|
|
396
|
+
|
|
397
|
+
`without_audit_log` silences audit tracking inside the block — useful in FactoryBot factories and seed data to avoid noise in the audit trail:
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
# spec/rails_helper.rb (or spec/support/factory_helpers.rb)
|
|
401
|
+
require "rails_audit_log/test_helpers"
|
|
402
|
+
|
|
403
|
+
RSpec.configure do |config|
|
|
404
|
+
config.include RailsAuditLog::TestHelpers
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Or include directly in FactoryBot definitions:
|
|
408
|
+
FactoryBot.define do
|
|
409
|
+
factory :post do
|
|
410
|
+
after(:create) { |p| without_audit_log { p.update!(cached_at: Time.current) } }
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`without_audit_log` is a prefix-free wrapper around `RailsAuditLog.disable` — thread-safe and restores tracking even if the block raises.
|
|
416
|
+
|
|
417
|
+
### Minitest assertions
|
|
418
|
+
|
|
419
|
+
Add to your `test/test_helper.rb`:
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
require "rails_audit_log/minitest_assertions"
|
|
423
|
+
|
|
424
|
+
class ActiveSupport::TestCase
|
|
425
|
+
include RailsAuditLog::MinitestAssertions
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Then use the assertions in any test:
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
assert_audit_log_entry post # any entry
|
|
433
|
+
assert_audit_log_entry post, event: :update # update entry
|
|
434
|
+
assert_audit_log_entry post, event: :update, touching: :title # touching title
|
|
435
|
+
refute_audit_log_entry post, event: :update # no update entry
|
|
436
|
+
assert_audit_log_entry post, event: :update, message: "custom" # custom failure message
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### RSpec matchers
|
|
440
|
+
|
|
441
|
+
Add to your `spec/rails_helper.rb` (or `spec_helper.rb`):
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
require "rails_audit_log/matchers"
|
|
445
|
+
|
|
446
|
+
RSpec.configure do |config|
|
|
447
|
+
config.include RailsAuditLog::Matchers
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Then use the matchers in any spec:
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
# Assert a record has a matching audit entry
|
|
455
|
+
expect(post).to have_audit_log_entry
|
|
456
|
+
expect(post).to have_audit_log_entry(:update)
|
|
457
|
+
expect(post).to have_audit_log_entry(:update).touching(:title)
|
|
458
|
+
|
|
459
|
+
# Assert a block creates a matching audit entry
|
|
460
|
+
expect { post.update!(title: "New") }.to create_audit_log_entry
|
|
461
|
+
expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update)
|
|
462
|
+
expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update).touching(:title)
|
|
463
|
+
```
|
|
464
|
+
|
|
308
465
|
## Requirements
|
|
309
466
|
|
|
310
467
|
- Ruby >= 3.3
|
|
@@ -3,10 +3,14 @@ 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,
|
|
9
|
-
class_attribute :_audit_log_associations,
|
|
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
|
|
10
|
+
class_attribute :_audit_log_version_limit, default: nil
|
|
11
|
+
class_attribute :_audit_log_async, default: false
|
|
12
|
+
|
|
13
|
+
_warn_if_audit_table_missing
|
|
10
14
|
|
|
11
15
|
has_many :audit_log_entries,
|
|
12
16
|
class_name: "RailsAuditLog::AuditLogEntry",
|
|
@@ -50,11 +54,25 @@ module RailsAuditLog
|
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
class_methods do
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
def _warn_if_audit_table_missing
|
|
58
|
+
return if connection.table_exists?("audit_log_entries")
|
|
59
|
+
|
|
60
|
+
warn "[RailsAuditLog] WARNING: #{name} includes RailsAuditLog::Auditable but the " \
|
|
61
|
+
"'audit_log_entries' table does not exist. " \
|
|
62
|
+
"Run `bin/rails generate rails_audit_log:install && bin/rails db:migrate` to create it."
|
|
63
|
+
rescue ActiveRecord::NoDatabaseError,
|
|
64
|
+
ActiveRecord::ConnectionNotEstablished,
|
|
65
|
+
ActiveRecord::StatementInvalid
|
|
66
|
+
# DB not reachable during this phase (e.g. before db:create) — skip the check
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, async: nil)
|
|
70
|
+
self._audit_log_only = only.map(&:to_s) if only
|
|
71
|
+
self._audit_log_ignore = ignore.map(&:to_s) if ignore
|
|
72
|
+
self._audit_log_meta = meta if meta
|
|
73
|
+
self._audit_log_associations = associations unless associations.nil?
|
|
74
|
+
self._audit_log_version_limit = version_limit unless version_limit.nil?
|
|
75
|
+
self._audit_log_async = async unless async.nil?
|
|
58
76
|
end
|
|
59
77
|
end
|
|
60
78
|
|
|
@@ -84,11 +102,12 @@ module RailsAuditLog
|
|
|
84
102
|
|
|
85
103
|
actor = RailsAuditLog.actor
|
|
86
104
|
meta = build_audit_metadata
|
|
87
|
-
|
|
105
|
+
write_audit_entry(
|
|
88
106
|
event: "update",
|
|
89
107
|
item_type: self.class.name,
|
|
90
108
|
item_id: id,
|
|
91
109
|
object_changes: { assoc_name => [before, after] },
|
|
110
|
+
object: nil,
|
|
92
111
|
reason: RailsAuditLog.reason,
|
|
93
112
|
metadata: meta.presence,
|
|
94
113
|
whodunnit_snapshot: actor ? RailsAuditLog.whodunnit_display.call(actor) : nil,
|
|
@@ -104,8 +123,8 @@ module RailsAuditLog
|
|
|
104
123
|
return if filtered.empty? && event == "update"
|
|
105
124
|
|
|
106
125
|
actor = RailsAuditLog.actor
|
|
107
|
-
meta
|
|
108
|
-
|
|
126
|
+
meta = build_audit_metadata
|
|
127
|
+
write_audit_entry(
|
|
109
128
|
event: event,
|
|
110
129
|
item_type: self.class.name,
|
|
111
130
|
item_id: id,
|
|
@@ -119,6 +138,29 @@ module RailsAuditLog
|
|
|
119
138
|
)
|
|
120
139
|
end
|
|
121
140
|
|
|
141
|
+
def write_audit_entry(entry_attrs)
|
|
142
|
+
if (buffer = RailsAuditLog.batch_audit_buffer)
|
|
143
|
+
buffer << entry_attrs.stringify_keys.merge("created_at" => Time.current)
|
|
144
|
+
elsif _audit_log_async || RailsAuditLog.async
|
|
145
|
+
limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
|
|
146
|
+
WriteAuditLogJob.perform_later(entry_attrs.stringify_keys, version_limit: limit)
|
|
147
|
+
else
|
|
148
|
+
RailsAuditLog::AuditLogEntry.create!(entry_attrs)
|
|
149
|
+
prune_audit_entries
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def prune_audit_entries
|
|
154
|
+
limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
|
|
155
|
+
return unless limit
|
|
156
|
+
|
|
157
|
+
count = audit_log_entries.count
|
|
158
|
+
excess = count - limit
|
|
159
|
+
return unless excess > 0
|
|
160
|
+
|
|
161
|
+
audit_log_entries.order(id: :asc).limit(excess).delete_all
|
|
162
|
+
end
|
|
163
|
+
|
|
122
164
|
def build_audit_metadata
|
|
123
165
|
meta = {}
|
|
124
166
|
if self.class._audit_log_meta
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
class WriteAuditLogJob < ApplicationJob
|
|
3
|
+
def perform(entry_attrs, version_limit: nil)
|
|
4
|
+
AuditLogEntry.create!(entry_attrs)
|
|
5
|
+
|
|
6
|
+
return unless version_limit
|
|
7
|
+
|
|
8
|
+
item_type = entry_attrs["item_type"]
|
|
9
|
+
item_id = entry_attrs["item_id"]
|
|
10
|
+
count = AuditLogEntry.where(item_type: item_type, item_id: item_id).count
|
|
11
|
+
excess = count - version_limit
|
|
12
|
+
return unless excess > 0
|
|
13
|
+
|
|
14
|
+
AuditLogEntry.where(item_type: item_type, item_id: item_id)
|
|
15
|
+
.order(id: :asc)
|
|
16
|
+
.limit(excess)
|
|
17
|
+
.delete_all
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -3,6 +3,13 @@ module RailsAuditLog
|
|
|
3
3
|
self.table_name = "audit_log_entries"
|
|
4
4
|
|
|
5
5
|
EVENTS = %w[create update destroy].freeze
|
|
6
|
+
BLOB_COLUMNS = %w[object_changes object metadata].freeze
|
|
7
|
+
|
|
8
|
+
def self.configure_connection!
|
|
9
|
+
return unless (opts = RailsAuditLog.connects_to)
|
|
10
|
+
|
|
11
|
+
connects_to(**opts)
|
|
12
|
+
end
|
|
6
13
|
|
|
7
14
|
belongs_to :item, polymorphic: true, optional: true
|
|
8
15
|
belongs_to :actor, polymorphic: true, optional: true
|
|
@@ -37,6 +44,9 @@ module RailsAuditLog
|
|
|
37
44
|
scope :since, ->(time) { where(created_at: time..) }
|
|
38
45
|
scope :until, ->(time) { where(created_at: ..time) }
|
|
39
46
|
|
|
47
|
+
# Projection scope — omits JSON blob columns for index/listing queries
|
|
48
|
+
scope :slim, -> { select(column_names - BLOB_COLUMNS) }
|
|
49
|
+
|
|
40
50
|
# Instance methods
|
|
41
51
|
|
|
42
52
|
def reify
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module RailsAuditLog
|
|
4
|
+
module Generators
|
|
5
|
+
class InitializerGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Creates a commented RailsAuditLog initializer in config/initializers."
|
|
9
|
+
|
|
10
|
+
def create_initializer_file
|
|
11
|
+
template "rails_audit_log.rb", "config/initializers/rails_audit_log.rb"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# config/initializers/rails_audit_log.rb
|
|
2
|
+
# Generated by `bin/rails generate rails_audit_log:initializer`
|
|
3
|
+
#
|
|
4
|
+
# All settings are optional. Remove the comments around any line you want to activate.
|
|
5
|
+
|
|
6
|
+
RailsAuditLog.configure do |config|
|
|
7
|
+
# Global columns excluded from all audited models. Default: ["updated_at"]
|
|
8
|
+
# config.ignored_attributes = %w[updated_at cached_at]
|
|
9
|
+
|
|
10
|
+
# Store a full attribute snapshot alongside object_changes. Default: true
|
|
11
|
+
# Disable to save storage; reify and version_at fall back to diff-only reconstruction.
|
|
12
|
+
# config.store_snapshot = false
|
|
13
|
+
|
|
14
|
+
# Capture remote_ip and user_agent into each entry's metadata column. Default: false
|
|
15
|
+
# Requires including RailsAuditLog::Controller in ApplicationController.
|
|
16
|
+
# config.capture_request_metadata = true
|
|
17
|
+
|
|
18
|
+
# Customise how the actor's display name is stored at write time. Default: actor.name
|
|
19
|
+
# config.whodunnit_display = ->(actor) { actor.email }
|
|
20
|
+
|
|
21
|
+
# Global cap on entries retained per tracked record. Default: nil (no limit)
|
|
22
|
+
# Per-model `audit_log version_limit: N` takes precedence.
|
|
23
|
+
# config.version_limit = 100
|
|
24
|
+
|
|
25
|
+
# Write all audit entries asynchronously via WriteAuditLogJob. Default: false
|
|
26
|
+
# Per-model `audit_log async: true` also works.
|
|
27
|
+
# config.async = true
|
|
28
|
+
|
|
29
|
+
# Route AuditLogEntry to a dedicated database (Rails multi-DB). Default: nil
|
|
30
|
+
# config.connects_to = { database: { writing: :audit_log, reading: :audit_log } }
|
|
31
|
+
end
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
3
|
isolate_namespace RailsAuditLog
|
|
4
|
+
|
|
5
|
+
initializer "rails_audit_log.connect_audit_db" do
|
|
6
|
+
ActiveSupport.on_load(:active_record) do
|
|
7
|
+
RailsAuditLog::AuditLogEntry.configure_connection!
|
|
8
|
+
end
|
|
9
|
+
end
|
|
4
10
|
end
|
|
5
11
|
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
module Matchers
|
|
3
|
+
def have_audit_log_entry(event = nil)
|
|
4
|
+
HaveAuditLogEntry.new(event)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def create_audit_log_entry(event: nil, touching: nil)
|
|
8
|
+
CreateAuditLogEntry.new(event: event, touching: touching)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class HaveAuditLogEntry
|
|
12
|
+
def initialize(event)
|
|
13
|
+
@event = event
|
|
14
|
+
@touching = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def touching(attribute)
|
|
18
|
+
@touching = attribute
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def matches?(record)
|
|
23
|
+
@record = record
|
|
24
|
+
scope = record.audit_log_entries
|
|
25
|
+
scope = scope.where(event: @event.to_s) if @event
|
|
26
|
+
scope = scope.touching(@touching) if @touching
|
|
27
|
+
scope.exists?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def failure_message
|
|
31
|
+
"expected #{@record.class}##{@record.id} to have an audit log entry#{qualifier}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def failure_message_when_negated
|
|
35
|
+
"expected #{@record.class}##{@record.id} not to have an audit log entry#{qualifier}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def description
|
|
39
|
+
"have an audit log entry#{qualifier}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def qualifier
|
|
45
|
+
parts = []
|
|
46
|
+
parts << " with event '#{@event}'" if @event
|
|
47
|
+
parts << " touching '#{@touching}'" if @touching
|
|
48
|
+
parts.join
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class CreateAuditLogEntry
|
|
53
|
+
def initialize(event:, touching:)
|
|
54
|
+
@event = event
|
|
55
|
+
@touching = touching
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def touching(attribute)
|
|
59
|
+
@touching = attribute
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def supports_block_expectations?
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def matches?(block)
|
|
68
|
+
@before = matching_scope.count
|
|
69
|
+
block.call
|
|
70
|
+
@after = matching_scope.count
|
|
71
|
+
@after > @before
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def failure_message
|
|
75
|
+
"expected block to create an audit log entry#{qualifier}, but none was created"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def failure_message_when_negated
|
|
79
|
+
"expected block not to create an audit log entry#{qualifier}, but one was created"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def description
|
|
83
|
+
"create an audit log entry#{qualifier}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def matching_scope
|
|
89
|
+
scope = RailsAuditLog::AuditLogEntry.all
|
|
90
|
+
scope = scope.where(event: @event.to_s) if @event
|
|
91
|
+
scope = scope.touching(@touching) if @touching
|
|
92
|
+
scope
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def qualifier
|
|
96
|
+
parts = []
|
|
97
|
+
parts << " with event '#{@event}'" if @event
|
|
98
|
+
parts << " touching '#{@touching}'" if @touching
|
|
99
|
+
parts.join
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
module MinitestAssertions
|
|
3
|
+
def assert_audit_log_entry(record, event: nil, touching: nil, message: nil)
|
|
4
|
+
scope = build_scope(record, event, touching)
|
|
5
|
+
msg = message || default_message("to have", record, event, touching)
|
|
6
|
+
assert scope.exists?, msg
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def refute_audit_log_entry(record, event: nil, touching: nil, message: nil)
|
|
10
|
+
scope = build_scope(record, event, touching)
|
|
11
|
+
msg = message || default_message("not to have", record, event, touching)
|
|
12
|
+
refute scope.exists?, msg
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def build_scope(record, event, touching)
|
|
18
|
+
scope = record.audit_log_entries
|
|
19
|
+
scope = scope.where(event: event.to_s) if event
|
|
20
|
+
scope = scope.touching(touching) if touching
|
|
21
|
+
scope
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_message(polarity, record, event, touching)
|
|
25
|
+
parts = []
|
|
26
|
+
parts << " with event '#{event}'" if event
|
|
27
|
+
parts << " touching '#{touching}'" if touching
|
|
28
|
+
"Expected #{record.class}##{record.id} #{polarity} an audit log entry#{parts.join}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -5,8 +5,15 @@ 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
|
+
|
|
9
|
+
def self.configure
|
|
10
|
+
yield self
|
|
11
|
+
end
|
|
8
12
|
mattr_accessor :store_snapshot, default: true
|
|
9
13
|
mattr_accessor :capture_request_metadata, default: false
|
|
14
|
+
mattr_accessor :version_limit, default: nil
|
|
15
|
+
mattr_accessor :async, default: false
|
|
16
|
+
mattr_accessor :connects_to, default: nil
|
|
10
17
|
mattr_accessor :whodunnit_display, default: ->(actor) {
|
|
11
18
|
actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
|
|
12
19
|
}
|
|
@@ -63,6 +70,24 @@ module RailsAuditLog
|
|
|
63
70
|
self.reason = previous
|
|
64
71
|
end
|
|
65
72
|
|
|
73
|
+
def self.batch_audit
|
|
74
|
+
return yield if Thread.current[:rails_audit_log_batch]
|
|
75
|
+
|
|
76
|
+
Thread.current[:rails_audit_log_batch] = []
|
|
77
|
+
begin
|
|
78
|
+
result = yield
|
|
79
|
+
batch = Thread.current[:rails_audit_log_batch]
|
|
80
|
+
AuditLogEntry.insert_all!(batch) if batch.any?
|
|
81
|
+
result
|
|
82
|
+
ensure
|
|
83
|
+
Thread.current[:rails_audit_log_batch] = nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.batch_audit_buffer
|
|
88
|
+
Thread.current[:rails_audit_log_batch]
|
|
89
|
+
end
|
|
90
|
+
|
|
66
91
|
def self.version_at(record, time)
|
|
67
92
|
entry = AuditLogEntry
|
|
68
93
|
.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.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -41,14 +41,20 @@ files:
|
|
|
41
41
|
- app/controllers/rails_audit_log/application_controller.rb
|
|
42
42
|
- app/helpers/rails_audit_log/application_helper.rb
|
|
43
43
|
- app/jobs/rails_audit_log/application_job.rb
|
|
44
|
+
- app/jobs/rails_audit_log/write_audit_log_job.rb
|
|
44
45
|
- app/models/rails_audit_log/application_record.rb
|
|
45
46
|
- app/models/rails_audit_log/audit_log_entry.rb
|
|
46
47
|
- app/views/layouts/rails_audit_log/application.html.erb
|
|
47
48
|
- config/routes.rb
|
|
49
|
+
- lib/generators/rails_audit_log/initializer/initializer_generator.rb
|
|
50
|
+
- lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb
|
|
48
51
|
- lib/generators/rails_audit_log/install/install_generator.rb
|
|
49
52
|
- lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb
|
|
50
53
|
- lib/rails_audit_log.rb
|
|
51
54
|
- lib/rails_audit_log/engine.rb
|
|
55
|
+
- lib/rails_audit_log/matchers.rb
|
|
56
|
+
- lib/rails_audit_log/minitest_assertions.rb
|
|
57
|
+
- lib/rails_audit_log/test_helpers.rb
|
|
52
58
|
- lib/rails_audit_log/version.rb
|
|
53
59
|
- lib/tasks/rails_audit_log_tasks.rake
|
|
54
60
|
homepage: https://github.com/eclectic-coding/rails_audit_log
|