hoardable 0.16.0 → 0.18.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: 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;$$;