recorder 1.3.0 → 1.4.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 +34 -1
- data/README.md +54 -4
- data/lib/recorder/changeset.rb +8 -2
- data/lib/recorder/observer.rb +2 -0
- data/lib/recorder/revision.rb +2 -2
- data/lib/recorder/tape/data.rb +18 -1
- data/lib/recorder/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 064c2afab28615f3f268bfacee354fdf6fceff7063190e084783dfe1640fe054
|
|
4
|
+
data.tar.gz: 6062f268b4a47ea089cf90fac14ac124fc2c5e07f65be6d37228a663ec4cfc5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed30006c3d6f960d017a8b97853d5737624c9690f31ffc60ee3d0dc2dbbb21507f3e52e8203dd103192ba8648ca6dc643e969a232fd40a7ee65d0dcc7f11a87b
|
|
7
|
+
data.tar.gz: 689acc4c8867035e627413e5586280e2b05327efa7a002713903300016f19a99caa1194e172de45633f6b4fb8771cdf3a380905784f327b3df01bca39ed93129
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.0]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- README coverage of what a revision's `data` holds: the complete attribute
|
|
15
|
+
snapshot on every event, when `changes` and `associations` are present, and
|
|
16
|
+
which events record a revision at all. No behaviour changes — the snapshot is
|
|
17
|
+
what the gem has always written, and it is now written down.
|
|
18
|
+
- A known issue for the `changes` key on `destroy` revisions, which describes
|
|
19
|
+
the record's last update rather than the deletion.
|
|
20
|
+
- A `changes:` option on `recorder` that merges extra entries into a revision's
|
|
21
|
+
`changes`. It takes a Proc evaluated on the record or the name of a method on it;
|
|
22
|
+
both receive the event.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `Recorder::Revision` declares both `belongs_to :user` and `belongs_to :item`
|
|
27
|
+
with `optional: true`. The associations have always been optional in practice,
|
|
28
|
+
but only because the gem is required before the Active Record railtie applies
|
|
29
|
+
`belongs_to_required_by_default`, so the reflections are built while the
|
|
30
|
+
default is still off. Had that ordering ever shifted, every revision without a
|
|
31
|
+
user would have failed validation, and so would every `destroy` revision,
|
|
32
|
+
which is written after its item's row is deleted. `Tape::Record#record` calls
|
|
33
|
+
`create`, not `create!`, so those revisions would have been dropped with
|
|
34
|
+
nothing raised anywhere. Recorded revisions are unchanged.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- `Recorder::Changeset#previous` and `#next` no longer raise `NoMethodError` when
|
|
39
|
+
the changes carry a key that is not an attribute of the model, and skip values
|
|
40
|
+
that are not a two-element `[old, new]` pair rather than indexing into them.
|
|
41
|
+
|
|
10
42
|
## [1.3.0] - 2026-09-16
|
|
11
43
|
|
|
12
44
|
### Added
|
|
@@ -104,7 +136,8 @@ Releases at and before 1.1.1 predate this changelog. See the
|
|
|
104
136
|
[commit history](https://github.com/jetrockets/recorder/commits/master) for
|
|
105
137
|
details.
|
|
106
138
|
|
|
107
|
-
[Unreleased]: https://github.com/jetrockets/recorder/compare/v1.
|
|
139
|
+
[Unreleased]: https://github.com/jetrockets/recorder/compare/v1.4.0...HEAD
|
|
140
|
+
[1.4.0]: https://github.com/jetrockets/recorder/compare/v1.3.0...v1.4.0
|
|
108
141
|
[1.3.0]: https://github.com/jetrockets/recorder/compare/v1.2.3...v1.3.0
|
|
109
142
|
[1.2.3]: https://github.com/jetrockets/recorder/compare/v1.2.2...v1.2.3
|
|
110
143
|
[1.2.2]: https://github.com/jetrockets/recorder/compare/v1.2.1...v1.2.2
|
data/README.md
CHANGED
|
@@ -73,10 +73,36 @@ Recorder supports the following options:
|
|
|
73
73
|
* `only: [array]` - only these attributes are logged, other attributes are ingored;
|
|
74
74
|
* `associations: {hash} (hash)` - allows to set what associations will be logged alongside with the model. For each association you can also set ignore and only options;
|
|
75
75
|
* `async: bool` - a logging strategy (true - asynchronous, false - synchronous).
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
* `changes: Proc | Symbol` - extra entries to merge into a revision's `changes`. A Proc
|
|
77
|
+
is evaluated on the record, a Symbol names a method on it; both receive the event
|
|
78
|
+
(`:create`, `:update` or `:destroy`) and return a hash of `name => [old, new]`, or
|
|
79
|
+
`nil` for nothing. Anything else raises `ArgumentError` where `recorder` is called.
|
|
80
|
+
|
|
81
|
+
The entries are merged after `only:` and `ignore:` have been applied, so those
|
|
82
|
+
filters never drop them, and a key that matches an attribute replaces that
|
|
83
|
+
attribute's entry. An update that reports only custom entries still records a
|
|
84
|
+
revision.
|
|
85
|
+
|
|
86
|
+
Inside the callback, read `saved_changes` and `saved_change_to_<attribute>?`, not
|
|
87
|
+
`<attribute>_changed?`: the callback runs from `after_create`/`after_update`, where
|
|
88
|
+
the dirty state has already been reset.
|
|
89
|
+
|
|
90
|
+
`Recorder::Changeset` rebuilds the previous and next versions by assigning each
|
|
91
|
+
change onto a copy of the record, and skips what it cannot assign: a key that is
|
|
92
|
+
not an attribute, and any value that is not a two-element `[old, new]` pair. To display such
|
|
93
|
+
a key, define `previous_<key>`/`next_<key>` on the model's changeset class, and
|
|
94
|
+
pick a name that `Recorder::Changeset` does not already answer to.
|
|
95
|
+
|
|
96
|
+
These per-model options, `changes:` included, do not reach the recorder yet — see [Known issues](#known-issues).
|
|
78
97
|
Until they do, every observed model records a full attribute snapshot, filtered
|
|
79
|
-
only by the global `Recorder.config.ignore`.
|
|
98
|
+
only by the global `Recorder.config.ignore`. Options declared as an instance
|
|
99
|
+
method are read, so that is the way to opt into any of them today:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
def recorder_options
|
|
103
|
+
{ignore: %i[identifier], changes: :extra_changes}
|
|
104
|
+
end
|
|
105
|
+
```
|
|
80
106
|
|
|
81
107
|
### Global configuration
|
|
82
108
|
|
|
@@ -143,6 +169,26 @@ revision.user_id
|
|
|
143
169
|
revision.action_date
|
|
144
170
|
```
|
|
145
171
|
|
|
172
|
+
`data` carries a complete attribute snapshot on every event, not only what
|
|
173
|
+
changed:
|
|
174
|
+
|
|
175
|
+
- `attributes` — every attribute of the record as it stood when the callback
|
|
176
|
+
ran, filtered by the model's `only:` or `ignore:` and, failing those, by
|
|
177
|
+
`Recorder.config.ignore`. Always present.
|
|
178
|
+
- `changes` — the same filter applied to `saved_changes`, as
|
|
179
|
+
`name => [old, new]`, plus any entries from `changes:`. Omitted when that
|
|
180
|
+
leaves nothing.
|
|
181
|
+
- `associations` — those two keys again, one entry per association named in
|
|
182
|
+
`associations:`. Omitted when no association reports anything.
|
|
183
|
+
|
|
184
|
+
The snapshot is the contract, not an accident of the implementation: a revision
|
|
185
|
+
is self-contained, so reconstructing a record at a point in time does not mean
|
|
186
|
+
replaying every prior diff. It is also what keeps a `destroy` revision useful,
|
|
187
|
+
since the row it describes is gone.
|
|
188
|
+
|
|
189
|
+
An `update` records a revision only when the record or one of its recorded
|
|
190
|
+
associations reports a change; `create` and `destroy` always record one.
|
|
191
|
+
|
|
146
192
|
`#item_changeset` wraps `data['changes']` in a `Recorder::Changeset`, which reads
|
|
147
193
|
the values back as the model's own types:
|
|
148
194
|
|
|
@@ -170,7 +216,7 @@ as `"#{model}Changeset"` — or point at another one with a
|
|
|
170
216
|
|
|
171
217
|
## Known issues
|
|
172
218
|
|
|
173
|
-
The gem is under active maintenance and these defects are known as of 1.
|
|
219
|
+
The gem is under active maintenance and these defects are known as of 1.3.0:
|
|
174
220
|
|
|
175
221
|
- The per-model options above (`ignore:`, `only:`, `associations:`, `async:`)
|
|
176
222
|
are not applied. `Recorder::Tape` asks the record instance for
|
|
@@ -182,6 +228,10 @@ The gem is under active maintenance and these defects are known as of 1.2.3:
|
|
|
182
228
|
`Recorder.store`, which `Recorder::Manager` drives.
|
|
183
229
|
- Collection associations are never recorded. `associations:` handles only
|
|
184
230
|
singular associations; a `has_many` reflection is skipped without a warning.
|
|
231
|
+
- A `destroy` revision carries a `changes` key describing the record's last
|
|
232
|
+
*update*. `destroy` does not clear `saved_changes`, and `data` is built the
|
|
233
|
+
same way for every event. The `attributes` snapshot is the accurate record of
|
|
234
|
+
what was deleted.
|
|
185
235
|
|
|
186
236
|
## Development
|
|
187
237
|
|
data/lib/recorder/changeset.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Recorder
|
|
|
33
33
|
@previous_version = item.dup
|
|
34
34
|
|
|
35
35
|
changes.each do |key, change|
|
|
36
|
-
@previous_version.
|
|
36
|
+
@previous_version.try("#{key}=", change[0]) if pair?(change)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
@previous_version
|
|
@@ -49,10 +49,16 @@ module Recorder
|
|
|
49
49
|
@next_version = item.dup
|
|
50
50
|
|
|
51
51
|
changes.each do |key, change|
|
|
52
|
-
@next_version.
|
|
52
|
+
@next_version.try("#{key}=", change[1]) if pair?(change)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
@next_version
|
|
56
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def pair?(change)
|
|
61
|
+
change.is_a?(Array) && change.size == 2
|
|
62
|
+
end
|
|
57
63
|
end
|
|
58
64
|
end
|
data/lib/recorder/observer.rb
CHANGED
data/lib/recorder/revision.rb
CHANGED
|
@@ -18,8 +18,8 @@ module Recorder
|
|
|
18
18
|
)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
belongs_to :item, polymorphic: true, inverse_of: :revisions
|
|
22
|
-
belongs_to :user
|
|
21
|
+
belongs_to :item, polymorphic: true, inverse_of: :revisions, optional: true
|
|
22
|
+
belongs_to :user, optional: true
|
|
23
23
|
|
|
24
24
|
validates :item_type, presence: true
|
|
25
25
|
validates :event, presence: true
|
data/lib/recorder/tape/data.rb
CHANGED
|
@@ -23,8 +23,14 @@ module Recorder
|
|
|
23
23
|
{attributes: sanitize_attributes(item.attributes, options)}
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def self.validate_changes_option!(callback)
|
|
27
|
+
return if callback.nil? || callback.is_a?(Proc) || callback.is_a?(Symbol) || callback.is_a?(String)
|
|
28
|
+
|
|
29
|
+
raise ArgumentError, "`changes:` expects a Proc or a method name, got #{callback.inspect}"
|
|
30
|
+
end
|
|
31
|
+
|
|
26
32
|
def changes_for(event, options)
|
|
27
|
-
changes = sanitize_attributes(item.saved_changes, options)
|
|
33
|
+
changes = sanitize_attributes(item.saved_changes, options).merge(custom_changes_for(event, options))
|
|
28
34
|
|
|
29
35
|
changes.present? ? {changes: changes} : {}
|
|
30
36
|
end
|
|
@@ -37,6 +43,17 @@ module Recorder
|
|
|
37
43
|
|
|
38
44
|
private
|
|
39
45
|
|
|
46
|
+
def custom_changes_for(event, options)
|
|
47
|
+
callback = options[:changes]
|
|
48
|
+
return {} unless callback
|
|
49
|
+
|
|
50
|
+
self.class.validate_changes_option!(callback)
|
|
51
|
+
|
|
52
|
+
changes = callback.is_a?(Proc) ? item.instance_exec(event, &callback) : item.send(callback, event)
|
|
53
|
+
|
|
54
|
+
changes ? Hash(changes).symbolize_keys : {}
|
|
55
|
+
end
|
|
56
|
+
|
|
40
57
|
def sanitize_attributes(attributes, options)
|
|
41
58
|
if options[:only].present?
|
|
42
59
|
only = wrap_options(options[:only])
|
data/lib/recorder/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: recorder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Igor Alexandrov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|