Packages
paper_trail
0.7.2
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.ex
# document bang function in README
defmodule PaperTrail do
import Ecto.Changeset
alias PaperTrail.VersionQueries
alias Ecto.Multi
alias PaperTrail.Version
@repo PaperTrail.RepoClient.repo()
@originator PaperTrail.RepoClient.originator()
@client PaperTrail.RepoClient
@doc """
Gets all the versions of a record given a module and its id
"""
def get_versions(model, id) do
VersionQueries.get_versions(model, id)
end
@doc """
Gets all the versions of a record
"""
def get_versions(record) do
VersionQueries.get_versions(record)
end
@doc """
Gets the last version of a record given its module reference and its id
"""
def get_version(model, id) do
VersionQueries.get_version(model, id)
end
@doc """
Gets the last version of a record
"""
def get_version(record) do
VersionQueries.get_version(record)
end
@doc """
Gets the current model record/struct of a version
"""
def get_current_model(version) do
VersionQueries.get_current_model(version)
end
@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]) do
transaction_order = case @client.strict_mode() do
true ->
Multi.new
|> Multi.run(:initial_version, fn %{} ->
version_id = get_sequence_id("versions") + 1
changeset_data = changeset.data |> 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(initial_version)
end)
|> Multi.run(:model, fn %{initial_version: initial_version} ->
updated_changeset = changeset |> change(%{
first_version_id: initial_version.id, current_version_id: initial_version.id
})
@repo.insert(updated_changeset)
end)
|> Multi.run(:version, fn %{initial_version: initial_version, model: model} ->
target_version = make_version_struct(%{event: "insert"}, model, options) |> serialize()
Version.changeset(initial_version, target_version) |> @repo.update
end)
_ ->
Multi.new
|> Multi.insert(:model, changeset)
|> Multi.run(:version, fn %{model: model} ->
version = make_version_struct(%{event: "insert"}, model, options)
@repo.insert(version)
end)
end
transaction = @repo.transaction(transaction_order)
case @client.strict_mode() do
true ->
case transaction do
{:error, :model, changeset, %{}} ->
filtered_changes = Map.drop(changeset.changes, [:current_version_id, :first_version_id])
{:error, Map.merge(changeset, %{repo: @repo, changes: filtered_changes})}
{:ok, map} -> {:ok, Map.drop(map, [:initial_version])}
end
_ ->
case transaction do
{:error, :model, changeset, %{}} -> {:error, Map.merge(changeset, %{repo: @repo})}
_ -> transaction
end
end
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]) do
@repo.transaction(fn ->
case @client.strict_mode() do
true ->
version_id = get_sequence_id("versions") + 1
changeset_data = changeset.data |> 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)
target_version = make_version_struct(%{event: "insert"}, model, options) |> serialize()
Version.changeset(initial_version, target_version) |> @repo.update!
model
_ ->
model = @repo.insert!(changeset)
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]) do
transaction_order = case @client.strict_mode() do
true ->
Multi.new
|> Multi.run(:initial_version, fn %{} ->
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)
@repo.insert(target_version)
end)
|> Multi.run(:model, fn %{initial_version: initial_version} ->
updated_changeset = changeset |> change(%{current_version_id: initial_version.id})
@repo.update(updated_changeset)
end)
|> Multi.run(:version, fn %{initial_version: initial_version} ->
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
end)
_ ->
Multi.new
|> Multi.update(:model, changeset)
|> Multi.run(:version, fn %{model: _model} ->
version = make_version_struct(%{event: "update"}, changeset, options)
@repo.insert(version)
end)
end
transaction = @repo.transaction(transaction_order)
case @client.strict_mode() do
true ->
case transaction do
{:error, :model, changeset, %{}} ->
filtered_changes = Map.drop(changeset.changes, [:current_version_id])
{:error, Map.merge(changeset, %{repo: @repo, changes: filtered_changes})}
{:ok, map} -> {:ok, Map.delete(map, :initial_version)}
end
_ ->
case transaction do
{:error, :model, changeset, %{}} -> {:error, Map.merge(changeset, %{repo: @repo})}
_ -> transaction
end
end
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]) do
@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]) do
transaction = Multi.new
|> Multi.delete(:model, struct)
|> Multi.run(:version, fn %{} ->
version = make_version_struct(%{event: "delete"}, struct, options)
@repo.insert(version)
end)
|> @repo.transaction
case transaction do
{:error, :model, changeset, %{}} -> {:error, Map.merge(changeset, %{repo: @repo})}
_ -> transaction
end
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]) do
@repo.transaction(fn ->
model = @repo.delete!(struct)
version_struct = make_version_struct(%{event: "delete"}, struct, options)
@repo.insert!(version_struct)
model
end) |> elem(1)
end
defp make_version_struct(%{event: "insert"}, model, options) do
originator_ref = options[@originator[:name]] || options[:originator]
%Version{
event: "insert",
item_type: model.__struct__ |> Module.split |> List.last,
item_id: model.id,
item_changes: serialize(model),
originator_id: case originator_ref do
nil -> nil
_ -> originator_ref |> Map.get(:id)
end,
origin: options[:origin],
meta: options[:meta]
}
end
defp make_version_struct(%{event: "update"}, changeset, options) do
originator_ref = options[@originator[:name]] || options[:originator]
%Version{
event: "update",
item_type: changeset.data.__struct__ |> Module.split |> List.last,
item_id: changeset.data.id,
item_changes: changeset.changes,
originator_id: case originator_ref do
nil -> nil
_ -> originator_ref |> Map.get(:id)
end,
origin: options[:origin],
meta: options[:meta]
}
end
defp make_version_struct(%{event: "delete"}, model, options) do
originator_ref = options[@originator[:name]] || options[:originator]
%Version{
event: "delete",
item_type: model.__struct__ |> Module.split |> List.last,
item_id: model.id,
item_changes: serialize(model),
originator_id: case originator_ref do
nil -> nil
_ -> originator_ref |> Map.get(:id)
end,
origin: options[:origin],
meta: options[:meta]
}
end
defp get_sequence_from_model(changeset) do
table_name = changeset.data.__struct__.__schema__(:source)
get_sequence_id(table_name)
end
defp get_sequence_id(table_name) do
Ecto.Adapters.SQL.query!(@repo, "select last_value FROM #{table_name}_id_seq").rows
|> List.first
|> List.first
end
defp serialize(model) do
relationships = model.__struct__.__schema__(:associations)
Map.drop(model, [:__struct__, :__meta__] ++ relationships)
end
end