hoardable 0.5.0 → 0.8.0

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