lib/rela.ex

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