From 8768c2c1341cec677a4f44b4b1835c26733eb941 Mon Sep 17 00:00:00 2001 From: Andrew Buntine Date: Mon, 21 Oct 2024 14:43:47 +1100 Subject: [PATCH] feat: Support custom temporal upper bound with Hoardable.on (#48) * feat: Support custom temporal upper bound with Hoardable.on * fix: Round time in test to prevent flakiness * fix: Linting * chore: Renamed .on to .travel_to for more clarity * Update lib/hoardable/engine.rb --------- Co-authored-by: justin talbott --- README.md | 15 +++++++++++++++ lib/hoardable/database_client.rb | 9 ++++++++- lib/hoardable/engine.rb | 10 ++++++++++ lib/hoardable/error.rb | 10 ++++++++++ lib/hoardable/version.rb | 2 +- test/test_model.rb | 29 +++++++++++++++++++++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a2d744..16a8b31 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,21 @@ version.changes # => { "title"=> ["Title", "New Title"] } version.hoardable_operation # => "update" ``` +### Overriding the temporal range + +When calculating the temporal range for a given version, the default upper bound is `Time.now.utc`. + +You can, however, use the `Hoardable.travel_to` class method to specify a custom upper bound for the time range. This allows +you to specify the datetime that a particular change should be recorded at by passing a block: + +```ruby +Hoardable.travel_to(2.weeks.ago) do + post.destroy! +end +``` + +Note: If the provided datetime pre-dates the calculated lower bound then an `InvalidTemporalUpperBoundError` will be raised. + ### Model Callbacks Sometimes you might want to do something with a version after it gets inserted to the database. You diff --git a/lib/hoardable/database_client.rb b/lib/hoardable/database_client.rb index 4b8f82a..d98bc5a 100644 --- a/lib/hoardable/database_client.rb +++ b/lib/hoardable/database_client.rb @@ -93,7 +93,14 @@ def refreshable_column_names end def initialize_temporal_range - ((previous_temporal_tsrange_end || hoardable_source_epoch)..Time.now.utc) + upper_bound = Hoardable.instance_variable_get("@travel_to") || Time.now.utc + lower_bound = (previous_temporal_tsrange_end || hoardable_source_epoch) + + if upper_bound < lower_bound + raise InvalidTemporalUpperBoundError.new(upper_bound, lower_bound) + end + + (lower_bound..upper_bound) end def initialize_hoardable_data diff --git a/lib/hoardable/engine.rb b/lib/hoardable/engine.rb index dddeb41..3e0d6f6 100644 --- a/lib/hoardable/engine.rb +++ b/lib/hoardable/engine.rb @@ -81,6 +81,16 @@ def at(datetime) @at = nil end + # Allows calling code to set the upper bound for the temporal range for recorded audits. + # + # @param datetime [DateTime] the datetime to temporally record versions at + def travel_to(datetime) + @travel_to = datetime + yield + ensure + @travel_to = nil + end + # @!visibility private def logger @logger ||= ActiveSupport::TaggedLogging.new(Logger.new($stdout)) diff --git a/lib/hoardable/error.rb b/lib/hoardable/error.rb index d8e0a05..279d713 100644 --- a/lib/hoardable/error.rb +++ b/lib/hoardable/error.rb @@ -24,4 +24,14 @@ def initialize(source_table_name) LOG end end + + # An error to be raised when the provided temporal upper bound is before the calcualated lower bound. + class InvalidTemporalUpperBoundError < Error + def initialize(upper, lower) + super(<<~LOG) + 'The supplied value to `Hoardable.travel_to` (#{upper}) is before the calculated lower bound (#{lower}). + You must provide a datetime > the lower bound. + LOG + end + end end diff --git a/lib/hoardable/version.rb b/lib/hoardable/version.rb index ee5de16..3913a80 100644 --- a/lib/hoardable/version.rb +++ b/lib/hoardable/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Hoardable - VERSION = "0.16.0" + VERSION = "0.16.1" end diff --git a/test/test_model.rb b/test/test_model.rb index 2f6026b..5062cc4 100644 --- a/test/test_model.rb +++ b/test/test_model.rb @@ -464,6 +464,35 @@ def create_comments_and_destroy_post end end + test "can influence the upper bound of the temporal range with Hoardable.travel_to" do + created_at = Time.now.utc - 10 * 86_400 # 10 days ago + deleted_at = Time.now.utc - 5 * 86_400 # 5 days ago + + comment = + post.comments.create!(body: "Comment 1", created_at: created_at, updated_at: created_at) + + Hoardable.travel_to(deleted_at) { comment.destroy! } + + temporal_range = CommentVersion.where(hoardable_id: comment.id).first._during + + assert_equal Comment.all.size, 0 + assert_equal temporal_range.max.round, deleted_at.round + end + + test "will error if the upper bound of the temporal range with Hoardable.travel_to is less than the lower bound" do + created_at = Time.now.utc - 10 * 86_400 # 10 days ago + deleted_at = Time.now.utc - 12 * 86_400 # 12 days ago + + comment = + post.comments.create!(body: "Comment 1", created_at: created_at, updated_at: created_at) + + Hoardable.travel_to(deleted_at) do + assert_raises(Hoardable::InvalidTemporalUpperBoundError) { comment.destroy! } + end + + assert_equal Comment.all.size, 1 + end + test "cannot save a hoardable source record that is actually a version" do post datetime = DateTime.now