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