paper_trail_diff 0.11.0 → 0.13.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/CHANGELOG.md +144 -0
- data/QUICKSTART.md +149 -79
- data/README.md +179 -1367
- data/docs/architecture.md +88 -0
- data/docs/performance.md +309 -0
- data/docs/reference.md +977 -0
- data/docs/testing.md +146 -0
- data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +2 -12
- data/lib/paper_trail_diff/activity_boundary.rb +20 -8
- data/lib/paper_trail_diff/activity_child_candidate_loader.rb +29 -0
- data/lib/paper_trail_diff/activity_collection_event_applier.rb +10 -7
- data/lib/paper_trail_diff/activity_collection_route_updater.rb +1 -11
- data/lib/paper_trail_diff/activity_event_record_normalizer.rb +4 -0
- data/lib/paper_trail_diff/activity_event_record_resolver.rb +66 -6
- data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +6 -4
- data/lib/paper_trail_diff/activity_grouping.rb +12 -0
- data/lib/paper_trail_diff/activity_history.rb +9 -3
- data/lib/paper_trail_diff/activity_relationship.rb +2 -0
- data/lib/paper_trail_diff/activity_root_snapshot_refresher.rb +1 -0
- data/lib/paper_trail_diff/activity_snapshot_sequence.rb +9 -2
- data/lib/paper_trail_diff/activity_timeline_builder.rb +39 -35
- data/lib/paper_trail_diff/activity_version_collector.rb +27 -18
- data/lib/paper_trail_diff/analysis.rb +9 -4
- data/lib/paper_trail_diff/analysis_batch.rb +28 -7
- data/lib/paper_trail_diff/association_traversal.rb +23 -0
- data/lib/paper_trail_diff/batch_boundary_resolver.rb +18 -26
- data/lib/paper_trail_diff/batch_safety.rb +55 -0
- data/lib/paper_trail_diff/batched_root_analyzer.rb +14 -5
- data/lib/paper_trail_diff/batched_root_versions.rb +9 -37
- data/lib/paper_trail_diff/branch_snapshot_refresher.rb +7 -8
- data/lib/paper_trail_diff/deleted_analysis_batch.rb +93 -0
- data/lib/paper_trail_diff/direct_record_state_loader.rb +81 -0
- data/lib/paper_trail_diff/endpoint.rb +10 -0
- data/lib/paper_trail_diff/engine.rb +4 -2
- data/lib/paper_trail_diff/errors.rb +9 -0
- data/lib/paper_trail_diff/historical_association_reifier.rb +26 -1
- data/lib/paper_trail_diff/historical_filter.rb +181 -0
- data/lib/paper_trail_diff/historical_snapshot_store.rb +51 -35
- data/lib/paper_trail_diff/instrumentation.rb +2 -2
- data/lib/paper_trail_diff/nested_comparator.rb +59 -24
- data/lib/paper_trail_diff/paper_trail_adapter.rb +109 -55
- data/lib/paper_trail_diff/prepared_activity_index.rb +121 -0
- data/lib/paper_trail_diff/prepared_edge_loader.rb +11 -11
- data/lib/paper_trail_diff/prepared_history.rb +21 -30
- data/lib/paper_trail_diff/prepared_history_loader.rb +14 -2
- data/lib/paper_trail_diff/prepared_record_index.rb +102 -29
- data/lib/paper_trail_diff/prepared_snapshot_builder.rb +64 -0
- data/lib/paper_trail_diff/prepared_state_association_reader.rb +77 -0
- data/lib/paper_trail_diff/root_version_plan.rb +25 -2
- data/lib/paper_trail_diff/root_version_selection.rb +16 -5
- data/lib/paper_trail_diff/scoped_analysis.rb +17 -2
- data/lib/paper_trail_diff/scoped_analysis_builder.rb +47 -0
- data/lib/paper_trail_diff/scoped_root_selection.rb +106 -13
- data/lib/paper_trail_diff/snapshot.rb +9 -0
- data/lib/paper_trail_diff/snapshot_normalizer.rb +41 -2
- data/lib/paper_trail_diff/step.rb +10 -5
- data/lib/paper_trail_diff/stored_time_precision.rb +68 -0
- data/lib/paper_trail_diff/support.rb +13 -3
- data/lib/paper_trail_diff/time_activity_timeline_builder.rb +19 -7
- data/lib/paper_trail_diff/time_range.rb +22 -2
- data/lib/paper_trail_diff/time_version_range.rb +1 -15
- data/lib/paper_trail_diff/timeline_builder.rb +5 -4
- data/lib/paper_trail_diff/timeline_range.rb +25 -43
- data/lib/paper_trail_diff/timeline_snapshot_provider.rb +13 -2
- data/lib/paper_trail_diff/traversal.rb +1 -1
- data/lib/paper_trail_diff/value_objects.rb +66 -0
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff/version_association_candidate_scope.rb +1 -1
- data/lib/paper_trail_diff/version_boundary.rb +48 -0
- data/lib/paper_trail_diff/version_query.rb +33 -0
- data/lib/paper_trail_diff/version_range.rb +10 -38
- data/lib/paper_trail_diff.rb +81 -43
- data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +0 -3
- data/sig/generated/paper_trail_diff/activity_boundary.rbs +9 -4
- data/sig/generated/paper_trail_diff/activity_child_candidate_loader.rbs +11 -0
- data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +4 -4
- data/sig/generated/paper_trail_diff/activity_collection_route_updater.rbs +0 -3
- data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +19 -3
- data/sig/generated/paper_trail_diff/activity_event_snapshot_refresher.rbs +2 -2
- data/sig/generated/paper_trail_diff/activity_grouping.rbs +3 -0
- data/sig/generated/paper_trail_diff/activity_history.rbs +6 -2
- data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +7 -2
- data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +8 -3
- data/sig/generated/paper_trail_diff/activity_version_collector.rbs +4 -8
- data/sig/generated/paper_trail_diff/analysis.rbs +2 -2
- data/sig/generated/paper_trail_diff/analysis_batch.rbs +14 -3
- data/sig/generated/paper_trail_diff/association_traversal.rbs +11 -0
- data/sig/generated/paper_trail_diff/batch_boundary_resolver.rbs +7 -10
- data/sig/generated/paper_trail_diff/batch_safety.rbs +24 -0
- data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +8 -2
- data/sig/generated/paper_trail_diff/batched_root_versions.rbs +4 -16
- data/sig/generated/paper_trail_diff/branch_snapshot_refresher.rbs +2 -2
- data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +44 -0
- data/sig/generated/paper_trail_diff/direct_record_state_loader.rbs +42 -0
- data/sig/generated/paper_trail_diff/endpoint.rbs +5 -0
- data/sig/generated/paper_trail_diff/engine.rbs +4 -2
- data/sig/generated/paper_trail_diff/errors.rbs +12 -0
- data/sig/generated/paper_trail_diff/historical_association_reifier.rbs +8 -0
- data/sig/generated/paper_trail_diff/historical_filter.rbs +75 -0
- data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +17 -4
- data/sig/generated/paper_trail_diff/nested_comparator.rbs +31 -18
- data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +36 -15
- data/sig/generated/paper_trail_diff/prepared_activity_index.rbs +56 -0
- data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +3 -3
- data/sig/generated/paper_trail_diff/prepared_history.rbs +12 -14
- data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +2 -0
- data/sig/generated/paper_trail_diff/prepared_record_index.rbs +43 -6
- data/sig/generated/paper_trail_diff/prepared_snapshot_builder.rbs +24 -0
- data/sig/generated/paper_trail_diff/prepared_state_association_reader.rbs +33 -0
- data/sig/generated/paper_trail_diff/root_version_plan.rbs +15 -2
- data/sig/generated/paper_trail_diff/root_version_selection.rbs +8 -1
- data/sig/generated/paper_trail_diff/scoped_analysis.rbs +14 -2
- data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +22 -0
- data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +46 -6
- data/sig/generated/paper_trail_diff/snapshot.rbs +4 -0
- data/sig/generated/paper_trail_diff/snapshot_normalizer.rbs +13 -0
- data/sig/generated/paper_trail_diff/step.rbs +8 -2
- data/sig/generated/paper_trail_diff/stored_time_precision.rbs +36 -0
- data/sig/generated/paper_trail_diff/support.rbs +7 -0
- data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +7 -2
- data/sig/generated/paper_trail_diff/time_range.rbs +9 -0
- data/sig/generated/paper_trail_diff/time_version_range.rbs +0 -6
- data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
- data/sig/generated/paper_trail_diff/timeline_range.rbs +10 -14
- data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +2 -0
- data/sig/generated/paper_trail_diff/value_objects.rbs +45 -0
- data/sig/generated/paper_trail_diff/version_boundary.rbs +23 -0
- data/sig/generated/paper_trail_diff/version_query.rbs +15 -0
- data/sig/generated/paper_trail_diff/version_range.rbs +5 -14
- data/sig/generated/paper_trail_diff.rbs +37 -27
- metadata +31 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4e484cc975317987834dd585f1bc5432985575c66b473b1db5c31b3feb4995d
|
|
4
|
+
data.tar.gz: 127f6d1e5ae63a839be293ea22beb6493108b90f3cbc71d34bdf1814613a54ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d65a4239a738367dd8b0536670b2f00cecfae7283db328c87308323fd19d8080fa13eed2a8910d7a0e67a2380f286c65d1b4c943a72bd1903f3d1695b2dfc18
|
|
7
|
+
data.tar.gz: f316b5892247e31b58a2531dd86073c812327e27bd36f5d4f020efd47d543e280b94ab513467d58a6445173106b89c1378716df471077ee7f1e35fe4bd0999d9
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,150 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file. The
|
|
4
4
|
project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.13.0] - 2026-10-28
|
|
7
|
+
|
|
8
|
+
### Breaking changes
|
|
9
|
+
|
|
10
|
+
- `:last` is no longer accepted as an endpoint. It named the last version, whose
|
|
11
|
+
recorded state is the one *before* the latest change, so ranges ending there
|
|
12
|
+
silently left that change out. Omit `to:` to end at current state, or pass a
|
|
13
|
+
version. Passing `:last` raises with that advice.
|
|
14
|
+
- `analyze_many` without `within:` now ends each record's history at its current
|
|
15
|
+
state, so the latest change is included. It previously stopped at the last
|
|
16
|
+
version.
|
|
17
|
+
- Batch methods reject scoped associations, default-scoped targets, and
|
|
18
|
+
`has_many :through` associations whose source is not a `belongs_to`, because
|
|
19
|
+
they need queries per record. The error names the association. Pass
|
|
20
|
+
`allow_expensive: true` to accept that cost.
|
|
21
|
+
- `Step#to_h` returns `{ from:, to:, diff: }`, the same shape as
|
|
22
|
+
`ActivityStep#to_h`, with boundary hashes on both sides. Read the version ids
|
|
23
|
+
from `from[:version_id]` and `to[:version_id]` instead of `from_version_id`
|
|
24
|
+
and `to_version_id`; `metadata: true` adds details to both boundaries.
|
|
25
|
+
- `analyze_many` takes only an array of records. Use `analyze_scope` with a
|
|
26
|
+
model or relation; `analyze_many(scope:, limit:)` and the scoped options it
|
|
27
|
+
accepted have been removed.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `report(scope, since:, limit:)` for recent activity through current state.
|
|
32
|
+
- `historical_filter:` on `analyze_scope` and `report`, selecting records by
|
|
33
|
+
values they had at any point in the window: their state when it started and
|
|
34
|
+
after each change inside it. Pass an attribute hash, which reads stored values
|
|
35
|
+
without reifying, or a callable. `max_filter_versions:` (default 10,000) caps
|
|
36
|
+
the versions it loads and raises `FilterLimitExceededError` beyond that.
|
|
37
|
+
- `include_deleted: true` on `analyze_scope` and `report`, returning analyses of
|
|
38
|
+
deleted records in `deleted` (`nil` when no destroy version was recorded).
|
|
39
|
+
Existing and deleted records together count toward `limit:`.
|
|
40
|
+
- `ScopedAnalysis#roots`, the selected records in relation order.
|
|
41
|
+
- `timeline`, `activity_timeline`, and `analyze` default to `from: :first` and
|
|
42
|
+
`to: record`, so `timeline(article)` covers the whole history through the
|
|
43
|
+
current saved record.
|
|
44
|
+
- `source_boundary` on both step types names the event whose change a step
|
|
45
|
+
reports. For a transaction group it is the first event.
|
|
46
|
+
- `metadata: true` on `to_h` for boundaries, steps, and analyses adds author,
|
|
47
|
+
event, transaction ID, and record. `ActivityStep#to_h(snapshots: true)` and
|
|
48
|
+
`Analysis#to_h(snapshots: true)` include retained states.
|
|
49
|
+
- `group:` and `snapshots:` on `analyze_many` and `analyze_scope` with
|
|
50
|
+
`activity: true`.
|
|
51
|
+
- Explicit current-record `to:` endpoints on `timeline` and `analyze`.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- A `within:` window with no version after its last change closes on the current
|
|
56
|
+
record instead of raising `IncompleteTimeRangeError`. Nothing has been
|
|
57
|
+
recorded since that change, so the current record holds what it produced.
|
|
58
|
+
This also applies to windows in which only chosen associations changed.
|
|
59
|
+
`close_on: :current` is no longer needed and is still accepted.
|
|
60
|
+
- Live-ended HABTM activity is rejected only when a range actually ends at the
|
|
61
|
+
current record, including in batches.
|
|
62
|
+
- `compare` with `associations:` reads association state from the prepared
|
|
63
|
+
history `compare_many` uses, instead of PT-AT's timestamp-only reifier.
|
|
64
|
+
- Plain recorded attributes of root and direct-child records are read without
|
|
65
|
+
building a model instance per historical state. Models with initialization
|
|
66
|
+
callbacks, custom types or defaults, default scopes, or encrypted attributes
|
|
67
|
+
still use PaperTrail's `reify`.
|
|
68
|
+
- Batch activity reports share association discovery and prepared history
|
|
69
|
+
across records, and each analysis reuses its checkpoint snapshots, so standard
|
|
70
|
+
reports no longer add queries per record.
|
|
71
|
+
- Activity without changesets uses prepared successor states instead of a query
|
|
72
|
+
per event.
|
|
73
|
+
- `has_many :through` reconstruction visits only the requested source
|
|
74
|
+
association.
|
|
75
|
+
- Scope selection keeps candidate records in a database subquery when
|
|
76
|
+
`versions.item_id` and the primary key share a column type, reads joined rows
|
|
77
|
+
in pages of 250, and sorts `unreachable`.
|
|
78
|
+
- `snapshots: true` without `activity: true` raises `ConfigurationError`, as
|
|
79
|
+
`group:` already did, instead of being ignored.
|
|
80
|
+
- The README is now a set of recipes; the full API reference and architecture
|
|
81
|
+
notes are separate pages.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- Association timelines no longer raise `duplicate record identity` when
|
|
86
|
+
`versions.item_id` is a string column (PaperTrail's setup for UUID keys) and
|
|
87
|
+
associated models use integer keys.
|
|
88
|
+
- Columns listed in `has_paper_trail skip:` are left out of snapshots. PaperTrail
|
|
89
|
+
never stores them, so comparing history with the current record reported them
|
|
90
|
+
as changed from their default.
|
|
91
|
+
- `analyze_many(..., activity: true)` reports association activity for a record
|
|
92
|
+
with no version of its own in the window, matching `analyze`. It previously
|
|
93
|
+
returned an empty analysis.
|
|
94
|
+
- Histories stored with PaperTrail's JSON serializer or a `json`/`jsonb` object
|
|
95
|
+
column no longer report every timestamp as changed when compared with the
|
|
96
|
+
current record.
|
|
97
|
+
- Association changes at versions sharing a timestamp are placed by version id,
|
|
98
|
+
so `compare`, `compare_many`, and `timeline` no longer put them on the wrong
|
|
99
|
+
side of a boundary. Ties that ids cannot order raise
|
|
100
|
+
`AmbiguousVersionOrderError`.
|
|
101
|
+
- A window that closes on the record's deletion ends its `diff` and
|
|
102
|
+
`to_snapshot` at "no record". A record created and deleted inside the window
|
|
103
|
+
previously read as added.
|
|
104
|
+
- `compare_many` resolves `:first` to each record's earliest version by time,
|
|
105
|
+
not the lowest id, which differs for UUID version ids.
|
|
106
|
+
- `version_scope:` is honored on activity timelines that end at the current
|
|
107
|
+
record.
|
|
108
|
+
- Batch, single-record, and scoped activity results agree on the reporting
|
|
109
|
+
window and closing state.
|
|
110
|
+
- A lone creation or update in an activity window that ends at the current
|
|
111
|
+
record is reported.
|
|
112
|
+
- Joined relations count unique records against `limit:` without adding SQL
|
|
113
|
+
`DISTINCT`, so ordering by joined columns works on PostgreSQL and SQL Server.
|
|
114
|
+
- Nested results are fully frozen, including nested paths and empty results.
|
|
115
|
+
- Nested object diffs match string and symbol keys consistently and raise
|
|
116
|
+
`AmbiguousNestedKeyError` on collisions instead of losing a change.
|
|
117
|
+
|
|
118
|
+
### Development
|
|
119
|
+
|
|
120
|
+
- CI runs both suites on PostgreSQL 17, MySQL 8.4, and SQL Server 2022 alongside
|
|
121
|
+
the SQLite Ruby/PaperTrail matrix. Database adapters are optional development
|
|
122
|
+
dependencies.
|
|
123
|
+
- The type-check gate fails on Steep internal errors and checks public API call
|
|
124
|
+
examples.
|
|
125
|
+
- RuboCop runs with the rubocop-performance and rubocop-rspec plugins.
|
|
126
|
+
- Consistency specs check that every method agrees on seeded, generated
|
|
127
|
+
histories across schema and serializer variants.
|
|
128
|
+
- Mutation testing with mutant covers the comparison code, in CI and through
|
|
129
|
+
`rake mutant`. New specs close the gaps it found: attributes present on only
|
|
130
|
+
one side, a record removed, singular relationships added or removed, and
|
|
131
|
+
JSON values held in Hash, Array, or String subclasses.
|
|
132
|
+
|
|
133
|
+
## [0.12.0] - 2026-08-19
|
|
134
|
+
|
|
135
|
+
### Changed
|
|
136
|
+
|
|
137
|
+
- Report an array inside a JSON column by membership rather than as one opaque
|
|
138
|
+
value. `nested_changes` now returns a `PaperTrailDiff::ArrayChange` for a pair
|
|
139
|
+
of arrays, carrying `added`, `removed` and `reordered?` alongside the whole
|
|
140
|
+
`from` and `to` it already had. Position is deliberately not reported:
|
|
141
|
+
elements carry no identity, so an insertion at the front makes every later
|
|
142
|
+
index look changed and one insertion reads as several edits. Membership is
|
|
143
|
+
answerable without claiming any pairing. Matching is by value and respects
|
|
144
|
+
duplicates, so `["a", "a"]` to `["a"]` reports one removal. The one thing
|
|
145
|
+
membership cannot see -- the same elements in a new order -- is named rather
|
|
146
|
+
than passed over. An element that is itself an object is reported whole,
|
|
147
|
+
since saying which of its fields changed would require pairing before- and
|
|
148
|
+
after-elements that nothing in the value licenses.
|
|
149
|
+
|
|
6
150
|
## [0.11.0] - 2026-08-18
|
|
7
151
|
|
|
8
152
|
### Added
|
data/QUICKSTART.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# Quickstart
|
|
2
2
|
|
|
3
|
-
This guide
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This guide sets up PaperTrail, saves a few versions of an article, and reads
|
|
4
|
+
its changes with `paper_trail_diff`. You need Ruby, Bundler, and Rails installed.
|
|
5
|
+
The gem supports Ruby 3.1+ with PaperTrail 16 or 17; your Rails and PaperTrail
|
|
6
|
+
versions may require a newer Ruby.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
Already using PaperTrail? Skip the setup and use your own model in
|
|
9
|
+
[Create a little history](#3-create-a-little-history).
|
|
10
|
+
|
|
11
|
+
Otherwise, start with a new Rails application. Run the following examples in
|
|
12
|
+
order. Commands run in your terminal; Ruby examples run in the Rails console
|
|
13
|
+
unless a file path is shown:
|
|
9
14
|
|
|
10
15
|
```console
|
|
11
16
|
rails new diff-demo --minimal
|
|
@@ -27,13 +32,14 @@ bin/rails db:migrate
|
|
|
27
32
|
```
|
|
28
33
|
|
|
29
34
|
Skip the generator if the application already has a PaperTrail `versions`
|
|
30
|
-
table.
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
table. `--with-changes` adds `object_changes`, which PaperTrail uses for
|
|
36
|
+
`version.changeset`. Keep it if you want PaperTrail to store the changed values
|
|
37
|
+
for each event. This gem can also compare the full earlier records saved in
|
|
38
|
+
PaperTrail's `object` column.
|
|
33
39
|
|
|
34
40
|
## 2. Version a model
|
|
35
41
|
|
|
36
|
-
Create
|
|
42
|
+
Create an article model and tell PaperTrail to track its changes:
|
|
37
43
|
|
|
38
44
|
```console
|
|
39
45
|
bin/rails generate model Article title:string
|
|
@@ -47,7 +53,7 @@ class Article < ApplicationRecord
|
|
|
47
53
|
end
|
|
48
54
|
```
|
|
49
55
|
|
|
50
|
-
Restart the Rails console after changing
|
|
56
|
+
Restart the Rails console after changing dependencies, model configuration, or an initializer.
|
|
51
57
|
|
|
52
58
|
## 3. Create a little history
|
|
53
59
|
|
|
@@ -64,8 +70,8 @@ article.update!(title: "Final")
|
|
|
64
70
|
published_version = article.versions.last
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
PaperTrail saves the record's values **before** each change. Its public
|
|
74
|
+
`reify` method builds an earlier record from a version:
|
|
69
75
|
|
|
70
76
|
```ruby
|
|
71
77
|
draft_version.reify.title # => "Draft"
|
|
@@ -93,13 +99,18 @@ diff.to_h
|
|
|
93
99
|
# }
|
|
94
100
|
```
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
Pass the PaperTrail versions themselves to `compare`; you do not need to call
|
|
103
|
+
`reify` first. The result has one entry per changed attribute, with `from` and
|
|
104
|
+
`to` values. Unchanged attributes are left out.
|
|
105
|
+
|
|
106
|
+
`compare` reports the net difference between the two records. For every change
|
|
107
|
+
in between, use `timeline`, shown below. Reading a diff does not save or modify
|
|
108
|
+
any record.
|
|
98
109
|
|
|
99
110
|
### Compare a version with current state
|
|
100
111
|
|
|
101
|
-
Pass
|
|
102
|
-
|
|
112
|
+
Pass `article` instead of a second version to compare with its current saved
|
|
113
|
+
values:
|
|
103
114
|
|
|
104
115
|
<!-- executable:quickstart-current -->
|
|
105
116
|
```ruby
|
|
@@ -108,12 +119,13 @@ diff = PaperTrailDiff.compare(published_version, article)
|
|
|
108
119
|
diff.attributes["title"].to # => "Final"
|
|
109
120
|
```
|
|
110
121
|
|
|
111
|
-
The
|
|
112
|
-
|
|
122
|
+
The article must still exist in the database and have no unsaved changes.
|
|
123
|
+
The gem reloads it before comparing. `article.versions.last` is different: it
|
|
124
|
+
contains the values from before the latest recorded change.
|
|
113
125
|
|
|
114
|
-
## 5.
|
|
126
|
+
## 5. Read changes between versions
|
|
115
127
|
|
|
116
|
-
|
|
128
|
+
Save another change so PaperTrail records the previous title, `"Final"`:
|
|
117
129
|
|
|
118
130
|
<!-- executable:quickstart-timeline-state -->
|
|
119
131
|
```ruby
|
|
@@ -121,7 +133,7 @@ article.update!(title: "Archived")
|
|
|
121
133
|
final_version = article.versions.last # reifies to "Final"
|
|
122
134
|
```
|
|
123
135
|
|
|
124
|
-
|
|
136
|
+
Read the changes between each pair of article versions:
|
|
125
137
|
|
|
126
138
|
<!-- executable:quickstart-timeline -->
|
|
127
139
|
```ruby
|
|
@@ -140,13 +152,17 @@ end
|
|
|
140
152
|
visible_steps = steps.reject(&:empty?)
|
|
141
153
|
```
|
|
142
154
|
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
Each item in `steps` has a `diff`, just like the comparison above. Steps with
|
|
156
|
+
no reported changes are kept; `reject(&:empty?)` removes them for display.
|
|
145
157
|
|
|
146
|
-
|
|
158
|
+
Without `from:` and `to:`, `PaperTrailDiff.timeline(article)` reads the whole
|
|
159
|
+
history through the current saved article, including the change to
|
|
160
|
+
`"Archived"`. Pass a version as `to:` when the result should stay the same after
|
|
161
|
+
later edits.
|
|
147
162
|
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
### Select changes by time
|
|
164
|
+
|
|
165
|
+
Use `within:` to choose changes by their PaperTrail version's `created_at`:
|
|
150
166
|
|
|
151
167
|
<!-- executable:quickstart-time-range -->
|
|
152
168
|
```ruby
|
|
@@ -160,28 +176,72 @@ end
|
|
|
160
176
|
# => [["Draft", "Published"], ["Published", "Final"]]
|
|
161
177
|
```
|
|
162
178
|
|
|
163
|
-
|
|
164
|
-
`
|
|
165
|
-
boundary needed to reveal the last selected mutation. PaperTrail versions are
|
|
166
|
-
pre-change snapshots, so a range containing mutations must have a later root
|
|
167
|
-
version. The gem raises `PaperTrailDiff::IncompleteTimeRangeError` instead of
|
|
168
|
-
assuming that the current record is the missing endpoint.
|
|
179
|
+
Ruby's `...` excludes the end of the range. Here the selected versions are
|
|
180
|
+
`draft_version` and `published_version`.
|
|
169
181
|
|
|
170
|
-
|
|
182
|
+
Each PaperTrail version stores the values **before** its own change, so the
|
|
183
|
+
values after the window's last change are in the next version, `final_version`.
|
|
184
|
+
The gem reads that version even though it is outside the window. When no later
|
|
185
|
+
version exists, nothing has been recorded since, and the gem reads the current
|
|
186
|
+
saved article instead.
|
|
171
187
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
### Report through now
|
|
189
|
+
|
|
190
|
+
To report on many articles, pass a model or ActiveRecord relation to `report`.
|
|
191
|
+
It includes changes from `since:` through now:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
report = PaperTrailDiff.report(
|
|
195
|
+
Article.all, since: 1.day.ago, limit: 100, include_deleted: true
|
|
196
|
+
)
|
|
197
|
+
report.analyses # Hash: [model name, ID string] => report for an existing record
|
|
198
|
+
report.deleted # Same keys for deleted records; nil if no destroy version was saved
|
|
199
|
+
report.analyses.each_value { |analysis| analysis.diff.to_h }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`limit: 100` is a safety ceiling. If more than 100 existing and deleted articles
|
|
203
|
+
are selected, the call raises instead of returning a partial report. Omit
|
|
204
|
+
`include_deleted:` if you only need articles that still exist.
|
|
205
|
+
|
|
206
|
+
An article needs its own PaperTrail version in the time window to be selected.
|
|
207
|
+
Editing a comment alone does not select its article. A relation's `where`
|
|
208
|
+
conditions check current values; they cannot check a deleted article's values.
|
|
209
|
+
|
|
210
|
+
For one article, use a window ending now:
|
|
211
|
+
|
|
212
|
+
<!-- executable:quickstart-current-window -->
|
|
213
|
+
```ruby
|
|
214
|
+
current_steps = PaperTrailDiff.timeline(article, within: 1.day.ago..Time.current)
|
|
215
|
+
```
|
|
179
216
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
own boundary and can end at an explicitly supplied current record.
|
|
217
|
+
The same `within:` option works on `activity_timeline`, `analyze`,
|
|
218
|
+
`analyze_many`, and `analyze_scope`.
|
|
183
219
|
|
|
184
|
-
##
|
|
220
|
+
## 6. Choose a method
|
|
221
|
+
|
|
222
|
+
| What you need | Method |
|
|
223
|
+
| --- | --- |
|
|
224
|
+
| Net difference between two versions, or a version and the current record | `compare(from, to)` |
|
|
225
|
+
| The same for a list of `{ from:, to: }` pairs, one per record | `compare_many(pairs)` |
|
|
226
|
+
| Each change to one record, version by version | `timeline(record)` |
|
|
227
|
+
| Each change to one record and its chosen associations, with who made it | `activity_timeline(record, associations: ...)` |
|
|
228
|
+
| Net difference and timeline together | `analyze(record)` |
|
|
229
|
+
| The same, plus the activity timeline | `analyze(record, activity: true)` |
|
|
230
|
+
| Analyses for an array of records | `analyze_many(records)` |
|
|
231
|
+
| Analyses for the records a relation selects, over a time window | `analyze_scope(scope, within:, limit:)` |
|
|
232
|
+
| Recent activity through now, for the records a relation selects | `report(scope, since:, limit:)` |
|
|
233
|
+
|
|
234
|
+
Use the batch methods for many records so they can share database queries.
|
|
235
|
+
Some scoped associations and `has_many :through` histories still need queries
|
|
236
|
+
for each record. Batch calls reject those with `UnsupportedBatchPathError`.
|
|
237
|
+
The error names the association. `allow_expensive: true` permits that extra work;
|
|
238
|
+
see the [performance guide](docs/performance.md) before using it in a large report.
|
|
239
|
+
|
|
240
|
+
When associations are included, `timeline` compares their values at the article's
|
|
241
|
+
versions. `activity_timeline` also reports the comments' own version events, even
|
|
242
|
+
when the article was not edited. The next section on associations shows this.
|
|
243
|
+
|
|
244
|
+
## 7. Choose fields to ignore
|
|
185
245
|
|
|
186
246
|
`updated_at` is ignored by default. Passing `ignore:` replaces that default:
|
|
187
247
|
|
|
@@ -196,16 +256,16 @@ PaperTrailDiff.compare(
|
|
|
196
256
|
PaperTrailDiff.compare(
|
|
197
257
|
draft_version,
|
|
198
258
|
published_version,
|
|
199
|
-
ignore: [] #
|
|
259
|
+
ignore: [] # include updated_at too
|
|
200
260
|
)
|
|
201
261
|
```
|
|
202
262
|
|
|
203
|
-
See the
|
|
263
|
+
See the [API reference](docs/reference.md) to ignore different fields on different associations.
|
|
204
264
|
|
|
205
265
|
## 8. Add association history when needed
|
|
206
266
|
|
|
207
|
-
|
|
208
|
-
|
|
267
|
+
To compare an article's earlier comments, install the optional
|
|
268
|
+
`paper_trail-association_tracking` plugin (called PT-AT in the reference):
|
|
209
269
|
|
|
210
270
|
```console
|
|
211
271
|
bundle add paper_trail-association_tracking
|
|
@@ -214,14 +274,18 @@ bin/rails db:migrate
|
|
|
214
274
|
```
|
|
215
275
|
|
|
216
276
|
The generator creates `version_associations` and enables
|
|
217
|
-
`PaperTrail.config.track_associations
|
|
277
|
+
`PaperTrail.config.track_associations` in an initializer. If your application
|
|
278
|
+
already has that initializer, check that it contains
|
|
279
|
+
`PaperTrail.config.track_associations = true`.
|
|
280
|
+
|
|
281
|
+
Add a comment model:
|
|
218
282
|
|
|
219
283
|
```console
|
|
220
284
|
bin/rails generate model Comment article:references body:string
|
|
221
285
|
bin/rails db:migrate
|
|
222
286
|
```
|
|
223
287
|
|
|
224
|
-
|
|
288
|
+
Add `has_paper_trail` to both models so their earlier values are recorded:
|
|
225
289
|
|
|
226
290
|
```ruby
|
|
227
291
|
class Article < ApplicationRecord
|
|
@@ -235,7 +299,14 @@ class Comment < ApplicationRecord
|
|
|
235
299
|
end
|
|
236
300
|
```
|
|
237
301
|
|
|
238
|
-
|
|
302
|
+
The article option `synchronize_version_creation_timestamp: false` lets
|
|
303
|
+
manually saved versions record the time they were taken. Restart the console
|
|
304
|
+
after this setup and reload your article with `article = Article.first!`.
|
|
305
|
+
|
|
306
|
+
Use PaperTrail's `paper_trail.save_with_version` to save an article version even
|
|
307
|
+
though none of its attributes changed. That gives the comparison a starting
|
|
308
|
+
point before adding the comment. The transaction also records the information
|
|
309
|
+
needed for association history:
|
|
239
310
|
|
|
240
311
|
<!-- executable:quickstart-association -->
|
|
241
312
|
```ruby
|
|
@@ -257,7 +328,7 @@ diff.associations["comments"].added.first.attributes["body"]
|
|
|
257
328
|
# => "First comment"
|
|
258
329
|
```
|
|
259
330
|
|
|
260
|
-
|
|
331
|
+
If your models also define `replies` and `author`, name those associations to include them:
|
|
261
332
|
|
|
262
333
|
```ruby
|
|
263
334
|
diff = PaperTrailDiff.compare(
|
|
@@ -267,7 +338,7 @@ diff = PaperTrailDiff.compare(
|
|
|
267
338
|
)
|
|
268
339
|
```
|
|
269
340
|
|
|
270
|
-
To
|
|
341
|
+
To show the comment creation as its own activity step, without updating the article:
|
|
271
342
|
|
|
272
343
|
<!-- executable:quickstart-activity -->
|
|
273
344
|
```ruby
|
|
@@ -279,47 +350,46 @@ steps = PaperTrailDiff.activity_timeline(
|
|
|
279
350
|
)
|
|
280
351
|
|
|
281
352
|
steps.reject(&:empty?).each do |step|
|
|
282
|
-
|
|
283
|
-
puts "#{
|
|
353
|
+
source = step.source_boundary
|
|
354
|
+
puts "#{source.item_type} ##{source.item_id}"
|
|
284
355
|
end
|
|
285
356
|
```
|
|
286
357
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
For a historical reporting window, replace `from:` and `to:` with `within:`:
|
|
358
|
+
`source_boundary` describes the version responsible for a step. Its `item_type`
|
|
359
|
+
and `item_id` are PaperTrail's model name and record ID. It also exposes the
|
|
360
|
+
version's `whodunnit` and `event`. See the README for a feed using these values.
|
|
291
361
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
within: report_start...report_end,
|
|
296
|
-
associations: ["comments"]
|
|
297
|
-
)
|
|
298
|
-
```
|
|
362
|
+
To keep the result fixed after later edits, save another article version with
|
|
363
|
+
`paper_trail.save_with_version` inside a transaction, then pass that version
|
|
364
|
+
instead of `to: current`.
|
|
299
365
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
366
|
+
You can also pass `within: start_time...end_time`. This single-article method
|
|
367
|
+
includes comment changes even if the article was not edited in that period.
|
|
368
|
+
Unlike this method, a relation-based `report` needs an article version in the
|
|
369
|
+
window to select that article.
|
|
303
370
|
|
|
304
371
|
## 9. Common surprises
|
|
305
372
|
|
|
306
|
-
-
|
|
307
|
-
|
|
308
|
-
-
|
|
309
|
-
|
|
310
|
-
|
|
373
|
+
- `article.versions.last.reify` gives the article **before** its latest recorded
|
|
374
|
+
change. Pass `article` to read the current saved values.
|
|
375
|
+
- PaperTrail's creation version returns `nil` from `reify`. There was no record
|
|
376
|
+
before creation; the diff reports the record being added.
|
|
377
|
+
- A range ending at the current article includes changes PaperTrail never
|
|
378
|
+
recorded, such as `update_columns`. Pass a version as `to:` to stop at
|
|
379
|
+
recorded history.
|
|
311
380
|
- `ignore:` replaces the default list. Include `updated_at` yourself when using
|
|
312
381
|
a custom list and you still want it ignored.
|
|
313
382
|
- Historical associations require PT-AT to be installed, loaded, migrated, and
|
|
314
383
|
enabled. Restart the console after setup.
|
|
315
|
-
- Only
|
|
316
|
-
|
|
384
|
+
- Only the associations listed in `associations:` are included. Asking for
|
|
385
|
+
comments does not automatically include every association on each comment.
|
|
317
386
|
- Historical output cannot contain data that PaperTrail or PT-AT did not record
|
|
318
387
|
or can no longer reconstruct.
|
|
319
388
|
|
|
320
|
-
For
|
|
321
|
-
|
|
322
|
-
[
|
|
389
|
+
For association support, performance, error handling, ignore rules, and all
|
|
390
|
+
available result methods, see the
|
|
391
|
+
[API reference](docs/reference.md). For complete activity-feed and batch-report
|
|
392
|
+
recipes, including deleted records, see the [README](README.md).
|
|
323
393
|
|
|
324
394
|
Upstream setup references:
|
|
325
395
|
[PaperTrail installation](https://github.com/paper-trail-gem/paper_trail#1b-installation)
|