diff --git a/CHANGELOG.md b/CHANGELOG.md index 80a0e0f0..d7df4197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,11 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/). ### Added -- None +- [#1093](https://github.com/paper-trail-gem/paper_trail/pull/1093) - + `PaperTrail.config.object_changes_adapter` - Allows specifying an adapter that will + determine how the changes for each version are stored in the object_changes column. + An example of this implementation using the hashdiff gem can be found here: + [paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff) ### Fixed diff --git a/README.md b/README.md index cfdf8c8e..489e3513 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ has been destroyed. - [6. Extensibility](#6-extensibility) - [6.a. Custom Version Classes](#6a-custom-version-classes) - [6.b. Custom Serializer](#6b-custom-serializer) + - [6.c. Custom Object Changes](#6c-custom-object-changes) - [7. Testing](#7-testing) - [7.a. Minitest](#7a-minitest) - [7.b. RSpec](#7b-rspec) @@ -1378,6 +1379,23 @@ class ConvertVersionsObjectToJson < ActiveRecord::Migration end ``` +### 6.c. Custom Object Changes + +By default, PaperTrail stores object changes in a before/after array of objects +containing keys of columns that have changed in that particular version. You can +override this behaviour by using the object_changes_adapter config option: + +```ruby +PaperTrail.config.object_changes_adapter = MyObjectChangesAdapter.new +``` + +A valid adapter is a class that contains 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. + +For an example of such an implementation, see [paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff) + ## 7. Testing You may want to turn PaperTrail off to speed up your tests. See [Turning diff --git a/lib/paper_trail/config.rb b/lib/paper_trail/config.rb index 5d1e4b5b..bd928b3b 100644 --- a/lib/paper_trail/config.rb +++ b/lib/paper_trail/config.rb @@ -26,7 +26,8 @@ module PaperTrail STR include Singleton - attr_accessor :serializer, :version_limit, :association_reify_error_behaviour + attr_accessor :serializer, :version_limit, :association_reify_error_behaviour, + :object_changes_adapter def initialize # Variables which affect all threads, whose access is synchronized. diff --git a/lib/paper_trail/queries/versions/where_object_changes.rb b/lib/paper_trail/queries/versions/where_object_changes.rb index 5a419aa2..74345915 100644 --- a/lib/paper_trail/queries/versions/where_object_changes.rb +++ b/lib/paper_trail/queries/versions/where_object_changes.rb @@ -23,6 +23,11 @@ module PaperTrail # @api private def execute + if PaperTrail.config.object_changes_adapter + return PaperTrail.config.object_changes_adapter.where_object_changes( + @version_model_class, @attributes + ) + end case @version_model_class.columns_hash["object_changes"].type when :jsonb jsonb diff --git a/lib/paper_trail/record_trail.rb b/lib/paper_trail/record_trail.rb index e6816972..a466ce98 100644 --- a/lib/paper_trail/record_trail.rb +++ b/lib/paper_trail/record_trail.rb @@ -418,6 +418,10 @@ module PaperTrail # # @api private def recordable_object_changes(changes) + if PaperTrail.config.object_changes_adapter + changes = PaperTrail.config.object_changes_adapter.diff(changes) + end + if @record.class.paper_trail.version_class.object_changes_col_is_json? changes else diff --git a/lib/paper_trail/version_concern.rb b/lib/paper_trail/version_concern.rb index 94220fe0..79361aba 100644 --- a/lib/paper_trail/version_concern.rb +++ b/lib/paper_trail/version_concern.rb @@ -265,6 +265,10 @@ module PaperTrail # @api private def load_changeset + if PaperTrail.config.object_changes_adapter + return PaperTrail.config.object_changes_adapter.load_changeset(self) + end + # First, deserialize the `object_changes` column. changes = HashWithIndifferentAccess.new(object_changes_deserialized) diff --git a/spec/models/version_spec.rb b/spec/models/version_spec.rb index e6ab005d..243f7eee 100644 --- a/spec/models/version_spec.rb +++ b/spec/models/version_spec.rb @@ -16,6 +16,28 @@ module PaperTrail end end + context "with object_changes_adapter" do + let(:adapter) { instance_spy("CustomObjectChangesAdapter") } + + before do + PaperTrail.config.object_changes_adapter = adapter + allow(adapter).to( + receive(:diff).with( + hash_including("name" => [nil, "Dashboard"]) + ).and_return([["name", nil, "Dashboard"]]) + ) + end + + after do + PaperTrail.config.object_changes_adapter = nil + end + + it "creates a version with custom changes" do + expect(widget.versions.last.object_changes).to eq("---\n- - name\n - \n - Dashboard\n") + expect(adapter).to have_received(:diff) + end + end + context "serializer is JSON" do before do PaperTrail.serializer = PaperTrail::Serializers::JSON @@ -202,6 +224,25 @@ module PaperTrail }.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 method" do + adapter = instance_spy("CustomObjectChangesAdapter") + bicycle = Bicycle.create!(name: "abc") + allow(adapter).to( + receive(:where_object_changes).with(Version, name: "abc") + ).and_return(bicycle.versions[0..1]) + PaperTrail.config.object_changes_adapter = adapter + expect( + bicycle.versions.where_object_changes(name: "abc") + ).to match_array(bicycle.versions[0..1]) + expect(adapter).to have_received(:where_object_changes) + end + end + # Only test json and jsonb columns. where_object_changes no longer # supports text columns. if column_datatype_override diff --git a/spec/paper_trail/model_spec.rb b/spec/paper_trail/model_spec.rb index b3037a8d..13f40d43 100644 --- a/spec/paper_trail/model_spec.rb +++ b/spec/paper_trail/model_spec.rb @@ -51,6 +51,28 @@ RSpec.describe(::PaperTrail, versioning: true) do expect(changeset["updated_at"][0]).to be_nil expect(changeset["updated_at"][1].to_i).to eq(@widget.updated_at.to_i) end + + context "custom object_changes_adapter" do + let(:adapter) { instance_spy("CustomObjectChangesAdapter") } + + before do + PaperTrail.config.object_changes_adapter = adapter + allow(adapter).to( + receive(:load_changeset).with(@widget.versions.last).and_return(a: "b", c: "d") + ) + end + + after do + PaperTrail.config.object_changes_adapter = nil + end + + it "calls the adapter's load_changeset method" do + changeset = @widget.versions.last.changeset + expect(changeset[:a]).to eq("b") + expect(changeset[:c]).to eq("d") + expect(adapter).to have_received(:load_changeset) + end + end end context "and then updated without any changes" do diff --git a/spec/support/custom_object_changes_adapter.rb b/spec/support/custom_object_changes_adapter.rb new file mode 100644 index 00000000..39f4aaf6 --- /dev/null +++ b/spec/support/custom_object_changes_adapter.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# This custom serializer excludes nil values +class CustomObjectChangesAdapter + def diff(changes) + changes + end + + def load_changeset(version) + version.changeset + end + + def where_object_changes(klass, attributes) + klass.where(attributes) + end +end