Packages
paper_trail
1.1.1
1.1.2
1.1.1
1.1.0
1.0.0
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.1
0.10.0
0.9.0
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.3
0.5.2
0.5.0
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
Track and record all the changes in your database. Revert back to anytime in history.
Current section
Files
Jump to
Current section
Files
lib/paper_trail/serializer.ex
defmodule PaperTrail.Serializer do
@moduledoc """
Serialization functions to create a version struct
"""
alias PaperTrail.RepoClient
alias PaperTrail.Version
@type model :: struct() | Ecto.Changeset.t()
@type options :: Keyword.t()
@type primary_key :: integer() | String.t()
@doc """
Creates a version struct for a model and a specific changeset action
"""
@spec make_version_struct(map(), model(), options()) :: Version.t()
def make_version_struct(%{event: "insert"}, model, options) do
originator = RepoClient.originator()
originator_ref = options[originator[:name]] || options[:originator]
%Version{
event: "insert",
item_type: get_item_type(model),
item_id: get_model_id(model),
item_changes: serialize(model),
originator_id:
case originator_ref do
nil -> nil
%{id: id} -> id
model when is_struct(model) -> get_model_id(originator_ref)
end,
origin: options[:origin],
meta: options[:meta]
}
|> add_prefix(options[:prefix])
end
def make_version_struct(%{event: "update"}, changeset, options) do
originator = RepoClient.originator()
originator_ref = options[originator[:name]] || options[:originator]
%Version{
event: "update",
item_type: get_item_type(changeset),
item_id: get_model_id(changeset),
item_changes: serialize_changes(changeset),
originator_id:
case originator_ref do
nil -> nil
%{id: id} -> id
model when is_struct(model) -> get_model_id(originator_ref)
end,
origin: options[:origin],
meta: options[:meta]
}
|> add_prefix(options[:prefix])
end
def make_version_struct(%{event: "delete"}, model_or_changeset, options) do
originator = RepoClient.originator()
originator_ref = options[originator[:name]] || options[:originator]
%Version{
event: "delete",
item_type: get_item_type(model_or_changeset),
item_id: get_model_id(model_or_changeset),
item_changes: serialize(model_or_changeset),
originator_id:
case originator_ref do
nil -> nil
%{id: id} -> id
model when is_struct(model) -> get_model_id(originator_ref)
end,
origin: options[:origin],
meta: options[:meta]
}
|> add_prefix(options[:prefix])
end
@doc """
Returns the last primary key value of a table
"""
@spec get_sequence_id(model() | String.t()) :: primary_key()
def get_sequence_id(%Ecto.Changeset{data: data}) do
get_sequence_id(data)
end
def get_sequence_id(%schema{}) do
:source
|> schema.__schema__()
|> get_sequence_id()
end
def get_sequence_id(table_name) when is_binary(table_name) do
Ecto.Adapters.SQL.query!(RepoClient.repo(), "select last_value FROM #{table_name}_id_seq").rows
|> List.first()
|> List.first()
end
@doc """
Shows DB representation of an Ecto model, filters relationships and virtual attributes from an Ecto.Changeset or %ModelStruct{}
"""
@spec serialize(nil | Ecto.Changeset.t() | struct()) :: nil | map()
def serialize(nil), do: nil
def serialize(%Ecto.Changeset{data: data}), do: serialize(data)
def serialize(%_schema{} = model), do: Ecto.embedded_dump(model, :json)
@doc """
Dumps changes using Ecto fields
"""
@spec serialize_changes(Ecto.Changeset.t()) :: map()
def serialize_changes(%Ecto.Changeset{changes: changes} = changeset) do
changeset
|> serialize_model_changes()
|> serialize()
|> Map.take(Map.keys(changes))
end
@doc """
Adds a prefix to the Ecto schema
"""
@spec add_prefix(Ecto.Schema.schema(), nil | String.t()) :: Ecto.Schema.schema()
def add_prefix(schema, nil), do: schema
def add_prefix(schema, prefix), do: Ecto.put_meta(schema, prefix: prefix)
@doc """
Returns the model type, which is the last module name
"""
@spec get_item_type(model()) :: String.t()
def get_item_type(%Ecto.Changeset{data: data}), do: get_item_type(data)
def get_item_type(%schema{}), do: schema |> Module.split() |> List.last()
@doc """
Returns the model primary id
"""
@spec get_model_id(model()) :: primary_key()
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))
case PaperTrail.Version.__schema__(:type, :item_id) do
:integer ->
model_id
_ ->
"#{model_id}"
end
end
@spec serialize_model_changes(nil) :: nil
defp serialize_model_changes(nil), do: nil
@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