Packages

Store changes to your models, for auditing or versioning.

Current section

Files

Jump to
ecto_cellar lib ecto_cellar.ex
Raw

lib/ecto_cellar.ex

defmodule EctoCellar do
@moduledoc """
Core module for EctoCellar.
Handles versions table created by `mix ecto_cellar.gen`.
You can use this module to store in the cellar and restore the version.
For a model whose primary_key is other than `id`, specify `id_type` and use it.
"""
alias EctoCellar.Version
@native_datetime_prefix "ecto_cellar_native_datetime_"
@doc """
Stores the changes at that time in the cellar.
"""
@spec store(struct(), atom()) :: {:ok, struct()} | {:error, struct()}
def store(%mod{} = model, id_type \\ :id) do
model_id = if id = Map.fetch!(model, id_type), do: to_string(id)
case Version.create(%{
model_name: mod |> inspect(),
model_id: model_id,
model_inserted_at: model.inserted_at,
version: model |> cast_format_map |> Jason.encode!()
}) do
{:ok, _version} -> {:ok, model}
error -> error
end
end
@doc """
Like store/2, except that if the record is invalid, raises an exception.
"""
@spec store!(struct()) :: struct()
def store!(%mod{} = model, id_type \\ :id) do
model_id = if id = Map.fetch!(model, id_type), do: to_string(id)
_version =
Version.create!(%{
model_name: mod |> inspect(),
model_id: model_id,
model_inserted_at: model.inserted_at,
version: model |> cast_format_map |> Jason.encode!()
})
model
end
@doc """
Returns a specific version of model from the cellar.
"""
@spec one(struct(), NaiveDateTime.t(), any()) :: struct()
def one(%mod{} = model, timestamp, id_type \\ :id) do
Version.one(
mod |> inspect(),
timestamp,
model |> Map.fetch!(id_type) |> to_string()
)
|> to_model(mod)
end
@doc """
Returns all versions of model from the cellar.
"""
@spec all(struct(), any()) :: list(struct())
def all(%mod{} = model, id_type \\ :id) do
Version.all(
mod |> inspect(),
model |> Map.fetch!(id_type) |> to_string()
)
|> to_models(mod)
end
def repo, do: Application.get_env(:ecto_cellar, :repo)
defp to_models(versions, mod) do
versions
|> Enum.map(&to_model(&1, mod))
end
defp to_model(version, mod) do
version =
Jason.decode!(version.version)
|> Enum.map(fn {key, value} ->
{
key |> String.to_existing_atom(),
if(is_stored_native_datetime(value), do: restore_native_datetime(value), else: value)
}
end)
struct(
mod.__struct__,
version
)
end
defp cast_format_map(%{} = model) do
model
|> Map.from_struct()
|> Map.drop([:__meta__, :__struct__])
|> Enum.map(fn {key, value} ->
{key, if(is_native_datetime(value), do: "#{@native_datetime_prefix}#{value}", else: value)}
end)
|> Map.new()
end
defp is_native_datetime(%NaiveDateTime{}), do: true
defp is_native_datetime(_), do: false
defp is_stored_native_datetime(datetime_str),
do: to_string(datetime_str) =~ @native_datetime_prefix
defp restore_native_datetime(datetime_str) do
datetime_str
|> String.replace(@native_datetime_prefix, "")
|> NaiveDateTime.from_iso8601!()
end
end