hoardable 0.1.1 → 0.1.2
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/README.md +67 -15
- data/lib/generators/hoardable/migration_generator.rb +8 -0
- data/lib/generators/hoardable/templates/migration.rb.erb +3 -3
- data/lib/generators/hoardable/templates/migration_6.rb.erb +3 -3
- data/lib/hoardable/hoardable.rb +1 -1
- data/lib/hoardable/model.rb +3 -3
- data/lib/hoardable/source_model.rb +20 -12
- data/lib/hoardable/version.rb +1 -1
- data/lib/hoardable/version_model.rb +6 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c73d162f69d7ff2984571b7e0aefa256357a835c2ecb22d1288a362dce38d73
|
4
|
+
data.tar.gz: 4b250ebb2bf536f94d3cd8e65f0bd884579bd6fc5d8fd89f5c9846554c4d0e7a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4503897e596e1694a49009aa3e964a86a8e317169c590a1079b075345b9116fb4af50abb68b17a4ea73c7bf1570f56906c11f5e57dbaf84044ab8ac11ca942f2
|
7
|
+
data.tar.gz: a8e2780685b3d0a00757c44ca6a38f58571ef617b1543b7ee8b8f3dcef18c9fed5f7c3e10f7d31677afe0fccafeffca4348d70b5afe3706ead0438488bf57166
|
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# Hoardable
|
1
|
+
# Hoardable 
|
2
2
|
|
3
3
|
Hoardable is an ActiveRecord extension for Ruby 2.6+, Rails 6.1+, and PostgreSQL that allows for
|
4
4
|
versioning and soft-deletion of records through the use of _uni-temporal inherited tables_.
|
5
5
|
|
6
|
-
####
|
6
|
+
#### huh?
|
7
7
|
|
8
8
|
[Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
|
9
9
|
where each row of a table contains data along with one or more time ranges. In the case of this gem,
|
@@ -48,10 +48,18 @@ end
|
|
48
48
|
Then, run the generator command to create a database migration and migrate it:
|
49
49
|
|
50
50
|
```
|
51
|
-
bin/rails g hoardable:migration
|
51
|
+
bin/rails g hoardable:migration Post
|
52
52
|
bin/rails db:migrate
|
53
53
|
```
|
54
54
|
|
55
|
+
By default, it will try to guess the foreign key type for the `_versions` table based on the primary
|
56
|
+
key of the model specified in the migration generator above. If you want/need to specify this
|
57
|
+
explicitly, you can do so:
|
58
|
+
|
59
|
+
```
|
60
|
+
bin/rails g hoardable:migration Post --foreign-key-type uuid
|
61
|
+
```
|
62
|
+
|
55
63
|
_Note:_ If you are on Rails 6.1, you might want to set `config.active_record.schema_format = :sql`
|
56
64
|
in `application.rb`, so that the enum type is captured in your schema dump. This is not required in
|
57
65
|
Rails 7.
|
@@ -75,10 +83,11 @@ A `Post` now `has_many :versions`. Whenever an update and deletion of a `Post` o
|
|
75
83
|
created (by default):
|
76
84
|
|
77
85
|
```ruby
|
78
|
-
post = Post.create!(
|
86
|
+
post = Post.create!(title: "Title")
|
79
87
|
post.versions.size # => 0
|
80
|
-
post.update!(title: "Title")
|
88
|
+
post.update!(title: "Revised Title")
|
81
89
|
post.versions.size # => 1
|
90
|
+
post.versions.first.title # => "Title"
|
82
91
|
post.destroy!
|
83
92
|
post.trashed? # true
|
84
93
|
post.versions.size # => 2
|
@@ -120,17 +129,12 @@ need to query versions often, you should add appropriate indexes to the `_versio
|
|
120
129
|
|
121
130
|
### Tracking contextual data
|
122
131
|
|
123
|
-
You’ll often want to track contextual data about the creation of a version.
|
124
|
-
|
125
|
-
[changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash and the
|
126
|
-
`operation` that cause the version (`update` or `delete`). It will also tag all versions created in
|
127
|
-
the same database transaction with a shared and unique `event_id`.
|
128
|
-
|
129
|
-
There 3 other optional keys that are provided for tracking contextual information:
|
132
|
+
You’ll often want to track contextual data about the creation of a version. There are 3 optional
|
133
|
+
symbol keys that are provided for tracking contextual information:
|
130
134
|
|
131
|
-
-
|
132
|
-
-
|
133
|
-
-
|
135
|
+
- `:whodunit` - an identifier for who is responsible for creating the version
|
136
|
+
- `:note` - a description regarding the versioning
|
137
|
+
- `:meta` - any other contextual information you’d like to store along with the version
|
134
138
|
|
135
139
|
This information is stored in a `jsonb` column. Each key’s value can be in the format of your
|
136
140
|
choosing.
|
@@ -171,6 +175,17 @@ class ApplicationController < ActionController::Base
|
|
171
175
|
end
|
172
176
|
```
|
173
177
|
|
178
|
+
`hoardable` will also automatically capture the ActiveRecord
|
179
|
+
[changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the
|
180
|
+
`operation` that cause the version (`update` or `delete`), and it will also tag all versions created
|
181
|
+
in the same database transaction with a shared and unique `event_uuid`. These are available as:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
version.changes
|
185
|
+
version.hoardable_operation
|
186
|
+
version.hoardable_event_uuid
|
187
|
+
```
|
188
|
+
|
174
189
|
### Model Callbacks
|
175
190
|
|
176
191
|
Sometimes you might want to do something with a version before or after it gets inserted to the
|
@@ -225,8 +240,45 @@ Hoardable.with(enabled: false) do
|
|
225
240
|
end
|
226
241
|
```
|
227
242
|
|
243
|
+
### Relationships
|
244
|
+
|
245
|
+
As in life, sometimes relationships can be hard. `hoardable` is still working out best practices and
|
246
|
+
features in this area, but here are a couple pointers.
|
247
|
+
|
248
|
+
Sometimes you’ll have a record that belongs to a record that you’ll trash. Now the child record’s
|
249
|
+
foreign key will point to the non-existent trashed version of the parent. If you would like this
|
250
|
+
`belongs_to` relationship to always resolve to the parent as if it was not trashed, you can include
|
251
|
+
the scope on the relationship definition:
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
belongs_to :parent, -> { include_versions }
|
255
|
+
```
|
256
|
+
|
257
|
+
Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and both the parent
|
258
|
+
and child model classes include `Hoardable::Model`. Whenever a hoardable version is created in a
|
259
|
+
database transaction, it will create or re-use a unique event UUID for that transaction and tag all
|
260
|
+
versions created with it. That way, when you `untrash!` a parent object, you can find and `untrash!`
|
261
|
+
the children like so:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
class Post < ActiveRecord::Base
|
265
|
+
include Hoardable::Model
|
266
|
+
has_many :comments, dependent: :destroy # `Comment` also includes `Hoardable::Model`
|
267
|
+
|
268
|
+
after_untrashed do
|
269
|
+
Comment
|
270
|
+
.version_class
|
271
|
+
.trashed
|
272
|
+
.with_hoardable_event_uuid(hoardable_event_uuid)
|
273
|
+
.find_each(&:untrash!)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
```
|
277
|
+
|
228
278
|
## Contributing
|
229
279
|
|
280
|
+
This gem is currently considered alpha and very open to feedback.
|
281
|
+
|
230
282
|
Bug reports and pull requests are welcome on GitHub at https://github.com/waymondo/hoardable.
|
231
283
|
|
232
284
|
## License
|
@@ -8,12 +8,20 @@ module Hoardable
|
|
8
8
|
class MigrationGenerator < ActiveRecord::Generators::Base
|
9
9
|
source_root File.expand_path('templates', __dir__)
|
10
10
|
include Rails::Generators::Migration
|
11
|
+
class_option :foreign_key_type, type: :string
|
11
12
|
|
12
13
|
def create_versions_table
|
13
14
|
migration_template migration_template_name, "db/migrate/create_#{singularized_table_name}_versions.rb"
|
14
15
|
end
|
15
16
|
|
16
17
|
no_tasks do
|
18
|
+
def foreign_key_type
|
19
|
+
options[:foreign_key_type] ||
|
20
|
+
class_name.singularize.constantize.columns.find { |col| col.name == 'id' }.sql_type
|
21
|
+
rescue StandardError
|
22
|
+
'bigint'
|
23
|
+
end
|
24
|
+
|
17
25
|
def migration_template_name
|
18
26
|
if Gem::Version.new(ActiveRecord::Migration.current_version.to_s) < Gem::Version.new('7')
|
19
27
|
'migration_6.rb.erb'
|
@@ -6,14 +6,14 @@ class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%=
|
|
6
6
|
create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
|
7
7
|
t.jsonb :_data
|
8
8
|
t.tsrange :_during, null: false
|
9
|
-
t.
|
10
|
-
t.
|
9
|
+
t.uuid :_event_uuid, null: false, index: true
|
10
|
+
t.enum :_operation, enum_type: 'hoardable_operation', null: false, index: true
|
11
|
+
t.<%= foreign_key_type %> :<%= singularized_table_name %>_id, null: false, index: true
|
11
12
|
end
|
12
13
|
add_index(
|
13
14
|
:<%= singularized_table_name %>_versions,
|
14
15
|
%i[_during <%= singularized_table_name %>_id],
|
15
16
|
name: 'idx_<%= singularized_table_name %>_versions_temporally'
|
16
17
|
)
|
17
|
-
add_index :<%= singularized_table_name %>_versions, :_operation
|
18
18
|
end
|
19
19
|
end
|
@@ -21,14 +21,14 @@ class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%=
|
|
21
21
|
create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
|
22
22
|
t.jsonb :_data
|
23
23
|
t.tsrange :_during, null: false
|
24
|
-
t.
|
25
|
-
t.
|
24
|
+
t.uuid :_event_uuid, null: false, index: true
|
25
|
+
t.column :_operation, :hoardable_operation, null: false, index: true
|
26
|
+
t.<%= foreign_key_type %> :<%= singularized_table_name %>_id, null: false, index: true
|
26
27
|
end
|
27
28
|
add_index(
|
28
29
|
:<%= singularized_table_name %>_versions,
|
29
30
|
%i[_during <%= singularized_table_name %>_id],
|
30
31
|
name: 'idx_<%= singularized_table_name %>_versions_temporally'
|
31
32
|
)
|
32
|
-
add_index :<%= singularized_table_name %>_versions, :_operation
|
33
33
|
end
|
34
34
|
end
|
data/lib/hoardable/hoardable.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# An ActiveRecord extension for keeping versions of records in temporal inherited tables
|
4
4
|
module Hoardable
|
5
|
-
DATA_KEYS = %i[meta whodunit note
|
5
|
+
DATA_KEYS = %i[meta whodunit note event_uuid].freeze
|
6
6
|
CONFIG_KEYS = %i[enabled save_trash].freeze
|
7
7
|
|
8
8
|
VERSION_CLASS_SUFFIX = 'Version'
|
data/lib/hoardable/model.rb
CHANGED
@@ -15,9 +15,9 @@ module Hoardable
|
|
15
15
|
next unless self == trace.self
|
16
16
|
|
17
17
|
version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
unless Object.const_defined?(version_class_name)
|
19
|
+
Object.const_set(version_class_name, Class.new(self) { include VersionModel })
|
20
|
+
end
|
21
21
|
|
22
22
|
include SourceModel
|
23
23
|
|
@@ -17,10 +17,12 @@ module Hoardable
|
|
17
17
|
around_update :insert_hoardable_version_on_update, if: :hoardable_callbacks_enabled
|
18
18
|
around_destroy :insert_hoardable_version_on_destroy, if: [:hoardable_callbacks_enabled, SAVE_TRASH_ENABLED]
|
19
19
|
before_destroy :delete_hoardable_versions, if: :hoardable_callbacks_enabled, unless: SAVE_TRASH_ENABLED
|
20
|
-
after_commit :
|
20
|
+
after_commit :unset_hoardable_version_and_event_uuid
|
21
21
|
|
22
22
|
attr_reader :hoardable_version
|
23
23
|
|
24
|
+
delegate :hoardable_event_uuid, :hoardable_operation, to: :hoardable_version, allow_nil: true
|
25
|
+
|
24
26
|
has_many(
|
25
27
|
:versions, -> { order(:_during) },
|
26
28
|
dependent: nil,
|
@@ -34,9 +36,17 @@ module Hoardable
|
|
34
36
|
end
|
35
37
|
|
36
38
|
def at(datetime)
|
39
|
+
raise(Error, 'Future state cannot be known') if datetime.future?
|
40
|
+
|
37
41
|
versions.find_by(DURING_QUERY, datetime) || self
|
38
42
|
end
|
39
43
|
|
44
|
+
def revert_to!(datetime)
|
45
|
+
return unless (version = at(datetime))
|
46
|
+
|
47
|
+
version.is_a?(self.class.version_class) ? version.revert! : self
|
48
|
+
end
|
49
|
+
|
40
50
|
private
|
41
51
|
|
42
52
|
def hoardable_callbacks_enabled
|
@@ -52,18 +62,15 @@ module Hoardable
|
|
52
62
|
end
|
53
63
|
|
54
64
|
def insert_hoardable_version(operation, attrs)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
yield
|
60
|
-
hoardable_version.save(validate: false, touch: false)
|
61
|
-
end
|
65
|
+
@hoardable_version = initialize_hoardable_version(operation, attrs)
|
66
|
+
run_callbacks(:versioned) do
|
67
|
+
yield
|
68
|
+
hoardable_version.save(validate: false, touch: false)
|
62
69
|
end
|
63
70
|
end
|
64
71
|
|
65
|
-
def
|
66
|
-
Thread.current[:
|
72
|
+
def find_or_initialize_hoardable_event_uuid
|
73
|
+
Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
|
67
74
|
end
|
68
75
|
|
69
76
|
def initialize_hoardable_version(operation, attrs)
|
@@ -71,6 +78,7 @@ module Hoardable
|
|
71
78
|
attrs.merge(
|
72
79
|
changes.transform_values { |h| h[0] },
|
73
80
|
{
|
81
|
+
_event_uuid: find_or_initialize_hoardable_event_uuid,
|
74
82
|
_operation: operation,
|
75
83
|
_data: initialize_hoardable_data.merge(changes: changes)
|
76
84
|
}
|
@@ -94,11 +102,11 @@ module Hoardable
|
|
94
102
|
versions.delete_all(:delete_all)
|
95
103
|
end
|
96
104
|
|
97
|
-
def
|
105
|
+
def unset_hoardable_version_and_event_uuid
|
98
106
|
@hoardable_version = nil
|
99
107
|
return if ActiveRecord::Base.connection.transaction_open?
|
100
108
|
|
101
|
-
Thread.current[:
|
109
|
+
Thread.current[:hoardable_event_uuid] = nil
|
102
110
|
end
|
103
111
|
end
|
104
112
|
end
|
data/lib/hoardable/version.rb
CHANGED
@@ -13,6 +13,9 @@ module Hoardable
|
|
13
13
|
self.table_name = "#{table_name.singularize}#{Hoardable::VERSION_TABLE_SUFFIX}"
|
14
14
|
|
15
15
|
alias_method :readonly?, :persisted?
|
16
|
+
alias_attribute :hoardable_operation, :_operation
|
17
|
+
alias_attribute :hoardable_event_uuid, :_event_uuid
|
18
|
+
alias_attribute :hoardable_during, :_during
|
16
19
|
|
17
20
|
before_create :assign_temporal_tsrange
|
18
21
|
|
@@ -22,10 +25,11 @@ module Hoardable
|
|
22
25
|
.where(_operation: 'delete')
|
23
26
|
}
|
24
27
|
scope :at, ->(datetime) { where(DURING_QUERY, datetime) }
|
28
|
+
scope :with_hoardable_event_uuid, ->(event_uuid) { where(_event_uuid: event_uuid) }
|
25
29
|
end
|
26
30
|
|
27
31
|
def revert!
|
28
|
-
raise(Error, 'Version is trashed, cannot revert') unless
|
32
|
+
raise(Error, 'Version is trashed, cannot revert') unless hoardable_operation == 'update'
|
29
33
|
|
30
34
|
transaction do
|
31
35
|
hoardable_source.tap do |reverted|
|
@@ -37,7 +41,7 @@ module Hoardable
|
|
37
41
|
end
|
38
42
|
|
39
43
|
def untrash!
|
40
|
-
raise(Error, 'Version is not trashed, cannot untrash') unless
|
44
|
+
raise(Error, 'Version is not trashed, cannot untrash') unless hoardable_operation == 'delete'
|
41
45
|
|
42
46
|
transaction do
|
43
47
|
superscope = self.class.superclass.unscoped
|
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.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- justin talbott
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-08-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|