From 38111619a30f508b50fe39a473e769eafc6aaefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Mon, 30 Nov 2020 18:38:35 +0100 Subject: [PATCH] Support Ecto Embed --- lib/paper_trail/serializer.ex | 49 ++++++- .../20160619190938_add_simple_people.exs | 2 + .../bang_functions_simple_mode_test.exs | 129 +++++++++++++++++- test/paper_trail/base_test.exs | 12 +- test/support/simple_models.exs | 20 +++ 5 files changed, 200 insertions(+), 12 deletions(-) diff --git a/lib/paper_trail/serializer.ex b/lib/paper_trail/serializer.ex index 5af4fde7..dc65daaa 100644 --- a/lib/paper_trail/serializer.ex +++ b/lib/paper_trail/serializer.ex @@ -109,9 +109,9 @@ defmodule PaperTrail.Serializer do Dumps changes using Ecto fields """ @spec serialize_changes(Ecto.Changeset.t()) :: map() - def serialize_changes(%Ecto.Changeset{data: %schema{}, changes: changes}) do - changes - |> schema.__struct__() + def serialize_changes(%Ecto.Changeset{changes: changes} = changeset) do + changeset + |> serialize_model_changes() |> serialize() |> Map.take(Map.keys(changes)) end @@ -147,4 +147,47 @@ defmodule PaperTrail.Serializer do "#{model_id}" end end + + @spec serialize_model_changes(Ecto.Changeset.t()) :: map() + defp serialize_model_changes(%Ecto.Changeset{data: %schema{}} = changeset) do + field_values = serialize_model_field_changes(changeset) + embed_values = serialize_model_embed_changes(changeset) + + field_values + |> Map.merge(embed_values) + |> schema.__struct__() + end + + defp serialize_model_field_changes(%Ecto.Changeset{data: %schema{}, changes: changes}) do + change_keys = changes |> Map.keys() |> MapSet.new() + + field_keys = + :fields + |> schema.__schema__() + |> MapSet.new() + |> MapSet.intersection(change_keys) + |> MapSet.to_list() + + Map.take(changes, field_keys) + end + + defp serialize_model_embed_changes(%Ecto.Changeset{data: %schema{}, changes: changes}) do + change_keys = changes |> Map.keys() |> MapSet.new() + + embed_keys = + :embeds + |> schema.__schema__() + |> MapSet.new() + |> MapSet.intersection(change_keys) + |> MapSet.to_list() + + changes + |> Map.take(embed_keys) + |> Map.new(fn {key, value} -> + case schema.__schema__(:embed, key) do + %Ecto.Embedded{cardinality: :one} -> {key, serialize_model_changes(value)} + %Ecto.Embedded{cardinality: :many} -> {key, Enum.map(value, &serialize_model_changes/1)} + end + end) + end end diff --git a/priv/repo/migrations/20160619190938_add_simple_people.exs b/priv/repo/migrations/20160619190938_add_simple_people.exs index e8e30058..0e69e5f8 100644 --- a/priv/repo/migrations/20160619190938_add_simple_people.exs +++ b/priv/repo/migrations/20160619190938_add_simple_people.exs @@ -8,6 +8,8 @@ defmodule Repo.Migrations.CreateSimplePeople do add :visit_count, :integer add :gender, :boolean add :birthdate, :date + add :singular, :map + add :plural, {:array, :map} add :company_id, references(:simple_companies), null: false diff --git a/test/paper_trail/bang_functions_simple_mode_test.exs b/test/paper_trail/bang_functions_simple_mode_test.exs index ba6cef55..4e026fc5 100644 --- a/test/paper_trail/bang_functions_simple_mode_test.exs +++ b/test/paper_trail/bang_functions_simple_mode_test.exs @@ -326,7 +326,9 @@ defmodule PaperTrailTest.SimpleModeBangFunctions do gender: true, visit_count: nil, birthdate: nil, - company_id: second_company.id + company_id: second_company.id, + plural: [], + singular: nil } assert Map.drop(version, [:id, :inserted_at]) == %{ @@ -342,6 +344,39 @@ defmodule PaperTrailTest.SimpleModeBangFunctions do assert person == first(Person, :id) |> repo().one |> serialize end + test "creating a person with embeds creates a person version with correct attributes" do + company = create_company_with_version() + + %Person{ + id: person_id, + plural: [%{id: _, name: "Plural"}], + singular: %{id: _, name: "Singular"} + } = + person = + %Person{} + |> Person.changeset(%{ + first_name: "Izel", + last_name: "Nakri", + gender: true, + company_id: company.id, + plural: [%{name: "Plural"}], + singular: %{name: "Singular"} + }) + |> PaperTrail.insert!() + + version = PaperTrail.get_version(person) + person_change = person |> serialize() |> convert_to_string_map + + assert %{ + event: "insert", + item_type: "SimplePerson", + item_id: ^person_id, + item_changes: ^person_change + } = version + + assert person == first(Person, :id) |> repo().one + end + test "updating a person creates a person version with correct attributes" do inserted_initial_company = create_company_with_version(%{ @@ -391,7 +426,9 @@ defmodule PaperTrailTest.SimpleModeBangFunctions do visit_count: 10, birthdate: ~D[1992-04-01], last_name: "Nakri", - gender: true + gender: true, + plural: [], + singular: nil } assert Map.drop(version, [:id, :inserted_at]) == %{ @@ -413,6 +450,44 @@ defmodule PaperTrailTest.SimpleModeBangFunctions do assert person == first(Person, :id) |> repo().one |> serialize end + test "updating a person with embeds creates a person version with correct attributes" do + company = create_company_with_version() + + %Person{ + id: person_id, + plural: [%{id: _, name: "Plural"}], + singular: %{id: _, name: "Singular"} + } = + person = + %Person{} + |> Person.changeset(%{ + first_name: "Izel", + last_name: "Nakri", + gender: true, + company_id: company.id + }) + |> PaperTrail.insert!() + |> Person.changeset(%{ + plural: [%{name: "Plural"}], + singular: %{name: "Singular"} + }) + |> PaperTrail.update!() + + version = PaperTrail.get_version(person) + + assert %{ + event: "update", + item_type: "SimplePerson", + item_id: ^person_id, + item_changes: %{ + "plural" => [%{"id" => _, "name" => "Plural"}], + "singular" => %{"id" => _, "name" => "Singular"} + } + } = version + + assert person == first(Person, :id) |> repo().one + end + test "deleting a person creates a person version with correct attributes" do create_company_with_version(%{name: "Acme LLC", website: "http://www.acme.com"}) @@ -475,7 +550,9 @@ defmodule PaperTrailTest.SimpleModeBangFunctions do gender: true, visit_count: 10, birthdate: ~D[1992-04-01], - company_id: inserted_target_company.id + company_id: inserted_target_company.id, + plural: [], + singular: nil }), originator_id: nil, origin: "admin", @@ -485,6 +562,40 @@ defmodule PaperTrailTest.SimpleModeBangFunctions do assert old_person == person_before_deletion end + test "deleting a person with embeds creates a person version with correct attributes" do + company = create_company_with_version() + + %Person{ + id: person_id, + plural: [%{id: _, name: "Plural"}], + singular: %{id: _, name: "Singular"} + } = + person = + %Person{} + |> Person.changeset(%{ + first_name: "Izel", + last_name: "Nakri", + gender: true, + company_id: company.id, + plural: [%{name: "Plural"}], + singular: %{name: "Singular"} + }) + |> PaperTrail.insert!() + |> PaperTrail.delete!() + + version = PaperTrail.get_version(person) + person_change = person |> serialize() |> convert_to_string_map + + assert %{ + event: "delete", + item_type: "SimplePerson", + item_id: ^person_id, + item_changes: ^person_change + } = version + + assert is_nil(first(Person, :id) |> repo().one) + end + # Multi tenant tests test "[multi tenant] creating a company creates a company version with correct attributes" do tenant = MultiTenant.tenant() @@ -791,7 +902,9 @@ defmodule PaperTrailTest.SimpleModeBangFunctions do gender: true, visit_count: nil, birthdate: nil, - company_id: second_company.id + company_id: second_company.id, + plural: [], + singular: nil } assert Map.drop(version, [:id, :inserted_at]) == %{ @@ -863,7 +976,9 @@ defmodule PaperTrailTest.SimpleModeBangFunctions do visit_count: 10, birthdate: ~D[1992-04-01], last_name: "Nakri", - gender: true + gender: true, + plural: [], + singular: nil } assert Map.drop(version, [:id, :inserted_at]) == %{ @@ -955,7 +1070,9 @@ defmodule PaperTrailTest.SimpleModeBangFunctions do gender: true, visit_count: 10, birthdate: ~D[1992-04-01], - company_id: inserted_target_company.id + company_id: inserted_target_company.id, + plural: [], + singular: nil }), originator_id: nil, origin: "admin", diff --git a/test/paper_trail/base_test.exs b/test/paper_trail/base_test.exs index bebb065b..de01e8b4 100644 --- a/test/paper_trail/base_test.exs +++ b/test/paper_trail/base_test.exs @@ -411,7 +411,9 @@ defmodule PaperTrailTest do gender: true, visit_count: nil, birthdate: nil, - company_id: new_company_result[:model].id + company_id: new_company_result[:model].id, + plural: [], + singular: nil } assert Map.drop(version, [:id, :inserted_at]) == %{ @@ -475,7 +477,9 @@ defmodule PaperTrailTest do visit_count: 10, birthdate: ~D[1992-04-01], last_name: "Nakri", - gender: true + gender: true, + plural: [], + singular: nil } assert Map.drop(version, [:id, :inserted_at]) == %{ @@ -557,7 +561,9 @@ defmodule PaperTrailTest do gender: true, visit_count: 10, birthdate: ~D[1992-04-01], - company_id: target_company_insertion[:model].id + company_id: target_company_insertion[:model].id, + plural: [], + singular: nil }, originator_id: nil, origin: "admin", diff --git a/test/support/simple_models.exs b/test/support/simple_models.exs index 8b1bf402..59cb75ed 100644 --- a/test/support/simple_models.exs +++ b/test/support/simple_models.exs @@ -97,6 +97,9 @@ defmodule SimplePerson do belongs_to(:company, SimpleCompany, foreign_key: :company_id) + embeds_one(:singular, SimpleEmbed) + embeds_many(:plural, SimpleEmbed) + timestamps() end @@ -113,6 +116,8 @@ defmodule SimplePerson do model |> cast(params, @optional_fields) |> foreign_key_constraint(:company_id) + |> cast_embed(:singular) + |> cast_embed(:plural) end def count do @@ -125,3 +130,18 @@ defmodule SimplePerson do |> PaperTrail.RepoClient.repo().one end end + +defmodule SimpleEmbed do + use Ecto.Schema + + import Ecto.Changeset + + embedded_schema do + field(:name, :string) + end + + def changeset(model, params \\ %{}) do + model + |> cast(params, [:name]) + end +end