defmodule Rela do
use Ecto.Schema
schema "" do
field(:actor_id, :integer)
field(:contractor_id, :integer)
field(:contractor, :string)
field(:created_at, :naive_datetime)
field(:is_deleted, :boolean)
end
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
import Ecto.Query
@sql Ecto.Adapters.SQL
@chng Ecto.Changeset
@repo opts[:repo]
@conns Enum.reduce(opts[:relations], %{}, fn
%{arity: :one_to_one, actor: actor, contractor: contractor}, acc ->
Map.merge(acc, %{{actor, contractor} => :one, {contractor, actor} => :one})
%{arity: :one_to_many, actor: actor, contractor: contractor}, acc ->
Map.merge(acc, %{{actor, contractor} => :many, {contractor, actor} => :one})
%{arity: :many_to_many, actor: actor, contractor: contractor}, acc ->
Map.merge(acc, %{{actor, contractor} => :many, {contractor, actor} => :many})
_ = type, _ ->
throw({:error, "INCORRECT TYPE FORMAT", type})
end)
def exists_relation?(left, right) do
exists_relation(left, right)
|> case do
{:ok, _} -> true
{:error, _} -> false
end
end
def exists_relation(left, _right) when not is_struct(left),
do: {:error, "left item is not a struct"}
def exists_relation(_left, right) when not is_struct(right),
do: {:error, "right item is not a struct"}
def exists_relation(
%{__meta__: %{source: left_table}, id: left_id},
%{__meta__: %{source: right_table}, __struct__: right_struct, id: right_id}
)
when not is_nil(left_id) and not is_nil(right_id) do
with true <- exists(left_table, left_id, right_id, right_table),
true <- exists(right_table, right_id, left_id, left_table) do
{:ok, "relation between #{left_table} and #{right_table} exists"}
else
false -> {:error, "relations between #{left_table} and #{right_table} not found"}
end
end
def exists_relation(left, _right) do
message = if is_nil(left[:id]), do: "left", else: "right"
{:error, "#{message} item has no id"}
end
def get_contractors!(left, right) do
get_contractors(left, right)
|> case do
{:ok, answer} -> answer
{:error, _} -> []
end
end
def get_contractors(left, _right) when not is_struct(left),
do: {:error, "left item is not a struct"}
def get_contractors(left, right) when not is_struct(right),
do: get_contractors(left, right.__struct__)
def get_contractors(
%{__meta__: %{source: left_table}, id: left_id},
%{__meta__: %{source: right_table}, __struct__: right_struct}
)
when not is_nil(left_id) do
with [_ | _] = right <- get(left_table, left_id, right_table),
contractors <-
Enum.map(
right,
fn a ->
Enum.at(
@repo.all(from(r in right_struct, where: r.id == ^a.contractor_id)),
0
)
end
) do
{:ok, contractors}
else
[] -> {:error, "relations between #{left_table} and #{right_table} not found"}
end
end
def get_contractors(left, _right), do: {:error, "left item has no id"}
def get_contractor!(left, right) do
get_contractor(left, right)
|> case do
{:ok, answer} -> answer
{:error, _} -> nil
end
end
def get_contractor(left, _right) when not is_struct(left),
do: {:error, "left item is not a struct"}
def get_contractor(left, right) when not is_struct(right),
do: get_contractor(left, right.__struct__)
def get_contractor(
%{__meta__: %{source: left_table}, __struct__: left_struct, id: left_id},
%{__meta__: %{source: right_table}, __struct__: right_struct}
)
when not is_nil(left_id) do
with [%{contractor_id: right_id} | _] <- get(left_table, left_id, right_table),
[contractor | _] <- @repo.all(from(r in right_struct, where: r.id == ^right_id)) do
{:ok, contractor}
else
[] -> {:error, "relation between #{left_table} and #{right_table} not found"}
end
end
def get_contractor(left, right), do: {:error, "left item has no id"}
def delete_rela(left, _right) when not is_struct(left),
do: {:error, "left item is not a struct"}
def delete_rela(_left, right) when not is_struct(right),
do: {:error, "right item is not a struct"}
def delete_rela(
%{__meta__: %{source: left_table}, __struct__: left_struct, id: left_id},
%{__meta__: %{source: right_table}, __struct__: right_struct, id: right_id}
)
when not is_nil(left_id) and not is_nil(right_id) do
with [left | _] <- get(left_table, left_id, right_id, right_table),
[right | _] <- get(right_table, right_id, left_id, left_table),
_ <- delete(left),
_ <- delete(right) do
{:ok, "relation between #{left_table} and #{right_table} deleted"}
else
[] -> {:error, "relation between #{left_table} and #{right_table} not found"}
end
end
def delete_rela(left, _right) do
if is_nil(left[:id]) do
{:error, "left item has no id"}
else
{:error, "right item has no id"}
end
end
def create(left, _right) when not is_struct(left),
do: {:error, "left item is not a struct"}
def create(_left, right) when not is_struct(right),
do: {:error, "right item is not a struct"}
def create(
%{__meta__: %{source: left_table}, __struct__: left_struct, id: left_id},
%{__meta__: %{source: right_table}, __struct__: right_struct, id: right_id}
)
when not is_nil(left_id) and not is_nil(right_id) do
with relation_arity when not is_tuple(relation_arity) <-
Map.get(
@conns,
{left_struct, right_struct},
{:error,
"relation between tables #{left_table} and #{right_table} doesn't exist"}
),
[] <-
if(relation_arity == :one,
do: get(left_table, left_id, right_table),
else: []
),
{1, _} <- insert(left_table, left_id, right_id, right_table),
{1, _} <- insert(right_table, right_id, left_id, left_table) do
{:ok, "relation between #{left_table} and #{right_table} created"}
else
[_ | _] -> {:error, "new relation violates arity agreements"}
any -> any
end
end
def create(left, _right) do
if is_nil(left[:id]) do
{:error, "left item has no id"}
else
{:error, "right item has no id"}
end
end
def get_conns(), do: @conns
def check_r_tables() do
Enum.map(@conns, fn {{actor, _}, _} -> actor.__struct__.__meta__.source end)
|> Enum.uniq()
|> Enum.each(
&@sql.query(
@repo,
"CREATE TABLE IF NOT EXISTS r_#{&1} (id serial PRIMARY KEY, actor_id INT NOT NULL, contractor_id INT NOT NULL, contractor VARCHAR(50) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), is_deleted BOOLEAN NOT NULL DEFAULT false, FOREIGN KEY (actor_id) REFERENCES #{&1} (id))"
)
)
end
defp get(table, actor_id, contractor) do
@repo.all(
from(r in {"r_#{table}", Rela},
where: r.actor_id == ^actor_id and r.contractor == ^contractor and not r.is_deleted,
select: r
)
)
end
defp get(table, actor_id, contractor_id, contractor) do
@repo.all(
from(r in {"r_#{table}", Rela},
where:
r.actor_id == ^actor_id and r.contractor == ^contractor and
r.contractor_id == ^contractor_id and not r.is_deleted,
select: r
)
)
end
defp insert(table, actor_id, contractor_id, contractor, first_time? \\ true) do
try do
if not exists(table, actor_id, contractor_id, contractor) do
@repo.insert_all("r_#{table}", [
%{actor_id: actor_id, contractor_id: contractor_id, contractor: contractor}
])
else
{:error, "relation between #{table} and #{contractor} already exists"}
end
rescue
any ->
IO.inspect(any)
check_r_tables()
if(first_time?,
do: insert(table, actor_id, contractor_id, contractor, false),
else: {:error, "something went wrong when inserting relation: #{any}"}
)
end
end
defp exists(table, actor_id, contractor_id, contractor) do
@repo.exists?(
from(r in {"r_#{table}", Rela},
where:
r.actor_id == ^actor_id and r.contractor_id == ^contractor_id and
r.contractor == ^contractor
)
)
end
defp delete(rela) do
rela
|> @chng.change(is_deleted: true)
|> @repo.update()
end
end
end
end