Skip to content

Commit

Permalink
Ignore binary ecto types when serializing changes (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
rschef authored Jun 1, 2020
1 parent bd6b948 commit 8f70ba4
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 5 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 27 additions & 5 deletions lib/paper_trail/serializer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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
7 changes: 7 additions & 0 deletions priv/uuid_repo/migrations/20170525142613_create_items.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions test/support/uuid_models.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions test/uuid/uuid_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8f70ba4

Please sign in to comment.