lib/supabase/client.ex

defmodule Supabase.Client do
  @moduledoc """
  A client for interacting with Supabase. This module is responsible for
  managing the connection pool and the connection options.

  ## Usage

  Usually you don't need to use this module directly, instead you should
  use the `Supabase` module, available on `:supabase_potion` application.

  However, if you want to manage clients manually, you can leverage this
  module to start and stop clients dynamically. To start a single
  client manually, you need to add it to your supervision tree:

      defmodule MyApp.Application do
        use Application

        def start(_type, _args) do
          children = [
            {Supabase.Client, name: :supabase, client_info: %Supabase.Client{}}
          ]

          opts = [strategy: :one_for_one, name: MyApp.Supervisor]
          Supervisor.start_link(children, opts)
        end
      end

  Notice that starting a Client in this way, Client options will not be
  validated, so you need to make sure that the options are correct. Otherwise
  application will crash.

  ## Examples

      iex> Supabase.Client.start_link(name: :supabase, client_info: client_info)
      {:ok, #PID<0.123.0>}

      iex> Supabase.Client.retrieve_client(:supabase)
      %Supabase.Client{
        name: :supabase,
        conn: %{
          base_url: "https://<app-name>.supabase.io",
          api_key: "<supabase-api-key>",
          access_token: "<supabase-access-token>"
        },
        db: %Supabase.Client.Db{
          schema: "public"
        },
        global: %Supabase.Client.Global{
          headers: %{}
        },
        auth: %Supabase.Client.Auth{
          auto_refresh_token: true,
          debug: false,
          detect_session_in_url: true,
          flow_type: :implicit,
          persist_session: true,
          storage: nil,
          storage_key: "sb-<host>-auth-token"
        }
      }

      iex> Supabase.Client.retrieve_connection(:supabase)
      %Supabase.Client.Conn{
        base_url: "https://<app-name>.supabase.io",
        api_key: "<supabase-api-key>",
        access_token: "<supabase-access-token>"
      }
  """

  use Agent
  use Ecto.Schema

  import Ecto.Changeset

  alias Supabase.Client.Auth
  alias Supabase.Client.Conn
  alias Supabase.Client.Db
  alias Supabase.Client.Global

  alias Supabase.ClientRegistry

  defguard is_client(v) when is_atom(v) or is_pid(v)

  @type t :: %__MODULE__{
          name: atom,
          conn: Conn.t(),
          db: Db.t(),
          global: Global.t(),
          auth: Auth.t()
        }

  @type params :: %{
          name: atom,
          conn: Conn.params(),
          db: Db.params(),
          global: Global.params(),
          auth: Auth.params()
        }

  @primary_key false
  embedded_schema do
    field(:name, Supabase.Types.Atom)

    embeds_one(:conn, Conn)
    embeds_one(:db, Db)
    embeds_one(:global, Global)
    embeds_one(:auth, Auth)
  end

  @spec parse(params) :: {:ok, Supabase.Client.t()} | {:error, Ecto.Changeset.t()}
  def parse(attrs) do
    %__MODULE__{}
    |> cast(attrs, [:name])
    |> cast_embed(:conn, required: true)
    |> cast_embed(:db, required: false)
    |> cast_embed(:global, required: false)
    |> cast_embed(:auth, required: false)
    |> validate_required([:name])
    |> maybe_put_assocs()
    |> apply_action(:parse)
  end

  @spec parse!(params) :: Supabase.Client.t()
  def parse!(attrs) do
    case parse(attrs) do
      {:ok, changeset} ->
        changeset

      {:error, changeset} ->
        raise Ecto.InvalidChangesetError, changeset: changeset, action: :parse
    end
  end

  defp maybe_put_assocs(%{valid?: false} = changeset), do: changeset

  defp maybe_put_assocs(changeset) do
    auth = get_change(changeset, :auth)
    db = get_change(changeset, :db)
    global = get_change(changeset, :global)

    changeset
    |> maybe_put_assoc(:auth, auth, %Auth{})
    |> maybe_put_assoc(:db, db, %Db{})
    |> maybe_put_assoc(:global, global, %Global{})
  end

  defp maybe_put_assoc(changeset, key, nil, default),
    do: put_change(changeset, key, default)

  defp maybe_put_assoc(changeset, _key, _assoc, _default), do: changeset

  def start_link(config) do
    name = Keyword.get(config, :name)
    client_info = Keyword.get(config, :client_info)

    Agent.start_link(fn -> maybe_parse(client_info) end, name: name || __MODULE__)
  end

  defp maybe_parse(%__MODULE__{} = client), do: client
  defp maybe_parse(params), do: parse!(params)

  @spec retrieve_client(name) :: Supabase.Client.t() | nil
        when name: atom | pid
  def retrieve_client(name) when is_atom(name) do
    pid = ClientRegistry.lookup(name)
    pid && Agent.get(pid, & &1)
  end

  def retrieve_client(pid) when is_pid(pid), do: Agent.get(pid, & &1)

  @spec retrieve_connection(name) :: Conn.t() | nil
        when name: atom | pid
  def retrieve_connection(name) when is_atom(name) do
    pid = ClientRegistry.lookup(name)
    pid && Agent.get(pid, &Map.get(&1, :conn))
  end

  def retrieve_connection(pid) when is_pid(pid), do: Agent.get(pid, &Map.get(&1, :conn))
end