Skip to content

Commit

Permalink
Support Ecto Embed
Browse files Browse the repository at this point in the history
  • Loading branch information
maennchen committed Nov 30, 2020
1 parent 8ecdf4e commit a1e3838
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 103 deletions.
135 changes: 47 additions & 88 deletions lib/paper_trail.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
defmodule PaperTrail do
import Ecto.Changeset

alias PaperTrail.Version
alias PaperTrail.RepoClient
alias PaperTrail.Serializer

defdelegate get_version(record), to: PaperTrail.VersionQueries
Expand All @@ -22,6 +19,9 @@ defmodule PaperTrail do
@doc """
Inserts a record to the database with a related version insertion in one transaction
"""
@spec insert(changeset :: Ecto.Changeset.t(model), options :: Keyword.t()) ::
{:ok, %{model: model, version: Version.t()}} | {:error, Ecto.Changeset.t(model) | term}
when model: struct
def insert(
changeset,
options \\ [
Expand All @@ -42,6 +42,8 @@ defmodule PaperTrail do
@doc """
Same as insert/2 but returns only the model struct or raises if the changeset is invalid.
"""
@spec insert!(changeset :: Ecto.Changeset.t(model), options :: Keyword.t()) :: model
when model: struct
def insert!(
changeset,
options \\ [
Expand All @@ -54,50 +56,17 @@ defmodule PaperTrail do
ecto_options: []
]
) do
repo = RepoClient.repo()
ecto_options = options[:ecto_options] || []

repo.transaction(fn ->
case RepoClient.strict_mode() do
true ->
version_id = get_sequence_id("versions") + 1

changeset_data =
Map.get(changeset, :data, changeset)
|> Map.merge(%{
id: get_sequence_id(changeset) + 1,
first_version_id: version_id,
current_version_id: version_id
})

initial_version =
make_version_struct(%{event: "insert"}, changeset_data, options)
|> repo.insert!

updated_changeset =
changeset
|> change(%{
first_version_id: initial_version.id,
current_version_id: initial_version.id
})

model = repo.insert!(updated_changeset, ecto_options)
target_version = make_version_struct(%{event: "insert"}, model, options) |> serialize()
Version.changeset(initial_version, target_version) |> repo.update!
model

_ ->
model = repo.insert!(changeset, ecto_options)
make_version_struct(%{event: "insert"}, model, options) |> repo.insert!
model
end
end)
|> elem(1)
changeset
|> insert(options)
|> model_or_error()
end

@doc """
Updates a record from the database with a related version insertion in one transaction
"""
@spec update(changeset :: Ecto.Changeset.t(model), options :: Keyword.t()) ::
{:ok, %{model: model, version: Version.t()}} | {:error, Ecto.Changeset.t(model) | term}
when model: struct
def update(changeset, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]) do
PaperTrail.Multi.new()
|> PaperTrail.Multi.update(changeset, options)
Expand All @@ -107,65 +76,55 @@ defmodule PaperTrail do
@doc """
Same as update/2 but returns only the model struct or raises if the changeset is invalid.
"""
@spec update!(changeset :: Ecto.Changeset.t(model), options :: Keyword.t()) :: model
when model: struct
def update!(changeset, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]) do
repo = PaperTrail.RepoClient.repo()
client = PaperTrail.RepoClient

repo.transaction(fn ->
case client.strict_mode() do
true ->
version_data =
changeset.data
|> Map.merge(%{
current_version_id: get_sequence_id("versions")
})

target_changeset = changeset |> Map.merge(%{data: version_data})
target_version = make_version_struct(%{event: "update"}, target_changeset, options)
initial_version = repo.insert!(target_version)
updated_changeset = changeset |> change(%{current_version_id: initial_version.id})
model = repo.update!(updated_changeset)

new_item_changes =
initial_version.item_changes
|> Map.merge(%{
current_version_id: initial_version.id
})

initial_version |> change(%{item_changes: new_item_changes}) |> repo.update!
model

_ ->
model = repo.update!(changeset)
version_struct = make_version_struct(%{event: "update"}, changeset, options)
repo.insert!(version_struct)
model
end
end)
|> elem(1)
changeset
|> update(options)
|> model_or_error()
end

@doc """
Deletes a record from the database with a related version insertion in one transaction
"""
def delete(struct, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]) do
@spec delete(model_or_changeset :: model | Ecto.Changeset.t(model), options :: Keyword.t()) ::
{:ok, %{model: model, version: Version.t()}} | {:error, Ecto.Changeset.t(model) | term}
when model: struct
def delete(
model_or_changeset,
options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]
) do
PaperTrail.Multi.new()
|> PaperTrail.Multi.delete(struct, options)
|> PaperTrail.Multi.delete(model_or_changeset, options)
|> PaperTrail.Multi.commit()
end

@doc """
Same as delete/2 but returns only the model struct or raises if the changeset is invalid.
"""
def delete!(struct, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]) do
repo = PaperTrail.RepoClient.repo()
@spec delete!(model_or_changeset :: model | Ecto.Changeset.t(model), options :: Keyword.t()) ::
model
when model: struct
def delete!(
model_or_changeset,
options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]
) do
model_or_changeset
|> delete(options)
|> model_or_error()
end

@spec model_or_error({:ok, %{model: model}}) :: model when model: struct()
defp model_or_error({:ok, %{model: model}}) do
model
end

@spec model_or_error({:error, reason :: term}) :: no_return
defp model_or_error({:error, %Ecto.Changeset{} = changeset}) do
raise Ecto.InvalidChangesetError, action: :update, changeset: changeset
end

repo.transaction(fn ->
model = repo.delete!(struct, options)
version_struct = make_version_struct(%{event: "delete"}, struct, options)
repo.insert!(version_struct, options)
model
end)
|> elem(1)
defp model_or_error({:error, reason}) do
raise reason
end
end
49 changes: 46 additions & 3 deletions lib/paper_trail/serializer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,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
Expand Down Expand Up @@ -144,4 +144,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
2 changes: 2 additions & 0 deletions priv/repo/migrations/20160619190938_add_simple_people.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit a1e3838

Please sign in to comment.