Query Versions Where Object Changed To Attributes

This extends to the public API to provide more targeted
querying of object changes. `where_object_changes` will look for either
side of the change of the attributes provided - either versions where
the attribute changed __from__ the provided value, or changed __to__ the
provided value.

The `where_object_changes_to` addition focuses only on one
side of that equation. If you want to find versions where the
attribute(s) explicitly changed *to* some known value, this will only
show those changes, as opposed to both *from* and *to*.
This commit is contained in:
Kevin Murphy 2021-02-22 19:44:41 -05:00 committed by Jared Beck
parent 584e2f732e
commit 06a7c12534
8 changed files with 186 additions and 0 deletions

View File

@ -30,6 +30,8 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).
### Added
- `where_object_changes_to` queries for versions where the object's attributes
changed to one set of known values from any other set of values.
- `where_object_changes_from` queries for versions where the object's attributes
changed from one set of known values to any other set of values.

View File

@ -1367,6 +1367,7 @@ An adapter can implement any or all of the following methods:
2. load_changeset: Returns the changeset for a given version object
3. where_object_changes: Returns the records resulting from the given hash of attributes.
4. where_object_changes_from: Returns the records resulting from the given hash of attributes where the attributes changed *from* the provided value(s).
5. where_object_changes_to: Returns the records resulting from the given hash of attributes where the attributes changed *to* the provided value(s).
Depending on what your adapter does, you may have to implement all three.

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
module PaperTrail
module Queries
module Versions
# For public API documentation, see `where_object_changes_to` in
# `paper_trail/version_concern.rb`.
# @api private
class WhereObjectChangesTo
# - version_model_class - The class that VersionConcern was mixed into.
# - attributes - A `Hash` of attributes and values. See the public API
# documentation for details.
# @api private
def initialize(version_model_class, attributes)
@version_model_class = version_model_class
@attributes = attributes
end
# @api private
def execute
if PaperTrail.config.object_changes_adapter&.respond_to?(:where_object_changes_to)
return PaperTrail.config.object_changes_adapter.where_object_changes_to(
@version_model_class, @attributes
)
end
case @version_model_class.columns_hash["object_changes"].type
when :jsonb, :json
json
else
text
end
end
private
# @api private
def json
predicates = []
values = []
@attributes.each do |field, value|
predicates.push(
"(object_changes->>? ILIKE ?)"
)
values.concat([field, "[%#{value.to_json}]"])
end
sql = predicates.join(" and ")
@version_model_class.where(sql, *values)
end
# @api private
def text
arel_field = @version_model_class.arel_table[:object_changes]
where_conditions = @attributes.map do |field, value|
::PaperTrail.serializer.where_object_changes_to_condition(arel_field, field, value)
end
where_conditions = where_conditions.reduce { |a, e| a.and(e) }
@version_model_class.where(where_conditions)
end
end
end
end
end

View File

@ -49,6 +49,14 @@ module PaperTrail
column. The json and jsonb datatypes are supported.
STR
end
# Raises an exception as this operation is not allowed from text columns.
def where_object_changes_to_condition(*)
raise <<-STR.squish.freeze
where_object_changes_to does not support reading JSON from a text
column. The json and jsonb datatypes are supported.
STR
end
end
end
end

View File

@ -46,6 +46,14 @@ module PaperTrail
column. The json and jsonb datatypes are supported.
STR
end
# Raises an exception as this operation is not allowed with YAML.
def where_object_changes_to_condition(*)
raise <<-STR.squish.freeze
where_object_changes_to does not support reading YAML from a text
column. The json and jsonb datatypes are supported.
STR
end
end
end
end

View File

@ -4,6 +4,7 @@ require "paper_trail/attribute_serializers/object_changes_attribute"
require "paper_trail/queries/versions/where_object"
require "paper_trail/queries/versions/where_object_changes"
require "paper_trail/queries/versions/where_object_changes_from"
require "paper_trail/queries/versions/where_object_changes_to"
module PaperTrail
# Originally, PaperTrail did not provide this module, and all of this
@ -131,6 +132,21 @@ module PaperTrail
Queries::Versions::WhereObjectChangesFrom.new(self, args).execute
end
# Given a hash of attributes like `name: 'Joan'`, query the
# `versions.objects_changes` column for changes where the version changed
# to the hash of attributes from other values.
#
# This is useful for finding versions where the attribute started with an
# unknown value and changed to a known value. This is in comparison to
# `where_object_changes` which will find both the changes before and
# after.
#
# @api public
def where_object_changes_to(args = {})
raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
Queries::Versions::WhereObjectChangesTo.new(self, args).execute
end
def primary_key_is_int?
@primary_key_is_int ||= columns_hash[primary_key].type == :integer
rescue StandardError # TODO: Rescue something more specific

View File

@ -350,6 +350,88 @@ module PaperTrail
end
end
end
describe "#where_object_changes_to", versioning: true do
it "requires its argument to be a Hash" do
expect {
PaperTrail::Version.where_object_changes_to(:foo)
}.to raise_error(ArgumentError)
expect {
PaperTrail::Version.where_object_changes_to([])
}.to raise_error(ArgumentError)
end
context "with object_changes_adapter configured" do
after do
PaperTrail.config.object_changes_adapter = nil
end
it "calls the adapter's where_object_changes_to method" do
adapter = instance_spy("CustomObjectChangesAdapter")
bicycle = Bicycle.create!(name: "abc")
bicycle.update!(name: "xyz")
allow(adapter).to(
receive(:where_object_changes_to).with(Version, name: "xyz")
).and_return([bicycle.versions[1]])
PaperTrail.config.object_changes_adapter = adapter
expect(
bicycle.versions.where_object_changes_to(name: "xyz")
).to match_array([bicycle.versions[1]])
expect(adapter).to have_received(:where_object_changes_to)
end
it "defaults to the original behavior" do
adapter = Class.new.new
PaperTrail.config.object_changes_adapter = adapter
bicycle = Bicycle.create!(name: "abc")
bicycle.update!(name: "xyz")
if column_datatype_override
expect(
bicycle.versions.where_object_changes_to(name: "xyz")
).to match_array([bicycle.versions[1]])
else
expect do
bicycle.versions.where_object_changes_to(name: "xyz")
end.to raise_error(/does not support reading YAML/)
end
end
end
# Only test json and jsonb columns. where_object_changes_to does
# not support text columns.
if column_datatype_override
it "locates versions according to their object_changes contents" do
widget.update!(name: name, an_integer: 0)
widget.update!(name: "foobar", an_integer: 100)
widget.update!(name: FFaker::Name.last_name, an_integer: int)
expect(
widget.versions.where_object_changes_to(name: name)
).to eq([widget.versions[0]])
expect(
widget.versions.where_object_changes_to(an_integer: 100)
).to eq([widget.versions[1]])
expect(
widget.versions.where_object_changes_to(an_integer: int)
).to eq([widget.versions[2]])
expect(
widget.versions.where_object_changes_to(an_integer: 100, name: "foobar")
).to eq([widget.versions[1]])
expect(
widget.versions.where_object_changes_to(an_integer: -1)
).to eq([])
end
else
it "raises error" do
expect {
widget.versions.where_object_changes_to(name: "foo").to_a
}.to(raise_error(/does not support reading YAML from a text column/))
end
end
end
end
end
end

View File

@ -17,4 +17,8 @@ class CustomObjectChangesAdapter
def where_object_changes_from(klass, attributes)
klass.where(attributes)
end
def where_object_changes_to(klass, attributes)
klass.where(attributes)
end
end