hoardable 0.14.2 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 ![gem version](https://img.shields.io/gem/v/hoardable?style=flat-square)
|
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]
|