hoardable 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile +1 -1
- data/README.md +122 -111
- data/lib/generators/hoardable/functions/set_hoardable_id.sql +7 -0
- data/lib/generators/hoardable/install_generator.rb +1 -1
- data/lib/generators/hoardable/migration_generator.rb +14 -0
- data/lib/generators/hoardable/templates/install.rb.erb +0 -1
- data/lib/generators/hoardable/templates/migration.rb.erb +2 -1
- data/lib/generators/hoardable/triggers/set_hoardable_id.sql +1 -1
- data/lib/hoardable/arel_visitors.rb +9 -3
- data/lib/hoardable/database_client.rb +8 -1
- data/lib/hoardable/engine.rb +11 -1
- data/lib/hoardable/error.rb +10 -0
- data/lib/hoardable/has_one.rb +3 -3
- data/lib/hoardable/schema_statements.rb +1 -1
- data/lib/hoardable/version.rb +1 -1
- metadata +8 -26
- data/lib/generators/hoardable/functions/hoardable_source_set_id.sql +0 -18
- /data/lib/generators/hoardable/{functions → install_functions}/hoardable_prevent_update_id.sql +0 -0
- /data/lib/generators/hoardable/{functions → install_functions}/hoardable_version_prevent_update.sql +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 888659d9c6655e69c72fd8cf5b10b3697d5a81f432a44c8fa39984a05f4b23d9
|
4
|
+
data.tar.gz: ba850c1bf2363dd965a6e7780435d4feaf2ad417f2712f5f4635398ad5e9913d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 851d039e113df11834b18bb69edf786ba1d91598e408a45fab6ae7bb65efbc37264e4b803b8c8335f6839784f964f20d29c4c94b02aa6b994a21f3fd346ec162
|
7
|
+
data.tar.gz: 022a7ada38d996f8116a7ab8ed3d671e29055a8c75cc4155789ea3be135d87f24625ab67311c22f3b89b172320a973cbf203586407c6788750443bec90b461be
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Hoardable 
|
2
2
|
|
3
|
-
Hoardable is an ActiveRecord extension for Ruby 3+, Rails 7+, and PostgreSQL that allows for
|
3
|
+
Hoardable is an ActiveRecord extension for Ruby 3+, Rails 7+, and PostgreSQL 9+ that allows for
|
4
4
|
versioning and soft-deletion of records through the use of _uni-temporal inherited tables_.
|
5
5
|
|
6
6
|
[Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
|
@@ -9,15 +9,13 @@ each database row has a time range that represents the row’s valid time range
|
|
9
9
|
"uni-temporal".
|
10
10
|
|
11
11
|
[Table inheritance](https://www.postgresql.org/docs/current/ddl-inherit.html) is a feature of
|
12
|
-
PostgreSQL that allows
|
13
|
-
|
14
|
-
|
12
|
+
PostgreSQL that allows one table to inherit all columns from a parent. The descendant table’s schema
|
13
|
+
will stay in sync with its parent; if a new column is added to or removed from the parent, the
|
14
|
+
schema change is reflected on its descendants.
|
15
15
|
|
16
16
|
With these concepts combined, `hoardable` offers a model versioning and soft deletion system for
|
17
17
|
Rails. Versions of records are stored in separate, inherited tables along with their valid time
|
18
|
-
ranges and contextual data.
|
19
|
-
be more explicit and obvious on the lower database level, while still familiar and convenient to use
|
20
|
-
within Ruby on Rails.
|
18
|
+
ranges and contextual data.
|
21
19
|
|
22
20
|
[👉 Documentation](https://www.rubydoc.info/gems/hoardable)
|
23
21
|
|
@@ -26,7 +24,7 @@ within Ruby on Rails.
|
|
26
24
|
Add this line to your application's Gemfile:
|
27
25
|
|
28
26
|
```ruby
|
29
|
-
gem
|
27
|
+
gem "hoardable"
|
30
28
|
```
|
31
29
|
|
32
30
|
Run `bundle install`, and then run:
|
@@ -36,10 +34,9 @@ bin/rails g hoardable:install
|
|
36
34
|
bin/rails db:migrate
|
37
35
|
```
|
38
36
|
|
39
|
-
### Model
|
37
|
+
### Model installation
|
40
38
|
|
41
|
-
|
42
|
-
of:
|
39
|
+
Include `Hoardable::Model` into an ActiveRecord model you would like to hoard versions of:
|
43
40
|
|
44
41
|
```ruby
|
45
42
|
class Post < ActiveRecord::Base
|
@@ -48,24 +45,15 @@ class Post < ActiveRecord::Base
|
|
48
45
|
end
|
49
46
|
```
|
50
47
|
|
51
|
-
|
48
|
+
Run the generator command to create a database migration and migrate it:
|
52
49
|
|
53
50
|
```
|
54
51
|
bin/rails g hoardable:migration Post
|
55
52
|
bin/rails db:migrate
|
56
53
|
```
|
57
54
|
|
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:
|
61
|
-
|
62
|
-
```
|
63
|
-
bin/rails g hoardable:migration Post --foreign-key-type uuid
|
64
|
-
```
|
65
|
-
|
66
55
|
_*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.
|
68
|
-
[here](https://github.com/waymondo/hoardable/issues/30) for more info.
|
56
|
+
need to query versions often, you should add appropriate indexes to the `_versions` tables.
|
69
57
|
|
70
58
|
## Usage
|
71
59
|
|
@@ -119,33 +107,48 @@ original primary key.
|
|
119
107
|
|
120
108
|
```ruby
|
121
109
|
post = Post.create!(title: "Title")
|
122
|
-
post.id # => 1
|
123
110
|
post.destroy!
|
124
111
|
post.versions.size # => 1
|
125
112
|
Post.find(post.id) # raises ActiveRecord::RecordNotFound
|
126
113
|
trashed_post = post.versions.trashed.last
|
127
|
-
trashed_post.id # => 2
|
128
114
|
trashed_post.untrash!
|
129
115
|
Post.find(post.id) # #<Post>
|
130
116
|
```
|
131
117
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
represents the primary key value of the original source record.
|
118
|
+
Source and version records pull from the same ID sequence. This allows for uniquely identifying
|
119
|
+
records from each other. Both source record and version have an automatically managed `hoardable_id`
|
120
|
+
attribute that always represents the primary key value of the original source record:
|
136
121
|
|
137
|
-
|
122
|
+
```ruby
|
123
|
+
post = Post.create!(title: "Title")
|
124
|
+
post.id # => 1
|
125
|
+
post.hoardable_id # => 1
|
126
|
+
post.version? # => false
|
127
|
+
post.update!(title: "New Title")
|
128
|
+
version = post.reload.versions.last
|
129
|
+
version.id # => 2
|
130
|
+
version.hoardable_id # => 1
|
131
|
+
version.version? # => true
|
132
|
+
```
|
138
133
|
|
139
|
-
|
140
|
-
|
134
|
+
### Querying and temporal lookup
|
135
|
+
|
136
|
+
Including `Hoardable::Model` into your source model modifies `default_scope` to make sure you only
|
137
|
+
ever query the parent table and not the inherited ones:
|
141
138
|
|
142
139
|
```ruby
|
143
140
|
Post.where(state: :draft).to_sql # => SELECT posts.* FROM ONLY posts WHERE posts.status = 'draft'
|
144
141
|
```
|
145
142
|
|
146
|
-
|
147
|
-
|
148
|
-
|
143
|
+
Note the `FROM ONLY` above. If you are executing raw SQL, you will need to include the `ONLY`
|
144
|
+
keyword if you do not wish to return versions in your results. This includes `JOIN`-ing on this
|
145
|
+
table as well.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
User.joins(:posts).to_sql # => SELECT users.* FROM users INNER JOIN ONLY posts ON posts.user_id = users.id
|
149
|
+
```
|
150
|
+
|
151
|
+
Learn more about table inheritance in [the PostgreSQL documentation](https://www.postgresql.org/docs/current/ddl-inherit.html).
|
149
152
|
|
150
153
|
Since a `PostVersion` is an `ActiveRecord` class, you can query them like another model resource:
|
151
154
|
|
@@ -153,6 +156,14 @@ Since a `PostVersion` is an `ActiveRecord` class, you can query them like anothe
|
|
153
156
|
post.versions.where(state: :draft)
|
154
157
|
```
|
155
158
|
|
159
|
+
By default, `hoardable` will keep copies of records you have destroyed. You can query them
|
160
|
+
specifically with:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
PostVersion.trashed.where(user_id: user.id)
|
164
|
+
Post.version_class.trashed.where(user_id: user.id) # <- same as above
|
165
|
+
```
|
166
|
+
|
156
167
|
If you want to look-up the version of a record at a specific time, you can use the `.at` method:
|
157
168
|
|
158
169
|
```ruby
|
@@ -169,24 +180,13 @@ Post.at(1.day.ago) # => [#<Post>, #<Post>]
|
|
169
180
|
```
|
170
181
|
|
171
182
|
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`.
|
173
|
-
|
174
|
-
There is also an `at` method on `Hoardable` itself for more complex and experimental temporal
|
175
|
-
resource querying. See [Relationships](#relationships) for more.
|
176
|
-
|
177
|
-
By default, `hoardable` will keep copies of records you have destroyed. You can query them
|
178
|
-
specifically with:
|
179
|
-
|
180
|
-
```ruby
|
181
|
-
PostVersion.trashed.where(user_id: user.id)
|
182
|
-
Post.version_class.trashed.where(user_id: user.id) # <- same as above
|
183
|
-
```
|
183
|
+
valid at that time, all cast as instances of `Post`. Updates to the versions table are forbidden in
|
184
|
+
this case by a database trigger.
|
184
185
|
|
185
|
-
|
186
|
-
|
187
|
-
`created_at` timestamp column. If this is missing, an error will be raised.
|
186
|
+
There is also `Hoardable.at` for more complex and experimental temporal resource querying. See
|
187
|
+
[Relationships](#relationships) for more.
|
188
188
|
|
189
|
-
### Tracking
|
189
|
+
### Tracking contextual data
|
190
190
|
|
191
191
|
You’ll often want to track contextual data about the creation of a version. There are 2 options that
|
192
192
|
can be provided for tracking this:
|
@@ -194,8 +194,7 @@ can be provided for tracking this:
|
|
194
194
|
- `:whodunit` - an identifier for who/what is responsible for creating the version
|
195
195
|
- `:meta` - any other contextual information you’d like to store along with the version
|
196
196
|
|
197
|
-
This information is stored in a `jsonb` column. Each
|
198
|
-
choosing.
|
197
|
+
This information is stored in a `jsonb` column. Each value can be the data type of your choosing.
|
199
198
|
|
200
199
|
One convenient way to assign contextual data to these is by defining a proc in an initializer, i.e.:
|
201
200
|
|
@@ -210,16 +209,7 @@ Current.set(user: User.find(123)) do
|
|
210
209
|
end
|
211
210
|
```
|
212
211
|
|
213
|
-
|
214
|
-
|
215
|
-
```ruby
|
216
|
-
Hoardable.meta = {note: "reverting due to accidental deletion"}
|
217
|
-
post.update!(title: "We’re back!")
|
218
|
-
Hoardable.meta = nil
|
219
|
-
post.reload.versions.last.hoardable_meta['note'] # => "reverting due to accidental deletion"
|
220
|
-
```
|
221
|
-
|
222
|
-
A more useful pattern would be to use `Hoardable.with` to set the context around a block. For
|
212
|
+
Another useful pattern would be to use `Hoardable.with` to set the context around a block. For
|
223
213
|
example, you could have the following in your `ApplicationController`:
|
224
214
|
|
225
215
|
```ruby
|
@@ -229,31 +219,42 @@ class ApplicationController < ActionController::Base
|
|
229
219
|
private
|
230
220
|
|
231
221
|
def use_hoardable_context
|
232
|
-
Hoardable.with(whodunit: current_user.id, meta: {request_uuid: request.uuid}) do
|
222
|
+
Hoardable.with(whodunit: current_user.id, meta: { request_uuid: request.uuid }) do
|
233
223
|
yield
|
234
224
|
end
|
235
|
-
# `Hoardable.whodunit` and `Hoardable.meta` are back to nil or their previously set values
|
236
225
|
end
|
237
226
|
end
|
238
227
|
```
|
239
228
|
|
240
|
-
|
241
|
-
|
242
|
-
`
|
243
|
-
in the same database transaction with a shared and unique `event_uuid` for that transaction. These
|
244
|
-
values are available as:
|
229
|
+
[ActiveRecord changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes)
|
230
|
+
are also automatically captured along with the `operation` that caused the version (`update` or
|
231
|
+
`delete`). These values are available as:
|
245
232
|
|
246
233
|
```ruby
|
247
|
-
version.changes
|
248
|
-
version.hoardable_operation
|
249
|
-
version.hoardable_event_uuid
|
234
|
+
version.changes # => { "title"=> ["Title", "New Title"] }
|
235
|
+
version.hoardable_operation # => "update"
|
250
236
|
```
|
251
237
|
|
238
|
+
### Overriding the temporal range
|
239
|
+
|
240
|
+
When calculating the temporal range for a given version, the default upper bound is `Time.now.utc`.
|
241
|
+
|
242
|
+
You can, however, use the `Hoardable.travel_to` class method to specify a custom upper bound for the time range. This allows
|
243
|
+
you to specify the datetime that a particular change should be recorded at by passing a block:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
Hoardable.travel_to(2.weeks.ago) do
|
247
|
+
post.destroy!
|
248
|
+
end
|
249
|
+
```
|
250
|
+
|
251
|
+
Note: If the provided datetime pre-dates the calculated lower bound then an `InvalidTemporalUpperBoundError` will be raised.
|
252
|
+
|
252
253
|
### Model Callbacks
|
253
254
|
|
254
255
|
Sometimes you might want to do something with a version after it gets inserted to the database. You
|
255
256
|
can access it in `after_versioned` callbacks on the source record as `hoardable_version`. These
|
256
|
-
happen within `ActiveRecord
|
257
|
+
happen within `ActiveRecord#save`'s transaction.
|
257
258
|
|
258
259
|
There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
|
259
260
|
on the source record after a version is reverted or untrashed.
|
@@ -286,9 +287,9 @@ end
|
|
286
287
|
The configurable options are:
|
287
288
|
|
288
289
|
```ruby
|
289
|
-
Hoardable.enabled # =>
|
290
|
-
Hoardable.version_updates # =>
|
291
|
-
Hoardable.save_trash # =>
|
290
|
+
Hoardable.enabled # => true
|
291
|
+
Hoardable.version_updates # => true
|
292
|
+
Hoardable.save_trash # => true
|
292
293
|
```
|
293
294
|
|
294
295
|
`Hoardable.enabled` globally controls whether versions will be ever be created.
|
@@ -299,7 +300,7 @@ Hoardable.save_trash # => default true
|
|
299
300
|
When this is set to `false`, all versions of a source record will be deleted when the record is
|
300
301
|
destroyed.
|
301
302
|
|
302
|
-
If you would like to temporarily set a config
|
303
|
+
If you would like to temporarily set a config value, you can use `Hoardable.with`:
|
303
304
|
|
304
305
|
```ruby
|
305
306
|
Hoardable.with(enabled: false) do
|
@@ -325,12 +326,11 @@ Comment.with_hoardable_config(version_updates: true) do
|
|
325
326
|
end
|
326
327
|
```
|
327
328
|
|
328
|
-
|
329
|
-
`Hoardable` config.
|
329
|
+
Model-level configuration overrides global configuration.
|
330
330
|
|
331
331
|
## Relationships
|
332
332
|
|
333
|
-
###
|
333
|
+
### `belongs_to`
|
334
334
|
|
335
335
|
Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child
|
336
336
|
record’s foreign key will point to the non-existent trashed version of the parent. If you would like
|
@@ -338,17 +338,28 @@ to have `belongs_to` resolve to the trashed parent model in this case, you can g
|
|
338
338
|
`trashable: true`:
|
339
339
|
|
340
340
|
```ruby
|
341
|
+
class Post
|
342
|
+
include Hoardable::Model
|
343
|
+
has_many :comments, dependent: nil
|
344
|
+
end
|
345
|
+
|
341
346
|
class Comment
|
342
347
|
include Hoardable::Associations # <- This includes is not required if this model already includes `Hoardable::Model`
|
343
348
|
belongs_to :post, trashable: true
|
344
349
|
end
|
350
|
+
|
351
|
+
post = Post.create!(title: "Title")
|
352
|
+
comment = post.comments.create!(body: "Comment")
|
353
|
+
post.destroy!
|
354
|
+
comment.post # => #<PostVersion>
|
345
355
|
```
|
346
356
|
|
347
|
-
###
|
357
|
+
### `has_many` & `has_one`
|
348
358
|
|
349
|
-
Sometimes you'll have a Hoardable record that `has_one` or `has_many` other Hoardable records and
|
350
|
-
want to know the state of both the parent record and the children at a certain point in time.
|
351
|
-
this by adding `hoardable: true` to the `has_many` relationship and using the
|
359
|
+
Sometimes you'll have a Hoardable record that `has_one` or `has_many` other Hoardable records and
|
360
|
+
you’ll want to know the state of both the parent record and the children at a certain point in time.
|
361
|
+
You can accomplish this by adding `hoardable: true` to the `has_many` relationship and using the
|
362
|
+
`Hoardable.at` method:
|
352
363
|
|
353
364
|
```ruby
|
354
365
|
class Post
|
@@ -364,6 +375,7 @@ post = Post.create!(title: "Title")
|
|
364
375
|
comment1 = post.comments.create!(body: "Comment")
|
365
376
|
comment2 = post.comments.create!(body: "Comment")
|
366
377
|
datetime = DateTime.current
|
378
|
+
|
367
379
|
comment2.destroy!
|
368
380
|
post.update!(title: "New Title")
|
369
381
|
post_id = post.id # 1
|
@@ -372,37 +384,31 @@ Hoardable.at(datetime) do
|
|
372
384
|
post = Post.find(post_id)
|
373
385
|
post.title # => "Title"
|
374
386
|
post.comments.size # => 2
|
375
|
-
post.id # => 2
|
376
387
|
post.version? # => true
|
388
|
+
post.id # => 2
|
377
389
|
post.hoardable_id # => 1
|
378
390
|
end
|
379
391
|
```
|
380
392
|
|
381
|
-
|
382
|
-
|
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.
|
386
|
-
|
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`.
|
390
|
-
|
391
|
-
_*Note*:_ `Hoardable.at` is still very experimental and is potentially not performant for querying
|
392
|
-
large data sets.
|
393
|
+
_*Note*:_ `Hoardable.at` is experimental and potentially not performant for querying very large data
|
394
|
+
sets.
|
393
395
|
|
394
396
|
### Cascading Untrashing
|
395
397
|
|
396
398
|
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
|
398
|
-
|
399
|
-
transaction
|
400
|
-
|
399
|
+
the parent record, you’ll want to also untrash the children. Whenever a hoardable versions are
|
400
|
+
created, it will share a unique event UUID for all other versions created in the same database
|
401
|
+
transaction. That way, when you `untrash!` a record, you could find and `untrash!` records that were
|
402
|
+
trashed with it:
|
401
403
|
|
402
404
|
```ruby
|
405
|
+
class Comment < ActiveRecord::Base
|
406
|
+
include Hoardable::Model
|
407
|
+
end
|
408
|
+
|
403
409
|
class Post < ActiveRecord::Base
|
404
410
|
include Hoardable::Model
|
405
|
-
has_many :comments, hoardable: true, dependent: :destroy
|
411
|
+
has_many :comments, hoardable: true, dependent: :destroy
|
406
412
|
|
407
413
|
after_untrashed do
|
408
414
|
Comment
|
@@ -430,8 +436,7 @@ Then in your model include `Hoardable::Model` and provide the `hoardable: true`
|
|
430
436
|
```ruby
|
431
437
|
class Post < ActiveRecord::Base
|
432
438
|
include Hoardable::Model # or `Hoardable::Associations` if you don't need `PostVersion`
|
433
|
-
has_rich_text :content, hoardable: true
|
434
|
-
# alternately, this could be `has_hoardable_rich_text :content`
|
439
|
+
has_rich_text :content, hoardable: true # or `has_hoardable_rich_text :content`
|
435
440
|
end
|
436
441
|
```
|
437
442
|
|
@@ -448,19 +453,18 @@ Hoardable.at(datetime) do
|
|
448
453
|
end
|
449
454
|
```
|
450
455
|
|
451
|
-
## Known
|
456
|
+
## Known gotchas
|
452
457
|
|
453
|
-
### Rails
|
458
|
+
### Rails fixtures
|
454
459
|
|
455
460
|
Rails uses a method called
|
456
461
|
[`disable_referential_integrity`](https://github.com/rails/rails/blob/06e9fbd954ab113108a7982357553fdef285bff1/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb#L7)
|
457
462
|
when inserting fixtures into the database. This disables PostgreSQL triggers, which Hoardable relies
|
458
463
|
on for assigning `hoardable_id` from the primary key’s value. If you would still like to use
|
459
464
|
fixtures, you must specify the primary key’s value and `hoardable_id` to the same identifier value
|
460
|
-
in the fixture.
|
461
|
-
[`world_factory`](https://github.com/FutureProofRetail/world_factory) however.
|
465
|
+
in the fixture.
|
462
466
|
|
463
|
-
## Gem
|
467
|
+
## Gem comparison
|
464
468
|
|
465
469
|
#### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
|
466
470
|
|
@@ -507,11 +511,18 @@ Instead of storing the previous versions or changes in a separate table, it stor
|
|
507
511
|
proprietary JSON format directly on the database row of the record itself. If does not support soft
|
508
512
|
deletion.
|
509
513
|
|
514
|
+
## Testing
|
515
|
+
|
516
|
+
Hoardable is tested against a matrix of Ruby 3 versions and Rails 7 & 8. To run tests locally, run:
|
517
|
+
|
518
|
+
```
|
519
|
+
rake
|
520
|
+
```
|
521
|
+
|
510
522
|
## Contributing
|
511
523
|
|
512
524
|
Bug reports and pull requests are welcome on GitHub at https://github.com/waymondo/hoardable.
|
513
525
|
|
514
526
|
## License
|
515
527
|
|
516
|
-
The gem is available as open source under the terms of the [MIT
|
517
|
-
License](https://opensource.org/licenses/MIT).
|
528
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -25,7 +25,7 @@ module Hoardable
|
|
25
25
|
|
26
26
|
def create_functions
|
27
27
|
Dir
|
28
|
-
.glob(File.join(__dir__, "
|
28
|
+
.glob(File.join(__dir__, "install_functions", "*.sql"))
|
29
29
|
.each do |file_path|
|
30
30
|
file_name = file_path.match(%r{([^/]+)\.sql})[1]
|
31
31
|
template file_path, "db/functions/#{file_name}_v01.sql"
|
@@ -36,7 +36,21 @@ module Hoardable
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
+
def create_function
|
40
|
+
template("../functions/set_hoardable_id.sql", "db/functions/#{function_name}_v01.sql")
|
41
|
+
end
|
42
|
+
|
39
43
|
no_tasks do
|
44
|
+
def function_name
|
45
|
+
"hoardable_set_hoardable_id_from_#{primary_key}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def table_name
|
49
|
+
class_name.singularize.constantize.table_name
|
50
|
+
rescue StandardError
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
40
54
|
def foreign_key_type
|
41
55
|
options[:foreign_key_type] ||
|
42
56
|
class_name.singularize.constantize.columns.find { |col| col.name == primary_key }.sql_type
|
@@ -4,7 +4,6 @@ class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.cur
|
|
4
4
|
def change
|
5
5
|
<% if postgres_version < 13 %>enable_extension :pgcrypto
|
6
6
|
<% end %>create_function :hoardable_prevent_update_id
|
7
|
-
create_function :hoardable_source_set_id
|
8
7
|
create_function :hoardable_version_prevent_update
|
9
8
|
create_enum :hoardable_operation, %w[update delete insert]
|
10
9
|
end
|
@@ -6,7 +6,7 @@ class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Mi
|
|
6
6
|
add_index :<%= table_name %>, :hoardable_id
|
7
7
|
create_table(
|
8
8
|
:<%= singularized_table_name %>_versions,
|
9
|
-
id: false,
|
9
|
+
id: false,
|
10
10
|
options: 'INHERITS (<%= table_name %>)',
|
11
11
|
) do |t|
|
12
12
|
t.jsonb :_data
|
@@ -25,6 +25,7 @@ class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Mi
|
|
25
25
|
:<%= singularized_table_name %>_versions_prevent_update,
|
26
26
|
on: :<%= singularized_table_name %>_versions
|
27
27
|
)
|
28
|
+
create_function :<%= function_name %>
|
28
29
|
create_trigger :<%= table_name %>_set_hoardable_id, on: :<%= table_name %>
|
29
30
|
create_trigger :<%= table_name %>_prevent_update_hoardable_id, on: :<%= table_name %>
|
30
31
|
change_column_null :<%= table_name %>, :hoardable_id, false
|
@@ -40,10 +40,16 @@ module Hoardable
|
|
40
40
|
end
|
41
41
|
|
42
42
|
private def hoardable_maybe_add_only(o, collector)
|
43
|
-
|
44
|
-
return if Hoardable.instance_variable_get("@at")
|
43
|
+
left = o.left
|
45
44
|
|
46
|
-
|
45
|
+
if left.is_a?(Arel::Nodes::TableAlias)
|
46
|
+
hoardable_maybe_add_only(left, collector)
|
47
|
+
else
|
48
|
+
return unless left.instance_variable_get("@klass").in?(Hoardable::REGISTRY)
|
49
|
+
return if Hoardable.instance_variable_get("@at")
|
50
|
+
|
51
|
+
collector << "ONLY "
|
52
|
+
end
|
47
53
|
end
|
48
54
|
end
|
49
55
|
end
|
@@ -93,7 +93,14 @@ module Hoardable
|
|
93
93
|
end
|
94
94
|
|
95
95
|
def initialize_temporal_range
|
96
|
-
(
|
96
|
+
upper_bound = Hoardable.instance_variable_get("@travel_to") || Time.now.utc
|
97
|
+
lower_bound = (previous_temporal_tsrange_end || hoardable_source_epoch)
|
98
|
+
|
99
|
+
if upper_bound < lower_bound
|
100
|
+
raise InvalidTemporalUpperBoundError.new(upper_bound, lower_bound)
|
101
|
+
end
|
102
|
+
|
103
|
+
(lower_bound..upper_bound)
|
97
104
|
end
|
98
105
|
|
99
106
|
def initialize_hoardable_data
|
data/lib/hoardable/engine.rb
CHANGED
@@ -81,6 +81,16 @@ module Hoardable
|
|
81
81
|
@at = nil
|
82
82
|
end
|
83
83
|
|
84
|
+
# Allows calling code to set the upper bound for the temporal range for recorded audits.
|
85
|
+
#
|
86
|
+
# @param datetime [DateTime] the datetime to temporally record versions at
|
87
|
+
def travel_to(datetime)
|
88
|
+
@travel_to = datetime
|
89
|
+
yield
|
90
|
+
ensure
|
91
|
+
@travel_to = nil
|
92
|
+
end
|
93
|
+
|
84
94
|
# @!visibility private
|
85
95
|
def logger
|
86
96
|
@logger ||= ActiveSupport::TaggedLogging.new(Logger.new($stdout))
|
@@ -101,7 +111,7 @@ module Hoardable
|
|
101
111
|
initializer "hoardable.schema_statements" do
|
102
112
|
ActiveSupport.on_load(:active_record_postgresqladapter) do
|
103
113
|
# We need to control the table dumping order of tables, so revert these to just +super+
|
104
|
-
Fx::SchemaDumper
|
114
|
+
Fx::SchemaDumper.module_eval("def tables(streams); super; end")
|
105
115
|
|
106
116
|
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(SchemaDumper)
|
107
117
|
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements.prepend(SchemaStatements)
|
data/lib/hoardable/error.rb
CHANGED
@@ -24,4 +24,14 @@ module Hoardable
|
|
24
24
|
LOG
|
25
25
|
end
|
26
26
|
end
|
27
|
+
|
28
|
+
# An error to be raised when the provided temporal upper bound is before the calcualated lower bound.
|
29
|
+
class InvalidTemporalUpperBoundError < Error
|
30
|
+
def initialize(upper, lower)
|
31
|
+
super(<<~LOG)
|
32
|
+
'The supplied value to `Hoardable.travel_to` (#{upper}) is before the calculated lower bound (#{lower}).
|
33
|
+
You must provide a datetime > the lower bound.
|
34
|
+
LOG
|
35
|
+
end
|
36
|
+
end
|
27
37
|
end
|
data/lib/hoardable/has_one.rb
CHANGED
@@ -9,13 +9,13 @@ module Hoardable
|
|
9
9
|
def has_one(*args)
|
10
10
|
options = args.extract_options!
|
11
11
|
hoardable = options.delete(:hoardable)
|
12
|
-
association = super(*args, **options)
|
13
12
|
name = args.first
|
14
|
-
|
13
|
+
association = super(*args, **options).symbolize_keys[name]
|
14
|
+
return unless hoardable || (association.options[:class_name].match?(/RichText$/))
|
15
15
|
|
16
16
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
17
17
|
def #{name}
|
18
|
-
reflection = _reflections[
|
18
|
+
reflection = _reflections.symbolize_keys[:#{name}]
|
19
19
|
return super if reflection.klass.name.match?(/^ActionText/)
|
20
20
|
return super unless (timestamp = hoardable_client.has_one_at_timestamp)
|
21
21
|
|
@@ -4,7 +4,7 @@ module Hoardable
|
|
4
4
|
module SchemaStatements
|
5
5
|
def table_options(table_name)
|
6
6
|
options = super || {}
|
7
|
-
if inherited_table_names = parent_table_names(table_name)
|
7
|
+
if !options[:options] && (inherited_table_names = parent_table_names(table_name))
|
8
8
|
options[:options] = "INHERITS (#{inherited_table_names.join(", ")})"
|
9
9
|
end
|
10
10
|
options
|
data/lib/hoardable/version.rb
CHANGED
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.17.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: 2024-
|
11
|
+
date: 2024-11-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -17,9 +17,6 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '7'
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: '8'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -27,9 +24,6 @@ dependencies:
|
|
27
24
|
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '7'
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '8'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: activesupport
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -37,9 +31,6 @@ dependencies:
|
|
37
31
|
- - ">="
|
38
32
|
- !ruby/object:Gem::Version
|
39
33
|
version: '7'
|
40
|
-
- - "<"
|
41
|
-
- !ruby/object:Gem::Version
|
42
|
-
version: '8'
|
43
34
|
type: :runtime
|
44
35
|
prerelease: false
|
45
36
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -47,9 +38,6 @@ dependencies:
|
|
47
38
|
- - ">="
|
48
39
|
- !ruby/object:Gem::Version
|
49
40
|
version: '7'
|
50
|
-
- - "<"
|
51
|
-
- !ruby/object:Gem::Version
|
52
|
-
version: '8'
|
53
41
|
- !ruby/object:Gem::Dependency
|
54
42
|
name: railties
|
55
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -57,9 +45,6 @@ dependencies:
|
|
57
45
|
- - ">="
|
58
46
|
- !ruby/object:Gem::Version
|
59
47
|
version: '7'
|
60
|
-
- - "<"
|
61
|
-
- !ruby/object:Gem::Version
|
62
|
-
version: '8'
|
63
48
|
type: :runtime
|
64
49
|
prerelease: false
|
65
50
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -67,16 +52,13 @@ dependencies:
|
|
67
52
|
- - ">="
|
68
53
|
- !ruby/object:Gem::Version
|
69
54
|
version: '7'
|
70
|
-
- - "<"
|
71
|
-
- !ruby/object:Gem::Version
|
72
|
-
version: '8'
|
73
55
|
- !ruby/object:Gem::Dependency
|
74
56
|
name: fx
|
75
57
|
requirement: !ruby/object:Gem::Requirement
|
76
58
|
requirements:
|
77
59
|
- - ">="
|
78
60
|
- !ruby/object:Gem::Version
|
79
|
-
version: '0.
|
61
|
+
version: '0.9'
|
80
62
|
- - "<"
|
81
63
|
- !ruby/object:Gem::Version
|
82
64
|
version: '1'
|
@@ -86,7 +68,7 @@ dependencies:
|
|
86
68
|
requirements:
|
87
69
|
- - ">="
|
88
70
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0.
|
71
|
+
version: '0.9'
|
90
72
|
- - "<"
|
91
73
|
- !ruby/object:Gem::Version
|
92
74
|
version: '1'
|
@@ -124,9 +106,9 @@ files:
|
|
124
106
|
- LICENSE.txt
|
125
107
|
- README.md
|
126
108
|
- Rakefile
|
127
|
-
- lib/generators/hoardable/functions/
|
128
|
-
- lib/generators/hoardable/
|
129
|
-
- lib/generators/hoardable/
|
109
|
+
- lib/generators/hoardable/functions/set_hoardable_id.sql
|
110
|
+
- lib/generators/hoardable/install_functions/hoardable_prevent_update_id.sql
|
111
|
+
- lib/generators/hoardable/install_functions/hoardable_version_prevent_update.sql
|
130
112
|
- lib/generators/hoardable/install_generator.rb
|
131
113
|
- lib/generators/hoardable/migration_generator.rb
|
132
114
|
- lib/generators/hoardable/templates/install.rb.erb
|
@@ -177,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
177
159
|
- !ruby/object:Gem::Version
|
178
160
|
version: '0'
|
179
161
|
requirements: []
|
180
|
-
rubygems_version: 3.5.
|
162
|
+
rubygems_version: 3.5.6
|
181
163
|
signing_key:
|
182
164
|
specification_version: 4
|
183
165
|
summary: An ActiveRecord extension for versioning and soft-deletion of records in
|
@@ -1,18 +0,0 @@
|
|
1
|
-
CREATE OR REPLACE FUNCTION hoardable_source_set_id() RETURNS trigger
|
2
|
-
LANGUAGE plpgsql AS
|
3
|
-
$$
|
4
|
-
DECLARE
|
5
|
-
_pk information_schema.constraint_column_usage.column_name%TYPE;
|
6
|
-
_id _pk%TYPE;
|
7
|
-
BEGIN
|
8
|
-
SELECT c.column_name
|
9
|
-
FROM information_schema.table_constraints t
|
10
|
-
JOIN information_schema.constraint_column_usage c
|
11
|
-
ON c.constraint_name = t.constraint_name
|
12
|
-
WHERE c.table_name = TG_TABLE_NAME AND t.constraint_type = 'PRIMARY KEY'
|
13
|
-
LIMIT 1
|
14
|
-
INTO _pk;
|
15
|
-
EXECUTE format('SELECT $1.%I', _pk) INTO _id USING NEW;
|
16
|
-
NEW.hoardable_id = _id;
|
17
|
-
RETURN NEW;
|
18
|
-
END;$$;
|
/data/lib/generators/hoardable/{functions → install_functions}/hoardable_prevent_update_id.sql
RENAMED
File without changes
|
/data/lib/generators/hoardable/{functions → install_functions}/hoardable_version_prevent_update.sql
RENAMED
File without changes
|