hoardable 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7380fb64f7dd3132bec65fd1cfc0c8317a52e6cb6f26895d0b83d7b78391c193
4
- data.tar.gz: 71241a1edc81c57d1bb0549685da47642036fc65ff8d659284c31a9784b0f571
3
+ metadata.gz: 3c73d162f69d7ff2984571b7e0aefa256357a835c2ecb22d1288a362dce38d73
4
+ data.tar.gz: 4b250ebb2bf536f94d3cd8e65f0bd884579bd6fc5d8fd89f5c9846554c4d0e7a
5
5
  SHA512:
6
- metadata.gz: 4f473b92097e223dd535512ec5850a6c00d9c22faba482787ffbf2ae7b0f6130d37b0a3ed34a46701f53ec4958e7293a69626bdc9693f1f7a51c0e35ff62bb2e
7
- data.tar.gz: 530e609fd4535b4d37f82fc4a39ce3322209451bdd0fa67079e74ed2ef658bd252d9a87ca7189a9332e0303b20c8cb073765bc1c99fcdb647c346f893a133ecd
6
+ metadata.gz: 4503897e596e1694a49009aa3e964a86a8e317169c590a1079b075345b9116fb4af50abb68b17a4ea73c7bf1570f56906c11f5e57dbaf84044ab8ac11ca942f2
7
+ data.tar.gz: a8e2780685b3d0a00757c44ca6a38f58571ef617b1543b7ee8b8f3dcef18c9fed5f7c3e10f7d31677afe0fccafeffca4348d70b5afe3706ead0438488bf57166
data/README.md CHANGED
@@ -1,9 +1,9 @@
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
- #### nice... huh?
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 posts
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!(attributes)
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. `hoardable` will
124
- automatically capture the ActiveRecord
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
- - `whodunit` - an identifier for who is responsible for creating the version
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
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.enum :_operation, enum_type: 'hoardable_operation', null: false
10
- t.bigint :<%= singularized_table_name %>_id, null: false, index: true
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.column :_operation, :hoardable_operation, null: false
25
- t.bigint :<%= singularized_table_name %>_id, null: false, index: true
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
@@ -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 event_id].freeze
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'
@@ -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
- next if Object.const_defined?(version_class_name)
19
-
20
- Object.const_set(version_class_name, Class.new(self) { include VersionModel })
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 :unset_hoardable_version_and_event_id
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
- event_id = find_or_initialize_hoardable_event_id
56
- Hoardable.with(event_id: event_id) do
57
- @hoardable_version = initialize_hoardable_version(operation, attrs)
58
- run_callbacks(:versioned) do
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 find_or_initialize_hoardable_event_id
66
- Thread.current[:hoardable_event_id] ||= SecureRandom.hex
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 unset_hoardable_version_and_event_id
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[:hoardable_event_id] = nil
109
+ Thread.current[:hoardable_event_uuid] = nil
102
110
  end
103
111
  end
104
112
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.2'
5
5
  end
@@ -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 _operation == 'update'
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 _operation == 'delete'
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.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-07-28 00:00:00.000000000 Z
11
+ date: 2022-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord