hoardable 0.14.2 → 0.15.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/.streerc +1 -0
- data/.tool-versions +2 -2
- data/CHANGELOG.md +15 -0
- data/Gemfile +9 -10
- data/README.md +157 -132
- data/Rakefile +22 -8
- data/lib/generators/hoardable/install_generator.rb +25 -26
- data/lib/generators/hoardable/migration_generator.rb +11 -8
- data/lib/generators/hoardable/templates/install.rb.erb +2 -25
- data/lib/generators/hoardable/templates/migration.rb.erb +7 -1
- data/lib/hoardable/arel_visitors.rb +51 -0
- data/lib/hoardable/database_client.rb +41 -23
- data/lib/hoardable/engine.rb +32 -33
- data/lib/hoardable/error.rb +4 -7
- data/lib/hoardable/finder_methods.rb +1 -3
- data/lib/hoardable/has_many.rb +6 -10
- data/lib/hoardable/has_rich_text.rb +14 -7
- data/lib/hoardable/model.rb +19 -16
- data/lib/hoardable/schema_dumper.rb +25 -0
- data/lib/hoardable/schema_statements.rb +33 -0
- data/lib/hoardable/scopes.rb +22 -29
- data/lib/hoardable/source_model.rb +6 -5
- data/lib/hoardable/version.rb +1 -1
- data/lib/hoardable/version_model.rb +30 -31
- data/lib/hoardable.rb +21 -18
- data/sig/hoardable.rbs +37 -12
- metadata +14 -11
- data/.rubocop.yml +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2b34416224e686978b85cd78c0a80eae6e09f727a87c58060355671c6af8334
|
4
|
+
data.tar.gz: 401049a8d781e695fd691f1a9cf0c0ea67560c535efd773b738c40041ca80869
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3b39f34db9a87e2403b6a7529a035e0ccea464fbc6cda100b7069a2cc271e63112f0299dcae283e8b7c1d8f17e14892d58b9089e9e4b7fcc178cd94e808014a1
|
7
|
+
data.tar.gz: e2a8b1e9c9e5362711b2a5170a74d6316a115c290bf285e0ff60f3eed6937a4e4838000b2042a19bc79a97f3d7970882894bb519f244f6bac1eee1d1e529acac
|
data/.streerc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--print-width=100
|
data/.tool-versions
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
ruby 3.
|
2
|
-
postgres
|
1
|
+
ruby 3.3.0
|
2
|
+
postgres 16.1
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
## 0.15.0
|
2
|
+
|
3
|
+
- *Breaking Change* - Support for Ruby 2.7 and Rails 6.1 is dropped
|
4
|
+
- *Breaking Change* - The default scoping clause that controls the inherited table SQL construction
|
5
|
+
changes from a where clause using `tableoid`s to using `FROM ONLY`
|
6
|
+
- Fixes an issue for Rails 7.1 regarding accessing version table columns through aliased attributes
|
7
|
+
- Fixes an issue where `Hoardable::RichText` couldn’t be loaded if `ActionText::RichText` wasn’t yet
|
8
|
+
loaded
|
9
|
+
- Supports dumping `INHERITS (table_name)` options to `schema.rb` and ensures the inherited tables
|
10
|
+
are dumped after their parents
|
11
|
+
|
12
|
+
## 0.14.3
|
13
|
+
|
14
|
+
- The migration template is updated to make the primary key on the versions table its actual primary key
|
15
|
+
|
1
16
|
## 0.14.2
|
2
17
|
|
3
18
|
- Fixes an eager loading issue regarding `ActionText::EncryptedRichText`
|
data/Gemfile
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
source
|
3
|
+
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem
|
6
|
-
|
7
|
-
gem
|
8
|
-
|
9
|
-
gem
|
10
|
-
|
11
|
-
gem
|
12
|
-
gem
|
13
|
-
gem 'yard', '~> 0.9'
|
5
|
+
gem "debug"
|
6
|
+
if (rails_version = ENV["RAILS_VERSION"])
|
7
|
+
gem "rails", "~> #{rails_version}.0"
|
8
|
+
else
|
9
|
+
gem "rails"
|
10
|
+
end
|
11
|
+
gem "syntax_tree"
|
12
|
+
gem "typeprof"
|
14
13
|
|
15
14
|
gemspec
|
data/README.md
CHANGED
@@ -1,21 +1,23 @@
|
|
1
1
|
# Hoardable 
|
2
2
|
|
3
|
-
Hoardable is an ActiveRecord extension for Ruby
|
4
|
-
and soft-deletion of records through the use of _uni-temporal inherited tables_.
|
3
|
+
Hoardable is an ActiveRecord extension for Ruby 3+, Rails 7+, and PostgreSQL that allows for
|
4
|
+
versioning 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
|
-
row of a table contains data along with one or more time ranges. In the case of this gem,
|
8
|
-
has a time range that represents the row’s valid time range - hence
|
6
|
+
[Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
|
7
|
+
where each row of a table contains data along with one or more time ranges. In the case of this gem,
|
8
|
+
each database row has a time range that represents the row’s valid time range - hence
|
9
|
+
"uni-temporal".
|
9
10
|
|
10
|
-
[Table inheritance](https://www.postgresql.org/docs/
|
11
|
-
allows a table to inherit all columns of a parent table. The descendant table’s
|
12
|
-
its parent. If a new column is added to or removed from the parent,
|
13
|
-
descendants.
|
11
|
+
[Table inheritance](https://www.postgresql.org/docs/current/ddl-inherit.html) is a feature of
|
12
|
+
PostgreSQL that allows a table to inherit all columns of a parent table. The descendant table’s
|
13
|
+
schema will stay in sync with its parent. If a new column is added to or removed from the parent,
|
14
|
+
the schema change is reflected on its descendants.
|
14
15
|
|
15
|
-
With these concepts combined, `hoardable` offers a
|
16
|
-
Versions of records are stored in separate, inherited tables along with their valid time
|
17
|
-
contextual data. Compared to other Rails-oriented versioning systems, this gem strives to
|
18
|
-
and obvious on the lower database level, while still familiar and convenient to use
|
16
|
+
With these concepts combined, `hoardable` offers a model versioning and soft deletion system for
|
17
|
+
Rails. Versions of records are stored in separate, inherited tables along with their valid time
|
18
|
+
ranges and contextual data. Compared to other Rails-oriented versioning systems, this gem strives to
|
19
|
+
be more explicit and obvious on the lower database level, while still familiar and convenient to use
|
20
|
+
within Ruby on Rails.
|
19
21
|
|
20
22
|
[👉 Documentation](https://www.rubydoc.info/gems/hoardable)
|
21
23
|
|
@@ -34,12 +36,10 @@ bin/rails g hoardable:install
|
|
34
36
|
bin/rails db:migrate
|
35
37
|
```
|
36
38
|
|
37
|
-
This will generate PostgreSQL functions, an enum and an initiailzer. It will also set
|
38
|
-
`config.active_record.schema_format = :sql` in `application.rb` if you are using Rails < 7.
|
39
|
-
|
40
39
|
### Model Installation
|
41
40
|
|
42
|
-
You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
|
41
|
+
You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
|
42
|
+
of:
|
43
43
|
|
44
44
|
```ruby
|
45
45
|
class Post < ActiveRecord::Base
|
@@ -55,31 +55,33 @@ bin/rails g hoardable:migration Post
|
|
55
55
|
bin/rails db:migrate
|
56
56
|
```
|
57
57
|
|
58
|
-
By default, it will guess the foreign key type for the `_versions` table based on the primary key of
|
59
|
-
model specified in the migration generator above. If you want/need to specify this explicitly,
|
58
|
+
By default, it will guess the foreign key type for the `_versions` table based on the primary key of
|
59
|
+
the model specified in the migration generator above. If you want/need to specify this explicitly,
|
60
|
+
you can do so:
|
60
61
|
|
61
62
|
```
|
62
63
|
bin/rails g hoardable:migration Post --foreign-key-type uuid
|
63
64
|
```
|
64
65
|
|
65
|
-
|
66
|
-
versions often, you should add appropriate indexes to the `_versions` tables.
|
66
|
+
_*Note*:_ Creating an inherited table does not inherit the indexes from the parent table. If you
|
67
|
+
need to query versions often, you should add appropriate indexes to the `_versions` tables. See
|
68
|
+
[here](https://github.com/waymondo/hoardable/issues/30) for more info.
|
67
69
|
|
68
70
|
## Usage
|
69
71
|
|
70
72
|
### Overview
|
71
73
|
|
72
|
-
Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass
|
73
|
-
model. As we continue our example from above:
|
74
|
+
Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass
|
75
|
+
of that model. As we continue our example from above:
|
74
76
|
|
75
77
|
```ruby
|
76
|
-
Post #=> Post(id: integer,
|
77
|
-
PostVersion #=> PostVersion(id: integer,
|
78
|
+
Post #=> Post(id: integer, ..., hoardable_id: integer)
|
79
|
+
PostVersion #=> PostVersion(id: integer, ..., hoardable_id: integer, _data: jsonb, _during: tsrange, _event_uuid: uuid, _operation: enum)
|
78
80
|
Post.version_class #=> same as `PostVersion`
|
79
81
|
```
|
80
82
|
|
81
|
-
A `Post` now `has_many :versions`. With the default configuration, whenever an update
|
82
|
-
`
|
83
|
+
A `Post` now `has_many :versions`. With the default configuration, whenever an update or deletion of
|
84
|
+
a `post` occurs, a version is created:
|
83
85
|
|
84
86
|
```ruby
|
85
87
|
post = Post.create!(title: "Title")
|
@@ -96,7 +98,7 @@ Post.find(post.id) # raises ActiveRecord::RecordNotFound
|
|
96
98
|
Each `PostVersion` has access to the same attributes, relationships, and other model behavior that
|
97
99
|
`Post` has, but as a read-only record:
|
98
100
|
|
99
|
-
```
|
101
|
+
```ruby
|
100
102
|
post.versions.last.update!(title: "Rewrite history") #=> raises ActiveRecord::ReadOnlyRecord
|
101
103
|
```
|
102
104
|
|
@@ -105,12 +107,15 @@ If you ever need to revert to a specific version, you can call `version.revert!`
|
|
105
107
|
```ruby
|
106
108
|
post = Post.create!(title: "Title")
|
107
109
|
post.update!(title: "Whoops")
|
108
|
-
post.reload.versions.last
|
110
|
+
version = post.reload.versions.last
|
111
|
+
version.title # -> "Title"
|
112
|
+
version.revert!
|
109
113
|
post.title # => "Title"
|
110
114
|
```
|
111
115
|
|
112
|
-
If you would like to untrash a specific version of a record you deleted, you can call
|
113
|
-
it. This will re-insert the model in the parent class’s table with the
|
116
|
+
If you would like to untrash a specific version of a record you deleted, you can call
|
117
|
+
`version.untrash!` on it. This will re-insert the model in the parent class’s table with the
|
118
|
+
original primary key.
|
114
119
|
|
115
120
|
```ruby
|
116
121
|
post = Post.create!(title: "Title")
|
@@ -124,13 +129,24 @@ trashed_post.untrash!
|
|
124
129
|
Post.find(post.id) # #<Post>
|
125
130
|
```
|
126
131
|
|
127
|
-
|
128
|
-
allows for uniquely identifying source records and versions when results are mixed
|
129
|
-
record and versions have an automatically managed `hoardable_id` that always
|
130
|
-
of the original source record.
|
132
|
+
_*Note*:_ You will notice above that both `posts` and `post_versions` pull from the same ID
|
133
|
+
sequence. This allows for uniquely identifying source records and versions when results are mixed
|
134
|
+
together. Both a source record and versions have an automatically managed `hoardable_id` that always
|
135
|
+
represents the primary key value of the original source record.
|
131
136
|
|
132
137
|
### Querying and Temporal Lookup
|
133
138
|
|
139
|
+
Including `Hoardable::Model` into your source model modifies its default scope to make sure you only
|
140
|
+
query the parent table:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
Post.where(state: :draft).to_sql # => SELECT posts.* FROM ONLY posts WHERE posts.status = 'draft'
|
144
|
+
```
|
145
|
+
|
146
|
+
_*Note*:_ If you are executing raw SQL, you will need to include the `ONLY` keyword you see above to
|
147
|
+
the select statement if you do not wish to return versions in the results. Learn more about table
|
148
|
+
inheritance in [the PostgreSQL documentation](https://www.postgresql.org/docs/current/ddl-inherit.html).
|
149
|
+
|
134
150
|
Since a `PostVersion` is an `ActiveRecord` class, you can query them like another model resource:
|
135
151
|
|
136
152
|
```ruby
|
@@ -152,32 +168,34 @@ The source model class also has an `.at` method:
|
|
152
168
|
Post.at(1.day.ago) # => [#<Post>, #<Post>]
|
153
169
|
```
|
154
170
|
|
155
|
-
This will return an ActiveRecord scoped query of all `Post` and `PostVersion` records that were
|
156
|
-
time, all cast as instances of `Post`.
|
171
|
+
This will return an ActiveRecord scoped query of all `Post` and `PostVersion` records that were
|
172
|
+
valid at that time, all cast as instances of `Post`.
|
157
173
|
|
158
|
-
There is also an `at` method on `Hoardable` itself for more complex and experimental temporal
|
159
|
-
querying. See [Relationships](#relationships) for more.
|
174
|
+
There is also an `at` method on `Hoardable` itself for more complex and experimental temporal
|
175
|
+
resource querying. See [Relationships](#relationships) for more.
|
160
176
|
|
161
|
-
By default, `hoardable` will keep copies of records you have destroyed. You can query them
|
177
|
+
By default, `hoardable` will keep copies of records you have destroyed. You can query them
|
178
|
+
specifically with:
|
162
179
|
|
163
180
|
```ruby
|
164
|
-
PostVersion.trashed
|
165
|
-
Post.version_class.trashed # <- same as above
|
181
|
+
PostVersion.trashed.where(user_id: user.id)
|
182
|
+
Post.version_class.trashed.where(user_id: user.id) # <- same as above
|
166
183
|
```
|
167
184
|
|
168
|
-
|
169
|
-
the first temporal period, you will need to ensure the source model table has a
|
170
|
-
column. If this is missing, an error will be raised.
|
185
|
+
_*Note*:_ A `Version` is not created upon initial source model creation. To accurately track the
|
186
|
+
beginning of the first temporal period, you will need to ensure the source model table has a
|
187
|
+
`created_at` timestamp column. If this is missing, an error will be raised.
|
171
188
|
|
172
189
|
### Tracking Contextual Data
|
173
190
|
|
174
|
-
You’ll often want to track contextual data about the creation of a version. There are 2 options that
|
175
|
-
provided for tracking
|
191
|
+
You’ll often want to track contextual data about the creation of a version. There are 2 options that
|
192
|
+
can be provided for tracking this:
|
176
193
|
|
177
|
-
- `:whodunit` - an identifier for who is responsible for creating the version
|
194
|
+
- `:whodunit` - an identifier for who/what is responsible for creating the version
|
178
195
|
- `:meta` - any other contextual information you’d like to store along with the version
|
179
196
|
|
180
|
-
This information is stored in a `jsonb` column. Each key’s value can be in the format of your
|
197
|
+
This information is stored in a `jsonb` column. Each key’s value can be in the format of your
|
198
|
+
choosing.
|
181
199
|
|
182
200
|
One convenient way to assign contextual data to these is by defining a proc in an initializer, i.e.:
|
183
201
|
|
@@ -186,22 +204,23 @@ One convenient way to assign contextual data to these is by defining a proc in a
|
|
186
204
|
Hoardable.whodunit = -> { Current.user&.id }
|
187
205
|
|
188
206
|
# somewhere in your app code
|
189
|
-
Current.user
|
190
|
-
post.update!(status:
|
191
|
-
post.reload.versions.last.hoardable_whodunit # => 123
|
207
|
+
Current.set(user: User.find(123)) do
|
208
|
+
post.update!(status: :live)
|
209
|
+
post.reload.versions.last.hoardable_whodunit # => 123
|
210
|
+
end
|
192
211
|
```
|
193
212
|
|
194
|
-
You can also set
|
213
|
+
You can also set these context values manually as well:
|
195
214
|
|
196
215
|
```ruby
|
197
|
-
Hoardable.meta = {
|
216
|
+
Hoardable.meta = {note: "reverting due to accidental deletion"}
|
198
217
|
post.update!(title: "We’re back!")
|
199
218
|
Hoardable.meta = nil
|
200
219
|
post.reload.versions.last.hoardable_meta['note'] # => "reverting due to accidental deletion"
|
201
220
|
```
|
202
221
|
|
203
|
-
A more useful pattern
|
204
|
-
could have the following in your `ApplicationController`:
|
222
|
+
A more useful pattern would be to use `Hoardable.with` to set the context around a block. For
|
223
|
+
example, you could have the following in your `ApplicationController`:
|
205
224
|
|
206
225
|
```ruby
|
207
226
|
class ApplicationController < ActionController::Base
|
@@ -210,7 +229,7 @@ class ApplicationController < ActionController::Base
|
|
210
229
|
private
|
211
230
|
|
212
231
|
def use_hoardable_context
|
213
|
-
Hoardable.with(whodunit: current_user.id, meta: {
|
232
|
+
Hoardable.with(whodunit: current_user.id, meta: {request_uuid: request.uuid}) do
|
214
233
|
yield
|
215
234
|
end
|
216
235
|
# `Hoardable.whodunit` and `Hoardable.meta` are back to nil or their previously set values
|
@@ -219,9 +238,10 @@ end
|
|
219
238
|
```
|
220
239
|
|
221
240
|
`hoardable` will also automatically capture the ActiveRecord
|
222
|
-
[changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the
|
223
|
-
that cause the version (`update` or `delete`), and it will also tag all versions created
|
224
|
-
transaction with a shared and unique `event_uuid` for that transaction. These
|
241
|
+
[changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the
|
242
|
+
`operation` that cause the version (`update` or `delete`), and it will also tag all versions created
|
243
|
+
in the same database transaction with a shared and unique `event_uuid` for that transaction. These
|
244
|
+
values are available as:
|
225
245
|
|
226
246
|
```ruby
|
227
247
|
version.changes
|
@@ -231,12 +251,12 @@ version.hoardable_event_uuid
|
|
231
251
|
|
232
252
|
### Model Callbacks
|
233
253
|
|
234
|
-
Sometimes you might want to do something with a version after it gets inserted to the database. You
|
235
|
-
access it in `after_versioned` callbacks on the source record as `hoardable_version`. These
|
236
|
-
`ActiveRecord`’s `.save`, which is enclosed in an ActiveRecord transaction.
|
254
|
+
Sometimes you might want to do something with a version after it gets inserted to the database. You
|
255
|
+
can access it in `after_versioned` callbacks on the source record as `hoardable_version`. These
|
256
|
+
happen within `ActiveRecord`’s `.save`, which is enclosed in an ActiveRecord transaction.
|
237
257
|
|
238
|
-
There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
|
239
|
-
source record after a version is reverted or untrashed.
|
258
|
+
There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
|
259
|
+
on the source record after a version is reverted or untrashed.
|
240
260
|
|
241
261
|
```ruby
|
242
262
|
class User
|
@@ -275,14 +295,15 @@ Hoardable.save_trash # => default true
|
|
275
295
|
|
276
296
|
`Hoardable.version_updates` globally controls whether versions get created on record updates.
|
277
297
|
|
278
|
-
`Hoardable.save_trash` globally controls whether to create versions upon record deletion.
|
279
|
-
`false`, all versions of a record will be deleted when the record is
|
298
|
+
`Hoardable.save_trash` globally controls whether to create versions upon source record deletion.
|
299
|
+
When this is set to `false`, all versions of a source record will be deleted when the record is
|
300
|
+
destroyed.
|
280
301
|
|
281
302
|
If you would like to temporarily set a config setting, you can use `Hoardable.with`:
|
282
303
|
|
283
304
|
```ruby
|
284
305
|
Hoardable.with(enabled: false) do
|
285
|
-
post.update!(title:
|
306
|
+
post.update!(title: "replace title without creating a version")
|
286
307
|
end
|
287
308
|
```
|
288
309
|
|
@@ -304,17 +325,17 @@ Comment.with_hoardable_config(version_updates: true) do
|
|
304
325
|
end
|
305
326
|
```
|
306
327
|
|
307
|
-
If a model-level option exists, it will use that. Otherwise, it will fall back to the global
|
308
|
-
config.
|
328
|
+
If a model-level option exists, it will use that. Otherwise, it will fall back to the global
|
329
|
+
`Hoardable` config.
|
309
330
|
|
310
331
|
## Relationships
|
311
332
|
|
312
333
|
### Belongs To Trashable
|
313
334
|
|
314
|
-
Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child
|
315
|
-
foreign key will point to the non-existent trashed version of the parent. If you would like
|
316
|
-
`belongs_to` resolve to the trashed parent model in this case, you can give it the option of
|
317
|
-
true`:
|
335
|
+
Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child
|
336
|
+
record’s foreign key will point to the non-existent trashed version of the parent. If you would like
|
337
|
+
to have `belongs_to` resolve to the trashed parent model in this case, you can give it the option of
|
338
|
+
`trashable: true`:
|
318
339
|
|
319
340
|
```ruby
|
320
341
|
class Comment
|
@@ -335,21 +356,21 @@ class Post
|
|
335
356
|
has_many :comments, hoardable: true
|
336
357
|
end
|
337
358
|
|
338
|
-
|
359
|
+
class Comment
|
339
360
|
include Hoardable::Model
|
340
361
|
end
|
341
362
|
|
342
|
-
post = Post.create!(title:
|
343
|
-
comment1 = post.comments.create!(body:
|
344
|
-
comment2 = post.comments.create!(body:
|
363
|
+
post = Post.create!(title: "Title")
|
364
|
+
comment1 = post.comments.create!(body: "Comment")
|
365
|
+
comment2 = post.comments.create!(body: "Comment")
|
345
366
|
datetime = DateTime.current
|
346
367
|
comment2.destroy!
|
347
|
-
post.update!(title:
|
368
|
+
post.update!(title: "New Title")
|
348
369
|
post_id = post.id # 1
|
349
370
|
|
350
371
|
Hoardable.at(datetime) do
|
351
372
|
post = Post.find(post_id)
|
352
|
-
post.title # =>
|
373
|
+
post.title # => "Title"
|
353
374
|
post.comments.size # => 2
|
354
375
|
post.id # => 2
|
355
376
|
post.version? # => true
|
@@ -357,26 +378,26 @@ Hoardable.at(datetime) do
|
|
357
378
|
end
|
358
379
|
```
|
359
380
|
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
a database trigger won’t allow you to.
|
381
|
+
You’ll notice above that the `post` within the `#at` block is actually a temporal `post_version`,
|
382
|
+
since it has been subsequently updated and has a different id - it is reified as a `post` for the
|
383
|
+
purposes of your business logic (serialization, rendering views, exporting, etc). Don’t fret - you
|
384
|
+
will not be able to commit any updates to the version, even though it is masquerading as a `Post`
|
385
|
+
because a database trigger won’t allow it.
|
366
386
|
|
367
|
-
If you are ever unsure if a Hoardable record is a source record or a version, you can be sure by
|
368
|
-
`version?` on it. If you want to get the true original source record ID, you can call
|
387
|
+
If you are ever unsure if a Hoardable record is a source record or a version, you can be sure by
|
388
|
+
calling `version?` on it. If you want to get the true original source record ID, you can call
|
389
|
+
`hoardable_id`.
|
369
390
|
|
370
|
-
|
371
|
-
data sets.
|
391
|
+
_*Note*:_ `Hoardable.at` is still very experimental and is potentially not performant for querying
|
392
|
+
large data sets.
|
372
393
|
|
373
394
|
### Cascading Untrashing
|
374
395
|
|
375
|
-
Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and if you untrash
|
376
|
-
record, you’ll want to also untrash the children. Whenever a hoardable version is created
|
377
|
-
transaction, it will create or re-use a unique event UUID for the current database
|
378
|
-
versions created with it. That way, when you `untrash!` a record, you could
|
379
|
-
were trashed with it:
|
396
|
+
Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and if you untrash
|
397
|
+
the parent record, you’ll want to also untrash the children. Whenever a hoardable version is created
|
398
|
+
in a database transaction, it will create or re-use a unique event UUID for the current database
|
399
|
+
transaction and tag all versions created with it. That way, when you `untrash!` a record, you could
|
400
|
+
find and `untrash!` records that were trashed with it:
|
380
401
|
|
381
402
|
```ruby
|
382
403
|
class Post < ActiveRecord::Base
|
@@ -395,20 +416,22 @@ end
|
|
395
416
|
|
396
417
|
### Action Text
|
397
418
|
|
398
|
-
Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a
|
399
|
-
table for `ActionText::RichText`:
|
419
|
+
Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a
|
420
|
+
temporal table for `ActionText::RichText`:
|
400
421
|
|
401
422
|
```
|
402
423
|
bin/rails g hoardable:migration ActionText::RichText
|
403
424
|
bin/rails db:migrate
|
404
425
|
```
|
405
426
|
|
406
|
-
Then in your model include `Hoardable::Model` and provide the `hoardable: true` keyword to
|
427
|
+
Then in your model include `Hoardable::Model` and provide the `hoardable: true` keyword to
|
428
|
+
`has_rich_text`:
|
407
429
|
|
408
430
|
```ruby
|
409
431
|
class Post < ActiveRecord::Base
|
410
432
|
include Hoardable::Model # or `Hoardable::Associations` if you don't need `PostVersion`
|
411
433
|
has_rich_text :content, hoardable: true
|
434
|
+
# alternately, this could be `has_hoardable_rich_text :content`
|
412
435
|
end
|
413
436
|
```
|
414
437
|
|
@@ -431,62 +454,64 @@ end
|
|
431
454
|
|
432
455
|
Rails uses a method called
|
433
456
|
[`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
|
435
|
-
assigning `hoardable_id` from the primary key’s value. If you would still like to use
|
436
|
-
specify the primary key’s value and `hoardable_id` to the same identifier value
|
437
|
-
an issue with fixture replacement libraries like `factory_bot` or
|
457
|
+
when inserting fixtures into the database. This disables PostgreSQL triggers, which Hoardable relies
|
458
|
+
on for assigning `hoardable_id` from the primary key’s value. If you would still like to use
|
459
|
+
fixtures, you must specify the primary key’s value and `hoardable_id` to the same identifier value
|
460
|
+
in the fixture. This is not an issue with fixture replacement libraries like `factory_bot` or
|
438
461
|
[`world_factory`](https://github.com/FutureProofRetail/world_factory) however.
|
439
462
|
|
440
463
|
## Gem Comparison
|
441
464
|
|
442
465
|
#### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
|
443
466
|
|
444
|
-
`paper_trail` is maybe the most popular and fully featured gem in this space. It works for other
|
445
|
-
types than PostgeSQL
|
446
|
-
table. It stores changes in a `text`, `json`, or `jsonb` column. In order to
|
447
|
-
table, a `jsonb` column should be used, which
|
448
|
-
configuration, all `versions` for all models types are in
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
467
|
+
`paper_trail` is maybe the most popular and fully featured gem in this space. It works for other
|
468
|
+
database types than PostgeSQL. Bby default it stores all versions of all versioned models in a
|
469
|
+
single `versions` table. It stores changes in a `text`, `json`, or `jsonb` column. In order to
|
470
|
+
efficiently query the `versions` table, a `jsonb` column should be used, which can take up a lot of
|
471
|
+
space to index. Unless you customize your configuration, all `versions` for all models types are in
|
472
|
+
the same table which is inefficient if you are only interested in querying versions of a single
|
473
|
+
model. By contrast, `hoardable` stores versions in smaller, isolated, inherited tables with the same
|
474
|
+
database columns as their parents, which are more efficient for querying as well as auditing for
|
475
|
+
truncating and dropping. The concept of a temporal timeframe does not exist for a single version
|
476
|
+
since there is only a `created_at` timestamp.
|
453
477
|
|
454
478
|
#### [`audited`](https://github.com/collectiveidea/audited)
|
455
479
|
|
456
|
-
`audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in
|
457
|
-
table, you must opt into using `jsonb` as the column type to store "changes", in case you
|
458
|
-
and there is no concept of a
|
459
|
-
contextual data requirements and stores them as top level data types on
|
480
|
+
`audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in
|
481
|
+
a single table, you must opt into using `jsonb` as the column type to store "changes", in case you
|
482
|
+
want to query them, and there is no concept of a temporal timeframe for a single version. It makes
|
483
|
+
opinionated decisions about contextual data requirements and stores them as top level data types on
|
484
|
+
the `audited` table.
|
460
485
|
|
461
486
|
#### [`discard`](https://github.com/jhawthorn/discard)
|
462
487
|
|
463
|
-
`discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through
|
464
|
-
time-stamping of a `discarded_at` column on the records table
|
465
|
-
caused the soft deletion unless you implement it yourself. Once the "discarded"
|
466
|
-
previous "discarded" awareness is lost. Since "discarded" records exist in
|
467
|
-
records, you must explicitly omit the discarded records from queries
|
468
|
-
leaking in.
|
488
|
+
`discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through
|
489
|
+
the time-stamping of a `discarded_at` column on the records table. There is no other capturing of
|
490
|
+
the event that caused the soft deletion unless you implement it yourself. Once the "discarded"
|
491
|
+
record is restored, the previous "discarded" awareness is lost. Since "discarded" records exist in
|
492
|
+
the same table as "undiscarded" records, you must explicitly omit the discarded records from queries
|
493
|
+
across your app to keep them from leaking in.
|
469
494
|
|
470
495
|
#### [`paranoia`](https://github.com/rubysherpas/paranoia)
|
471
496
|
|
472
|
-
`paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead
|
473
|
-
`paranoia` because of the fact they override ActiveRecord’s `delete` and `destroy` methods.
|
474
|
-
employs callbacks to create trashed versions instead of overriding methods. Otherwise,
|
475
|
-
similarly to `discard` in that it keeps deleted records in the same table and tags
|
476
|
-
timestamp. No other information about the soft-deletion event is stored.
|
497
|
+
`paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead
|
498
|
+
of `paranoia` because of the fact they override ActiveRecord’s `delete` and `destroy` methods.
|
499
|
+
`hoardable` employs callbacks to create trashed versions instead of overriding methods. Otherwise,
|
500
|
+
`paranoia` works similarly to `discard` in that it keeps deleted records in the same table and tags
|
501
|
+
them with a `deleted_at` timestamp. No other information about the soft-deletion event is stored.
|
477
502
|
|
478
503
|
#### [`logidze`](https://github.com/palkan/logidze)
|
479
504
|
|
480
|
-
`logidze` is an interesting versioning alternative that leverages the power of PostgreSQL triggers.
|
481
|
-
of storing the previous versions or changes in a separate table, it stores them in a
|
482
|
-
directly on the database row of the record itself. If does not support soft
|
505
|
+
`logidze` is an interesting versioning alternative that leverages the power of PostgreSQL triggers.
|
506
|
+
Instead of storing the previous versions or changes in a separate table, it stores them in a
|
507
|
+
proprietary JSON format directly on the database row of the record itself. If does not support soft
|
508
|
+
deletion.
|
483
509
|
|
484
510
|
## Contributing
|
485
511
|
|
486
|
-
This gem still quite new and very open to feedback.
|
487
|
-
|
488
512
|
Bug reports and pull requests are welcome on GitHub at https://github.com/waymondo/hoardable.
|
489
513
|
|
490
514
|
## License
|
491
515
|
|
492
|
-
The gem is available as open source under the terms of the [MIT
|
516
|
+
The gem is available as open source under the terms of the [MIT
|
517
|
+
License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -1,16 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
require "syntax_tree/rake_tasks"
|
5
6
|
|
6
7
|
Rake::TestTask.new(:test) do |t|
|
7
|
-
t.libs <<
|
8
|
-
t.libs <<
|
9
|
-
t.test_files = FileList[
|
8
|
+
t.libs << "test"
|
9
|
+
t.libs << "lib"
|
10
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
10
11
|
end
|
11
12
|
|
12
|
-
|
13
|
+
SOURCE_FILES = %w[test/**/*.rb lib/**/*.rb Rakefile Gemfile bin/console hoardable.gemspec]
|
13
14
|
|
14
|
-
|
15
|
+
SyntaxTree::Rake::CheckTask.new(:check) do |t|
|
16
|
+
t.source_files = SOURCE_FILES
|
17
|
+
t.print_width = 100
|
18
|
+
end
|
19
|
+
|
20
|
+
SyntaxTree::Rake::WriteTask.new(:write) do |t|
|
21
|
+
t.source_files = SOURCE_FILES
|
22
|
+
t.print_width = 100
|
23
|
+
end
|
24
|
+
|
25
|
+
task :typeprof do
|
26
|
+
`typeprof lib/hoardable.rb`
|
27
|
+
end
|
15
28
|
|
16
|
-
task default: %i[test
|
29
|
+
task default: %i[check test]
|
30
|
+
task pre_commit: %i[write typeprof]
|