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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7380fb64f7dd3132bec65fd1cfc0c8317a52e6cb6f26895d0b83d7b78391c193
4
- data.tar.gz: 71241a1edc81c57d1bb0549685da47642036fc65ff8d659284c31a9784b0f571
3
+ metadata.gz: a5e5c29b0b6d4a24e7b41a9b4846fd1288caabd48158b517a4b9b61e53421d1c
4
+ data.tar.gz: 1e863147db8e3a39e4ed3d465855d465f14c4c8a444426abce72534e8bae4d9c
5
5
  SHA512:
6
- metadata.gz: 4f473b92097e223dd535512ec5850a6c00d9c22faba482787ffbf2ae7b0f6130d37b0a3ed34a46701f53ec4958e7293a69626bdc9693f1f7a51c0e35ff62bb2e
7
- data.tar.gz: 530e609fd4535b4d37f82fc4a39ce3322209451bdd0fa67079e74ed2ef658bd252d9a87ca7189a9332e0303b20c8cb073765bc1c99fcdb647c346f893a133ecd
6
+ metadata.gz: 4c997b34958fbe6253098778c03bf9226eac02b7fd218af1a1d1bd52527b7b1a0b0406a6ecf0cc8e3ea45d70e5b8823dc23a0e48fcc741c470831b98adb3fd65
7
+ data.tar.gz: 44b9c1cfe68aa6ab18370b90632d5563dda8ef50b0cde425be3b44eb7f2027fa6e61d609538653cc15adab20f9d79ba3423f614d95c9413f8db4c0ced3a2f4be
data/Gemfile CHANGED
@@ -8,5 +8,6 @@ gem 'rake', '~> 13.0'
8
8
  gem 'rubocop', '~> 1.21'
9
9
  gem 'rubocop-minitest', '~> 0.20'
10
10
  gem 'rubocop-rake', '~> 0.6'
11
+ gem 'yard', '~> 0.9'
11
12
 
12
13
  gemspec
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
- #### nice... huh?
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 posts
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!(attributes)
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. `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`.
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
- There 3 other optional keys that are provided for tracking contextual information:
130
-
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
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 two configurable options currently:
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 created at all.
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` as well:
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 for an inherited temporal table of a model including {Hoardable::Model}
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.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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- # A subclass of StandardError for general use within Hoardable
4
+ # A subclass of +StandardError+ for general use within {Hoardable}.
5
5
  class Error < StandardError; end
6
6
  end
@@ -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
- DATA_KEYS = %i[meta whodunit note event_id].freeze
6
- CONFIG_KEYS = %i[enabled save_trash].freeze
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
- SAVE_TRASH_ENABLED = -> { Hoardable.save_trash }.freeze
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
@@ -1,11 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- # This concern dynamically generates the Version variant of the class module Model and includes
5
- # the API methods and relationships on the source model
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
- next if Object.const_defined?(version_class_name)
19
-
20
- Object.const_set(version_class_name, Class.new(self) { include VersionModel })
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 a Source model
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: :hoardable_callbacks_enabled
18
- around_destroy :insert_hoardable_version_on_destroy, if: [:hoardable_callbacks_enabled, SAVE_TRASH_ENABLED]
19
- before_destroy :delete_hoardable_versions, if: :hoardable_callbacks_enabled, unless: SAVE_TRASH_ENABLED
20
- after_commit :unset_hoardable_version_and_event_id
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
- Hoardable.enabled && !self.class.name.end_with?(VERSION_CLASS_SUFFIX)
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
- 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
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 find_or_initialize_hoardable_event_id
66
- Thread.current[:hoardable_event_id] ||= SecureRandom.hex
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 unset_hoardable_version_and_event_id
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[:hoardable_event_id] = nil
138
+ Thread.current[:hoardable_event_uuid] = nil
102
139
  end
103
140
  end
104
141
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- # This concern provides support for PostgreSQL's tableoid system column
4
+ # This concern provides support for PostgreSQLs 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
@@ -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.4'
5
5
  end
@@ -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 models.
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 _operation == 'update'
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 _operation == 'delete'
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
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
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.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-07-28 00:00:00.000000000 Z
11
+ date: 2022-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord