diff --git a/lib/paper_trail/serializer.ex b/lib/paper_trail/serializer.ex index 888d2546..3e48b85a 100644 --- a/lib/paper_trail/serializer.ex +++ b/lib/paper_trail/serializer.ex @@ -2,12 +2,13 @@ defmodule PaperTrail.Serializer do @moduledoc false import Ecto.Query + alias Ecto.UUID alias PaperTrail.RepoClient alias PaperTrail.Version @type options :: PaperTrail.options() - @default_ignored_ecto_types [Ecto.UUID, :binary_id, :binary] + @default_ignored_ecto_types [UUID, :binary_id, :binary] def make_version_struct(%{event: "insert"}, model, options) do originator = RepoClient.originator() @@ -221,17 +222,37 @@ defmodule PaperTrail.Serializer do def get_item_type(%Ecto.Changeset{data: data}), do: get_item_type(data) def get_item_type(%schema{}), do: schema |> Module.split() |> List.last() + @spec get_model_id(Ecto.Changeset.t() | struct()) :: String.t() | integer() def get_model_id(%Ecto.Changeset{data: data}), do: get_model_id(data) def get_model_id(model) do - {_, model_id} = List.first(Ecto.primary_key(model)) + model_id = + case Ecto.primary_key(model) do + [{_, model_id}] -> + model_id + + composite_primary_keys -> + composite_primary_keys + |> Enum.sort_by(fn {field, _value} -> field end, :asc) + |> Enum.map_join(":", fn {_field, value} -> to_string(value) end) + end case PaperTrail.Version.__schema__(:type, :item_id) do :integer -> model_id - _ -> - "#{model_id}" + UUID -> + if UUID.cast(model_id) == :error do + model_id + |> then(&:crypto.hash(:sha256, &1)) + |> :binary.part(0, 16) + |> UUID.cast!() + else + model_id + end + + _else -> + to_string(model_id) end end diff --git a/priv/uuid_repo/migrations/20170525142613_create_items.exs b/priv/uuid_repo/migrations/20170525142613_create_items.exs index 1be5c273..c77b0e4a 100644 --- a/priv/uuid_repo/migrations/20170525142613_create_items.exs +++ b/priv/uuid_repo/migrations/20170525142613_create_items.exs @@ -1,30 +1,38 @@ defmodule PaperTrail.UUIDRepo.Migrations.CreateItems do + @moduledoc false use Ecto.Migration def change do create table(:items) do - add :item_id, :binary_id, null: false, primary_key: true - add :title, :string, null: false + add(:item_id, :binary_id, null: false, primary_key: true) + add(:title, :string, null: false) timestamps() end create table(:foo_items) do - add :title, :string, null: false + add(:title, :string, null: false) timestamps() end create table(:bar_items, primary_key: false) do - add :item_id, :string, primary_key: true - add :title, :string, null: false + add(:item_id, :string, primary_key: true) + add(:title, :string, null: false) timestamps() end create table(:uuid_items) do - add :item_id, :uuid, null: false, primary_key: true - add :title, :string, null: false + add(:item_id, :uuid, null: false, primary_key: true) + add(:title, :string, null: false) + + timestamps() + end + + create table(:composite_primary_keys_items, primary_key: false) do + add(:item_id, :uuid, primary_key: true) + add(:bar_id, :uuid, primary_key: true) timestamps() end diff --git a/test/support/uuid_models.exs b/test/support/uuid_models.exs index 836536c3..fee05bce 100644 --- a/test/support/uuid_models.exs +++ b/test/support/uuid_models.exs @@ -1,4 +1,5 @@ defmodule Product do + @moduledoc false use Ecto.Schema import Ecto.Changeset @@ -18,7 +19,9 @@ defmodule Product do end defmodule Admin do + @moduledoc false use Ecto.Schema + import Ecto.Changeset @primary_key {:id, :binary_id, autogenerate: true} @@ -36,7 +39,9 @@ defmodule Admin do end defmodule Item do + @moduledoc false use Ecto.Schema + import Ecto.Changeset @primary_key {:item_id, :binary_id, autogenerate: true} @@ -54,7 +59,9 @@ defmodule Item do end defmodule UUIDItem do + @moduledoc false use Ecto.Schema + import Ecto.Changeset @primary_key {:item_id, Ecto.UUID, autogenerate: true} @@ -72,7 +79,9 @@ defmodule UUIDItem do end defmodule FooItem do + @moduledoc false use Ecto.Schema + import Ecto.Changeset @primary_key {:id, :id, autogenerate: true} @@ -90,7 +99,9 @@ defmodule FooItem do end defmodule BarItem do + @moduledoc false use Ecto.Schema + import Ecto.Changeset @primary_key {:item_id, :string, autogenerate: false} @@ -106,3 +117,24 @@ defmodule BarItem do |> validate_required([:item_id, :title]) end end + +defmodule CompositePkItem do + @moduledoc false + use Ecto.Schema + + import Ecto.Changeset + + @primary_key false + schema "composite_primary_keys_items" do + field(:item_id, Ecto.UUID, primary_key: true) + field(:bar_id, Ecto.UUID, primary_key: true) + + timestamps() + end + + def changeset(model, params \\ %{}) do + model + |> cast(params, [:item_id, :bar_id]) + |> validate_required([:item_id, :bar_id]) + end +end diff --git a/test/uuid/uuid_test.exs b/test/uuid/uuid_test.exs index 92a54ef9..b8723792 100644 --- a/test/uuid/uuid_test.exs +++ b/test/uuid/uuid_test.exs @@ -4,6 +4,7 @@ defmodule PaperTrailTest.UUIDTest do import Ecto.Query import PaperTrail.RepoClient, only: [repo: 0] + alias Ecto.UUID alias PaperTrail.Version defmodule CustomPaperTrail do @@ -86,6 +87,50 @@ defmodule PaperTrailTest.UUIDTest do assert version.item_id == uuid_item.item_id end + test "versioning models with composite primary keys" do + item_id1 = UUID.generate() + bar_id1 = UUID.generate() + + item1 = + %CompositePkItem{} + |> CompositePkItem.changeset(%{item_id: item_id1, bar_id: bar_id1}) + |> CustomPaperTrail.insert!() + + version1 = Version |> last() |> repo().one + + assert version1 == PaperTrail.get_version(item1) + + assert %{ + event: "insert", + item_changes: %{"bar_id" => ^bar_id1, "item_id" => ^item_id1}, + item_id: version_id1, + item_type: "CompositePkItem" + } = version1 + + assert match?({:ok, _}, UUID.cast(version_id1)) + + item_id2 = UUID.generate() + bar_id2 = UUID.generate() + + item2 = + %CompositePkItem{} + |> CompositePkItem.changeset(%{item_id: item_id2, bar_id: bar_id2}) + |> CustomPaperTrail.insert!() + + version2 = PaperTrail.get_version(item2) + + assert %{ + event: "insert", + item_changes: %{"bar_id" => ^bar_id2, "item_id" => ^item_id2}, + item_id: version_id2, + item_type: "CompositePkItem" + } = version2 + + assert match?({:ok, _}, UUID.cast(version_id2)) + + assert version_id1 != version_id2 + end + test "test INTEGER primary key for item_type == :string" do if PaperTrail.Version.__schema__(:type, :item_id) == :string do item =