Skip to content

Commit

Permalink
Query Versions Where Object Changed To Attributes
Browse files Browse the repository at this point in the history
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*.
  • Loading branch information
kevin-j-m committed Feb 23, 2021
1 parent 0007864 commit 211075a
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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.

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,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.

Expand Down
65 changes: 65 additions & 0 deletions lib/paper_trail/queries/versions/where_object_changes_to.rb
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions lib/paper_trail/serializers/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ def where_object_changes_from_condition(*)
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
8 changes: 8 additions & 0 deletions lib/paper_trail/serializers/yaml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ def where_object_changes_from_condition(*)
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
16 changes: 16 additions & 0 deletions lib/paper_trail/version_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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
Expand Down Expand Up @@ -136,6 +137,21 @@ def where_object_changes_from(args = {})
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
Expand Down
82 changes: 82 additions & 0 deletions spec/models/version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,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
Expand Down
4 changes: 4 additions & 0 deletions spec/support/custom_object_changes_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ def where_object_changes(klass, attributes)
def where_object_changes_from(klass, attributes)
klass.where(attributes)
end

def where_object_changes_to(klass, attributes)
klass.where(attributes)
end
end

0 comments on commit 211075a

Please sign in to comment.