rails_audit_log 0.9.0 → 1.0.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: 9c9a9fcdb866f5300a3a2fd9bd6026ad93530d15b0b26d771ef1a28a47985280
4
- data.tar.gz: 01ac4b3f95fd1c2067a8aa5936122ff86dc264fe771881515dd31d3401377b66
3
+ metadata.gz: aa624cc1cc0ba6984f41de2f19b22891f9a7c376aae0625f94ebe86f2c48ef78
4
+ data.tar.gz: 75bfb038bb1fe170bdf1a893ec87c40b3e7711602ad355a9c0788d20558ef337
5
5
  SHA512:
6
- metadata.gz: 4c155747f1219996a6fde0fcc0d00c764df0f296c7b099418b7020ef90984690400300174b5c1cc1dc4842451a3f53d35e4b710542693683c78d4706c0eca1c1
7
- data.tar.gz: 256ef73b739193443718481ef7fa1c0920ae4b4d424e3466b9ac3dbaf9a0bf8b7cf4823d0fd7d000652710985ac60fa6df6252e5997266bb40eda6bde9ba9941
6
+ metadata.gz: fd59c42a1522f4301e8c46c5b25186784847f5e56529fb7baf4d7b3d87d0f5391fb0bbcb33b8abac53bbf47a85a7529eae02d878c0484148a496b28950573541
7
+ data.tar.gz: 7b2be961c2b1f6091c1426d7be966ceb99647a7415faa4a540f3876d63ea6ff2a10fab515b61074b9538a7f43b63d99481c1743e35aeb09e313d2e76c4c3107f
data/README.md CHANGED
@@ -6,10 +6,45 @@
6
6
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-ruby)](https://www.ruby-lang.org)
7
7
  [![codecov](https://codecov.io/gh/eclectic-coding/rails_audit_log/branch/main/graph/badge.svg)](https://codecov.io/gh/eclectic-coding/rails_audit_log)
8
8
 
9
- A modern, Zeitwerk-native Rails engine for auditing ActiveRecord changes. Tracks `create`, `update`, and `destroy` events with JSON-first storage, whodunnit actor context, and a clean query API.
9
+ Audit logging for Rails. Tracks `create`, `update`, and `destroy` events as structured JSON records with a mountable web dashboard, whodunnit actor context, batch writes, time-travel reconstruction, and test helpers — a clean, well-documented replacement for PaperTrail with a built-in migration path.
10
+
11
+ ## Table of contents
12
+
13
+ - [Installation](#installation)
14
+ - [Configuration](#configuration)
15
+ - [Web dashboard](#web-dashboard)
16
+ - [Usage](#usage)
17
+ - [Tracking a model](#tracking-a-model)
18
+ - [Recording who made the change](#recording-who-made-the-change)
19
+ - [Actor context outside of controllers](#actor-context-outside-of-controllers)
20
+ - [Querying the audit log](#querying-the-audit-log)
21
+ - [Lightweight queries](#lightweight-queries)
22
+ - [Association tracking](#association-tracking)
23
+ - [Bulk audit writes](#bulk-audit-writes)
24
+ - [Async audit writes](#async-audit-writes)
25
+ - [Capping history per record](#capping-history-per-record)
26
+ - [Selective tracking](#selective-tracking)
27
+ - [Disabling auditing](#disabling-auditing)
28
+ - [Object reconstruction](#object-reconstruction)
29
+ - [Attaching a reason](#attaching-a-reason)
30
+ - [Arbitrary metadata](#arbitrary-metadata)
31
+ - [Request metadata capture](#request-metadata-capture)
32
+ - [Actor display name snapshot](#actor-display-name-snapshot)
33
+ - [Object snapshot storage](#object-snapshot-storage)
34
+ - [Separate audit database](#separate-audit-database)
35
+ - [Test helpers](#test-helpers)
36
+ - [Stability and versioning](#stability-and-versioning)
37
+ - [Migrating from PaperTrail](#migrating-from-papertrail)
38
+ - [Performance](#performance)
39
+ - [Companion gems](#companion-gems)
40
+ - [Requirements](#requirements)
41
+ - [Contributing](#contributing)
42
+ - [License](#license)
10
43
 
11
44
  ## Installation
12
45
 
46
+ [↑ Table of contents](#table-of-contents)
47
+
13
48
  Add to your `Gemfile`:
14
49
 
15
50
  ```ruby
@@ -23,16 +58,67 @@ bin/rails generate rails_audit_log:install
23
58
  bin/rails db:migrate
24
59
  ```
25
60
 
26
- Optionally scaffold a commented initializer with every configuration option:
61
+ ## Configuration
62
+
63
+ [↑ Table of contents](#table-of-contents)
64
+
65
+ Run the initializer generator to create `config/initializers/rails_audit_log.rb` with every option documented as a commented example:
27
66
 
28
67
  ```bash
29
68
  bin/rails generate rails_audit_log:initializer
30
69
  ```
31
70
 
32
- This creates `config/initializers/rails_audit_log.rb` with all settings documented as commented examples inside a `RailsAuditLog.configure` block.
71
+ The generated file (shown below with all options) uses the block-style `configure` API. Every setting has a sensible default — uncomment only what you need:
72
+
73
+ ```ruby
74
+ # config/initializers/rails_audit_log.rb
75
+
76
+ RailsAuditLog.configure do |config|
77
+ # Global columns excluded from all audited models.
78
+ # Default: ["updated_at"]
79
+ # config.ignored_attributes = %w[updated_at cached_at]
80
+
81
+ # Store a full attribute snapshot alongside object_changes.
82
+ # Default: true
83
+ # Disable to save storage; reify and version_at fall back to diff-only reconstruction.
84
+ # config.store_snapshot = false
85
+
86
+ # Capture remote_ip and user_agent into each entry's metadata column.
87
+ # Default: false — requires RailsAuditLog::Controller in ApplicationController.
88
+ # config.capture_request_metadata = true
89
+
90
+ # Customise how the actor's display name is stored at write time.
91
+ # Default: actor.name if available, otherwise actor.to_s
92
+ # config.whodunnit_display = ->(actor) { actor.email }
93
+
94
+ # Global cap on entries retained per tracked record. nil = no limit.
95
+ # Per-model `audit_log version_limit: N` takes precedence.
96
+ # config.version_limit = 100
97
+
98
+ # Write all audit entries asynchronously via WriteAuditLogJob.
99
+ # Default: false — per-model `audit_log async: true` also works.
100
+ # config.async = true
101
+
102
+ # Route AuditLogEntry to a dedicated database (Rails multi-DB).
103
+ # config.connects_to = { database: { writing: :audit_log, reading: :audit_log } }
104
+
105
+ # Entries per page in the web dashboard. Default: 25
106
+ # config.page_size = 50
107
+
108
+ # Gate web dashboard access. Block runs in controller context so controller
109
+ # helpers like current_user are available directly. Falls back to HTTP Basic
110
+ # auth when the block returns falsy. Leave unset for unauthenticated access.
111
+ # config.authenticate { current_user&.admin? }
112
+ # config.authenticate { |c| c.current_user&.admin? }
113
+ end
114
+ ```
115
+
116
+ Each option is documented in detail in its own Usage section below.
33
117
 
34
118
  ## Web dashboard
35
119
 
120
+ [↑ Table of contents](#table-of-contents)
121
+
36
122
  Mount the engine in `config/routes.rb` to enable the built-in audit trail browser:
37
123
 
38
124
  ```ruby
@@ -43,6 +129,8 @@ Then visit `/audit` to browse all audit entries. The dashboard is delivered via
43
129
 
44
130
  ## Usage
45
131
 
132
+ [↑ Table of contents](#table-of-contents)
133
+
46
134
  ### Tracking a model
47
135
 
48
136
  Include `RailsAuditLog::Auditable` in any ActiveRecord model:
@@ -120,19 +208,6 @@ entry.diff
120
208
  # => { "title" => { from: "Hello", to: "World" } }
121
209
  ```
122
210
 
123
- ### Separate audit database
124
-
125
- Route all audit writes to a dedicated database by setting `connects_to` in an initializer. The engine applies it to `AuditLogEntry` at boot:
126
-
127
- ```ruby
128
- # config/initializers/rails_audit_log.rb
129
- RailsAuditLog.connects_to = {
130
- database: { writing: :audit_log, reading: :audit_log }
131
- }
132
- ```
133
-
134
- 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.
135
-
136
211
  ### Lightweight queries
137
212
 
138
213
  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):
@@ -293,22 +368,20 @@ article.skip_audit_log { article.update!(cached_at: Time.current) }
293
368
 
294
369
  ### Object reconstruction
295
370
 
296
- #### Reify a single entry
297
-
298
- `AuditLogEntry#reify` returns an unsaved ActiveRecord instance reflecting the record's state **before** the entry was recorded:
371
+ **Reify a single entry** — `AuditLogEntry#reify` returns an unsaved ActiveRecord instance reflecting the record's state **before** the entry was recorded:
299
372
 
300
373
  ```ruby
301
374
  article.update!(title: "v2")
302
375
  entry = article.audit_log_entries.updated_events.last
303
376
 
304
377
  previous = entry.reify
305
- previous.title # => "v1" (the pre-update state)
378
+ previous.title # => "v1" (the pre-update state)
306
379
  previous.persisted? # => false
307
380
  ```
308
381
 
309
382
  Returns `nil` for `create` entries (nothing existed before).
310
383
 
311
- #### Reconstruct state at any point in time
384
+ **Reconstruct state at any point in time:**
312
385
 
313
386
  ```ruby
314
387
  snapshot = RailsAuditLog.version_at(article, 1.week.ago)
@@ -317,7 +390,7 @@ snapshot.title # => whatever the title was a week ago
317
390
 
318
391
  Returns `nil` if the record had no history at that time or was already destroyed.
319
392
 
320
- #### Navigate the version chain
393
+ **Navigate the version chain:**
321
394
 
322
395
  ```ruby
323
396
  entry = article.audit_log_entries.updated_events.last
@@ -402,31 +475,58 @@ To save storage at the cost of reduced reification accuracy, switch to diff-only
402
475
  RailsAuditLog.store_snapshot = false
403
476
  ```
404
477
 
405
- ### Test helper
478
+ ### Separate audit database
479
+
480
+ Route all audit writes to a dedicated database by setting `connects_to` in an initializer. The engine applies it to `AuditLogEntry` at boot:
481
+
482
+ ```ruby
483
+ # config/initializers/rails_audit_log.rb
484
+ RailsAuditLog.connects_to = {
485
+ database: { writing: :audit_log, reading: :audit_log }
486
+ }
487
+ ```
488
+
489
+ 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.
490
+
491
+ ### Test helpers
406
492
 
407
- `without_audit_log` silences audit tracking inside the block useful in FactoryBot factories and seed data to avoid noise in the audit trail:
493
+ **`RailsAuditLog::TestHelpers`** silences audit tracking inside a block; useful in FactoryBot factories and seed data:
408
494
 
409
495
  ```ruby
410
- # spec/rails_helper.rb (or spec/support/factory_helpers.rb)
496
+ # spec/rails_helper.rb
411
497
  require "rails_audit_log/test_helpers"
412
498
 
413
499
  RSpec.configure do |config|
414
500
  config.include RailsAuditLog::TestHelpers
415
501
  end
502
+ ```
416
503
 
417
- # Or include directly in FactoryBot definitions:
418
- FactoryBot.define do
419
- factory :post do
420
- after(:create) { |p| without_audit_log { p.update!(cached_at: Time.current) } }
421
- end
422
- end
504
+ ```ruby
505
+ # Or in a FactoryBot factory:
506
+ after(:create) { |p| without_audit_log { p.update!(cached_at: Time.current) } }
423
507
  ```
424
508
 
425
509
  `without_audit_log` is a prefix-free wrapper around `RailsAuditLog.disable` — thread-safe and restores tracking even if the block raises.
426
510
 
427
- ### Minitest assertions
511
+ **`RailsAuditLog::Matchers`** (RSpec) — add to `spec/rails_helper.rb`:
512
+
513
+ ```ruby
514
+ require "rails_audit_log/matchers"
428
515
 
429
- Add to your `test/test_helper.rb`:
516
+ RSpec.configure do |config|
517
+ config.include RailsAuditLog::Matchers
518
+ end
519
+ ```
520
+
521
+ ```ruby
522
+ expect(post).to have_audit_log_entry
523
+ expect(post).to have_audit_log_entry(:update)
524
+ expect(post).to have_audit_log_entry(:update).touching(:title)
525
+
526
+ expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update).touching(:title)
527
+ ```
528
+
529
+ **`RailsAuditLog::MinitestAssertions`** — add to `test/test_helper.rb`:
430
530
 
431
531
  ```ruby
432
532
  require "rails_audit_log/minitest_assertions"
@@ -436,51 +536,180 @@ class ActiveSupport::TestCase
436
536
  end
437
537
  ```
438
538
 
439
- Then use the assertions in any test:
440
-
441
539
  ```ruby
442
- assert_audit_log_entry post # any entry
443
- assert_audit_log_entry post, event: :update # update entry
444
- assert_audit_log_entry post, event: :update, touching: :title # touching title
445
- refute_audit_log_entry post, event: :update # no update entry
446
- assert_audit_log_entry post, event: :update, message: "custom" # custom failure message
540
+ assert_audit_log_entry post, event: :update, touching: :title
541
+ refute_audit_log_entry post, event: :destroy
542
+ ```
543
+
544
+ ## Stability and versioning
545
+
546
+ [↑ Table of contents](#table-of-contents)
547
+
548
+ `rails_audit_log` follows [Semantic Versioning](https://semver.org/) from 1.0.0. The public API is everything documented in this README. Anything not listed here is internal and may change between minor versions.
549
+
550
+ | Version bump | Meaning |
551
+ |---|---|
552
+ | **Patch** (1.0.x) | Bug fixes only — no API changes |
553
+ | **Minor** (1.x.0) | New features, backward-compatible |
554
+ | **Major** (x.0.0) | Breaking changes with a documented migration path |
555
+
556
+ ### Public API surface
557
+
558
+ **Module-level** — `RailsAuditLog`
559
+
560
+ | Method / accessor | Purpose |
561
+ |---|---|
562
+ | `.configure { \|config\| }` | Block-style configuration |
563
+ | `.with_actor(actor) { }` | Set whodunnit context for a block |
564
+ | `.disable { }` | Suppress all audit writes for a block |
565
+ | `.enabled?` | Whether audit writes are active |
566
+ | `.audit_log_reason(value) { }` | Attach a free-text reason for a block |
567
+ | `.batch_audit { }` | Buffer writes and flush with a single `insert_all!` |
568
+ | `.version_at(record, time)` | Reconstruct record state at a point in time |
569
+ | `.authenticate { }` | Gate web dashboard access |
570
+ | `.ignored_attributes=` | Global columns excluded from all models |
571
+ | `.store_snapshot=` | Store full object snapshot alongside diffs |
572
+ | `.capture_request_metadata=` | Capture `remote_ip` and `user_agent` |
573
+ | `.version_limit=` | Global entry cap per record |
574
+ | `.async=` | Write audit entries via `WriteAuditLogJob` |
575
+ | `.connects_to=` | Route `AuditLogEntry` to a separate database |
576
+ | `.page_size=` | Entries per page in the web dashboard |
577
+ | `.whodunnit_display=` | Proc for actor display name snapshot |
578
+ | `.retention_period=` | _(1.1.0)_ Global time-based TTL |
579
+
580
+ **Concerns**
581
+
582
+ | Class | Include in | Key methods |
583
+ |---|---|---|
584
+ | `RailsAuditLog::Auditable` | ActiveRecord models | `audit_log(only:, ignore:, meta:, associations:, version_limit:, async:)`, `skip_audit_log { }` |
585
+ | `RailsAuditLog::Controller` | ActionController | `audit_log_actor { }` |
586
+
587
+ **Model — `RailsAuditLog::AuditLogEntry`**
588
+
589
+ Scopes: `created_events`, `updated_events`, `destroyed_events`, `by_actor`, `for_resource`, `since`, `until`, `touching`, `slim`, `for_period`
590
+
591
+ Instance methods: `reify`, `previous`, `next`, `diff`, `changed_attributes`, `actor`
592
+
593
+ Constants: `EVENTS`, `BLOB_COLUMNS`, `PERIODS`
594
+
595
+ **Testing helpers**
596
+
597
+ | Module | Require | Usage |
598
+ |---|---|---|
599
+ | `RailsAuditLog::Matchers` | `require "rails_audit_log/matchers"` | RSpec: `have_audit_log_entry`, `create_audit_log_entry` |
600
+ | `RailsAuditLog::MinitestAssertions` | `require "rails_audit_log/minitest_assertions"` | Minitest: `assert_audit_log_entry`, `refute_audit_log_entry` |
601
+ | `RailsAuditLog::TestHelpers` | `require "rails_audit_log/test_helpers"` | `without_audit_log { }` |
602
+
603
+ **Jobs** (enqueued internally; configure via ActiveJob)
604
+
605
+ `RailsAuditLog::WriteAuditLogJob` — do not instantiate directly; enqueued when `async: true` is set.
606
+
607
+ **Generators**
608
+
609
+ `rails generate rails_audit_log:install` · `rails generate rails_audit_log:initializer`
610
+
611
+ ---
612
+
613
+ ## Migrating from PaperTrail
614
+
615
+ [↑ Table of contents](#table-of-contents)
616
+
617
+ Run the migration generator to produce a timestamped data migration:
618
+
619
+ ```bash
620
+ bin/rails generate rails_audit_log:migrate_from_paper_trail
621
+ bin/rails db:migrate
447
622
  ```
448
623
 
449
- ### RSpec matchers
624
+ The generated migration reads every row from PaperTrail's `versions` table and inserts it into `audit_log_entries`.
625
+
626
+ ### Column mapping
627
+
628
+ | PaperTrail `versions` | `audit_log_entries` | Notes |
629
+ |---|---|---|
630
+ | `item_type` | `item_type` | Direct copy |
631
+ | `item_id` | `item_id` | Direct copy |
632
+ | `event` | `event` | `create` / `update` / `destroy` only; other values are skipped |
633
+ | `object_changes` | `object_changes` | YAML or JSON → JSON (see below) |
634
+ | `object` | `object` | YAML or JSON → JSON (see below) |
635
+ | `whodunnit` | `whodunnit_snapshot` | PaperTrail stores actor as a plain string |
636
+ | `created_at` | `created_at` | Direct copy |
637
+ | — | `actor_type` / `actor_id` | **Not populated** — cannot be inferred from a string `whodunnit` |
450
638
 
451
- Add to your `spec/rails_helper.rb` (or `spec_helper.rb`):
639
+ ### YAML and JSON serialization
640
+
641
+ PaperTrail serializes `object` and `object_changes` as **YAML** by default and as **JSON** when `PaperTrail.serializer = PaperTrail::Serializers::JSON` is configured. The migration handles both transparently — it tries JSON first, then falls back to YAML (with a permissive class list for the Ruby types PaperTrail commonly serializes).
642
+
643
+ ### Compatibility shim for gradual migration
644
+
645
+ If you need your codebase to keep using PaperTrail's API while migrating, include `RailsAuditLog::PaperTrailCompat` alongside `Auditable`:
452
646
 
453
647
  ```ruby
454
- require "rails_audit_log/matchers"
648
+ require "rails_audit_log/paper_trail_compat"
455
649
 
456
- RSpec.configure do |config|
457
- config.include RailsAuditLog::Matchers
650
+ class Article < ApplicationRecord
651
+ include RailsAuditLog::Auditable
652
+ include RailsAuditLog::PaperTrailCompat
458
653
  end
459
654
  ```
460
655
 
461
- Then use the matchers in any spec:
656
+ This adds the familiar PaperTrail surface:
462
657
 
463
658
  ```ruby
464
- # Assert a record has a matching audit entry
465
- expect(post).to have_audit_log_entry
466
- expect(post).to have_audit_log_entry(:update)
467
- expect(post).to have_audit_log_entry(:update).touching(:title)
468
-
469
- # Assert a block creates a matching audit entry
470
- expect { post.update!(title: "New") }.to create_audit_log_entry
471
- expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update)
472
- expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update).touching(:title)
659
+ article.versions # audit_log_entries ordered oldest-first
660
+ article.paper_trail.version # most recent AuditLogEntry
661
+ article.paper_trail.previous_version # reconstructed previous state (reify)
662
+ article.paper_trail.originator # whodunnit_snapshot string
663
+ article.paper_trail.version_at(1.week.ago) # reconstructed state at a point in time
473
664
  ```
474
665
 
666
+ `AuditLogEntry#reify` already matches PaperTrail's `Version#reify` — no additional alias needed.
667
+
668
+ ### What is not migrated
669
+
670
+ - `versions` rows with an `event` value outside `create`, `update`, `destroy` (custom events added by some apps)
671
+ - `actor_type` and `actor_id` — PaperTrail's `whodunnit` is a plain string and does not identify the actor model
672
+
673
+ ---
674
+
675
+ ## Companion gems
676
+
677
+ [↑ Table of contents](#table-of-contents)
678
+
679
+ > Coming in the 1.x series
680
+
681
+ | Gem | What it adds |
682
+ |---|---|
683
+ | `rails_audit_log-graphql` | Mountable GraphQL endpoint at `/audit/graphql` — queryable audit trail for API-first apps without forcing `graphql-ruby` on users who don't need it |
684
+
685
+ Each companion gem declares `rails_audit_log` as a dependency so you only add what you use.
686
+
475
687
  ## Requirements
476
688
 
689
+ [↑ Table of contents](#table-of-contents)
690
+
477
691
  - Ruby >= 3.3
478
692
  - Rails >= 7.2
479
693
 
694
+ ## Performance
695
+
696
+ [↑ Table of contents](#table-of-contents)
697
+
698
+ See [BENCHMARKS.md](BENCHMARKS.md) for write throughput, `batch_audit` gains, query performance, storage efficiency, and notes on comparing against PaperTrail. To run the suite locally:
699
+
700
+ ```bash
701
+ bundle exec rake dev:setup
702
+ bundle exec rake benchmark
703
+ ```
704
+
480
705
  ## Contributing
481
706
 
707
+ [↑ Table of contents](#table-of-contents)
708
+
482
709
  Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/rails_audit_log).
483
710
 
484
711
  ## License
485
712
 
713
+ [↑ Table of contents](#table-of-contents)
714
+
486
715
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -16,6 +16,11 @@ end
16
16
 
17
17
  task default: ["bundle:audit:update", "bundle:audit:check", :rubocop, :zeitwerk, :spec]
18
18
 
19
+ desc "Run the performance benchmark suite (requires dev:setup first)"
20
+ task :benchmark do
21
+ sh "bundle exec ruby benchmarks/suite.rb"
22
+ end
23
+
19
24
  # Development database tasks (operate on spec/dummy)
20
25
  namespace :dev do
21
26
  dummy_env = { "RAILS_ENV" => "development" }
@@ -1,4 +1,24 @@
1
1
  module RailsAuditLog
2
+ # Include in any ActiveRecord model to automatically track +create+, +update+,
3
+ # and +destroy+ events as {AuditLogEntry} records.
4
+ #
5
+ # == Basic usage
6
+ #
7
+ # class Article < ApplicationRecord
8
+ # include RailsAuditLog::Auditable
9
+ # end
10
+ #
11
+ # == Configuring tracking
12
+ #
13
+ # class Article < ApplicationRecord
14
+ # include RailsAuditLog::Auditable
15
+ # audit_log only: %i[title body],
16
+ # meta: { tenant_id: -> { Current.tenant_id } },
17
+ # version_limit: 50,
18
+ # async: true
19
+ # end
20
+ #
21
+ # Adds a polymorphic +has_many :audit_log_entries+ association to the model.
2
22
  module Auditable
3
23
  extend ActiveSupport::Concern
4
24
 
@@ -12,6 +32,8 @@ module RailsAuditLog
12
32
 
13
33
  _warn_if_audit_table_missing
14
34
 
35
+ # All {AuditLogEntry} records for this object, newest first by default.
36
+ # Destroyed when the object is destroyed.
15
37
  has_many :audit_log_entries,
16
38
  class_name: "RailsAuditLog::AuditLogEntry",
17
39
  as: :item,
@@ -54,6 +76,7 @@ module RailsAuditLog
54
76
  end
55
77
 
56
78
  class_methods do
79
+ # @api private
57
80
  def _warn_if_audit_table_missing
58
81
  return if connection.table_exists?("audit_log_entries")
59
82
 
@@ -66,6 +89,33 @@ module RailsAuditLog
66
89
  # DB not reachable during this phase (e.g. before db:create) — skip the check
67
90
  end
68
91
 
92
+ # Configures auditing options for this model. Call once in the class body
93
+ # after +include RailsAuditLog::Auditable+.
94
+ #
95
+ # @param only [Array<Symbol>, nil] whitelist of attributes to track;
96
+ # when set, all other attributes are ignored regardless of +ignore:+
97
+ # @param ignore [Array<Symbol>, nil] additional attributes to exclude on
98
+ # top of {RailsAuditLog.ignored_attributes}; ignored when +only:+ is set
99
+ # @param meta [Hash{Symbol => Proc}, nil] per-entry metadata; each value
100
+ # is a lambda called at write time — zero-argument lambdas receive no
101
+ # arguments, one-argument lambdas receive the record instance
102
+ # @param associations [Boolean, Array<Symbol>, nil] when +true+, tracks
103
+ # all +has_many+ and +has_and_belongs_to_many+ associations; pass an
104
+ # array of association names to track only specific ones
105
+ # @param version_limit [Integer, nil] maximum number of entries to retain
106
+ # per record; oldest entries are pruned after each write; overrides
107
+ # {RailsAuditLog.version_limit} for this model
108
+ # @param async [Boolean, nil] when +true+, writes are dispatched via
109
+ # +WriteAuditLogJob+; overrides {RailsAuditLog.async} for this model
110
+ # @return [void]
111
+ # @example
112
+ # class Article < ApplicationRecord
113
+ # include RailsAuditLog::Auditable
114
+ # audit_log only: %i[title body published_at],
115
+ # meta: { tenant_id: -> { Current.tenant_id } },
116
+ # associations: %i[tags],
117
+ # version_limit: 100
118
+ # end
69
119
  def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, async: nil)
70
120
  self._audit_log_only = only.map(&:to_s) if only
71
121
  self._audit_log_ignore = ignore.map(&:to_s) if ignore
@@ -76,6 +126,13 @@ module RailsAuditLog
76
126
  end
77
127
  end
78
128
 
129
+ # Executes the block with audit logging disabled for this record's writes.
130
+ # A convenience wrapper around {RailsAuditLog.disable}.
131
+ #
132
+ # @yield executes the block without recording any audit entries
133
+ # @return [Object] the return value of the block
134
+ # @example Skip auditing during a bulk update
135
+ # post.skip_audit_log { post.update!(cached_at: Time.current) }
79
136
  def skip_audit_log
80
137
  RailsAuditLog.disable { yield }
81
138
  end
@@ -1,4 +1,21 @@
1
1
  module RailsAuditLog
2
+ # Include in +ApplicationController+ (or any controller) to automatically
3
+ # set and clear the current actor for every request, so that audit entries
4
+ # written during the request are tagged with the signed-in user.
5
+ #
6
+ # == Usage
7
+ #
8
+ # class ApplicationController < ActionController::Base
9
+ # include RailsAuditLog::Controller
10
+ # audit_log_actor { current_user }
11
+ # end
12
+ #
13
+ # The block passed to {audit_log_actor} is evaluated in controller instance
14
+ # context on every request via a +before_action+. The actor is cleared in an
15
+ # +after_action+ so it never leaks between requests.
16
+ #
17
+ # Request metadata (+remote_ip+, +user_agent+) is captured automatically when
18
+ # {RailsAuditLog.capture_request_metadata} is +true+.
2
19
  module Controller
3
20
  extend ActiveSupport::Concern
4
21
 
@@ -10,10 +27,22 @@ module RailsAuditLog
10
27
  end
11
28
 
12
29
  class_methods do
30
+ # Registers a block that resolves the current actor for each request.
31
+ # The block is evaluated in controller instance context, so any helper
32
+ # method available to the controller (e.g. +current_user+) can be used.
33
+ #
34
+ # @yield block evaluated in controller context; should return the actor
35
+ # @return [void]
36
+ # @example
37
+ # audit_log_actor { current_user }
38
+ #
39
+ # # With a one-argument lambda style (actor = the controller instance)
40
+ # audit_log_actor { |c| c.current_user }
13
41
  def audit_log_actor(&block)
14
42
  @audit_log_actor_block = block
15
43
  end
16
44
 
45
+ # @api private
17
46
  def audit_log_actor_block
18
47
  @audit_log_actor_block
19
48
  end
@@ -1,4 +1,5 @@
1
1
  module RailsAuditLog
2
+ # @api private
2
3
  class ApplicationController < ActionController::Base
3
4
  include Pagy::Method
4
5
 
@@ -1,4 +1,5 @@
1
1
  module RailsAuditLog
2
+ # @api private
2
3
  class AuditLogEntriesController < ApplicationController
3
4
  def index
4
5
  set_filters
@@ -1,4 +1,5 @@
1
1
  module RailsAuditLog
2
+ # @api private
2
3
  class ResourcesController < ApplicationController
3
4
  def show
4
5
  @item_type = params[:item_type]
@@ -1,4 +1,5 @@
1
1
  module RailsAuditLog
2
+ # @api private
2
3
  module ApplicationHelper
3
4
  def format_diff_value(value)
4
5
  return "—" if value.nil?
@@ -1,4 +1,5 @@
1
1
  module RailsAuditLog
2
+ # @api private
2
3
  class ApplicationJob < ActiveJob::Base
3
4
  end
4
5
  end
@@ -1,4 +1,5 @@
1
1
  module RailsAuditLog
2
+ # @api private
2
3
  class ApplicationRecord < ActiveRecord::Base
3
4
  self.abstract_class = true
4
5
  end