diff --git a/README.md b/README.md index 9345d1e6..8f19860e 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,23 @@ config :paper_trail, item_type: Ecto.UUID, Remember to edit the types accordingly in the generated migration. +#### Serialization and Ecto types + +Ecto uses the schema defined [Ecto types](https://hexdocs.pm/ecto/Ecto.Type.html) and the [Repo adapter](https://hexdocs.pm/ecto/Ecto.Adapter.html) dumpers to serialize data to primitive types, which are then inserted in the database by the adapter. + +In PaperTrail, all changes are stored in the `item_changes` field as a map, which will be later converted by the Repo adapter to an equivalent database type, such as json. + +Using the same approach as Ecto, PaperTrail serializes these changes using their respective Ecto type and `dump/1` callback. Nevertheless, it can cause issues later when the Repo adapter tries to convert the serialized map to json, since it's not possible to convert binary values to json. + +In order to avoid these issues, some Ecto types are ignored by default, as they dump binary values: `[Ecto.UUID, :binary_id, :binary]` + +You can also configure additional Ecto types to be ignored: + +```elixir +config :paper_trail, + not_dumped_ecto_types: [MyCustomType] +``` + ### Version origin references: PaperTrail records have a string field called ```origin```. ```PaperTrail.insert/2```, ```PaperTrail.update/2```, ```PaperTrail.delete/2``` functions accept a second argument to describe the origin of this version: ```elixir diff --git a/lib/paper_trail/serializer.ex b/lib/paper_trail/serializer.ex index d28e2d48..5b1d34ec 100644 --- a/lib/paper_trail/serializer.ex +++ b/lib/paper_trail/serializer.ex @@ -4,6 +4,10 @@ defmodule PaperTrail.Serializer do alias PaperTrail.RepoClient alias PaperTrail.Version + @type options :: PaperTrail.options() + + @default_ignored_ecto_types [Ecto.UUID, :binary_id, :binary] + def make_version_struct(%{event: "insert"}, model, options) do originator = RepoClient.originator() originator_ref = options[originator[:name]] || options[:originator] @@ -113,6 +117,7 @@ defmodule PaperTrail.Serializer do |> List.first() end + @spec serialize(nil | Ecto.Changeset.t() | struct, options) :: nil | map def serialize(nil, _options), do: nil def serialize(%Ecto.Changeset{data: data}, options), do: serialize(data, options) @@ -129,19 +134,25 @@ defmodule PaperTrail.Serializer do |> Map.new() end + @spec dump_fields!(module, map, module, module) :: Keyword.t() defp dump_fields!(schema, changes, dumper, adapter) do for {field, value} <- changes do {alias, type} = Map.fetch!(dumper, field) - {alias, dump_field!(schema, field, type, value, adapter)} + + dumped_value = + if( + type in ignored_ecto_types(), + do: value, + else: dump_field!(schema, field, type, value, adapter) + ) + + {alias, dumped_value} end end + @spec dump_field!(module, atom, atom, any, module) :: any defp dump_field!(schema, field, type, value, adapter) do case Ecto.Type.adapter_dump(adapter, type, value) do - {:ok, <<_::128>> = binary} -> - {:ok, string} = Ecto.UUID.load(binary) - string - {:ok, value} -> value @@ -178,4 +189,15 @@ defmodule PaperTrail.Serializer do "#{model_id}" end end + + @spec ignored_ecto_types :: [atom] + defp ignored_ecto_types do + :not_dumped_ecto_types + |> get_env([]) + |> Kernel.++(@default_ignored_ecto_types) + |> Enum.uniq() + end + + @spec get_env(atom, any) :: any + defp get_env(key, default), do: Application.get_env(:paper_trail, key, default) end diff --git a/priv/uuid_repo/migrations/20170525142613_create_items.exs b/priv/uuid_repo/migrations/20170525142613_create_items.exs index b9811cb8..1be5c273 100644 --- a/priv/uuid_repo/migrations/20170525142613_create_items.exs +++ b/priv/uuid_repo/migrations/20170525142613_create_items.exs @@ -21,5 +21,12 @@ defmodule PaperTrail.UUIDRepo.Migrations.CreateItems do timestamps() end + + create table(:uuid_items) do + add :item_id, :uuid, null: false, primary_key: true + add :title, :string, null: false + + timestamps() + end end end diff --git a/test/support/uuid_models.exs b/test/support/uuid_models.exs index 405087e9..836536c3 100644 --- a/test/support/uuid_models.exs +++ b/test/support/uuid_models.exs @@ -53,6 +53,24 @@ defmodule Item do end end +defmodule UUIDItem do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:item_id, Ecto.UUID, autogenerate: true} + schema "uuid_items" do + field(:title, :string) + + timestamps() + end + + def changeset(model, params \\ %{}) do + model + |> cast(params, [:title]) + |> validate_required(:title) + end +end + defmodule FooItem do use Ecto.Schema import Ecto.Changeset diff --git a/test/uuid/uuid_test.exs b/test/uuid/uuid_test.exs index 9d900add..4fbbc63e 100644 --- a/test/uuid/uuid_test.exs +++ b/test/uuid/uuid_test.exs @@ -65,6 +65,14 @@ defmodule PaperTrailTest.UUIDTest do version = Version |> last |> repo().one assert version.item_id == item.item_id + + uuid_item = + %UUIDItem{} + |> UUIDItem.changeset(%{title: "hello"}) + |> PaperTrail.insert!() + + version = Version |> last |> repo().one + assert version.item_id == uuid_item.item_id end test "test INTEGER primary key for item_type == :string" do