hoardable 0.9.1 → 0.10.1

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: 5b85dfaf658e447049fcb09281d34dd25188be79f948eda294398f47b19b8244
4
- data.tar.gz: 67219612736259e2dbc542230cc1490a156a15bc679340176aca35b76aaf516c
3
+ metadata.gz: dad9a103d70ce10905528d1d7e56e67a4a8fb89e675b3097165ccede6262a015
4
+ data.tar.gz: a2dc95873a08175254cca315caa3de9b087674df1e4dadd17f42fb26a9080a9d
5
5
  SHA512:
6
- metadata.gz: cf3c5edfeac5526e7520dc2584caba8bb5399df43acf1ce6bd320b19f44981c154624b9b80c5b415aaeb840fc5d718f1999b4bbaf82eb3ff76149e09621ab0e5
7
- data.tar.gz: 52112fb9bc55bec2009656cce9571887130333b6460d5478c1a0c113895ccb977ffcc80172954911c6e70d2012f136bf68c64b17bc69ffd7deeaa40a56c97646
6
+ metadata.gz: 02b542ba1fe562750eb16bf6f310b08e2340610e01e7db68eaedb9185e4da4a1d7e7a5a42885fb09376759bf936b8b527de4bedec8399b71119541162e57604f
7
+ data.tar.gz: 77edd48d420fea18333996280d6e31e0daba198b96e67fdc49b4fc7fe1884cb86cc88416e1f486691e2292465818fb50e3c4fc4367d83cc6d751d0adcb39acf6
data/CHANGELOG.md CHANGED
@@ -2,57 +2,3 @@
2
2
 
3
3
  - Stability is coming.
4
4
 
5
- ## [0.9.0] - 2022-10-02
6
-
7
- - **Breaking Change** - `Hoardable.return_everything` was removed in favor of the newly added
8
- `Hoardable.at`.
9
-
10
- ## [0.8.0] - 2022-10-01
11
-
12
- - **Breaking Change** - Due to the performance benefit of using `insert` for database injection of
13
- versions, and a personal opinion that only an `after_versioned` hook might be needed, the
14
- `before_versioned` and `around_versioned` ActiveRecord hooks are removed.
15
-
16
- - **Breaking Change** - Another side effect of the performance benefit gained by using `insert` is
17
- that a source model will need to be reloaded before a call to `versions` on it can access the
18
- latest version after an `update` on the source record.
19
-
20
- - **Breaking Change** - Previously the inherited `_versions` tables did not have a unique index on
21
- the ID column, though it still pulled from the same sequence as the parent table. Prior to version
22
- 0.4.0 though, it was possible to have multiple trashed versions with the same ID. Adding unique
23
- indexes to version tables prior to version 0.4.0 could result in issues.
24
-
25
- ## [0.7.0] - 2022-09-29
26
-
27
- - **Breaking Change** - Continuing along with the change below, the `foreign_key` on the `_versions`
28
- tables is now changed to `hoardable_source_id` instead of the i18n model name dervied foreign key.
29
- The intent is to never leave room for conflict of foreign keys for existing relationships. This
30
- can be resolved by renaming the foreign key columns from their i18n model name derived column
31
- names to `hoardable_source_id`, i.e. `rename_column :post_versions, :post_id, :hoardable_source_id`.
32
-
33
- ## [0.6.0] - 2022-09-28
34
-
35
- - **Breaking Change** - Previously, a source model would `has_many :versions` with an inverse
36
- relationship based on the i18n interpreted name of the source model. Now it simply `has_many
37
- :versions, inverse_of :hoardable_source` to not potentially conflict with previously existing
38
- relationships.
39
-
40
- ## [0.5.0] - 2022-09-25
41
-
42
- - **Breaking Change** - Untrashing a version will now insert a version for the untrash event with
43
- it's own temporal timespan. This simplifies the ability to query versions temporarily for when
44
- they were trashed or not. This changes, but corrects, temporal query results using `.at`.
45
-
46
- - **Breaking Change** - Because of the above, a new operation enum value of "insert" was added. If
47
- you already have the `hoardable_operation` enum in your PostgreSQL schema, you can add it by
48
- executing the following SQL in a new migration: `ALTER TYPE hoardable_operation ADD VALUE
49
- 'insert';`.
50
-
51
- ## [0.4.0] - 2022-09-24
52
-
53
- - **Breaking Change** - Trashed versions now pull from the same postgres sequenced used by the
54
- source model’s table.
55
-
56
- ## [0.1.0] - 2022-07-23
57
-
58
- - Initial release
data/README.md CHANGED
@@ -164,12 +164,11 @@ specifically with:
164
164
  ```ruby
165
165
  PostVersion.trashed
166
166
  Post.version_class.trashed # <- same thing as above
167
- PostVersion.trashed.first.trashed? # <- true
168
167
  ```
169
168
 
170
169
  _Note:_ A `Version` is not created upon initial parent model creation. To accurately track the
171
170
  beginning of the first temporal period, you will need to ensure the source model table has a
172
- `created_at` timestamp column.
171
+ `created_at` timestamp column. If this is missing, an error will be raised.
173
172
 
174
173
  ### Tracking Contextual Data
175
174
 
@@ -318,25 +317,25 @@ with `Hoardable` considerations.
318
317
 
319
318
  Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child
320
319
  record’s foreign key will point to the non-existent trashed version of the parent. If you would like
321
- to have `belongs_to` resolve to the trashed parent model in this case, you can use
322
- `belongs_to_trashable` in place of `belongs_to`:
320
+ to have `belongs_to` resolve to the trashed parent model in this case, you can give it the option of
321
+ `trashable: true`:
323
322
 
324
323
  ```ruby
325
324
  class Comment
326
325
  include Hoardable::Associations # <- This includes is not required if this model already includes `Hoardable::Model`
327
- belongs_to_trashable :post, -> { where(status: 'published') }, class_name: 'Article' # <- Accepts normal `belongs_to` arguments
326
+ belongs_to :post, trashable: true
328
327
  end
329
328
  ```
330
329
 
331
330
  Sometimes you'll have a Hoardable record that `has_many` other Hoardable records and you will want
332
331
  to know the state of both the parent record and the children at a cetain point in time. You
333
- accomplish this by establishing a `has_many_hoardable` relationship and using the `Hoardable.at`
334
- method:
332
+ accomplish this by adding `hoardable: true` to the `has_many` relationship and using the
333
+ `Hoardable.at` method:
335
334
 
336
335
  ```ruby
337
336
  class Post
338
337
  include Hoardable::Model
339
- has_many_hoardable :comments
338
+ has_many :comments, hoardable: true
340
339
  end
341
340
 
342
341
  def Comment
@@ -352,7 +351,7 @@ post.update!(title: 'New Title')
352
351
  post_id = post.id # 1
353
352
 
354
353
  Hoardable.at(datetime) do
355
- post = Post.hoardable.find(post_id)
354
+ post = Post.find(post_id)
356
355
  post.title # => 'Title'
357
356
  post.comments.size # => 2
358
357
  post.id # => 2
@@ -370,9 +369,7 @@ version, even though it is masquerading as a `Post`.
370
369
 
371
370
  If you are ever unsure if a Hoardable record is a "source" or a "version", you can be sure by
372
371
  calling `version?` on it. If you want to get the true original source record ID, you can call
373
- `hoardable_source_id`. Finally, if you prepend `.hoardable` to a `.find` call on the source model
374
- class, you can always find the relevant source or temporal version record using just the original
375
- source record’s id.
372
+ `hoardable_source_id`.
376
373
 
377
374
  Sometimes you’ll trash something that `has_many_hoardable :children, dependent: :destroy` and want
378
375
  to untrash everything in a similar dependent manner. Whenever a hoardable version is created in a
@@ -8,8 +8,8 @@ module Hoardable
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  # An +ActiveRecord+ extension that allows looking up {VersionModel}s by +hoardable_source_id+ as
11
- # if they were {SourceModel}s.
12
- module HasManyScope
11
+ # if they were {SourceModel}s when using {Hoardable#at}.
12
+ module HasManyExtension
13
13
  def scope
14
14
  @scope ||= hoardable_scope
15
15
  end
@@ -25,35 +25,85 @@ module Hoardable
25
25
  end
26
26
  end
27
27
  end
28
- private_constant :HasManyScope
28
+ private_constant :HasManyExtension
29
29
 
30
- class_methods do
31
- # A wrapper for +ActiveRecord+’s +belongs_to+ that allows for falling back to the most recent
32
- # trashed +version+, in the case that the related source has been trashed.
33
- def belongs_to_trashable(name, scope = nil, **options)
34
- belongs_to(name, scope, **options)
30
+ # A private service class for installing +ActiveRecord+ association overrides.
31
+ class Overrider
32
+ attr_reader :klass
35
33
 
36
- trashable_relationship_name = "trashable_#{name}"
34
+ def initialize(klass)
35
+ @klass = klass
36
+ end
37
37
 
38
- define_method(trashable_relationship_name) do
39
- source_reflection = self.class.reflections[name.to_s]
40
- version_class = source_reflection.version_class
41
- version_class.trashed.only_most_recent.find_by(
38
+ def override_belongs_to(name)
39
+ klass.define_method("trashable_#{name}") do
40
+ source_reflection = klass.reflections[name.to_s]
41
+ source_reflection.version_class.trashed.only_most_recent.find_by(
42
42
  hoardable_source_id: source_reflection.foreign_key
43
43
  )
44
44
  end
45
45
 
46
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
46
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
+ def #{name}
48
+ super || trashable_#{name}
49
+ end
50
+ RUBY
51
+ end
52
+
53
+ def override_has_one(name)
54
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
55
+ def #{name}
56
+ return super unless (at = Hoardable.instance_variable_get('@at'))
57
+
58
+ super&.version_at(at) ||
59
+ _reflections["profile"].klass.where(_reflections["profile"].foreign_key => id).first
60
+ end
61
+ RUBY
62
+ end
63
+
64
+ def override_has_many(name)
65
+ # This hack is needed to force Rails to not use any existing method cache so that the
66
+ # {HasManyExtension} scope is always used.
67
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
68
  def #{name}
48
- super || #{trashable_relationship_name}
69
+ super.extending
49
70
  end
50
71
  RUBY
51
72
  end
73
+ end
74
+ private_constant :Overrider
75
+
76
+ class_methods do
77
+ def belongs_to(*args)
78
+ options = args.extract_options!
79
+ trashable = options.delete(:trashable)
80
+ super(*args, **options)
81
+ return unless trashable
82
+
83
+ hoardable_association_overrider.override_belongs_to(args.first)
84
+ end
85
+
86
+ def has_one(*args)
87
+ options = args.extract_options!
88
+ hoardable = options.delete(:hoardable)
89
+ super(*args, **options)
90
+ return unless hoardable
91
+
92
+ hoardable_association_overrider.override_has_one(args.first)
93
+ end
94
+
95
+ def has_many(*args, &block)
96
+ options = args.extract_options!
97
+ options[:extend] = Array(options[:extend]).push(HasManyExtension) if options.delete(:hoardable)
98
+ super(*args, **options, &block)
99
+
100
+ hoardable_association_overrider.override_has_many(args.first)
101
+ end
102
+
103
+ private
52
104
 
53
- # A wrapper for +ActiveRecord+’s +has_many+ that allows for finding temporal versions of a
54
- # record cast as instances of the {SourceModel}, when doing a {Hoardable#at} query.
55
- def has_many_hoardable(name, scope = nil, **options)
56
- has_many(name, scope, **options) { include HasManyScope }
105
+ def hoardable_association_overrider
106
+ @hoardable_association_overrider ||= Overrider.new(self)
57
107
  end
58
108
  end
59
109
  end
@@ -73,25 +73,9 @@ module Hoardable
73
73
  end
74
74
 
75
75
  def hoardable_source_epoch
76
- if source_record.class.column_names.include?('created_at')
77
- source_record.created_at
78
- else
79
- maybe_warn_about_missing_created_at_column
80
- Time.at(0).utc
81
- end
82
- end
76
+ return source_record.created_at if source_record.class.column_names.include?('created_at')
83
77
 
84
- def maybe_warn_about_missing_created_at_column
85
- return unless source_record.class.hoardable_config[:warn_on_missing_created_at_column]
86
-
87
- source_table_name = source_record.class.table_name
88
- Hoardable.logger.info(
89
- <<~LOG
90
- '#{source_table_name}' does not have a 'created_at' column, so the first version’s temporal period
91
- will begin at the unix epoch instead. Add a 'created_at' column to '#{source_table_name}'
92
- or set 'Hoardable.warn_on_missing_created_at_column = false' to disable this message.
93
- LOG
94
- )
78
+ raise CreatedAtColumnMissingError, source_record.class.table_name
95
79
  end
96
80
  end
97
81
  private_constant :DatabaseClient
@@ -3,4 +3,16 @@
3
3
  module Hoardable
4
4
  # A subclass of +StandardError+ for general use within {Hoardable}.
5
5
  class Error < StandardError; end
6
+
7
+ # An error to be raised when 'created_at' columns are missing for {Hoardable::Model}s.
8
+ class CreatedAtColumnMissingError < Error
9
+ def initialize(source_table_name)
10
+ super(
11
+ <<~LOG
12
+ '#{source_table_name}' does not have a 'created_at' column, so the start of the first
13
+ version’s temporal period cannot be known. Add a 'created_at' column to '#{source_table_name}'.
14
+ LOG
15
+ )
16
+ end
17
+ end
6
18
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # A module for overriding +ActiveRecord#find_one+ and +ActiveRecord#find_some+ in the case you are
5
+ # doing a temporal query and the current {SourceModel} record may in fact be a {VersionModel}
6
+ # record. This is extended into the current scope with {Hoardable#at} but can also be opt-ed into
7
+ # with the class method +hoardable+.
8
+ module FinderMethods
9
+ def find_one(id)
10
+ super(hoardable_source_ids([id])[0])
11
+ end
12
+
13
+ def find_some(ids)
14
+ super(hoardable_source_ids(ids))
15
+ end
16
+
17
+ private
18
+
19
+ def hoardable_source_ids(ids)
20
+ ids.map do |id|
21
+ version_class.where(hoardable_source_id: id).select(primary_key).ids[0] || id
22
+ end
23
+ end
24
+ end
25
+ 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 warn_on_missing_created_at_column].freeze
11
+ CONFIG_KEYS = %i[enabled version_updates save_trash].freeze
12
12
 
13
13
  VERSION_CLASS_SUFFIX = 'Version'
14
14
  private_constant :VERSION_CLASS_SUFFIX
@@ -60,9 +60,13 @@ module Hoardable
60
60
  # Returns instances of the source model and versions that were valid at the supplied
61
61
  # +datetime+ or +time+, all cast as instances of the source model.
62
62
  scope :at, lambda { |datetime|
63
+ raise(CreatedAtColumnMissingError, @klass.table_name) unless @klass.column_names.include?('created_at')
64
+
63
65
  include_versions.where(id: version_class.at(datetime).select('id')).or(
64
- where.not(id: version_class.select(:hoardable_source_id).where(DURING_QUERY, datetime))
65
- )
66
+ exclude_versions
67
+ .where("#{table_name}.created_at < ?", datetime)
68
+ .where.not(id: version_class.select(:hoardable_source_id).where(DURING_QUERY, datetime))
69
+ ).hoardable
66
70
  }
67
71
  end
68
72
 
@@ -16,16 +16,6 @@ module Hoardable
16
16
  # @return [String] The database operation that created the +version+ - either +update+ or +delete+.
17
17
  delegate :hoardable_event_uuid, :hoardable_operation, to: :hoardable_version, allow_nil: true
18
18
 
19
- # A module for overriding +ActiveRecord#find_one+’ in the case you are doing a temporal query
20
- # and the current {SourceModel} record may in fact be a {VersionModel} record.
21
- module FinderMethods
22
- def find_one(id)
23
- conditions = { primary_key => [id, *version_class.where(hoardable_source_id: id).select(primary_key).ids] }
24
- find_by(conditions) || where(conditions).raise_record_not_found_exception!
25
- end
26
- end
27
- private_constant :FinderMethods
28
-
29
19
  class_methods do
30
20
  # The dynamically generated +Version+ class for this model.
31
21
  def version_class
@@ -50,7 +40,7 @@ module Hoardable
50
40
  end
51
41
 
52
42
  before_destroy(if: HOARDABLE_CALLBACKS_ENABLED, unless: HOARDABLE_SAVE_TRASH) do
53
- versions.delete_all(:delete_all)
43
+ versions.delete_all
54
44
  end
55
45
 
56
46
  after_commit { hoardable_client.unset_hoardable_version_and_event_uuid }
@@ -81,14 +71,21 @@ module Hoardable
81
71
  !!hoardable_client.hoardable_version_source_id
82
72
  end
83
73
 
84
- # Returns the +version+ at the supplied +datetime+ or +time+. It will return +self+ if there is
85
- # none. This will raise an error if you try to find a version in the future.
74
+ # Returns the +version+ at the supplied +datetime+ or +time+, or +self+ if there is none.
86
75
  #
87
76
  # @param datetime [DateTime, Time]
88
77
  def at(datetime)
78
+ version_at(datetime) || (self if created_at < datetime)
79
+ end
80
+
81
+ # Returns the +version+ at the supplied +datetime+ or +time+. This will raise an error if you
82
+ # try to find a version in the future.
83
+ #
84
+ # @param datetime [DateTime, Time]
85
+ def version_at(datetime)
89
86
  raise(Error, 'Future state cannot be known') if datetime.future?
90
87
 
91
- versions.at(datetime).first || self
88
+ versions.at(datetime).limit(1).first
92
89
  end
93
90
 
94
91
  # If a version is found at the supplied datetime, it will +revert!+ to it and return it. This
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = '0.9.1'
4
+ VERSION = '0.10.1'
5
5
  end
data/lib/hoardable.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'hoardable/version'
4
4
  require_relative 'hoardable/hoardable'
5
+ require_relative 'hoardable/finder_methods'
5
6
  require_relative 'hoardable/scopes'
6
7
  require_relative 'hoardable/error'
7
8
  require_relative 'hoardable/database_client'
data/sig/hoardable.rbs CHANGED
@@ -1,7 +1,7 @@
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, :warn_on_missing_created_at_column]
4
+ CONFIG_KEYS: [:enabled, :version_updates, :save_trash]
5
5
  VERSION_CLASS_SUFFIX: String
6
6
  VERSION_TABLE_SUFFIX: String
7
7
  DURING_QUERY: String
@@ -46,7 +46,6 @@ module Hoardable
46
46
  def unset_hoardable_version_and_event_uuid: -> nil
47
47
  def previous_temporal_tsrange_end: -> untyped
48
48
  def hoardable_source_epoch: -> Time
49
- def maybe_warn_about_missing_created_at_column: -> nil
50
49
  end
51
50
 
52
51
  module SourceModel
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.9.1
4
+ version: 0.10.1
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-10-03 00:00:00.000000000 Z
11
+ date: 2022-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -112,6 +112,7 @@ files:
112
112
  - lib/hoardable/associations.rb
113
113
  - lib/hoardable/database_client.rb
114
114
  - lib/hoardable/error.rb
115
+ - lib/hoardable/finder_methods.rb
115
116
  - lib/hoardable/hoardable.rb
116
117
  - lib/hoardable/model.rb
117
118
  - lib/hoardable/scopes.rb