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 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