hoardable 0.9.1 → 0.10.1

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