Current section

Files

Jump to
sorcery lib storage adapters ecto ecto_adapter.ex
Raw

lib/storage/adapters/ecto/ecto_adapter.ex

defmodule Sorcery.Storage.EctoAdapter do
use Norm
alias Sorcery.Specs.Primative, as: T
alias Sorcery.Storage.GenserverAdapter.Specs, as: AdapterT
#alias Sorcery.Utils.Maps
def int_id(), do: spec(is_integer())
def placeholder_id(), do: spec(is_binary() and fn id ->
"$sorcery:" <> str_int = id
String.to_integer(str_int)
end)
def multi_name(), do: spec(fn name ->
case name do
"$sorcery:" <> _ -> true
"tk:" <> _ -> true
_ -> false
end
end)
@contract persist_src(T.src(), AdapterT.client_state()) :: T.db()
@doc """
Takes the src, and makes it permanent with an ecto transaction
"""
def persist_src(src, client) do
%{repo: repo} = client
multi = multi_mod(client).new()
src = separate_inserts(src)
multi
|> build_multi_inserts(src, client)
|> build_multi_updates(src, client)
|> build_multi_deletes(src, client)
|> repo.transaction()
|> case do
{:ok, ops} ->
Enum.reduce(ops, %{}, fn {name, entity}, acc ->
tk_str = case String.split(name, ":") do
[tk_str, _id_str] -> tk_str
["$sorcery", _id_str, tk_str] -> tk_str
end
tk = String.to_existing_atom(tk_str)
acc
|> Map.put_new(tk, %{})
|> put_in([tk, entity.id], Map.from_struct(entity))
end)
error -> error
end
end
defp build_multi_inserts(multi, src, client) do
Enum.reduce(src.inserts, multi, fn {tk, table}, multi ->
Enum.reduce(table, multi, fn {id, entity}, multi ->
# Every id here should be in the format of "$sorcery:int"
schema = client.tables[tk].schema
cs = schema.sorcery_insert(struct(schema), entity)
multi_mod(client).insert(multi, id <> ":#{tk}", cs)
end)
end)
end
defp build_multi_updates(multi, src, client) do
Enum.reduce(src.changes_db, multi, fn {tk, table}, multi ->
Enum.reduce(table, multi, fn {id, entity}, multi ->
# Every id here should be an integer
schema = client.tables[tk].schema
multi_mod(client).update(multi, "#{tk}:#{id}", fn prev_ops ->
original_entity = get_original_entity(client.db, tk, id, schema)
new_entity = resolve_placeholder_ids(entity, prev_ops)
cs = schema.sorcery_update(original_entity, new_entity)
cs
end)
end)
end)
end
defp get_original_entity(db, tk, id, schema) do
table = Map.get(db, tk, %{})
entity = Map.get(table, id, %{})
defaults = struct(schema)
Map.merge(defaults, entity)
end
defp build_multi_deletes(multi, src, client) do
Enum.reduce(src.deletes, multi, fn {tk, id}, multi ->
schema = client.tables[tk].schema
cs = struct(schema, %{id: id})
multi_mod(client).delete(multi, "#{tk}:#{id}", cs)
end)
end
defp resolve_placeholder_ids(entity, ops) do
Enum.reduce(entity, entity, fn {k, v}, acc ->
case v do
"$sorcery:" <> _ ->
new_v = Map.get(ops, v).id
Map.put(acc, k, new_v)
_ -> acc
end
end)
end
# We must be careful to handle the placeholders first.
# They can go under :inserts
# And they can be removed from :changes_db
# While we're in there, we can also un-struct to avoid Access errors.
defp separate_inserts(src) do
empty_src =
src
|> Map.from_struct()
|> Map.put(:inserts, %{})
|> Map.put(:changes_db, %{})
Enum.reduce(src.changes_db, empty_src, fn {tk, table}, acc ->
{i, u} = Enum.reduce(table, {%{}, %{}}, fn {id, entity}, {i, u} ->
entity = if is_struct(entity), do: Map.from_struct(entity), else: entity
case id do
"$sorcery:" <> _ -> {Map.put(i, id, entity), u}
_ -> {i, Map.put(u, id, entity)}
end
end)
inserts = Map.merge(acc.inserts, i)
updates = Map.merge(acc.changes_db, u)
acc
|> put_in([:inserts, tk], inserts)
|> put_in([:changes_db, tk], updates)
end)
end
# Helpers
defp multi_mod(%{ecto: ecto}) do
Module.concat([ecto, "Multi"])
end
end