1
0
Fork 0
mirror of https://github.com/paper-trail-gem/paper_trail.git synced 2022-11-09 11:33:19 -05:00

Query Versions Where Object Changed From Attributes (#1286)

* Add rails-controller-testing

In order to get all of the tests to pass, this adds in and configures
rails-controller-testing so that `assigns`is available in controller and
integration tests.

* Query Versions Where Object Changed From Attributes

This proposes an extension 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 proposed `where_object_changes_from` addition focuses only on one
side of that equation. If you want to find versions where the
attribute(s) explicitly changed *from* some known value, this will only
show those changes, as opposed to both *from* and *to*.

* Custom Serializer where_object_changes_from implementation

This mirrors the implementation of WhereObjectChanges#text in
WhereObjectChangesFrom#text in the case of a custom serializer. The
provided YAML serializer does not support this method.

* Document New API Method for Custom Adapter

This provides awareness of the new method that custom object changes
adapters may need to implement to support this change.

* remove rails-controller-testing
This commit is contained in:
Kevin Murphy 2021-02-22 18:50:57 -05:00 committed by GitHub
parent c3c58b739a
commit 0007864e2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 183 additions and 1 deletions

View file

@ -15,7 +15,8 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).
### Added
- None
- `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.
### Fixed

View file

@ -1338,6 +1338,7 @@ An adapter can implement any or all of the following methods:
1. diff: Returns the changeset in the desired format given the changeset in the original format
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).
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_from` in
# `paper_trail/version_concern.rb`.
# @api private
class WhereObjectChangesFrom
# - 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_from)
return PaperTrail.config.object_changes_adapter.where_object_changes_from(
@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_from_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

@ -41,6 +41,14 @@ module PaperTrail
discussion at https://github.com/paper-trail-gem/paper_trail/issues/803
STR
end
# Raises an exception as this operation is not allowed from text columns.
def where_object_changes_from_condition(*)
raise <<-STR.squish.freeze
where_object_changes_from does not support reading JSON from a text
column. The json and jsonb datatypes are supported.
STR
end
end
end
end

View file

@ -38,6 +38,14 @@ module PaperTrail
discussion at https://github.com/paper-trail-gem/paper_trail/pull/997
STR
end
# Raises an exception as this operation is not allowed with YAML.
def where_object_changes_from_condition(*)
raise <<-STR.squish.freeze
where_object_changes_from does not support reading YAML from a text
column. The json and jsonb datatypes are supported.
STR
end
end
end
end

View file

@ -3,6 +3,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"
module PaperTrail
# Originally, PaperTrail did not provide this module, and all of this
@ -120,6 +121,21 @@ module PaperTrail
Queries::Versions::WhereObjectChanges.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
# from the hash of attributes to other values.
#
# This is useful for finding versions where the attribute started with a
# known value and changed to something else. This is in comparison to
# `where_object_changes` which will find both the changes before and
# after.
#
# @api public
def where_object_changes_from(args = {})
raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
Queries::Versions::WhereObjectChangesFrom.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

@ -273,6 +273,85 @@ module PaperTrail
end
end
end
describe "#where_object_changes_from", versioning: true do
it "requires its argument to be a Hash" do
expect {
PaperTrail::Version.where_object_changes_from(:foo)
}.to raise_error(ArgumentError)
expect {
PaperTrail::Version.where_object_changes_from([])
}.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_from method" do
adapter = instance_spy("CustomObjectChangesAdapter")
bicycle = Bicycle.create!(name: "abc")
bicycle.update!(name: "xyz")
allow(adapter).to(
receive(:where_object_changes_from).with(Version, name: "abc")
).and_return([bicycle.versions[1]])
PaperTrail.config.object_changes_adapter = adapter
expect(
bicycle.versions.where_object_changes_from(name: "abc")
).to match_array([bicycle.versions[1]])
expect(adapter).to have_received(:where_object_changes_from)
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_from(name: "abc")
).to match_array([bicycle.versions[1]])
else
expect do
bicycle.versions.where_object_changes_from(name: "abc")
end.to raise_error(/does not support reading YAML/)
end
end
end
# Only test json and jsonb columns. where_object_changes_from 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_from(name: name)
).to eq([widget.versions[1]])
expect(
widget.versions.where_object_changes_from(an_integer: 100)
).to eq([widget.versions[2]])
expect(
widget.versions.where_object_changes_from(an_integer: int)
).to eq([])
expect(
widget.versions.where_object_changes_from(an_integer: 100, name: "foobar")
).to eq([widget.versions[2]])
end
else
it "raises error" do
expect {
widget.versions.where_object_changes_from(name: "foo").to_a
}.to(raise_error(/does not support reading YAML from a text column/))
end
end
end
end
end
end

View file

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