hoardable 0.1.1 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +88 -18
- data/lib/generators/hoardable/migration_generator.rb +10 -1
- data/lib/generators/hoardable/templates/migration.rb.erb +3 -3
- data/lib/generators/hoardable/templates/migration_6.rb.erb +3 -3
- data/lib/hoardable/error.rb +1 -1
- data/lib/hoardable/hoardable.rb +16 -5
- data/lib/hoardable/model.rb +30 -5
- data/lib/hoardable/source_model.rb +54 -17
- data/lib/hoardable/tableoid.rb +21 -1
- data/lib/hoardable/version.rb +1 -1
- data/lib/hoardable/version_model.rb +35 -3
- data/sig/hoardable.rbs +83 -1
- 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: a5e5c29b0b6d4a24e7b41a9b4846fd1288caabd48158b517a4b9b61e53421d1c
|
4
|
+
data.tar.gz: 1e863147db8e3a39e4ed3d465855d465f14c4c8a444426abce72534e8bae4d9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4c997b34958fbe6253098778c03bf9226eac02b7fd218af1a1d1bd52527b7b1a0b0406a6ecf0cc8e3ea45d70e5b8823dc23a0e48fcc741c470831b98adb3fd65
|
7
|
+
data.tar.gz: 44b9c1cfe68aa6ab18370b90632d5563dda8ef50b0cde425be3b44eb7f2027fa6e61d609538653cc15adab20f9d79ba3423f614d95c9413f8db4c0ced3a2f4be
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
# Hoardable
|
1
|
+
# Hoardable ![gem version](https://img.shields.io/gem/v/hoardable?style=flat-square)
|
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
|
+
[👉 Documentation](https://www.rubydoc.info/gems/hoardable)
|
7
|
+
|
8
|
+
### huh?
|
7
9
|
|
8
10
|
[Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
|
9
11
|
where each row of a table contains data along with one or more time ranges. In the case of this gem,
|
@@ -48,10 +50,18 @@ end
|
|
48
50
|
Then, run the generator command to create a database migration and migrate it:
|
49
51
|
|
50
52
|
```
|
51
|
-
bin/rails g hoardable:migration
|
53
|
+
bin/rails g hoardable:migration Post
|
52
54
|
bin/rails db:migrate
|
53
55
|
```
|
54
56
|
|
57
|
+
By default, it will try to guess the foreign key type for the `_versions` table based on the primary
|
58
|
+
key of the model specified in the migration generator above. If you want/need to specify this
|
59
|
+
explicitly, you can do so:
|
60
|
+
|
61
|
+
```
|
62
|
+
bin/rails g hoardable:migration Post --foreign-key-type uuid
|
63
|
+
```
|
64
|
+
|
55
65
|
_Note:_ If you are on Rails 6.1, you might want to set `config.active_record.schema_format = :sql`
|
56
66
|
in `application.rb`, so that the enum type is captured in your schema dump. This is not required in
|
57
67
|
Rails 7.
|
@@ -75,10 +85,11 @@ A `Post` now `has_many :versions`. Whenever an update and deletion of a `Post` o
|
|
75
85
|
created (by default):
|
76
86
|
|
77
87
|
```ruby
|
78
|
-
post = Post.create!(
|
88
|
+
post = Post.create!(title: "Title")
|
79
89
|
post.versions.size # => 0
|
80
|
-
post.update!(title: "Title")
|
90
|
+
post.update!(title: "Revised Title")
|
81
91
|
post.versions.size # => 1
|
92
|
+
post.versions.first.title # => "Title"
|
82
93
|
post.destroy!
|
83
94
|
post.trashed? # true
|
84
95
|
post.versions.size # => 2
|
@@ -120,17 +131,12 @@ need to query versions often, you should add appropriate indexes to the `_versio
|
|
120
131
|
|
121
132
|
### Tracking contextual data
|
122
133
|
|
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`.
|
134
|
+
You’ll often want to track contextual data about the creation of a version. There are 3 optional
|
135
|
+
symbol keys that are provided for tracking contextual information:
|
128
136
|
|
129
|
-
|
130
|
-
|
131
|
-
- `
|
132
|
-
- `note` - a string containing a description regarding the versioning
|
133
|
-
- `meta` - any other contextual information you’d like to store along with the version
|
137
|
+
- `:whodunit` - an identifier for who is responsible for creating the version
|
138
|
+
- `:note` - a description regarding the versioning
|
139
|
+
- `:meta` - any other contextual information you’d like to store along with the version
|
134
140
|
|
135
141
|
This information is stored in a `jsonb` column. Each key’s value can be in the format of your
|
136
142
|
choosing.
|
@@ -171,6 +177,17 @@ class ApplicationController < ActionController::Base
|
|
171
177
|
end
|
172
178
|
```
|
173
179
|
|
180
|
+
`hoardable` will also automatically capture the ActiveRecord
|
181
|
+
[changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the
|
182
|
+
`operation` that cause the version (`update` or `delete`), and it will also tag all versions created
|
183
|
+
in the same database transaction with a shared and unique `event_uuid`. These are available as:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
version.changes
|
187
|
+
version.hoardable_operation
|
188
|
+
version.hoardable_event_uuid
|
189
|
+
```
|
190
|
+
|
174
191
|
### Model Callbacks
|
175
192
|
|
176
193
|
Sometimes you might want to do something with a version before or after it gets inserted to the
|
@@ -205,19 +222,22 @@ end
|
|
205
222
|
|
206
223
|
### Configuration
|
207
224
|
|
208
|
-
There are
|
225
|
+
There are three configurable options currently:
|
209
226
|
|
210
227
|
```ruby
|
211
228
|
Hoardable.enabled # => default true
|
229
|
+
Hoardable.version_updates # => default true
|
212
230
|
Hoardable.save_trash # => default true
|
213
231
|
```
|
214
232
|
|
215
|
-
`Hoardable.enabled` controls whether versions will be
|
233
|
+
`Hoardable.enabled` controls whether versions will be ever be created.
|
234
|
+
|
235
|
+
`Hoardable.version_updates` controls whether versions get created on record updates.
|
216
236
|
|
217
237
|
`Hoardable.save_trash` controls whether to create versions upon record deletion. When this is set to
|
218
238
|
`false`, all versions of a record will be deleted when the record is destroyed.
|
219
239
|
|
220
|
-
If you would like to temporarily set a config setting, you can use `Hoardable.with
|
240
|
+
If you would like to temporarily set a config setting, you can use `Hoardable.with`:
|
221
241
|
|
222
242
|
```ruby
|
223
243
|
Hoardable.with(enabled: false) do
|
@@ -225,8 +245,58 @@ Hoardable.with(enabled: false) do
|
|
225
245
|
end
|
226
246
|
```
|
227
247
|
|
248
|
+
You can also configure these variables per `ActiveRecord` class as well using `hoardable_options`:
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
class Comment < ActiveRecord::Base
|
252
|
+
include Hoardable::Model
|
253
|
+
hoardable_options version_updates: false
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
If either the model-level option or global option for a configuration variable is set to `false`,
|
258
|
+
that behavior will be disabled.
|
259
|
+
|
260
|
+
### Relationships
|
261
|
+
|
262
|
+
As in life, sometimes relationships can be hard. `hoardable` is still working out best practices and
|
263
|
+
features in this area, but here are a couple pointers.
|
264
|
+
|
265
|
+
Sometimes you’ll have a record that belongs to a record that you’ll trash. Now the child record’s
|
266
|
+
foreign key will point to the non-existent trashed version of the parent. If you would like this
|
267
|
+
`belongs_to` relationship to always resolve to the parent as if it was not trashed, you can include
|
268
|
+
the scope on the relationship definition:
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
belongs_to :parent, -> { include_versions }
|
272
|
+
```
|
273
|
+
|
274
|
+
Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and both the parent
|
275
|
+
and child model classes include `Hoardable::Model`. Whenever a hoardable version is created in a
|
276
|
+
database transaction, it will create or re-use a unique event UUID for that transaction and tag all
|
277
|
+
versions created with it. That way, when you `untrash!` a parent object, you can find and `untrash!`
|
278
|
+
the children like so:
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
class Post < ActiveRecord::Base
|
282
|
+
include Hoardable::Model
|
283
|
+
has_many :comments, dependent: :destroy # `Comment` also includes `Hoardable::Model`
|
284
|
+
|
285
|
+
after_untrashed do
|
286
|
+
Comment
|
287
|
+
.version_class
|
288
|
+
.trashed
|
289
|
+
.where(post_id: id)
|
290
|
+
.with_hoardable_event_uuid(hoardable_event_uuid)
|
291
|
+
.find_each(&:untrash!)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
```
|
295
|
+
|
228
296
|
## Contributing
|
229
297
|
|
298
|
+
This gem is currently considered alpha and very open to feedback.
|
299
|
+
|
230
300
|
Bug reports and pull requests are welcome on GitHub at https://github.com/waymondo/hoardable.
|
231
301
|
|
232
302
|
## License
|
@@ -4,16 +4,25 @@ require 'rails/generators'
|
|
4
4
|
require 'rails/generators/active_record/migration/migration_generator'
|
5
5
|
|
6
6
|
module Hoardable
|
7
|
-
# Generates a migration
|
7
|
+
# Generates a migration to create an inherited uni-temporal table of a model including
|
8
|
+
# {Hoardable::Model}, for the storage of +versions+.
|
8
9
|
class MigrationGenerator < ActiveRecord::Generators::Base
|
9
10
|
source_root File.expand_path('templates', __dir__)
|
10
11
|
include Rails::Generators::Migration
|
12
|
+
class_option :foreign_key_type, type: :string
|
11
13
|
|
12
14
|
def create_versions_table
|
13
15
|
migration_template migration_template_name, "db/migrate/create_#{singularized_table_name}_versions.rb"
|
14
16
|
end
|
15
17
|
|
16
18
|
no_tasks do
|
19
|
+
def foreign_key_type
|
20
|
+
options[:foreign_key_type] ||
|
21
|
+
class_name.singularize.constantize.columns.find { |col| col.name == 'id' }.sql_type
|
22
|
+
rescue StandardError
|
23
|
+
'bigint'
|
24
|
+
end
|
25
|
+
|
17
26
|
def migration_template_name
|
18
27
|
if Gem::Version.new(ActiveRecord::Migration.current_version.to_s) < Gem::Version.new('7')
|
19
28
|
'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/error.rb
CHANGED
data/lib/hoardable/hoardable.rb
CHANGED
@@ -1,13 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# An ActiveRecord extension for keeping versions of records in temporal inherited tables
|
3
|
+
# An +ActiveRecord+ extension for keeping versions of records in uni-temporal inherited tables.
|
4
4
|
module Hoardable
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
# Symbols for use with setting contextual data, when creating versions. See
|
6
|
+
# {file:README.md#tracking-contextual-data README} for more.
|
7
|
+
DATA_KEYS = %i[meta whodunit note event_uuid].freeze
|
8
|
+
# Symbols for use with setting {Hoardable} configuration. See {file:README.md#configuration
|
9
|
+
# README} for more.
|
10
|
+
CONFIG_KEYS = %i[enabled version_updates save_trash].freeze
|
11
|
+
|
12
|
+
# @!visibility private
|
8
13
|
VERSION_CLASS_SUFFIX = 'Version'
|
14
|
+
|
15
|
+
# @!visibility private
|
9
16
|
VERSION_TABLE_SUFFIX = "_#{VERSION_CLASS_SUFFIX.tableize}"
|
10
|
-
|
17
|
+
|
18
|
+
# @!visibility private
|
11
19
|
DURING_QUERY = '_during @> ?::timestamp'
|
12
20
|
|
13
21
|
@context = {}
|
@@ -36,6 +44,9 @@ module Hoardable
|
|
36
44
|
end
|
37
45
|
end
|
38
46
|
|
47
|
+
# This is a general use method for setting {DATA_KEYS} or {CONFIG_KEYS} around a scoped block.
|
48
|
+
#
|
49
|
+
# @param hash [Hash] Options and contextual data to set within a block
|
39
50
|
def with(hash)
|
40
51
|
current_config = @config
|
41
52
|
current_context = @context
|
data/lib/hoardable/model.rb
CHANGED
@@ -1,11 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Hoardable
|
4
|
-
# This concern
|
5
|
-
# the
|
4
|
+
# This concern is the main entrypoint for using {Hoardable}. When included into an +ActiveRecord+
|
5
|
+
# class, it dynamically generates the +Version+ variant of that class (with {VersionModel}) and
|
6
|
+
# includes the {Hoardable} API methods and relationships on the source model class (through
|
7
|
+
# {SourceModel}).
|
6
8
|
module Model
|
7
9
|
extend ActiveSupport::Concern
|
8
10
|
|
11
|
+
class_methods do
|
12
|
+
# @!visibility private
|
13
|
+
attr_reader :_hoardable_options
|
14
|
+
|
15
|
+
# If called with a hash, this will set the model-level +Hoardable+ configuration variables. If
|
16
|
+
# called without an argument it will return the computed +Hoardable+ configuration considering
|
17
|
+
# both model-level and global values.
|
18
|
+
#
|
19
|
+
# @param hash [Hash] The +Hoardable+ configuration for the model. Keys must be present in
|
20
|
+
# {CONFIG_KEYS}
|
21
|
+
# @return [Hash]
|
22
|
+
def hoardable_options(hash = nil)
|
23
|
+
if hash
|
24
|
+
@_hoardable_options = hash.slice(*Hoardable::CONFIG_KEYS)
|
25
|
+
else
|
26
|
+
@_hoardable_options ||= {}
|
27
|
+
Hoardable::CONFIG_KEYS.to_h do |key|
|
28
|
+
[key, Hoardable.send(key) != false && @_hoardable_options[key] != false]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
9
34
|
included do
|
10
35
|
define_model_callbacks :versioned
|
11
36
|
define_model_callbacks :reverted, only: :after
|
@@ -15,9 +40,9 @@ module Hoardable
|
|
15
40
|
next unless self == trace.self
|
16
41
|
|
17
42
|
version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
|
18
|
-
|
19
|
-
|
20
|
-
|
43
|
+
unless Object.const_defined?(version_class_name)
|
44
|
+
Object.const_set(version_class_name, Class.new(self) { include VersionModel })
|
45
|
+
end
|
21
46
|
|
22
47
|
include SourceModel
|
23
48
|
|
@@ -1,11 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Hoardable
|
4
|
-
# This concern contains the relationships, callbacks, and API methods for
|
4
|
+
# This concern contains the {Hoardable} relationships, callbacks, and API methods for an
|
5
|
+
# +ActiveRecord+. It is included by {Hoardable::Model} after the dynamic generation of the
|
6
|
+
# +Version+ class variant.
|
5
7
|
module SourceModel
|
6
8
|
extend ActiveSupport::Concern
|
7
9
|
|
8
10
|
class_methods do
|
11
|
+
# The dynamically generated +Version+ class for this model.
|
9
12
|
def version_class
|
10
13
|
"#{name}#{VERSION_CLASS_SUFFIX}".constantize
|
11
14
|
end
|
@@ -14,13 +17,22 @@ module Hoardable
|
|
14
17
|
included do
|
15
18
|
include Tableoid
|
16
19
|
|
17
|
-
around_update :insert_hoardable_version_on_update, if:
|
18
|
-
around_destroy :insert_hoardable_version_on_destroy, if: [
|
19
|
-
before_destroy :delete_hoardable_versions, if: :hoardable_callbacks_enabled, unless:
|
20
|
-
after_commit :
|
20
|
+
around_update :insert_hoardable_version_on_update, if: %i[hoardable_callbacks_enabled hoardable_version_updates]
|
21
|
+
around_destroy :insert_hoardable_version_on_destroy, if: %i[hoardable_callbacks_enabled hoardable_save_trash]
|
22
|
+
before_destroy :delete_hoardable_versions, if: :hoardable_callbacks_enabled, unless: :hoardable_save_trash
|
23
|
+
after_commit :unset_hoardable_version_and_event_uuid
|
21
24
|
|
25
|
+
# This will contain the +Version+ class instance for use within +versioned+, +reverted+, and
|
26
|
+
# +untrashed+ callbacks.
|
22
27
|
attr_reader :hoardable_version
|
23
28
|
|
29
|
+
# @!attribute [r] hoardable_event_uuid
|
30
|
+
# @return [String] A postgres UUID that represents the +version+’s +ActiveRecord+ database transaction
|
31
|
+
# @!attribute [r] hoardable_operation
|
32
|
+
# @return [String] The database operation that created the +version+ - either +update+ or +delete+.
|
33
|
+
delegate :hoardable_event_uuid, :hoardable_operation, to: :hoardable_version, allow_nil: true
|
34
|
+
|
35
|
+
# Returns all +versions+ in ascending order of their temporal timeframes.
|
24
36
|
has_many(
|
25
37
|
:versions, -> { order(:_during) },
|
26
38
|
dependent: nil,
|
@@ -29,18 +41,45 @@ module Hoardable
|
|
29
41
|
)
|
30
42
|
end
|
31
43
|
|
44
|
+
# Returns a boolean of whether the record is actually a trashed +version+.
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
32
47
|
def trashed?
|
33
48
|
versions.trashed.limit(1).order(_during: :desc).first&.send(:hoardable_source_attributes) == attributes
|
34
49
|
end
|
35
50
|
|
51
|
+
# Returns the +version+ at the supplied +datetime+ or +time+. It will return +self+ if there is
|
52
|
+
# none. This will raise an error if you try to find a version in the future.
|
53
|
+
#
|
54
|
+
# @param datetime [DateTime, Time]
|
36
55
|
def at(datetime)
|
56
|
+
raise(Error, 'Future state cannot be known') if datetime.future?
|
57
|
+
|
37
58
|
versions.find_by(DURING_QUERY, datetime) || self
|
38
59
|
end
|
39
60
|
|
61
|
+
# If a version is found at the supplied datetime, it will +revert!+ to it and return it. This
|
62
|
+
# will raise an error if you try to revert to a version in the future.
|
63
|
+
#
|
64
|
+
# @param datetime [DateTime, Time]
|
65
|
+
def revert_to!(datetime)
|
66
|
+
return unless (version = at(datetime))
|
67
|
+
|
68
|
+
version.is_a?(self.class.version_class) ? version.revert! : self
|
69
|
+
end
|
70
|
+
|
40
71
|
private
|
41
72
|
|
42
73
|
def hoardable_callbacks_enabled
|
43
|
-
|
74
|
+
self.class.hoardable_options[:enabled] && !self.class.name.end_with?(VERSION_CLASS_SUFFIX)
|
75
|
+
end
|
76
|
+
|
77
|
+
def hoardable_save_trash
|
78
|
+
self.class.hoardable_options[:save_trash]
|
79
|
+
end
|
80
|
+
|
81
|
+
def hoardable_version_updates
|
82
|
+
self.class.hoardable_options[:version_updates]
|
44
83
|
end
|
45
84
|
|
46
85
|
def insert_hoardable_version_on_update(&block)
|
@@ -52,18 +91,15 @@ module Hoardable
|
|
52
91
|
end
|
53
92
|
|
54
93
|
def insert_hoardable_version(operation, attrs)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
yield
|
60
|
-
hoardable_version.save(validate: false, touch: false)
|
61
|
-
end
|
94
|
+
@hoardable_version = initialize_hoardable_version(operation, attrs)
|
95
|
+
run_callbacks(:versioned) do
|
96
|
+
yield
|
97
|
+
hoardable_version.save(validate: false, touch: false)
|
62
98
|
end
|
63
99
|
end
|
64
100
|
|
65
|
-
def
|
66
|
-
Thread.current[:
|
101
|
+
def find_or_initialize_hoardable_event_uuid
|
102
|
+
Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
|
67
103
|
end
|
68
104
|
|
69
105
|
def initialize_hoardable_version(operation, attrs)
|
@@ -71,6 +107,7 @@ module Hoardable
|
|
71
107
|
attrs.merge(
|
72
108
|
changes.transform_values { |h| h[0] },
|
73
109
|
{
|
110
|
+
_event_uuid: find_or_initialize_hoardable_event_uuid,
|
74
111
|
_operation: operation,
|
75
112
|
_data: initialize_hoardable_data.merge(changes: changes)
|
76
113
|
}
|
@@ -94,11 +131,11 @@ module Hoardable
|
|
94
131
|
versions.delete_all(:delete_all)
|
95
132
|
end
|
96
133
|
|
97
|
-
def
|
134
|
+
def unset_hoardable_version_and_event_uuid
|
98
135
|
@hoardable_version = nil
|
99
136
|
return if ActiveRecord::Base.connection.transaction_open?
|
100
137
|
|
101
|
-
Thread.current[:
|
138
|
+
Thread.current[:hoardable_event_uuid] = nil
|
102
139
|
end
|
103
140
|
end
|
104
141
|
end
|
data/lib/hoardable/tableoid.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Hoardable
|
4
|
-
# This concern provides support for PostgreSQL
|
4
|
+
# This concern provides support for PostgreSQL’s tableoid system column to {SourceModel}.
|
5
5
|
module Tableoid
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
+
# @!visibility private
|
8
9
|
TABLEOID_AREL_CONDITIONS = lambda do |arel_table, condition|
|
9
10
|
arel_table[:tableoid].send(
|
10
11
|
condition,
|
@@ -13,13 +14,32 @@ module Hoardable
|
|
13
14
|
end.freeze
|
14
15
|
|
15
16
|
included do
|
17
|
+
# @!visibility private
|
16
18
|
attr_writer :tableoid
|
17
19
|
|
20
|
+
# By default, {Hoardable} only returns instances of the parent table, and not the +versions+
|
21
|
+
# in the inherited table.
|
18
22
|
default_scope { where(TABLEOID_AREL_CONDITIONS.call(arel_table, :eq)) }
|
23
|
+
|
24
|
+
# @!scope class
|
25
|
+
# @!method include_versions
|
26
|
+
# @return [ActiveRecord<Object>]
|
27
|
+
#
|
28
|
+
# Returns +versions+ along with instances of the source models, all cast as instances of the
|
29
|
+
# source model’s class.
|
19
30
|
scope :include_versions, -> { unscope(where: [:tableoid]) }
|
31
|
+
|
32
|
+
# @!scope class
|
33
|
+
# @!method versions
|
34
|
+
# @return [ActiveRecord<Object>]
|
35
|
+
#
|
36
|
+
# Returns only +versions+ of the parent +ActiveRecord+ class, cast as instances of the source
|
37
|
+
# model’s class.
|
20
38
|
scope :versions, -> { include_versions.where(TABLEOID_AREL_CONDITIONS.call(arel_table, :not_eq)) }
|
21
39
|
end
|
22
40
|
|
41
|
+
private
|
42
|
+
|
23
43
|
def tableoid
|
24
44
|
connection.execute("SELECT oid FROM pg_class WHERE relname = '#{table_name}'")[0]['oid']
|
25
45
|
end
|
data/lib/hoardable/version.rb
CHANGED
@@ -1,31 +1,58 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Hoardable
|
4
|
-
# This concern is included into the dynamically generated Version
|
4
|
+
# This concern is included into the dynamically generated +Version+ kind of the parent
|
5
|
+
# +ActiveRecord+ class.
|
5
6
|
module VersionModel
|
6
7
|
extend ActiveSupport::Concern
|
7
8
|
|
8
9
|
included do
|
9
10
|
hoardable_source_key = superclass.model_name.i18n_key
|
11
|
+
|
12
|
+
# A +version+ belongs to it’s parent +ActiveRecord+ source.
|
10
13
|
belongs_to hoardable_source_key, inverse_of: :versions
|
11
14
|
alias_method :hoardable_source, hoardable_source_key
|
12
15
|
|
13
16
|
self.table_name = "#{table_name.singularize}#{Hoardable::VERSION_TABLE_SUFFIX}"
|
14
17
|
|
15
18
|
alias_method :readonly?, :persisted?
|
19
|
+
alias_attribute :hoardable_operation, :_operation
|
20
|
+
alias_attribute :hoardable_event_uuid, :_event_uuid
|
21
|
+
alias_attribute :hoardable_during, :_during
|
16
22
|
|
17
23
|
before_create :assign_temporal_tsrange
|
18
24
|
|
25
|
+
# @!scope class
|
26
|
+
# @!method trashed
|
27
|
+
# @return [ActiveRecord<Object>]
|
28
|
+
#
|
29
|
+
# Returns only trashed +versions+ that are orphans.
|
19
30
|
scope :trashed, lambda {
|
20
31
|
left_outer_joins(hoardable_source_key)
|
21
32
|
.where(superclass.table_name => { id: nil })
|
22
33
|
.where(_operation: 'delete')
|
23
34
|
}
|
35
|
+
|
36
|
+
# @!scope class
|
37
|
+
# @!method at
|
38
|
+
# @return [ActiveRecord<Object>]
|
39
|
+
#
|
40
|
+
# Returns +versions+ that were valid at the supplied +datetime+ or +time+.
|
24
41
|
scope :at, ->(datetime) { where(DURING_QUERY, datetime) }
|
42
|
+
|
43
|
+
# @!scope class
|
44
|
+
# @!method with_hoardable_event_uuid
|
45
|
+
# @return [ActiveRecord<Object>]
|
46
|
+
#
|
47
|
+
# Returns all +versions+ that were created as part of the same +ActiveRecord+ database
|
48
|
+
# transaction of the supplied +event_uuid+. Useful in +reverted+ and +untrashed+ callbacks.
|
49
|
+
scope :with_hoardable_event_uuid, ->(event_uuid) { where(_event_uuid: event_uuid) }
|
25
50
|
end
|
26
51
|
|
52
|
+
# Reverts the parent +ActiveRecord+ instance to the saved attributes of this +version+. Raises
|
53
|
+
# an error if the version is trashed.
|
27
54
|
def revert!
|
28
|
-
raise(Error, 'Version is trashed, cannot revert') unless
|
55
|
+
raise(Error, 'Version is trashed, cannot revert') unless hoardable_operation == 'update'
|
29
56
|
|
30
57
|
transaction do
|
31
58
|
hoardable_source.tap do |reverted|
|
@@ -36,8 +63,10 @@ module Hoardable
|
|
36
63
|
end
|
37
64
|
end
|
38
65
|
|
66
|
+
# Inserts a trashed +version+ back into its parent +ActiveRecord+ table with its original
|
67
|
+
# primary key. Raises an error if the version is not trashed.
|
39
68
|
def untrash!
|
40
|
-
raise(Error, 'Version is not trashed, cannot untrash') unless
|
69
|
+
raise(Error, 'Version is not trashed, cannot untrash') unless hoardable_operation == 'delete'
|
41
70
|
|
42
71
|
transaction do
|
43
72
|
superscope = self.class.superclass.unscoped
|
@@ -55,6 +84,9 @@ module Hoardable
|
|
55
84
|
end
|
56
85
|
end
|
57
86
|
|
87
|
+
# Returns the +ActiveRecord+
|
88
|
+
# {https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes changes} that
|
89
|
+
# were present during version creation.
|
58
90
|
def changes
|
59
91
|
_data&.dig('changes')
|
60
92
|
end
|
data/sig/hoardable.rbs
CHANGED
@@ -1,4 +1,86 @@
|
|
1
1
|
module Hoardable
|
2
2
|
VERSION: String
|
3
|
-
|
3
|
+
DATA_KEYS: [:meta, :whodunit, :note, :event_uuid]
|
4
|
+
CONFIG_KEYS: [:enabled, :version_updates, :save_trash]
|
5
|
+
VERSION_CLASS_SUFFIX: String
|
6
|
+
VERSION_TABLE_SUFFIX: String
|
7
|
+
DURING_QUERY: String
|
8
|
+
self.@context: Hash[untyped, untyped]
|
9
|
+
self.@config: untyped
|
10
|
+
|
11
|
+
def self.with: (untyped hash) -> untyped
|
12
|
+
|
13
|
+
module Tableoid
|
14
|
+
TABLEOID_AREL_CONDITIONS: Proc
|
15
|
+
|
16
|
+
private
|
17
|
+
def tableoid: -> untyped
|
18
|
+
|
19
|
+
public
|
20
|
+
attr_writer tableoid: untyped
|
21
|
+
end
|
22
|
+
|
23
|
+
class Error < StandardError
|
24
|
+
end
|
25
|
+
|
26
|
+
module SourceModel
|
27
|
+
include Tableoid
|
28
|
+
|
29
|
+
def trashed?: -> untyped
|
30
|
+
def at: (untyped datetime) -> SourceModel
|
31
|
+
def revert_to!: (untyped datetime) -> SourceModel?
|
32
|
+
|
33
|
+
private
|
34
|
+
def hoardable_callbacks_enabled: -> untyped
|
35
|
+
def hoardable_save_trash: -> untyped
|
36
|
+
def hoardable_version_updates: -> untyped
|
37
|
+
def insert_hoardable_version_on_update: -> untyped
|
38
|
+
def insert_hoardable_version_on_destroy: -> untyped
|
39
|
+
def insert_hoardable_version: (String operation, untyped attrs) -> untyped
|
40
|
+
def find_or_initialize_hoardable_event_uuid: -> untyped
|
41
|
+
def initialize_hoardable_version: (String operation, untyped attrs) -> untyped
|
42
|
+
def initialize_hoardable_data: -> untyped
|
43
|
+
def assign_hoardable_context: (:event_uuid | :meta | :note | :whodunit key) -> nil
|
44
|
+
def delete_hoardable_versions: -> untyped
|
45
|
+
def unset_hoardable_version_and_event_uuid: -> nil
|
46
|
+
|
47
|
+
public
|
48
|
+
def version_class: -> untyped
|
49
|
+
attr_reader hoardable_version: nil
|
50
|
+
end
|
51
|
+
|
52
|
+
module VersionModel
|
53
|
+
@hoardable_source_attributes: untyped
|
54
|
+
@hoardable_source_foreign_key: String
|
55
|
+
@hoardable_source_foreign_id: untyped
|
56
|
+
|
57
|
+
def revert!: -> untyped
|
58
|
+
def untrash!: -> untyped
|
59
|
+
def changes: -> untyped
|
60
|
+
|
61
|
+
private
|
62
|
+
def untrashable_hoardable_source_attributes: -> untyped
|
63
|
+
def hoardable_source_attributes: -> untyped
|
64
|
+
def hoardable_source_foreign_key: -> String
|
65
|
+
def hoardable_source_foreign_id: -> untyped
|
66
|
+
def previous_temporal_tsrange_end: -> untyped
|
67
|
+
def assign_temporal_tsrange: -> Range
|
68
|
+
end
|
69
|
+
|
70
|
+
module Model
|
71
|
+
include VersionModel
|
72
|
+
include SourceModel
|
73
|
+
|
74
|
+
attr_reader _hoardable_options: Hash[untyped, untyped]
|
75
|
+
def hoardable_options: (?nil hash) -> untyped
|
76
|
+
end
|
77
|
+
|
78
|
+
class MigrationGenerator
|
79
|
+
@singularized_table_name: untyped
|
80
|
+
|
81
|
+
def create_versions_table: -> untyped
|
82
|
+
def foreign_key_type: -> String
|
83
|
+
def migration_template_name: -> String
|
84
|
+
def singularized_table_name: -> untyped
|
85
|
+
end
|
4
86
|
end
|
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.4
|
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-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|