hoardable 0.12.6 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +149 -141
- data/lib/generators/hoardable/install_generator.rb +4 -0
- data/lib/generators/hoardable/templates/install.rb.erb +3 -2
- data/lib/hoardable/database_client.rb +15 -1
- data/lib/hoardable/engine.rb +5 -2
- data/lib/hoardable/has_one.rb +3 -4
- data/lib/hoardable/scopes.rb +1 -1
- data/lib/hoardable/source_model.rb +2 -0
- data/lib/hoardable/version.rb +1 -1
- data/lib/hoardable/version_model.rb +5 -2
- data/sig/hoardable.rbs +2 -2
- 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: 28edc85fff69a3851a5d2c653368d732bd9e6fe8ad5544bfa7f81147c6007aab
|
4
|
+
data.tar.gz: 2d7c3abc9eedaddf3180b9f368c90fa25addaffa6c54bc76bf82880b54e7fc72
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76dfd695ad62332f876aad4d25e8d1b220970bcd8f2d2eeb69a91e4a441300fc356984dc5ef25be9dd01e745a305448a7f55b439199ef475932910bde02e8f9d
|
7
|
+
data.tar.gz: '081e30627e731ed8d9b4f42536d9e67926bb37348075efa10d2ee3f5ee3eb37476ba3b0ecf1c6ac6fb4dfa7fa700e4d02fd10c3c3f537ef7ba433e605c9d1039'
|
data/README.md
CHANGED
@@ -1,23 +1,21 @@
|
|
1
1
|
# Hoardable ![gem version](https://img.shields.io/gem/v/hoardable?style=flat-square)
|
2
2
|
|
3
|
-
Hoardable is an ActiveRecord extension for Ruby 2.6+, Rails 6.1+, and PostgreSQL that allows for
|
4
|
-
|
3
|
+
Hoardable is an ActiveRecord extension for Ruby 2.6+, Rails 6.1+, and PostgreSQL that allows for versioning
|
4
|
+
and soft-deletion of records through the use of _uni-temporal inherited tables_.
|
5
5
|
|
6
|
-
[Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
|
7
|
-
|
8
|
-
|
9
|
-
"uni-temporal".
|
6
|
+
[Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern where each
|
7
|
+
row of a table contains data along with one or more time ranges. In the case of this gem, each database row
|
8
|
+
has a time range that represents the row’s valid time range - hence "uni-temporal".
|
10
9
|
|
11
|
-
[Table inheritance](https://www.postgresql.org/docs/14/ddl-inherit.html) is a feature of PostgreSQL
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
[Table inheritance](https://www.postgresql.org/docs/14/ddl-inherit.html) is a feature of PostgreSQL that
|
11
|
+
allows a table to inherit all columns of a parent table. The descendant table’s schema will stay in sync with
|
12
|
+
its parent. If a new column is added to or removed from the parent, the schema change is reflected on its
|
13
|
+
descendants.
|
15
14
|
|
16
|
-
With these concepts combined, `hoardable` offers a simple and effective model versioning system for
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
within Ruby on Rails.
|
15
|
+
With these concepts combined, `hoardable` offers a simple and effective model versioning system for Rails.
|
16
|
+
Versions of records are stored in separate, inherited tables along with their valid time ranges and
|
17
|
+
contextual data. Compared to other Rails-oriented versioning systems, this gem strives to be more explicit
|
18
|
+
and obvious on the lower database level, while still familiar and convenient to use within Ruby on Rails.
|
21
19
|
|
22
20
|
[👉 Documentation](https://www.rubydoc.info/gems/hoardable)
|
23
21
|
|
@@ -36,22 +34,16 @@ bin/rails g hoardable:install
|
|
36
34
|
bin/rails db:migrate
|
37
35
|
```
|
38
36
|
|
39
|
-
This will generate
|
40
|
-
|
41
|
-
_Note:_ It is recommended to set `config.active_record.schema_format = :sql` in `application.rb`, so
|
42
|
-
that the function and triggers in the migrations that prevent updates to the versions table get
|
43
|
-
captured in your schema.
|
37
|
+
This will generate PostgreSQL functions, an initiailzer, and set `config.active_record.schema_format = :sql`
|
38
|
+
in `application.rb`.
|
44
39
|
|
45
40
|
### Model Installation
|
46
41
|
|
47
|
-
You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
|
48
|
-
of:
|
42
|
+
You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions of:
|
49
43
|
|
50
44
|
```ruby
|
51
45
|
class Post < ActiveRecord::Base
|
52
46
|
include Hoardable::Model
|
53
|
-
belongs_to :user
|
54
|
-
has_many :comments, dependent: :destroy
|
55
47
|
...
|
56
48
|
end
|
57
49
|
```
|
@@ -63,34 +55,31 @@ bin/rails g hoardable:migration Post
|
|
63
55
|
bin/rails db:migrate
|
64
56
|
```
|
65
57
|
|
66
|
-
By default, it will
|
67
|
-
|
68
|
-
explicitly, you can do so:
|
58
|
+
By default, it will guess the foreign key type for the `_versions` table based on the primary key of the
|
59
|
+
model specified in the migration generator above. If you want/need to specify this explicitly, you can do so:
|
69
60
|
|
70
61
|
```
|
71
62
|
bin/rails g hoardable:migration Post --foreign-key-type uuid
|
72
63
|
```
|
73
64
|
|
74
|
-
_Note:_ Creating an inherited table does not
|
75
|
-
|
65
|
+
_Note:_ Creating an inherited table does not inherit the indexes from the parent table. If you need to query
|
66
|
+
versions often, you should add appropriate indexes to the `_versions` tables.
|
76
67
|
|
77
68
|
## Usage
|
78
69
|
|
79
70
|
### Overview
|
80
71
|
|
81
|
-
Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass
|
82
|
-
|
72
|
+
Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass of that
|
73
|
+
model. As we continue our example from above:
|
83
74
|
|
84
|
-
```
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
>> PostVersion
|
89
|
-
=> PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, hoardable_id: integer, _data: jsonb, _during: tsrange)
|
75
|
+
```ruby
|
76
|
+
Post #=> Post(id: integer, created_at: datetime, updated_at: datetime, hoardable_id: integer)
|
77
|
+
PostVersion #=> PostVersion(id: integer, created_at: datetime, updated_at: datetime, hoardable_id: integer, _data: jsonb, _during: tsrange, _event_uuid: uuid, _operation: enum)
|
78
|
+
Post.version_class #=> same as `PostVersion`
|
90
79
|
```
|
91
80
|
|
92
|
-
A `Post` now `has_many :versions`. With the default configuration, whenever an update and deletion
|
93
|
-
|
81
|
+
A `Post` now `has_many :versions`. With the default configuration, whenever an update and deletion of a
|
82
|
+
`Post` occurs, a version is created:
|
94
83
|
|
95
84
|
```ruby
|
96
85
|
post = Post.create!(title: "Title")
|
@@ -105,7 +94,11 @@ Post.find(post.id) # raises ActiveRecord::RecordNotFound
|
|
105
94
|
```
|
106
95
|
|
107
96
|
Each `PostVersion` has access to the same attributes, relationships, and other model behavior that
|
108
|
-
`Post` has, but as a read-only record
|
97
|
+
`Post` has, but as a read-only record:
|
98
|
+
|
99
|
+
``` ruby
|
100
|
+
post.versions.last.update!(title: "Rewrite history") #=> raises ActiveRecord::ReadOnlyRecord
|
101
|
+
```
|
109
102
|
|
110
103
|
If you ever need to revert to a specific version, you can call `version.revert!` on it.
|
111
104
|
|
@@ -116,8 +109,8 @@ post.reload.versions.last.revert!
|
|
116
109
|
post.title # => "Title"
|
117
110
|
```
|
118
111
|
|
119
|
-
If you would like to untrash a specific version, you can call `version.untrash!` on
|
120
|
-
re-insert the model in the parent class’s table with the original primary key.
|
112
|
+
If you would like to untrash a specific version of a record you deleted, you can call `version.untrash!` on
|
113
|
+
it. This will re-insert the model in the parent class’s table with the original primary key.
|
121
114
|
|
122
115
|
```ruby
|
123
116
|
post = Post.create!(title: "Title")
|
@@ -131,12 +124,17 @@ trashed_post.untrash!
|
|
131
124
|
Post.find(post.id) # #<Post>
|
132
125
|
```
|
133
126
|
|
127
|
+
_Note:_ You will notice above that both `posts` and `post_versions` pull from the same ID sequence. This
|
128
|
+
allows for uniquely identifying source records and versions when results are mixed together. Both a source
|
129
|
+
record and versions have an automatically managed `hoardable_id` that always represents the primary key value
|
130
|
+
of the original source record.
|
131
|
+
|
134
132
|
### Querying and Temporal Lookup
|
135
133
|
|
136
134
|
Since a `PostVersion` is an `ActiveRecord` class, you can query them like another model resource:
|
137
135
|
|
138
136
|
```ruby
|
139
|
-
post.versions.where(
|
137
|
+
post.versions.where(state: :draft)
|
140
138
|
```
|
141
139
|
|
142
140
|
If you want to look-up the version of a record at a specific time, you can use the `.at` method:
|
@@ -144,7 +142,8 @@ If you want to look-up the version of a record at a specific time, you can use t
|
|
144
142
|
```ruby
|
145
143
|
post.at(1.day.ago) # => #<PostVersion>
|
146
144
|
# or you can use the scope on the version model class
|
147
|
-
|
145
|
+
post.versions.at(1.day.ago) # => #<PostVersion>
|
146
|
+
PostVersion.at(1.day.ago).find_by(hoardable_id: post.id) # => same as above
|
148
147
|
```
|
149
148
|
|
150
149
|
The source model class also has an `.at` method:
|
@@ -153,35 +152,32 @@ The source model class also has an `.at` method:
|
|
153
152
|
Post.at(1.day.ago) # => [#<Post>, #<Post>]
|
154
153
|
```
|
155
154
|
|
156
|
-
This will return an ActiveRecord scoped query of all `Post` and `PostVersion` records that were
|
157
|
-
|
155
|
+
This will return an ActiveRecord scoped query of all `Post` and `PostVersion` records that were valid at that
|
156
|
+
time, all cast as instances of `Post`.
|
158
157
|
|
159
|
-
There is also an `at` method on `Hoardable` itself for more complex temporal resource
|
160
|
-
[Relationships](#relationships) for more.
|
158
|
+
There is also an `at` method on `Hoardable` itself for more complex and experimental temporal resource
|
159
|
+
querying. See [Relationships](#relationships) for more.
|
161
160
|
|
162
|
-
By default, `hoardable` will keep copies of records you have destroyed. You can query them
|
163
|
-
specifically with:
|
161
|
+
By default, `hoardable` will keep copies of records you have destroyed. You can query them specifically with:
|
164
162
|
|
165
163
|
```ruby
|
166
164
|
PostVersion.trashed
|
167
|
-
Post.version_class.trashed # <- same
|
165
|
+
Post.version_class.trashed # <- same as above
|
168
166
|
```
|
169
167
|
|
170
|
-
_Note:_ A `Version` is not created upon initial parent model creation. To accurately track the
|
171
|
-
|
172
|
-
|
168
|
+
_Note:_ A `Version` is not created upon initial parent model creation. To accurately track the beginning of
|
169
|
+
the first temporal period, you will need to ensure the source model table has a `created_at` timestamp
|
170
|
+
column. If this is missing, an error will be raised.
|
173
171
|
|
174
172
|
### Tracking Contextual Data
|
175
173
|
|
176
|
-
You’ll often want to track contextual data about the creation of a version. There are
|
177
|
-
|
174
|
+
You’ll often want to track contextual data about the creation of a version. There are 2 options that can be
|
175
|
+
provided for tracking contextual information:
|
178
176
|
|
179
177
|
- `:whodunit` - an identifier for who is responsible for creating the version
|
180
|
-
- `:note` - a description regarding the versioning
|
181
178
|
- `:meta` - any other contextual information you’d like to store along with the version
|
182
179
|
|
183
|
-
This information is stored in a `jsonb` column. Each key’s value can be in the format of your
|
184
|
-
choosing.
|
180
|
+
This information is stored in a `jsonb` column. Each key’s value can be in the format of your choosing.
|
185
181
|
|
186
182
|
One convenient way to assign contextual data to these is by defining a proc in an initializer, i.e.:
|
187
183
|
|
@@ -195,17 +191,17 @@ post.update!(status: 'live')
|
|
195
191
|
post.reload.versions.last.hoardable_whodunit # => 123
|
196
192
|
```
|
197
193
|
|
198
|
-
You can also set this context manually as well
|
194
|
+
You can also set this context manually as well:
|
199
195
|
|
200
196
|
```ruby
|
201
|
-
Hoardable.
|
197
|
+
Hoardable.meta = { note: "reverting due to accidental deletion" }
|
202
198
|
post.update!(title: "We’re back!")
|
203
|
-
Hoardable.
|
204
|
-
post.reload.versions.last.
|
199
|
+
Hoardable.meta = nil
|
200
|
+
post.reload.versions.last.hoardable_meta['note'] # => "reverting due to accidental deletion"
|
205
201
|
```
|
206
202
|
|
207
|
-
A more useful pattern is to use `Hoardable.with` to set the context around a block.
|
208
|
-
|
203
|
+
A more useful pattern however is to use `Hoardable.with` to set the context around a block. For example, you
|
204
|
+
could have the following in your `ApplicationController`:
|
209
205
|
|
210
206
|
```ruby
|
211
207
|
class ApplicationController < ActionController::Base
|
@@ -223,9 +219,9 @@ end
|
|
223
219
|
```
|
224
220
|
|
225
221
|
`hoardable` will also automatically capture the ActiveRecord
|
226
|
-
[changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the
|
227
|
-
|
228
|
-
|
222
|
+
[changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the `operation`
|
223
|
+
that cause the version (`update` or `delete`), and it will also tag all versions created in the same database
|
224
|
+
transaction with a shared and unique `event_uuid` for that transaction. These are available as:
|
229
225
|
|
230
226
|
```ruby
|
231
227
|
version.changes
|
@@ -235,12 +231,12 @@ version.hoardable_event_uuid
|
|
235
231
|
|
236
232
|
### Model Callbacks
|
237
233
|
|
238
|
-
Sometimes you might want to do something with a version after it gets inserted to the database. You
|
239
|
-
|
240
|
-
|
234
|
+
Sometimes you might want to do something with a version after it gets inserted to the database. You can
|
235
|
+
access it in `after_versioned` callbacks on the source record as `hoardable_version`. These happen within
|
236
|
+
`ActiveRecord`’s `.save`, which is enclosed in an ActiveRecord transaction.
|
241
237
|
|
242
|
-
There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
|
243
|
-
|
238
|
+
There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called on the
|
239
|
+
source record after a version is reverted or untrashed.
|
244
240
|
|
245
241
|
```ruby
|
246
242
|
class User
|
@@ -275,11 +271,11 @@ Hoardable.version_updates # => default true
|
|
275
271
|
Hoardable.save_trash # => default true
|
276
272
|
```
|
277
273
|
|
278
|
-
`Hoardable.enabled` controls whether versions will be ever be created.
|
274
|
+
`Hoardable.enabled` globally controls whether versions will be ever be created.
|
279
275
|
|
280
|
-
`Hoardable.version_updates` controls whether versions get created on record updates.
|
276
|
+
`Hoardable.version_updates` globally controls whether versions get created on record updates.
|
281
277
|
|
282
|
-
`Hoardable.save_trash` controls whether to create versions upon record deletion. When this is set to
|
278
|
+
`Hoardable.save_trash` globally controls whether to create versions upon record deletion. When this is set to
|
283
279
|
`false`, all versions of a record will be deleted when the record is destroyed.
|
284
280
|
|
285
281
|
If you would like to temporarily set a config setting, you can use `Hoardable.with`:
|
@@ -290,7 +286,7 @@ Hoardable.with(enabled: false) do
|
|
290
286
|
end
|
291
287
|
```
|
292
288
|
|
293
|
-
You can also configure these
|
289
|
+
You can also configure these settings per `ActiveRecord` class using `hoardable_config`:
|
294
290
|
|
295
291
|
```ruby
|
296
292
|
class Comment < ActiveRecord::Base
|
@@ -308,18 +304,17 @@ Comment.with_hoardable_config(version_updates: true) do
|
|
308
304
|
end
|
309
305
|
```
|
310
306
|
|
311
|
-
If a model-level option exists, it will use that. Otherwise, it will fall back to the global
|
312
|
-
|
307
|
+
If a model-level option exists, it will use that. Otherwise, it will fall back to the global `Hoardable`
|
308
|
+
config.
|
313
309
|
|
314
|
-
|
310
|
+
## Relationships
|
315
311
|
|
316
|
-
|
317
|
-
with `Hoardable` considerations.
|
312
|
+
### Belongs To Trashable
|
318
313
|
|
319
|
-
Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child
|
320
|
-
|
321
|
-
|
322
|
-
|
314
|
+
Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child record’s
|
315
|
+
foreign key will point to the non-existent trashed version of the parent. If you would like to have
|
316
|
+
`belongs_to` resolve to the trashed parent model in this case, you can give it the option of `trashable:
|
317
|
+
true`:
|
323
318
|
|
324
319
|
```ruby
|
325
320
|
class Comment
|
@@ -328,10 +323,11 @@ class Comment
|
|
328
323
|
end
|
329
324
|
```
|
330
325
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
326
|
+
### Hoardable Has Many & Has One
|
327
|
+
|
328
|
+
Sometimes you'll have a Hoardable record that `has_one` or `has_many` other Hoardable records and you will
|
329
|
+
want to know the state of both the parent record and the children at a cetain point in time. You accomplish
|
330
|
+
this by adding `hoardable: true` to the `has_many` relationship and using the `Hoardable.at` method:
|
335
331
|
|
336
332
|
```ruby
|
337
333
|
class Post
|
@@ -361,22 +357,26 @@ Hoardable.at(datetime) do
|
|
361
357
|
end
|
362
358
|
```
|
363
359
|
|
364
|
-
There are some additional details to point out above. Firstly, it is important to note that the
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
360
|
+
There are some additional details to point out above. Firstly, it is important to note that the final
|
361
|
+
`post.id` yields a different value than the originally created `Post`. This is because the `post` within the
|
362
|
+
`#at` block is actually a temporal version, since it has been subsequently updated, but is reified as a
|
363
|
+
`Post` for the purposes of your business logic (serialization, rendering views, exporting, etc). Don’t fret -
|
364
|
+
you will not be able to commit any updates to the version, even though it is masquerading as a `Post` because
|
365
|
+
a database trigger won’t allow you to.
|
366
|
+
|
367
|
+
If you are ever unsure if a Hoardable record is a source record or a version, you can be sure by calling
|
368
|
+
`version?` on it. If you want to get the true original source record ID, you can call `hoardable_id`.
|
370
369
|
|
371
|
-
|
372
|
-
|
373
|
-
`hoardable_id`.
|
370
|
+
_Note:_ `Hoardable.at` is still very experimental and is potentially not very performant for querying large
|
371
|
+
data sets.
|
374
372
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
373
|
+
### Cascading Untrashing
|
374
|
+
|
375
|
+
Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and if you untrash the parent
|
376
|
+
record, you’ll want to also untrash the children. Whenever a hoardable version is created in a database
|
377
|
+
transaction, it will create or re-use a unique event UUID for the current database transaction and tag all
|
378
|
+
versions created with it. That way, when you `untrash!` a record, you could find and `untrash!` records that
|
379
|
+
were trashed with it:
|
380
380
|
|
381
381
|
```ruby
|
382
382
|
class Post < ActiveRecord::Base
|
@@ -393,20 +393,19 @@ class Post < ActiveRecord::Base
|
|
393
393
|
end
|
394
394
|
```
|
395
395
|
|
396
|
-
|
396
|
+
### Action Text
|
397
397
|
|
398
|
-
Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a
|
399
|
-
|
398
|
+
Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a temporal
|
399
|
+
table for `ActionText::RichText`:
|
400
400
|
|
401
401
|
```
|
402
402
|
bin/rails g hoardable:migration ActionText::RichText
|
403
403
|
bin/rails db:migrate
|
404
404
|
```
|
405
405
|
|
406
|
-
Then in your model
|
407
|
-
`has_rich_text`:
|
406
|
+
Then in your model include `Hoardable::Model` and provide the `hoardable: true` keyword to `has_rich_text`:
|
408
407
|
|
409
|
-
```
|
408
|
+
```ruby
|
410
409
|
class Post < ActiveRecord::Base
|
411
410
|
include Hoardable::Model # or `Hoardable::Associations` if you don't need `PostVersion`
|
412
411
|
has_rich_text :content, hoardable: true
|
@@ -415,7 +414,7 @@ end
|
|
415
414
|
|
416
415
|
Now the `rich_text_content` relationship will be managed as a Hoardable `has_one` relationship:
|
417
416
|
|
418
|
-
```
|
417
|
+
```ruby
|
419
418
|
post = Post.create!(content: '<div>Hello World</div>')
|
420
419
|
datetime = DateTime.current
|
421
420
|
post.update!(content: '<div>Goodbye Cruel World</div>')
|
@@ -426,52 +425,61 @@ Hoardable.at(datetime) do
|
|
426
425
|
end
|
427
426
|
```
|
428
427
|
|
428
|
+
## Known Gotchas
|
429
|
+
|
430
|
+
### Rails Fixtures
|
431
|
+
|
432
|
+
Rails uses a method called
|
433
|
+
[`disable_referential_integrity`](https://github.com/rails/rails/blob/06e9fbd954ab113108a7982357553fdef285bff1/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb#L7)
|
434
|
+
when inserting fixtures into the database. This disables PostgreSQL triggers, which Hoardable relies on for
|
435
|
+
assigning `hoardable_id` from the primary key’s value. If you would still like to use fixtures, you must
|
436
|
+
specify the primary key’s value and `hoardable_id` to the same identifier value in the fixture. This is not
|
437
|
+
an issue with fixture replacement libraries like `factory_bot` or
|
438
|
+
[`world_factory`](https://github.com/FutureProofRetail/world_factory) however.
|
439
|
+
|
429
440
|
## Gem Comparison
|
430
441
|
|
431
442
|
#### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
|
432
443
|
|
433
|
-
`paper_trail` is maybe the most popular and fully featured gem in this space. It works for other
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
in
|
439
|
-
|
440
|
-
|
441
|
-
for
|
442
|
-
version since there is only a `created_at` timestamp.
|
444
|
+
`paper_trail` is maybe the most popular and fully featured gem in this space. It works for other database
|
445
|
+
types than PostgeSQL and (by default) stores all versions of all versioned models in a single `versions`
|
446
|
+
table. It stores changes in a `text`, `json`, or `jsonb` column. In order to efficiently query the `versions`
|
447
|
+
table, a `jsonb` column should be used, which takes up a lot of space to index. Unless you customize your
|
448
|
+
configuration, all `versions` for all models types are in the same table which is inefficient if you are only
|
449
|
+
interested in querying versions of a single model. By contrast, `hoardable` stores versions in smaller,
|
450
|
+
isolated and inherited tables with the same database columns as their parents, which are more efficient for
|
451
|
+
querying as well as auditing for truncating and dropping. The concept of a `temporal` time-frame does not
|
452
|
+
exist for a single version since there is only a `created_at` timestamp.
|
443
453
|
|
444
454
|
#### [`audited`](https://github.com/collectiveidea/audited)
|
445
455
|
|
446
|
-
`audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
types on the `audited` table.
|
456
|
+
`audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in a single
|
457
|
+
table, you must opt into using `jsonb` as the column type to store "changes", in case you want to query them,
|
458
|
+
and there is no concept of a `temporal` time-frame for a single version. It makes opinionated decisions about
|
459
|
+
contextual data requirements and stores them as top level data types on the `audited` table.
|
451
460
|
|
452
461
|
#### [`discard`](https://github.com/jhawthorn/discard)
|
453
462
|
|
454
|
-
`discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
463
|
+
`discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through the
|
464
|
+
time-stamping of a `discarded_at` column on the records table; there is no other capturing of the event that
|
465
|
+
caused the soft deletion unless you implement it yourself. Once the "discarded" record is restored, the
|
466
|
+
previous "discarded" awareness is lost. Since "discarded" records exist in the same table as "undiscarded"
|
467
|
+
records, you must explicitly omit the discarded records from queries across your app to keep them from
|
468
|
+
leaking in.
|
460
469
|
|
461
470
|
#### [`paranoia`](https://github.com/rubysherpas/paranoia)
|
462
471
|
|
463
|
-
`paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
472
|
+
`paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead of
|
473
|
+
`paranoia` because of the fact they override ActiveRecord’s `delete` and `destroy` methods. `hoardable`
|
474
|
+
employs callbacks to create trashed versions instead of overriding methods. Otherwise, `paranoia` works
|
475
|
+
similarly to `discard` in that it keeps deleted records in the same table and tags them with a `deleted_at`
|
476
|
+
timestamp. No other information about the soft-deletion event is stored.
|
468
477
|
|
469
478
|
#### [`logidze`](https://github.com/palkan/logidze)
|
470
479
|
|
471
|
-
`logidze` is an interesting versioning alternative that leverages the power of PostgreSQL triggers.
|
472
|
-
|
473
|
-
|
474
|
-
deletion.
|
480
|
+
`logidze` is an interesting versioning alternative that leverages the power of PostgreSQL triggers. Instead
|
481
|
+
of storing the previous versions or changes in a separate table, it stores them in a proprietary JSON format
|
482
|
+
directly on the database row of the record itself. If does not support soft deletion.
|
475
483
|
|
476
484
|
## Contributing
|
477
485
|
|
@@ -26,6 +26,10 @@ module Hoardable
|
|
26
26
|
migration_template 'install.rb.erb', 'db/migrate/install_hoardable.rb'
|
27
27
|
end
|
28
28
|
|
29
|
+
def change_schema_format_to_sql
|
30
|
+
application 'config.active_record.schema_format = :sql'
|
31
|
+
end
|
32
|
+
|
29
33
|
def self.next_migration_number(dir)
|
30
34
|
::ActiveRecord::Generators::Base.next_migration_number(dir)
|
31
35
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
4
4
|
def up
|
5
5
|
execute(
|
6
|
-
<<~SQL
|
6
|
+
<<~SQL.squish
|
7
7
|
DO $$
|
8
8
|
BEGIN
|
9
9
|
IF NOT EXISTS (
|
@@ -51,9 +51,10 @@ class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.cur
|
|
51
51
|
SQL
|
52
52
|
)
|
53
53
|
end
|
54
|
+
|
54
55
|
def down
|
55
56
|
execute(
|
56
|
-
<<~SQL
|
57
|
+
<<~SQL.squish
|
57
58
|
DROP TYPE IF EXISTS hoardable_operation;
|
58
59
|
DROP FUNCTION IF EXISTS hoardable_version_prevent_update();
|
59
60
|
DROP FUNCTION IF EXISTS hoardable_source_set_id();
|
@@ -55,7 +55,21 @@ module Hoardable
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def source_attributes_without_primary_key
|
58
|
-
source_record.
|
58
|
+
source_record.attributes.without(source_primary_key, *generated_column_names).merge(
|
59
|
+
source_record.class.select(refreshable_column_names).find(source_record.id).slice(refreshable_column_names)
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def generated_column_names
|
64
|
+
@generated_column_names ||= source_record.class.columns.select(&:virtual?).map(&:name)
|
65
|
+
rescue NoMethodError
|
66
|
+
[]
|
67
|
+
end
|
68
|
+
|
69
|
+
def refreshable_column_names
|
70
|
+
@refreshable_column_names ||= source_record.class.columns.select(&:default_function).reject do |column|
|
71
|
+
column.name == source_primary_key || column.name.in?(generated_column_names)
|
72
|
+
end.map(&:name)
|
59
73
|
end
|
60
74
|
|
61
75
|
def initialize_temporal_range
|
data/lib/hoardable/engine.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
module Hoardable
|
5
5
|
# Symbols for use with setting contextual data, when creating versions. See
|
6
6
|
# {file:README.md#tracking-contextual-data README} for more.
|
7
|
-
DATA_KEYS = %i[meta whodunit
|
7
|
+
DATA_KEYS = %i[meta whodunit event_uuid].freeze
|
8
8
|
|
9
9
|
# Symbols for use with setting {Hoardable} configuration. See {file:README.md#configuration
|
10
10
|
# README} for more.
|
@@ -34,9 +34,12 @@ module Hoardable
|
|
34
34
|
end.freeze
|
35
35
|
private_constant :HOARDABLE_VERSION_UPDATES
|
36
36
|
|
37
|
-
SUPPORTS_ENCRYPTED_ACTION_TEXT = ActiveRecord.version >= ::Gem::Version.new('7.0')
|
37
|
+
SUPPORTS_ENCRYPTED_ACTION_TEXT = ActiveRecord.version >= ::Gem::Version.new('7.0.4')
|
38
38
|
private_constant :SUPPORTS_ENCRYPTED_ACTION_TEXT
|
39
39
|
|
40
|
+
SUPPORTS_VIRTUAL_COLUMNS = ActiveRecord.version >= ::Gem::Version.new('7.0.0')
|
41
|
+
private_constant :SUPPORTS_VIRTUAL_COLUMNS
|
42
|
+
|
40
43
|
@context = {}
|
41
44
|
@config = CONFIG_KEYS.to_h do |key|
|
42
45
|
[key, true]
|
data/lib/hoardable/has_one.rb
CHANGED
@@ -17,11 +17,10 @@ module Hoardable
|
|
17
17
|
def #{name}
|
18
18
|
reflection = _reflections['#{name}']
|
19
19
|
return super if reflection.klass.name.match?(/^ActionText/)
|
20
|
+
return super unless (timestamp = hoardable_client.has_one_at_timestamp)
|
20
21
|
|
21
|
-
super&.at(
|
22
|
-
reflection.klass.at(
|
23
|
-
hoardable_client.has_one_find_conditions(reflection)
|
24
|
-
)
|
22
|
+
super&.at(timestamp) ||
|
23
|
+
reflection.klass.at(timestamp).find_by(hoardable_client.has_one_find_conditions(reflection))
|
25
24
|
end
|
26
25
|
RUBY
|
27
26
|
end
|
data/lib/hoardable/scopes.rb
CHANGED
@@ -73,7 +73,7 @@ module Hoardable
|
|
73
73
|
private
|
74
74
|
|
75
75
|
def tableoid
|
76
|
-
connection.execute("SELECT oid FROM pg_class WHERE relname = '#{table_name}'")[0]['oid']
|
76
|
+
@tableoid ||= connection.execute("SELECT oid FROM pg_class WHERE relname = '#{table_name}'")[0]['oid']
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
data/lib/hoardable/version.rb
CHANGED
@@ -86,7 +86,7 @@ module Hoardable
|
|
86
86
|
|
87
87
|
transaction do
|
88
88
|
hoardable_source.tap do |reverted|
|
89
|
-
reverted.update!(hoardable_source_attributes.without(self.class.superclass.primary_key))
|
89
|
+
reverted.reload.update!(hoardable_source_attributes.without(self.class.superclass.primary_key))
|
90
90
|
reverted.instance_variable_set(:@hoardable_version, self)
|
91
91
|
reverted.run_callbacks(:reverted)
|
92
92
|
end
|
@@ -130,7 +130,10 @@ module Hoardable
|
|
130
130
|
end
|
131
131
|
|
132
132
|
def hoardable_source_attributes
|
133
|
-
|
133
|
+
attributes.without(
|
134
|
+
(self.class.column_names - self.class.superclass.column_names) +
|
135
|
+
(SUPPORTS_VIRTUAL_COLUMNS ? self.class.columns.select(&:virtual?).map(&:name) : [])
|
136
|
+
)
|
134
137
|
end
|
135
138
|
end
|
136
139
|
end
|
data/sig/hoardable.rbs
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Hoardable
|
2
2
|
VERSION: String
|
3
|
-
DATA_KEYS: [:meta, :whodunit, :
|
3
|
+
DATA_KEYS: [:meta, :whodunit, :event_uuid]
|
4
4
|
CONFIG_KEYS: [:enabled, :version_updates, :save_trash]
|
5
5
|
VERSION_CLASS_SUFFIX: String
|
6
6
|
VERSION_TABLE_SUFFIX: String
|
@@ -57,7 +57,7 @@ module Hoardable
|
|
57
57
|
def source_attributes_without_primary_key: -> untyped
|
58
58
|
def initialize_temporal_range: -> Range
|
59
59
|
def initialize_hoardable_data: -> untyped
|
60
|
-
def assign_hoardable_context: (:event_uuid | :meta | :
|
60
|
+
def assign_hoardable_context: (:event_uuid | :meta | :whodunit key) -> nil
|
61
61
|
def unset_hoardable_version_and_event_uuid: -> nil
|
62
62
|
def previous_temporal_tsrange_end: -> untyped
|
63
63
|
def hoardable_source_epoch: -> untyped
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hoardable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- justin talbott
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-12-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|