Packages

Track changes to your database with Ecto.

Current section

Files

Jump to
footprint lib footprint.ex
Raw

lib/footprint.ex

defmodule Footprint do
@moduledoc """
## TODO
- [x] insert! and insert func.
- [x] update! and update func.
- [x] delete! and delete func.
- [ ] insert_or_update! and insert_or_update func.
- [x] get_version and get_versions func.
- [ ] version diff and apply func.
- [x] footprint.inspect func.
"""
alias Ecto.Multi
alias Footprint.Version
alias Footprint.Client, as: FC
defdelegate get_version(record), to: Footprint.VersionQueries
defdelegate get_version(model_or_record, id_or_options), to: Footprint.VersionQueries
defdelegate get_version(model, id, options), to: Footprint.VersionQueries
defdelegate get_versions(record), to: Footprint.VersionQueries
defdelegate get_versions(model_or_record, id_or_options), to: Footprint.VersionQueries
defdelegate get_versions(model, id, options), to: Footprint.VersionQueries
defdelegate get_current_model(version), to: Footprint.VersionQueries
@doc """
Same as insert/2
## Example
iex> Post.changeset(%Post{}, %{title: "t1", body: "b1"}) |> Footprint.insert()
{:ok, %{model: %Dummy.CMS.Post{}, version: %Footprint.Version{}}
iex> Post.changeset(%Post{}, %{title: "t1"}) |> Footprint.insert
{:error, :model, #Ecto.Changeset<>, %{}}
"""
@spec insert(Ecto.Changeset.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def insert(changeset, options \\ [meta: nil]) do
repo = FC.repo()
Multi.new()
|> Multi.insert(:model, changeset)
|> Multi.run(:version, fn repo, %{model: model} ->
make_version_struct(%{event: "insert"}, changeset, model, options)
|> repo.insert()
end)
|> repo.transaction()
end
@doc """
Same as insert/2 but returns only the model struct or raises if the changeset is invalid.
## Examples
iex> Post.changeset(%Post{}, %{title: "t1", body: "b1"}) |> Footprint.insert!()
%Dummy.CMS.Post{}
iex> Post.changeset(%Post{}, %{title: "t1"}) |> Footprint.insert!()
** (Ecto.InvalidChangesetError)
"""
def insert!(changeset, options \\ [meta: nil]) do
repo = FC.repo()
repo.transaction(fn ->
model = repo.insert!(changeset)
make_version_struct(%{event: "insert"}, changeset, model, options) |> repo.insert!()
model
end)
|> elem(1)
end
@doc """
Same as update/2
## Examples
iex> p1 = Repo.get(Post, 1)
iex> Post.changeset(p1, %{title: "t2"}) |> Footprint.update()
{:ok, %{model: %Dummy.CMS.Post{}, version: %Footprint.Version{}}
iex> Post.changeset(p1, %{title: "tttttt7"}) |> Footprint.update()
{:error, :model, #Ecto.Changeset<>, %{}}
"""
@spec update(Ecto.Changeset.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def update(changeset, options \\ [meta: nil]) do
repo = FC.repo()
Multi.new()
|> Multi.update(:model, changeset)
|> Multi.run(:version, fn repo, %{model: model} ->
make_version_struct(%{event: "update"}, changeset, model, options)
|> repo.insert()
end)
|> repo.transaction()
end
@doc """
Same as update!/2
## Examples
iex> p1 = Repo.get(Post, 1)
iex> Post.changeset(p1, %{title: "t2"}) |> Footprint.update()
%Dummy.CMS.Post{}
iex> Post.changeset(p1, %{title: "tttttt7"}) |> Footprint.update!()
** (Ecto.InvalidChangesetError)
"""
def update!(changeset, options \\ [meta: nil]) do
repo = FC.repo()
repo.transaction(fn ->
model = repo.update!(changeset)
make_version_struct(%{event: "update"}, changeset, model, options) |> repo.insert!()
model
end)
|> elem(1)
end
@doc """
Same as delete/2
## Examples
iex> p1 = Repo.get(Post, 1)
iex> p1 |> Footprint.delete()
{:ok, %{model: %Dummy.CMS.Post{}, version: %Footprint.Version{}}
"""
@spec delete(Ecto.Schema.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def delete(struct, options \\ [meta: nil]) do
repo = FC.repo()
Multi.new()
|> Multi.delete(:model, struct)
|> Multi.run(:version, fn repo, %{} ->
make_version_struct(%{event: "delete"}, struct, options)
|> repo.insert()
end)
|> repo.transaction()
end
@doc """
Sames as delete!/2
## Examples
iex> p1 = Repo.get(Post, 1)
iex> p1 |> Footprint.delete!()
%Dummy.CMS.Post{}
"""
def delete!(struct, options \\ [meta: nil]) do
repo = FC.repo()
repo.transaction(fn ->
model = repo.delete!(struct, options)
make_version_struct(%{event: "delete"}, struct, options) |> repo.insert!()
model
end)
|> elem(1)
end
@doc """
Prepase add footprint_fields_labels to origin Module.
## Examples
Post.footprint_field_labels = %{title: "Title", body: "Body"}
iex> version1 |> Footprint.inspect
%{"Body" => [nil, "body2"], "Title" => [nil, "title1"]}
"""
def inspect(version) do
module_base = version |> Map.get(:item_base) |> String.to_atom()
item_changes = version |> Map.get(:item_changes)
module_base
|> apply(:__info__, [:functions])
|> Keyword.has_key?(:footprint_field_labels)
|> case do
true ->
footprint_labels =
module_base
|> apply(:footprint_field_labels, [])
item_changes
|> Enum.into(%{}, fn {k, v} -> {Map.get(footprint_labels, String.to_atom(k), k), v} end)
_ ->
item_changes
end
end
defp make_version_struct(%{event: "insert"}, changeset, model, options) do
%Version{
no: 1,
event: "insert",
item_base: model.__struct__ |> to_string(),
item_type: model.__struct__ |> Module.split() |> List.last(),
item_id: model.id,
item_prev: build_item_prev(changeset),
item_changes: bulid_item_changes(changeset),
item_current: build_item_current(changeset),
meta: options[:meta],
originator_id:
case options[:originator] do
nil -> nil
_ -> options[:originator] |> Map.get(:id)
end,
origin: options[:origin]
}
end
defp make_version_struct(%{event: "update"}, changeset, model, options) do
%Version{
no: build_item_version_no(model),
event: "update",
item_base: changeset.data.__struct__ |> to_string(),
item_type: changeset.data.__struct__ |> Module.split() |> List.last(),
item_id: changeset.data.id,
item_prev: build_item_prev(changeset),
item_changes: bulid_item_changes(changeset),
item_current: build_item_current(changeset),
meta: options[:meta],
originator_id:
case options[:originator] do
nil -> nil
_ -> options[:originator] |> Map.get(:id)
end,
origin: options[:origin]
}
end
defp make_version_struct(%{event: "delete"}, model, options) do
%Version{
no: build_item_version_no(model),
event: "delete",
item_base: model.__struct__ |> to_string(),
item_type: model.__struct__ |> Module.split() |> List.last(),
item_id: model.id,
item_prev: model |> Map.from_struct() |> Map.delete(:__meta__),
item_changes: model |> Map.from_struct() |> Map.delete(:__meta__),
item_current: %{},
meta: options[:meta],
originator_id:
case options[:originator] do
nil -> nil
_ -> options[:originator] |> Map.get(:id)
end,
origin: options[:origin]
}
end
defp bulid_item_changes(changeset) do
changeset
|> build_item_current()
|> Enum.into(%{}, fn {k, v} -> {k, [Map.get(changeset.data, k), v]} end)
end
defp build_item_current(changeset) do
changeset
|> serialize()
end
defp build_item_prev(changeset) do
changeset
|> build_item_current()
|> Enum.into(%{}, fn {k, _v} -> {k, Map.get(changeset.data, k)} end)
end
defp serialize(changeset) do
relationships = changeset.data.__struct__.__schema__(:associations)
Map.drop(changeset.changes, relationships)
end
defp build_item_version_no(model) do
model
|> get_version()
|> Map.get(:no)
|> Kernel.+(1)
end
end