Current section

Files

Jump to
paper_trail lib paper_trail.ex
Raw

lib/paper_trail.ex

defmodule PaperTrail do
import Ecto.Changeset
alias PaperTrail.Version
alias PaperTrail.RepoClient
defdelegate get_version(record), to: PaperTrail.VersionQueries
defdelegate get_version(model_or_record, id_or_options), to: PaperTrail.VersionQueries
defdelegate get_version(model, id, options), to: PaperTrail.VersionQueries
defdelegate get_versions(record), to: PaperTrail.VersionQueries
defdelegate get_versions(model_or_record, id_or_options), to: PaperTrail.VersionQueries
defdelegate get_versions(model, id, options), to: PaperTrail.VersionQueries
defdelegate get_current_model(version), to: PaperTrail.VersionQueries
@doc """
Inserts a record to the database with a related version insertion in one transaction
"""
def insert(changeset, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil, model_key: :model, version_key: :version, ecto_options: []]) do
PaperTrail.Multi.new()
|> PaperTrail.Multi.insert(changeset, options)
|> PaperTrail.Multi.commit()
end
@doc """
Same as insert/2 but returns only the model struct or raises if the changeset is invalid.
"""
def insert!(changeset, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil, model_key: :model, version_key: :version, 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_from_model(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)
end
@doc """
Updates a record from the database with a related version insertion in one transaction
"""
def update(changeset, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]) do
PaperTrail.Multi.new()
|> PaperTrail.Multi.update(changeset, options)
|> PaperTrail.Multi.commit()
end
@doc """
Same as update/2 but returns only the model struct or raises if the changeset is invalid.
"""
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)
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
PaperTrail.Multi.new()
|> PaperTrail.Multi.delete(struct, 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()
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)
end
defp make_version_struct(%{event: "insert"}, model, options) do
originator = PaperTrail.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
_ -> originator_ref |> Map.get(:id)
end,
origin: options[:origin],
meta: options[:meta]
}
|> add_prefix(options[:prefix])
end
defp make_version_struct(%{event: "update"}, changeset, options) do
originator = PaperTrail.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: changeset.changes,
originator_id:
case originator_ref do
nil -> nil
_ -> originator_ref |> Map.get(:id)
end,
origin: options[:origin],
meta: options[:meta]
}
|> add_prefix(options[:prefix])
end
defp make_version_struct(%{event: "delete"}, model_or_changeset, options) do
originator = PaperTrail.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
_ -> originator_ref |> Map.get(:id)
end,
origin: options[:origin],
meta: options[:meta]
}
|> add_prefix(options[:prefix])
end
defp get_sequence_from_model(changeset) do
table_name =
case Map.get(changeset, :data) do
nil -> changeset.__struct__.__schema__(:source)
_ -> changeset.data.__struct__.__schema__(:source)
end
get_sequence_id(table_name)
end
defp get_sequence_id(table_name) do
Ecto.Adapters.SQL.query!(RepoClient.repo(), "select last_value FROM #{table_name}_id_seq").rows
|> List.first()
|> List.first()
end
defp serialize(%Ecto.Changeset{data: data}), do: serialize(data)
defp serialize(model) do
relationships = model.__struct__.__schema__(:associations)
Map.drop(model, [:__struct__, :__meta__] ++ relationships)
end
defp add_prefix(changeset, nil), do: changeset
defp add_prefix(changeset, prefix), do: Ecto.put_meta(changeset, prefix: prefix)
defp get_item_type(%Ecto.Changeset{data: data}), do: get_item_type(data)
defp get_item_type(model), do: model.__struct__ |> Module.split() |> List.last()
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
end