hoardable 0.1.1 → 0.1.4
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/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 
|
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
|