lib/ex_tenant/schema.ex

defmodule ExTenant.Schema do
  @moduledoc """

    To use the Extenant.Schema in your schema modules
    -------------------------------------------------

    - it injects the belongs_to relationship to the tenant
    - it also raises a compiler error if the `tenanted()` call was omitted
    - it works with the `tenant_id` field set in Config.exs

    Here is an example
    ------------------

      defmodule Post do
        use ExTenant.Schema
        use ExTenant.Changeset

        tenanted_schema "posts" do
          field(:name, :string)
          field(:body, :string)

          tenanted()
        end
      end

  """
  require Ecto.Schema

  @doc false
  defmacro __using__(_opts) do
    quote do
      import ExTenant.Schema,
        only: [
          tenanted_schema: 2,
          tenanted_embedded_schema: 1,
          tenanted: 0
        ]

      use Ecto.Schema
    end
  end

  @doc """
    injects belongs_to relationship to the tenant model

    Example
    -------

      tenanted_schema "posts" do
        field(:name, :string)
        tenanted()
      end
  """
  defmacro tenanted do
    owner = __CALLER__.module

    quote do
      tenanted_field = ExTenant.Utils.tenanted_field()
      tenant_id_field = String.to_existing_atom(tenanted_field)

      Ecto.Schema.belongs_to(:tenant, unquote(owner), references: tenant_id_field)
    end
  end

  @doc """
    Replaces `Ecto.Schema.embedded_schema/2` - checking whether `tenanted()` has been
    called - that injects the `belongs_to` tenant relationship
  """
  defmacro tenanted_embedded_schema(do: block) do
    quote do
      Ecto.Schema.embedded_schema do
        unquote(inner(block))
      end
    end
  end

  @doc """
    Replaces `Ecto.Schema.schema/2` - checking whether `tenanted()` has been
    called - that injects the `belongs_to` tenant relationship
  """
  defmacro tenanted_schema(table_name, do: block) do
    quote do
      Ecto.Schema.schema unquote(table_name) do
        unquote(inner(block))
      end
    end
  end

  defp inner(block) do
    quote do
      unquote(apply_to_block(block))
    end
  end

  defp apply_to_block(block) do
    calls =
      case block do
        {:__block__, _, calls} ->
          calls

        call ->
          [call]
      end

    raise_if_tenanted_association_not_exists?(calls)

    {:__block__, [], calls}
  end

  defp raise_if_tenanted_association_not_exists?(calls) do
    calls
    |> Enum.filter(fn n ->
      key = elem(n, 0)
      key == :tenanted
    end)
    |> List.first()
    |> raise_error()
  end

  defp raise_error(nil), do: raise(ExTenant.TenantNotCalledInSchemaError)
  defp raise_error(_), do: :ok
end