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 +4 -4
- data/CHANGELOG.md +0 -54
- data/README.md +9 -12
- data/lib/hoardable/associations.rb +69 -19
- data/lib/hoardable/database_client.rb +2 -18
- data/lib/hoardable/error.rb +12 -0
- data/lib/hoardable/finder_methods.rb +25 -0
- data/lib/hoardable/hoardable.rb +1 -1
- data/lib/hoardable/scopes.rb +6 -2
- data/lib/hoardable/source_model.rb +11 -14
- data/lib/hoardable/version.rb +1 -1
- data/lib/hoardable.rb +1 -0
- data/sig/hoardable.rbs +1 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dad9a103d70ce10905528d1d7e56e67a4a8fb89e675b3097165ccede6262a015
|
4
|
+
data.tar.gz: a2dc95873a08175254cca315caa3de9b087674df1e4dadd17f42fb26a9080a9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
322
|
-
`
|
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
|
-
|
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
|
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
|
-
|
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.
|
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`.
|
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
|
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 :
|
28
|
+
private_constant :HasManyExtension
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
34
|
+
def initialize(klass)
|
35
|
+
@klass = klass
|
36
|
+
end
|
37
37
|
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
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
|
-
|
54
|
-
|
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
|
-
|
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
|
data/lib/hoardable/error.rb
CHANGED
@@ -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
|
data/lib/hoardable/hoardable.rb
CHANGED
@@ -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
|
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
|
data/lib/hoardable/scopes.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
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
|
data/lib/hoardable/version.rb
CHANGED
data/lib/hoardable.rb
CHANGED
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
|
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.
|
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-
|
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
|