hoardable 0.16.0 → 0.18.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: 8c41a6c69f62f7e2a99fefe00229a7244f5cd0dcb9ed3b0d243db9adf9cabd98
4
- data.tar.gz: 30dc9d7a551c29331a7736f7b825c0b5f9db83bf5c5cd537d5d2910908f683ff
3
+ metadata.gz: 36d1113a71b72492244fadc8172ef4a08f28bdff546b7344bb385ce38e82ecbd
4
+ data.tar.gz: a4cee0c629c4c1d59f6c0b632c0499fc5e094e099c7e0dc4bf3e0d79271f6eb8
5
5
  SHA512:
6
- metadata.gz: ad46ae05e3241052089aaf432016b906f648051fae6405e2ce4a90c7f247942a09a011d2fc35e6558695814949ef37a0398d36baaa71500f2c31d33436a7eac7
7
- data.tar.gz: b3307392cdff1b14a37192cd1a004099c5d99d7dd0ce3d0f8fe230c1dac4b59697c4708f5859cb7b2b831f45f1a3a6489cdce2eaf464fef2d9e135bd902e4f26
6
+ metadata.gz: 3cb288b7dbd5938056d3fd5fae396ad6d7caca20a98902ebb228a75d4c05d5e689807ea90f4e6f96dc5e1ac20bbb028fa12717676da3f60163ca4a362bf33db1
7
+ data.tar.gz: 44bba102025a18563d29d76430d27f06299316d69b37e08fa3b216453dfe6c4890f82e6f065843c127cf26fb0da39f7ce76194d2e31dc2bd0021d9973e6c9748
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.17.0
2
+
3
+ - Much improved performance of setting `hoardable_id` for versions.
4
+
1
5
  ## 0.16.0
2
6
 
3
7
  - Rails 8 support introduced
data/README.md CHANGED
@@ -41,7 +41,6 @@ Include `Hoardable::Model` into an ActiveRecord model you would like to hoard ve
41
41
  ```ruby
42
42
  class Post < ActiveRecord::Base
43
43
  include Hoardable::Model
44
- ...
45
44
  end
46
45
  ```
47
46
 
@@ -235,6 +234,21 @@ version.changes # => { "title"=> ["Title", "New Title"] }
235
234
  version.hoardable_operation # => "update"
236
235
  ```
237
236
 
237
+ ### Overriding the temporal range
238
+
239
+ When calculating the temporal range for a given version, the default upper bound is `Time.now.utc`.
240
+
241
+ You can, however, use the `Hoardable.travel_to` class method to specify a custom upper bound for the time range. This allows
242
+ you to specify the datetime that a particular change should be recorded at by passing a block:
243
+
244
+ ```ruby
245
+ Hoardable.travel_to(2.weeks.ago) do
246
+ post.destroy!
247
+ end
248
+ ```
249
+
250
+ Note: If the provided datetime pre-dates the calculated lower bound then an `InvalidTemporalUpperBoundError` will be raised.
251
+
238
252
  ### Model Callbacks
239
253
 
240
254
  Sometimes you might want to do something with a version after it gets inserted to the database. You
@@ -313,6 +327,14 @@ end
313
327
 
314
328
  Model-level configuration overrides global configuration.
315
329
 
330
+ ### Single Table Inheritance
331
+
332
+ Hoardable works for [Single Table
333
+ Inheritance](https://guides.rubyonrails.org/association_basics.html#single-table-inheritance-sti). You
334
+ will need to include `Hoardable::Model` in each child model you'd like to version, as that is what
335
+ generates the model's version class. The migration generator only needs to be run for the parent
336
+ model, as the versions will similarly be stored in a single table.
337
+
316
338
  ## Relationships
317
339
 
318
340
  ### `belongs_to`
@@ -0,0 +1,7 @@
1
+ CREATE OR REPLACE FUNCTION <%= function_name %>() RETURNS trigger
2
+ LANGUAGE plpgsql AS
3
+ $$
4
+ BEGIN
5
+ NEW.hoardable_id = NEW.<%= primary_key %>;
6
+ RETURN NEW;
7
+ END;$$;
@@ -25,7 +25,7 @@ module Hoardable
25
25
 
26
26
  def create_functions
27
27
  Dir
28
- .glob(File.join(__dir__, "functions", "*.sql"))
28
+ .glob(File.join(__dir__, "install_functions", "*.sql"))
29
29
  .each do |file_path|
30
30
  file_name = file_path.match(%r{([^/]+)\.sql})[1]
31
31
  template file_path, "db/functions/#{file_name}_v01.sql"
@@ -36,22 +36,33 @@ module Hoardable
36
36
  end
37
37
  end
38
38
 
39
+ def create_function
40
+ template("../functions/set_hoardable_id.sql", "db/functions/#{function_name}_v01.sql")
41
+ end
42
+
39
43
  no_tasks do
44
+ def function_name
45
+ "hoardable_set_hoardable_id_from_#{primary_key}"
46
+ end
47
+
48
+ def klass
49
+ @klass ||= class_name.singularize.constantize
50
+ end
51
+
40
52
  def table_name
41
- class_name.singularize.constantize.table_name
53
+ klass.table_name
42
54
  rescue StandardError
43
55
  super
44
56
  end
45
57
 
46
58
  def foreign_key_type
47
- options[:foreign_key_type] ||
48
- class_name.singularize.constantize.columns.find { |col| col.name == primary_key }.sql_type
59
+ options[:foreign_key_type] || klass.columns.find { |col| col.name == primary_key }.sql_type
49
60
  rescue StandardError
50
61
  "bigint"
51
62
  end
52
63
 
53
64
  def primary_key
54
- options[:primary_key] || class_name.singularize.constantize.primary_key
65
+ options[:primary_key] || klass.primary_key
55
66
  rescue StandardError
56
67
  "id"
57
68
  end
@@ -4,7 +4,6 @@ class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.cur
4
4
  def change
5
5
  <% if postgres_version < 13 %>enable_extension :pgcrypto
6
6
  <% end %>create_function :hoardable_prevent_update_id
7
- create_function :hoardable_source_set_id
8
7
  create_function :hoardable_version_prevent_update
9
8
  create_enum :hoardable_operation, %w[update delete insert]
10
9
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
3
+ class Create<%= singularized_table_name.classify %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
4
  def change
5
5
  add_column :<%= table_name %>, :hoardable_id, :<%= foreign_key_type %>
6
6
  add_index :<%= table_name %>, :hoardable_id
7
7
  create_table(
8
8
  :<%= singularized_table_name %>_versions,
9
- id: false,
9
+ id: false,
10
10
  options: 'INHERITS (<%= table_name %>)',
11
11
  ) do |t|
12
12
  t.jsonb :_data
@@ -25,6 +25,7 @@ class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Mi
25
25
  :<%= singularized_table_name %>_versions_prevent_update,
26
26
  on: :<%= singularized_table_name %>_versions
27
27
  )
28
+ create_function :<%= function_name %>
28
29
  create_trigger :<%= table_name %>_set_hoardable_id, on: :<%= table_name %>
29
30
  create_trigger :<%= table_name %>_prevent_update_hoardable_id, on: :<%= table_name %>
30
31
  change_column_null :<%= table_name %>, :hoardable_id, false
@@ -1,3 +1,3 @@
1
1
  CREATE TRIGGER <%= table_name %>_set_hoardable_id
2
2
  BEFORE INSERT ON <%= table_name %> FOR EACH ROW
3
- EXECUTE PROCEDURE hoardable_source_set_id();
3
+ EXECUTE PROCEDURE <%= function_name %>();
@@ -93,7 +93,14 @@ module Hoardable
93
93
  end
94
94
 
95
95
  def initialize_temporal_range
96
- ((previous_temporal_tsrange_end || hoardable_source_epoch)..Time.now.utc)
96
+ upper_bound = Hoardable.instance_variable_get("@travel_to") || Time.now.utc
97
+ lower_bound = (previous_temporal_tsrange_end || hoardable_source_epoch)
98
+
99
+ if upper_bound < lower_bound
100
+ raise InvalidTemporalUpperBoundError.new(upper_bound, lower_bound)
101
+ end
102
+
103
+ (lower_bound..upper_bound)
97
104
  end
98
105
 
99
106
  def initialize_hoardable_data
@@ -81,6 +81,16 @@ module Hoardable
81
81
  @at = nil
82
82
  end
83
83
 
84
+ # Allows calling code to set the upper bound for the temporal range for recorded audits.
85
+ #
86
+ # @param datetime [DateTime] the datetime to temporally record versions at
87
+ def travel_to(datetime)
88
+ @travel_to = datetime
89
+ yield
90
+ ensure
91
+ @travel_to = nil
92
+ end
93
+
84
94
  # @!visibility private
85
95
  def logger
86
96
  @logger ||= ActiveSupport::TaggedLogging.new(Logger.new($stdout))
@@ -101,7 +111,7 @@ module Hoardable
101
111
  initializer "hoardable.schema_statements" do
102
112
  ActiveSupport.on_load(:active_record_postgresqladapter) do
103
113
  # We need to control the table dumping order of tables, so revert these to just +super+
104
- Fx::SchemaDumper::Trigger.module_eval("def tables(streams); super; end")
114
+ Fx::SchemaDumper.module_eval("def tables(streams); super; end")
105
115
 
106
116
  ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(SchemaDumper)
107
117
  ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements.prepend(SchemaStatements)
@@ -24,4 +24,14 @@ module Hoardable
24
24
  LOG
25
25
  end
26
26
  end
27
+
28
+ # An error to be raised when the provided temporal upper bound is before the calcualated lower bound.
29
+ class InvalidTemporalUpperBoundError < Error
30
+ def initialize(upper, lower)
31
+ super(<<~LOG)
32
+ 'The supplied value to `Hoardable.travel_to` (#{upper}) is before the calculated lower bound (#{lower}).
33
+ You must provide a datetime > the lower bound.
34
+ LOG
35
+ end
36
+ end
27
37
  end
@@ -26,16 +26,21 @@ module Hoardable
26
26
  class_methods do
27
27
  def has_many(*args, &block)
28
28
  options = args.extract_options!
29
- options[:extend] = Array(options[:extend]).push(HasManyExtension) if options.delete(
30
- :hoardable
31
- )
29
+ hoardable_option = options.delete(:hoardable)
30
+ options[:extend] = Array(options[:extend]).push(HasManyExtension) if hoardable_option
31
+
32
32
  super(*args, **options, &block)
33
+ return unless hoardable_option
33
34
 
34
35
  # This hack is needed to force Rails to not use any existing method cache so that the
35
- # {HasManyExtension} scope is always used.
36
+ # {HasManyExtension} scope is always used when using {Hoardable.at}.
36
37
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
37
38
  def #{args.first}
38
- super.extending
39
+ if Hoardable.instance_variable_get("@at")
40
+ super.extending
41
+ else
42
+ super
43
+ end
39
44
  end
40
45
  RUBY
41
46
  end
@@ -50,12 +50,14 @@ module Hoardable
50
50
  define_model_callbacks :versioned, only: :after
51
51
  define_model_callbacks :reverted, only: :after
52
52
  define_model_callbacks :untrashed, only: :after
53
+ end
53
54
 
55
+ def self.included(base)
54
56
  TracePoint
55
57
  .new(:end) do |trace|
56
- next unless self == trace.self
58
+ next unless base == trace.self
57
59
 
58
- full_version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
60
+ full_version_class_name = "#{base.name}#{VERSION_CLASS_SUFFIX}"
59
61
  if (namespace_match = full_version_class_name.match(/(.*)::(.*)/))
60
62
  object_namespace = namespace_match[1].constantize
61
63
  version_class_name = namespace_match[2]
@@ -64,10 +66,10 @@ module Hoardable
64
66
  version_class_name = full_version_class_name
65
67
  end
66
68
  unless Object.const_defined?(full_version_class_name)
67
- object_namespace.const_set(version_class_name, Class.new(self) { include VersionModel })
69
+ object_namespace.const_set(version_class_name, Class.new(base) { include VersionModel })
68
70
  end
69
- include SourceModel
70
- REGISTRY.add(self)
71
+ base.class_eval { include SourceModel }
72
+ REGISTRY.add(base)
71
73
 
72
74
  trace.disable
73
75
  end
@@ -9,13 +9,20 @@ module Hoardable
9
9
  included do
10
10
  # By default {Hoardable} only returns instances of the parent table, and not the +versions+ in
11
11
  # the inherited table. This can be bypassed by using the {.include_versions} scope or wrapping
12
- # the code in a `Hoardable.at(datetime)` block.
12
+ # the code in a `Hoardable.at(datetime)` block. When this is a version class that is an STI
13
+ # model, also scope to them.
13
14
  default_scope do
14
- if (hoardable_at = Hoardable.instance_variable_get("@at"))
15
- at(hoardable_at)
16
- else
17
- exclude_versions
18
- end
15
+ scope =
16
+ (
17
+ if (hoardable_at = Hoardable.instance_variable_get("@at"))
18
+ at(hoardable_at)
19
+ else
20
+ exclude_versions
21
+ end
22
+ )
23
+ next scope unless klass == version_class && "type".in?(column_names)
24
+
25
+ scope.where(type: superclass.sti_name)
19
26
  end
20
27
 
21
28
  # @!scope class
@@ -40,20 +40,24 @@ module Hoardable
40
40
  end
41
41
 
42
42
  before_destroy(if: HOARDABLE_CALLBACKS_ENABLED, unless: HOARDABLE_SAVE_TRASH) do
43
- versions.delete_all
43
+ versions.extending.delete_all
44
44
  end
45
45
 
46
46
  after_commit { hoardable_client.unset_hoardable_version_and_event_uuid }
47
+ end
47
48
 
48
- # Returns all +versions+ in ascending order of their temporal timeframes.
49
- has_many(
50
- :versions,
51
- -> { order("UPPER(_during) ASC") },
52
- dependent: nil,
53
- class_name: version_class.to_s,
54
- inverse_of: :hoardable_source,
55
- foreign_key: :hoardable_id
56
- )
49
+ def self.included(base)
50
+ base.class_eval do
51
+ # Returns all +versions+ in ascending order of their temporal timeframes.
52
+ has_many(
53
+ :versions,
54
+ -> { order("UPPER(_during) ASC") },
55
+ dependent: nil,
56
+ class_name: version_class.to_s,
57
+ inverse_of: :hoardable_source,
58
+ foreign_key: :hoardable_id
59
+ )
60
+ end
57
61
  end
58
62
 
59
63
  # Returns a boolean of whether the record is actually a trashed +version+ cast as an instance of the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = "0.16.0"
4
+ VERSION = "0.18.1"
5
5
  end
@@ -24,6 +24,9 @@ module Hoardable
24
24
  class_name: superclass.model_name
25
25
  )
26
26
 
27
+ # disable STI on versions tables
28
+ self.inheritance_column = :_
29
+
27
30
  self.table_name = "#{table_name.singularize}#{VERSION_TABLE_SUFFIX}"
28
31
 
29
32
  alias_method :readonly?, :persisted?
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.16.0
4
+ version: 0.18.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: 2024-10-11 00:00:00.000000000 Z
11
+ date: 2025-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -58,7 +58,7 @@ dependencies:
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0.8'
61
+ version: '0.9'
62
62
  - - "<"
63
63
  - !ruby/object:Gem::Version
64
64
  version: '1'
@@ -68,7 +68,7 @@ dependencies:
68
68
  requirements:
69
69
  - - ">="
70
70
  - !ruby/object:Gem::Version
71
- version: '0.8'
71
+ version: '0.9'
72
72
  - - "<"
73
73
  - !ruby/object:Gem::Version
74
74
  version: '1'
@@ -100,15 +100,14 @@ extensions: []
100
100
  extra_rdoc_files: []
101
101
  files:
102
102
  - ".streerc"
103
- - ".tool-versions"
104
103
  - CHANGELOG.md
105
104
  - Gemfile
106
105
  - LICENSE.txt
107
106
  - README.md
108
107
  - Rakefile
109
- - lib/generators/hoardable/functions/hoardable_prevent_update_id.sql
110
- - lib/generators/hoardable/functions/hoardable_source_set_id.sql
111
- - lib/generators/hoardable/functions/hoardable_version_prevent_update.sql
108
+ - lib/generators/hoardable/functions/set_hoardable_id.sql
109
+ - lib/generators/hoardable/install_functions/hoardable_prevent_update_id.sql
110
+ - lib/generators/hoardable/install_functions/hoardable_version_prevent_update.sql
112
111
  - lib/generators/hoardable/install_generator.rb
113
112
  - lib/generators/hoardable/migration_generator.rb
114
113
  - lib/generators/hoardable/templates/install.rb.erb
@@ -159,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
159
158
  - !ruby/object:Gem::Version
160
159
  version: '0'
161
160
  requirements: []
162
- rubygems_version: 3.5.6
161
+ rubygems_version: 3.5.16
163
162
  signing_key:
164
163
  specification_version: 4
165
164
  summary: An ActiveRecord extension for versioning and soft-deletion of records in
data/.tool-versions DELETED
@@ -1,2 +0,0 @@
1
- ruby 3.3.0
2
- postgres 16.1
@@ -1,18 +0,0 @@
1
- CREATE OR REPLACE FUNCTION hoardable_source_set_id() RETURNS trigger
2
- LANGUAGE plpgsql AS
3
- $$
4
- DECLARE
5
- _pk information_schema.constraint_column_usage.column_name%TYPE;
6
- _id _pk%TYPE;
7
- BEGIN
8
- SELECT c.column_name
9
- FROM information_schema.table_constraints t
10
- JOIN information_schema.constraint_column_usage c
11
- ON c.constraint_name = t.constraint_name
12
- WHERE c.table_name = TG_TABLE_NAME AND t.constraint_type = 'PRIMARY KEY'
13
- LIMIT 1
14
- INTO _pk;
15
- EXECUTE format('SELECT $1.%I', _pk) INTO _id USING NEW;
16
- NEW.hoardable_id = _id;
17
- RETURN NEW;
18
- END;$$;