hoardable 0.5.0 → 0.8.0

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: 4c4bc9bc4cdd73263a6afe5551b59f085cbf7dc3877e472df8abdba62f5fcbd6
4
- data.tar.gz: 7aa8ea828b2f6083a80c93e34a1fcc5756508db15f2a317dc95bfea2ae8b1e28
3
+ metadata.gz: 1cd77fbd3d35df59423ba95f7286703ae0d3b0f78bd9097b3574c713c38aa4c7
4
+ data.tar.gz: d5971e9daf59a9fa5343c13e389b20fc5b6ff193b398d05c958f34dfd056d0cb
5
5
  SHA512:
6
- metadata.gz: 915cba36e937b34667b2ad31ae8dd780224a417669a6bb3c1abdfb2c8a19c10525e7d29a0a4f264aa475362f9b823104bd5a40d71bfc01848db2eb6a463f7c1d
7
- data.tar.gz: ab0faaab94b03c5b6452b7aad505e16b4bb98538ae8649cfd65e17d397e3d917e6e61c752ed39b820dc30d72503e993ec5d6fbe353391c29025759a4bedeff74
6
+ metadata.gz: 5de03adf485f38e3ee3a24999bf770d92792404894529a5317f9788d15b784c0bcd9aa28b3d2966340a1244296cdc7b4ebe6fdccd6791172b44ca498c4bf5fca
7
+ data.tar.gz: a39bd22b66c727ec791e704ec3b24592c068dd2eb29ae0fa7d70a440073bd6d635fd7f24f12d3ba34bf05976b311bab9ec6a5a6fdf9f9069311347d89ae8f8c0
data/.rubocop.yml CHANGED
@@ -10,5 +10,9 @@ Metrics/ClassLength:
10
10
  Exclude:
11
11
  - 'test/**/*.rb'
12
12
 
13
+ Metrics/BlockLength:
14
+ Exclude:
15
+ - 'test/**/*.rb'
16
+
13
17
  Style/DocumentDynamicEvalDefinition:
14
18
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  ## [Unreleased]
2
2
 
3
+ - Stability is coming.
4
+
5
+ ## [0.8.0] - 2022-10-01
6
+
7
+ - **Breaking Change** - Due to the performance benefit of using `insert` for database injection of
8
+ versions, and a personal opinion that only an `after_versioned` hook might be needed, the
9
+ `before_versioned` and `around_versioned` ActiveRecord hooks are removed.
10
+
11
+ - **Breaking Change** - Another side effect of the performance benefit gained by using `insert` is
12
+ that a source model will need to be reloaded before a call to `versions` on it can access the
13
+ latest version after an `update` on the source record.
14
+
15
+ - **Breaking Change** - Previously the inherited `_versions` tables did not have a unique index on
16
+ the ID column, though it still pulled from the same sequence as the parent table. Prior to version
17
+ 0.4.0 though, it was possible to have multiple trashed versions with the same ID. Adding unique
18
+ indexes to version tables prior to version 0.4.0 could result in issues.
19
+
20
+ ## [0.7.0] - 2022-09-29
21
+
22
+ - **Breaking Change** - Continuing along with the change below, the `foreign_key` on the `_versions`
23
+ tables is now changed to `hoardable_source_id` instead of the i18n model name dervied foreign key.
24
+ The intent is to never leave room for conflict of foreign keys for existing relationships. This
25
+ can be resolved by renaming the foreign key columns from their i18n model name derived column
26
+ names to `hoardable_source_id`, i.e. `rename_column :post_versions, :post_id, :hoardable_source_id`.
27
+
28
+ ## [0.6.0] - 2022-09-28
29
+
30
+ - **Breaking Change** - Previously, a source model would `has_many :versions` with an inverse
31
+ relationship based on the i18n interpreted name of the source model. Now it simply `has_many
32
+ :versions, inverse_of :hoardable_source` to not potentially conflict with previously existing
33
+ relationships.
34
+
3
35
  ## [0.5.0] - 2022-09-25
4
36
 
5
37
  - **Breaking Change** - Untrashing a version will now insert a version for the untrash event with
data/Gemfile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
+ gem 'benchmark-ips', '~> 2.10'
5
6
  gem 'debug', '~> 1.6'
6
7
  gem 'minitest', '~> 5.0'
7
8
  gem 'rake', '~> 13.0'
data/README.md CHANGED
@@ -76,7 +76,7 @@ $ irb
76
76
  >> Post
77
77
  => Post(id: integer, body: text, user_id: integer, created_at: datetime)
78
78
  >> PostVersion
79
- => PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, _data: jsonb, _during: tsrange, post_id: integer)
79
+ => PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, _data: jsonb, _during: tsrange, hoardable_source_id: integer)
80
80
  ```
81
81
 
82
82
  A `Post` now `has_many :versions`. With the default configuration, whenever an update and deletion
@@ -86,7 +86,7 @@ of a `Post` occurs, a version is created:
86
86
  post = Post.create!(title: "Title")
87
87
  post.versions.size # => 0
88
88
  post.update!(title: "Revised Title")
89
- post.versions.size # => 1
89
+ post.reload.versions.size # => 1
90
90
  post.versions.first.title # => "Title"
91
91
  post.destroy!
92
92
  post.trashed? # true
@@ -102,7 +102,7 @@ If you ever need to revert to a specific version, you can call `version.revert!`
102
102
  ``` ruby
103
103
  post = Post.create!(title: "Title")
104
104
  post.update!(title: "Whoops")
105
- post.versions.last.revert!
105
+ post.reload.versions.last.revert!
106
106
  post.title # => "Title"
107
107
  ```
108
108
 
@@ -134,7 +134,7 @@ If you want to look-up the version of a record at a specific time, you can use t
134
134
  ```ruby
135
135
  post.at(1.day.ago) # => #<PostVersion>
136
136
  # or you can use the scope on the version model class
137
- PostVersion.at(1.day.ago).find_by(post_id: post.id) # => #<PostVersion>
137
+ PostVersion.at(1.day.ago).find_by(hoardable_source_id: post.id) # => #<PostVersion>
138
138
  ```
139
139
 
140
140
  The source model class also has an `.at` method:
@@ -182,7 +182,7 @@ Hoardable.whodunit = -> { Current.user&.id }
182
182
  # somewhere in your app code
183
183
  Current.user = User.find(123)
184
184
  post.update!(status: 'live')
185
- post.versions.last.hoardable_whodunit # => 123
185
+ post.reload.versions.last.hoardable_whodunit # => 123
186
186
  ```
187
187
 
188
188
  You can also set this context manually as well, just remember to clear them afterwards.
@@ -191,7 +191,7 @@ You can also set this context manually as well, just remember to clear them afte
191
191
  Hoardable.note = "reverting due to accidental deletion"
192
192
  post.update!(title: "We’re back!")
193
193
  Hoardable.note = nil
194
- post.versions.last.hoardable_note # => "reverting due to accidental deletion"
194
+ post.reload.versions.last.hoardable_note # => "reverting due to accidental deletion"
195
195
  ```
196
196
 
197
197
  A more useful pattern is to use `Hoardable.with` to set the context around a block. A good example
@@ -225,9 +225,9 @@ version.hoardable_event_uuid
225
225
 
226
226
  ### Model Callbacks
227
227
 
228
- Sometimes you might want to do something with a version before or after it gets inserted to the
229
- database. You can access it in `before/after/around_versioned` callbacks on the source record as
230
- `hoardable_version`. These happen around `.save`, which is enclosed in an ActiveRecord transaction.
228
+ Sometimes you might want to do something with a version after it gets inserted to the database. You
229
+ can access it in `after_versioned` callbacks on the source record as `hoardable_version`. These
230
+ happen within `ActiveRecord`’s `.save`, which is enclosed in an ActiveRecord transaction.
231
231
 
232
232
  There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
233
233
  on the source record after a version is reverted or untrashed.
@@ -235,14 +235,14 @@ on the source record after a version is reverted or untrashed.
235
235
  ```ruby
236
236
  class User
237
237
  include Hoardable::Model
238
- before_versioned :sanitize_version
238
+ after_versioned :track_versioned_event
239
239
  after_reverted :track_reverted_event
240
240
  after_untrashed :track_untrashed_event
241
241
 
242
242
  private
243
243
 
244
- def sanitize_version
245
- hoardable_version.sanitize_password
244
+ def track_versioned_event
245
+ track_event(:user_versioned, hoardable_version)
246
246
  end
247
247
 
248
248
  def track_reverted_event
@@ -338,14 +338,13 @@ class Post < ActiveRecord::Base
338
338
  Comment
339
339
  .version_class
340
340
  .trashed
341
- .where(post_id: id)
342
341
  .with_hoardable_event_uuid(hoardable_event_uuid)
343
342
  .find_each(&:untrash!)
344
343
  end
345
344
  end
346
345
  ```
347
346
 
348
- If there are models that might be related to versions that are trashed or otherwise, and/or might
347
+ If there are models that might be related to versions that are trashed or otherwise, and/or might be
349
348
  trashed themselves, you can bypass the inherited tables query handling altogether by using the
350
349
  `return_everything` configuration variable in `Hoardable.with`. This will ensure that you always see
351
350
  all records, including update and trashed versions.
@@ -8,11 +8,12 @@ class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%=
8
8
  t.tsrange :_during, null: false
9
9
  t.uuid :_event_uuid, null: false, index: true
10
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
+ t.<%= foreign_key_type %> :hoardable_source_id, null: false, index: true
12
12
  end
13
+ add_index(:<%= singularized_table_name %>_versions, :id, unique: true)
13
14
  add_index(
14
15
  :<%= singularized_table_name %>_versions,
15
- %i[_during <%= singularized_table_name %>_id],
16
+ %i[_during hoardable_source_id],
16
17
  name: 'idx_<%= singularized_table_name %>_versions_temporally'
17
18
  )
18
19
  end
@@ -23,11 +23,12 @@ class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%=
23
23
  t.tsrange :_during, null: false
24
24
  t.uuid :_event_uuid, null: false, index: true
25
25
  t.column :_operation, :hoardable_operation, null: false, index: true
26
- t.<%= foreign_key_type %> :<%= singularized_table_name %>_id, null: false, index: true
26
+ t.<%= foreign_key_type %> :hoardable_source_id, null: false, index: true
27
27
  end
28
+ add_index(:<%= singularized_table_name %>_versions, :id, unique: true)
28
29
  add_index(
29
30
  :<%= singularized_table_name %>_versions,
30
- %i[_during <%= singularized_table_name %>_id],
31
+ %i[_during hoardable_source_id],
31
32
  name: 'idx_<%= singularized_table_name %>_versions_temporally'
32
33
  )
33
34
  end
@@ -17,9 +17,9 @@ module Hoardable
17
17
 
18
18
  define_method(trashable_relationship_name) do
19
19
  source_reflection = self.class.reflections[name.to_s]
20
- version_class = source_reflection.klass.version_class
20
+ version_class = source_reflection.version_class
21
21
  version_class.trashed.only_most_recent.find_by(
22
- version_class.hoardable_source_foreign_key => source_reflection.foreign_key
22
+ hoardable_source_id: source_reflection.foreign_key
23
23
  )
24
24
  end
25
25
 
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # This is a private service class that manages the insertion of {VersionModel}s into the
5
+ # PostgreSQL database.
6
+ class DatabaseClient
7
+ attr_reader :source_record
8
+
9
+ def initialize(source_record)
10
+ @source_record = source_record
11
+ end
12
+
13
+ delegate :version_class, to: :source_record
14
+
15
+ def insert_hoardable_version(operation, &block)
16
+ version = version_class.insert(initialize_version_attributes(operation), returning: :id)
17
+ version_id = version[0]['id']
18
+ source_record.instance_variable_set('@hoardable_version', version_class.find(version_id))
19
+ source_record.run_callbacks(:versioned, &block)
20
+ end
21
+
22
+ def find_or_initialize_hoardable_event_uuid
23
+ Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
24
+ end
25
+
26
+ def initialize_version_attributes(operation)
27
+ source_record.attributes_before_type_cast.without('id').merge(
28
+ source_record.changes.transform_values { |h| h[0] },
29
+ {
30
+ 'hoardable_source_id' => source_record.id,
31
+ '_event_uuid' => find_or_initialize_hoardable_event_uuid,
32
+ '_operation' => operation,
33
+ '_data' => initialize_hoardable_data.merge(changes: source_record.changes),
34
+ '_during' => initialize_temporal_range
35
+ }
36
+ )
37
+ end
38
+
39
+ def initialize_temporal_range
40
+ ((previous_temporal_tsrange_end || hoardable_source_epoch)..Time.now.utc)
41
+ end
42
+
43
+ def initialize_hoardable_data
44
+ DATA_KEYS.to_h do |key|
45
+ [key, assign_hoardable_context(key)]
46
+ end
47
+ end
48
+
49
+ def assign_hoardable_context(key)
50
+ return nil if (value = Hoardable.public_send(key)).nil?
51
+
52
+ value.is_a?(Proc) ? value.call : value
53
+ end
54
+
55
+ def unset_hoardable_version_and_event_uuid
56
+ source_record.instance_variable_set('@hoardable_version', nil)
57
+ return if source_record.class.connection.transaction_open?
58
+
59
+ Thread.current[:hoardable_event_uuid] = nil
60
+ end
61
+
62
+ def previous_temporal_tsrange_end
63
+ source_record.versions.only_most_recent.pluck('_during').first&.end
64
+ end
65
+
66
+ def hoardable_source_epoch
67
+ if source_record.class.column_names.include?('created_at')
68
+ source_record.created_at
69
+ else
70
+ maybe_warn_about_missing_created_at_column
71
+ Time.at(0).utc
72
+ end
73
+ end
74
+
75
+ def maybe_warn_about_missing_created_at_column
76
+ return unless source_record.class.hoardable_config[:warn_on_missing_created_at_column]
77
+
78
+ source_table_name = source_record.class.table_name
79
+ Hoardable.logger.info(
80
+ <<~LOG
81
+ '#{source_table_name}' does not have a 'created_at' column, so the first version’s temporal period
82
+ will begin at the unix epoch instead. Add a 'created_at' column to '#{source_table_name}'
83
+ or set 'Hoardable.warn_on_missing_created_at_column = false' to disable this message.
84
+ LOG
85
+ )
86
+ end
87
+ end
88
+ private_constant :DatabaseClient
89
+ end
@@ -8,7 +8,7 @@ module Hoardable
8
8
 
9
9
  # Symbols for use with setting {Hoardable} configuration. See {file:README.md#configuration
10
10
  # README} for more.
11
- CONFIG_KEYS = %i[enabled version_updates save_trash return_everything].freeze
11
+ CONFIG_KEYS = %i[enabled version_updates save_trash return_everything warn_on_missing_created_at_column].freeze
12
12
 
13
13
  VERSION_CLASS_SUFFIX = 'Version'
14
14
  private_constant :VERSION_CLASS_SUFFIX
@@ -19,6 +19,21 @@ module Hoardable
19
19
  DURING_QUERY = '_during @> ?::timestamp'
20
20
  private_constant :DURING_QUERY
21
21
 
22
+ HOARDABLE_CALLBACKS_ENABLED = proc do |source_model|
23
+ source_model.class.hoardable_config[:enabled] && !source_model.class.name.end_with?(VERSION_CLASS_SUFFIX)
24
+ end.freeze
25
+ private_constant :HOARDABLE_CALLBACKS_ENABLED
26
+
27
+ HOARDABLE_SAVE_TRASH = proc do |source_model|
28
+ source_model.class.hoardable_config[:save_trash]
29
+ end.freeze
30
+ private_constant :HOARDABLE_SAVE_TRASH
31
+
32
+ HOARDABLE_VERSION_UPDATES = proc do |source_model|
33
+ source_model.class.hoardable_config[:version_updates]
34
+ end.freeze
35
+ private_constant :HOARDABLE_VERSION_UPDATES
36
+
22
37
  @context = {}
23
38
  @config = CONFIG_KEYS.to_h do |key|
24
39
  [key, key != :return_everything]
@@ -59,5 +74,10 @@ module Hoardable
59
74
  @config = current_config
60
75
  @context = current_context
61
76
  end
77
+
78
+ # @!visibility private
79
+ def logger
80
+ @logger ||= ActiveSupport::TaggedLogging.new(Logger.new($stdout))
81
+ end
62
82
  end
63
83
  end
@@ -46,7 +46,7 @@ module Hoardable
46
46
 
47
47
  included do
48
48
  include Associations
49
- define_model_callbacks :versioned
49
+ define_model_callbacks :versioned, only: :after
50
50
  define_model_callbacks :reverted, only: :after
51
51
  define_model_callbacks :untrashed, only: :after
52
52
 
@@ -7,6 +7,15 @@ module Hoardable
7
7
  module SourceModel
8
8
  extend ActiveSupport::Concern
9
9
 
10
+ # The +Version+ class instance for use within +versioned+, +reverted+, and +untrashed+ callbacks.
11
+ attr_reader :hoardable_version
12
+
13
+ # @!attribute [r] hoardable_event_uuid
14
+ # @return [String] A postgres UUID that represents the +version+’s +ActiveRecord+ database transaction
15
+ # @!attribute [r] hoardable_operation
16
+ # @return [String] The database operation that created the +version+ - either +update+ or +delete+.
17
+ delegate :hoardable_event_uuid, :hoardable_operation, to: :hoardable_version, allow_nil: true
18
+
10
19
  class_methods do
11
20
  # The dynamically generated +Version+ class for this model.
12
21
  def version_class
@@ -17,27 +26,27 @@ module Hoardable
17
26
  included do
18
27
  include Tableoid
19
28
 
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
29
+ around_update(if: [HOARDABLE_CALLBACKS_ENABLED, HOARDABLE_VERSION_UPDATES]) do |_, block|
30
+ hoardable_client.insert_hoardable_version('update', &block)
31
+ end
32
+
33
+ around_destroy(if: [HOARDABLE_CALLBACKS_ENABLED, HOARDABLE_SAVE_TRASH]) do |_, block|
34
+ hoardable_client.insert_hoardable_version('delete', &block)
35
+ end
24
36
 
25
- # This will contain the +Version+ class instance for use within +versioned+, +reverted+, and
26
- # +untrashed+ callbacks.
27
- attr_reader :hoardable_version
37
+ before_destroy(if: HOARDABLE_CALLBACKS_ENABLED, unless: HOARDABLE_SAVE_TRASH) do
38
+ versions.delete_all(:delete_all)
39
+ end
28
40
 
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
41
+ after_commit { hoardable_client.unset_hoardable_version_and_event_uuid }
34
42
 
35
43
  # Returns all +versions+ in ascending order of their temporal timeframes.
36
44
  has_many(
37
45
  :versions, -> { order('UPPER(_during) ASC') },
38
46
  dependent: nil,
39
47
  class_name: version_class.to_s,
40
- inverse_of: model_name.i18n_key
48
+ inverse_of: :hoardable_source,
49
+ foreign_key: :hoardable_source_id
41
50
  )
42
51
 
43
52
  # @!scope class
@@ -47,11 +56,8 @@ module Hoardable
47
56
  # Returns instances of the source model and versions that were valid at the supplied
48
57
  # +datetime+ or +time+, all cast as instances of the source model.
49
58
  scope :at, lambda { |datetime|
50
- versioned = version_class.at(datetime)
51
- trashed = version_class.trashed_at(datetime)
52
- foreign_key = version_class.hoardable_source_foreign_key
53
- include_versions.where(id: versioned.select('id')).or(
54
- where.not(id: versioned.select(foreign_key)).where.not(id: trashed.select(foreign_key))
59
+ include_versions.where(id: version_class.at(datetime).select('id')).or(
60
+ where.not(id: version_class.select(:hoardable_source_id).where(DURING_QUERY, datetime))
55
61
  )
56
62
  }
57
63
  end
@@ -70,7 +76,7 @@ module Hoardable
70
76
  def at(datetime)
71
77
  raise(Error, 'Future state cannot be known') if datetime.future?
72
78
 
73
- versions.find_by(DURING_QUERY, datetime) || self
79
+ versions.at(datetime).first || self
74
80
  end
75
81
 
76
82
  # If a version is found at the supplied datetime, it will +revert!+ to it and return it. This
@@ -80,81 +86,15 @@ module Hoardable
80
86
  def revert_to!(datetime)
81
87
  return unless (version = at(datetime))
82
88
 
83
- version.is_a?(self.class.version_class) ? version.revert! : self
84
- end
85
-
86
- private
87
-
88
- def hoardable_callbacks_enabled
89
- self.class.hoardable_config[:enabled] && !self.class.name.end_with?(VERSION_CLASS_SUFFIX)
89
+ version.is_a?(version_class) ? version.revert! : self
90
90
  end
91
91
 
92
- def hoardable_save_trash
93
- self.class.hoardable_config[:save_trash]
94
- end
95
-
96
- def hoardable_version_updates
97
- self.class.hoardable_config[:version_updates]
98
- end
99
-
100
- def insert_hoardable_version_on_update(&block)
101
- insert_hoardable_version('update', &block)
102
- end
103
-
104
- def insert_hoardable_version_on_destroy(&block)
105
- insert_hoardable_version('delete', &block)
106
- end
107
-
108
- def insert_hoardable_version_on_untrashed
109
- initialize_hoardable_version('insert').save(validate: false, touch: false)
110
- end
92
+ delegate :version_class, to: :class
111
93
 
112
- def insert_hoardable_version(operation)
113
- @hoardable_version = initialize_hoardable_version(operation)
114
- run_callbacks(:versioned) do
115
- yield
116
- hoardable_version.save(validate: false, touch: false)
117
- end
118
- end
119
-
120
- def find_or_initialize_hoardable_event_uuid
121
- Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
122
- end
123
-
124
- def initialize_hoardable_version(operation)
125
- versions.new(
126
- attributes_before_type_cast.without('id').merge(
127
- changes.transform_values { |h| h[0] },
128
- {
129
- _event_uuid: find_or_initialize_hoardable_event_uuid,
130
- _operation: operation,
131
- _data: initialize_hoardable_data.merge(changes: changes)
132
- }
133
- )
134
- )
135
- end
136
-
137
- def initialize_hoardable_data
138
- DATA_KEYS.to_h do |key|
139
- [key, assign_hoardable_context(key)]
140
- end
141
- end
142
-
143
- def assign_hoardable_context(key)
144
- return nil if (value = Hoardable.public_send(key)).nil?
145
-
146
- value.is_a?(Proc) ? value.call : value
147
- end
148
-
149
- def delete_hoardable_versions
150
- versions.delete_all(:delete_all)
151
- end
152
-
153
- def unset_hoardable_version_and_event_uuid
154
- @hoardable_version = nil
155
- return if ActiveRecord::Base.connection.transaction_open?
94
+ private
156
95
 
157
- Thread.current[:hoardable_event_uuid] = nil
96
+ def hoardable_client
97
+ @hoardable_client ||= DatabaseClient.new(self)
158
98
  end
159
99
  end
160
100
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = '0.5.0'
4
+ VERSION = '0.8.0'
5
5
  end
@@ -7,18 +7,21 @@ module Hoardable
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  class_methods do
10
- # Returns the foreign column that holds the reference to the source model of the version.
11
- def hoardable_source_foreign_key
12
- @hoardable_source_foreign_key ||= "#{superclass.model_name.i18n_key}_id"
10
+ # This is needed to omit the pseudo row of 'tableoid' when using +ActiveRecord+’s +insert+.
11
+ #
12
+ # @!visibility private
13
+ def scope_attributes
14
+ super.without('tableoid')
13
15
  end
14
16
  end
15
17
 
16
18
  included do
17
- hoardable_source_key = superclass.model_name.i18n_key
18
-
19
19
  # A +version+ belongs to it’s parent +ActiveRecord+ source.
20
- belongs_to hoardable_source_key, inverse_of: :versions
21
- alias_method :hoardable_source, hoardable_source_key
20
+ belongs_to(
21
+ :hoardable_source,
22
+ inverse_of: :versions,
23
+ class_name: superclass.model_name
24
+ )
22
25
 
23
26
  self.table_name = "#{table_name.singularize}#{VERSION_TABLE_SUFFIX}"
24
27
 
@@ -27,15 +30,13 @@ module Hoardable
27
30
  alias_attribute :hoardable_event_uuid, :_event_uuid
28
31
  alias_attribute :hoardable_during, :_during
29
32
 
30
- before_create :assign_temporal_tsrange
31
-
32
33
  # @!scope class
33
34
  # @!method trashed
34
35
  # @return [ActiveRecord<Object>]
35
36
  #
36
37
  # Returns only trashed +versions+ that are currently orphans.
37
38
  scope :trashed, lambda {
38
- left_outer_joins(hoardable_source_key)
39
+ left_outer_joins(:hoardable_source)
39
40
  .where(superclass.table_name => { id: nil })
40
41
  .where(_operation: 'delete')
41
42
  }
@@ -90,12 +91,11 @@ module Hoardable
90
91
  raise(Error, 'Version is not trashed, cannot untrash') unless hoardable_operation == 'delete'
91
92
 
92
93
  transaction do
93
- superscope = self.class.superclass.unscoped
94
- superscope.insert(hoardable_source_attributes.merge('id' => hoardable_source_foreign_id))
95
- superscope.find(hoardable_source_foreign_id).tap do |untrashed|
96
- untrashed.send('insert_hoardable_version_on_untrashed')
97
- untrashed.instance_variable_set(:@hoardable_version, self)
98
- untrashed.run_callbacks(:untrashed)
94
+ insert_untrashed_source.tap do |untrashed|
95
+ untrashed.send('hoardable_client').insert_hoardable_version('insert') do
96
+ untrashed.instance_variable_set(:@hoardable_version, self)
97
+ untrashed.run_callbacks(:untrashed)
98
+ end
99
99
  end
100
100
  end
101
101
  end
@@ -115,34 +115,22 @@ module Hoardable
115
115
 
116
116
  # Returns the foreign reference that represents the source model of the version.
117
117
  def hoardable_source_foreign_id
118
- @hoardable_source_foreign_id ||= public_send(hoardable_source_foreign_key)
118
+ @hoardable_source_foreign_id ||= public_send(:hoardable_source_id)
119
119
  end
120
120
 
121
121
  private
122
122
 
123
- delegate :hoardable_source_foreign_key, to: :class
123
+ def insert_untrashed_source
124
+ superscope = self.class.superclass.unscoped
125
+ superscope.insert(hoardable_source_attributes.merge('id' => hoardable_source_foreign_id))
126
+ superscope.find(hoardable_source_foreign_id)
127
+ end
124
128
 
125
129
  def hoardable_source_attributes
126
130
  @hoardable_source_attributes ||=
127
131
  attributes_before_type_cast
128
- .without(hoardable_source_foreign_key)
132
+ .without('hoardable_source_id')
129
133
  .reject { |k, _v| k.start_with?('_') }
130
134
  end
131
-
132
- def previous_temporal_tsrange_end
133
- hoardable_source.versions.only_most_recent.pluck('_during').first&.end
134
- end
135
-
136
- def assign_temporal_tsrange
137
- range_start = (
138
- previous_temporal_tsrange_end ||
139
- if hoardable_source.class.column_names.include?('created_at')
140
- hoardable_source.created_at
141
- else
142
- Time.at(0).utc
143
- end
144
- )
145
- self._during = (range_start..Time.now.utc)
146
- end
147
135
  end
148
136
  end
data/lib/hoardable.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'hoardable/version'
4
4
  require_relative 'hoardable/hoardable'
5
5
  require_relative 'hoardable/tableoid'
6
6
  require_relative 'hoardable/error'
7
+ require_relative 'hoardable/database_client'
7
8
  require_relative 'hoardable/source_model'
8
9
  require_relative 'hoardable/version_model'
9
10
  require_relative 'hoardable/model'
data/sig/hoardable.rbs CHANGED
@@ -1,14 +1,19 @@
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, :return_everything]
4
+ CONFIG_KEYS: [:enabled, :version_updates, :save_trash, :return_everything, :warn_on_missing_created_at_column]
5
5
  VERSION_CLASS_SUFFIX: String
6
6
  VERSION_TABLE_SUFFIX: String
7
7
  DURING_QUERY: String
8
+ HOARDABLE_CALLBACKS_ENABLED: ^(untyped) -> untyped
9
+ HOARDABLE_SAVE_TRASH: ^(untyped) -> untyped
10
+ HOARDABLE_VERSION_UPDATES: ^(untyped) -> untyped
8
11
  self.@context: Hash[untyped, untyped]
9
12
  self.@config: untyped
13
+ self.@logger: untyped
10
14
 
11
15
  def self.with: (untyped hash) -> untyped
16
+ def self.logger: -> untyped
12
17
 
13
18
  module Tableoid
14
19
  TABLEOID_AREL_CONDITIONS: Proc
@@ -23,59 +28,68 @@ module Hoardable
23
28
  class Error < StandardError
24
29
  end
25
30
 
31
+ class DatabaseClient
32
+ attr_reader source_model: SourceModel
33
+ def initialize: (SourceModel source_model) -> void
34
+ def insert_hoardable_version: (untyped operation) -> untyped
35
+ def find_or_initialize_hoardable_event_uuid: -> untyped
36
+ def initialize_version_attributes: (untyped operation) -> untyped
37
+ def initialize_temporal_range: -> Range
38
+ def initialize_hoardable_data: -> untyped
39
+ def assign_hoardable_context: (:event_uuid | :meta | :note | :whodunit key) -> nil
40
+ def unset_hoardable_version_and_event_uuid: -> nil
41
+ def previous_temporal_tsrange_end: -> untyped
42
+ def hoardable_source_epoch: -> Time
43
+ def maybe_warn_about_missing_created_at_column: -> nil
44
+ end
45
+
26
46
  module SourceModel
27
47
  include Tableoid
48
+ @hoardable_client: DatabaseClient
28
49
 
50
+ attr_reader hoardable_version: untyped
29
51
  def trashed?: -> untyped
30
52
  def at: (untyped datetime) -> SourceModel
31
53
  def revert_to!: (untyped datetime) -> SourceModel?
32
54
 
33
55
  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
56
+ def hoardable_client: -> DatabaseClient
46
57
 
47
58
  public
48
59
  def version_class: -> untyped
49
- attr_reader hoardable_version: nil
50
60
  end
51
61
 
52
62
  module VersionModel
53
- @hoardable_source_attributes: untyped
54
- @hoardable_source_foreign_key: String
55
63
  @hoardable_source_foreign_id: untyped
64
+ @hoardable_source_attributes: untyped
56
65
 
57
66
  def revert!: -> untyped
58
67
  def untrash!: -> untyped
59
68
  def changes: -> untyped
69
+ def hoardable_source_foreign_id: -> untyped
60
70
 
61
71
  private
62
- def untrashable_hoardable_source_attributes: -> untyped
72
+ def insert_untrashed_source: -> untyped
63
73
  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
74
+
75
+ public
76
+ def scope_attributes: -> untyped
68
77
  end
69
78
 
70
79
  module Model
71
80
  include VersionModel
72
81
  include SourceModel
82
+ include Associations
73
83
 
74
84
  attr_reader _hoardable_config: Hash[untyped, untyped]
75
85
  def hoardable_config: (?nil hash) -> untyped
76
86
  def with_hoardable_config: (untyped hash) -> untyped
77
87
  end
78
88
 
89
+ module Associations
90
+ def belongs_to_trashable: (untyped name, ?nil scope, **untyped) -> untyped
91
+ end
92
+
79
93
  class MigrationGenerator
80
94
  @singularized_table_name: untyped
81
95
 
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.5.0
4
+ version: 0.8.0
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-09-25 00:00:00.000000000 Z
11
+ date: 2022-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -109,6 +109,7 @@ files:
109
109
  - lib/generators/hoardable/templates/migration_6.rb.erb
110
110
  - lib/hoardable.rb
111
111
  - lib/hoardable/associations.rb
112
+ - lib/hoardable/database_client.rb
112
113
  - lib/hoardable/error.rb
113
114
  - lib/hoardable/hoardable.rb
114
115
  - lib/hoardable/model.rb