defmodule Supabase.Client do @moduledoc """ A client for interacting with Supabase. This module is responsible for managing the connection options for your Supabase project. ## Usage Generally, you can start a client by calling `Supabase.init_client/3`: iex> base_url = "https://.supabase.io" iex> api_key = "" iex> Supabase.init_client(base_url, api_key, %{}) {:ok, %Supabase.Client{}} > That way of initialisation is useful when you want to manage the client state by yourself or create one off clients. However, starting a client directly means you have to manage the client state by yourself. To make it easier, you can use the `Supabase.Client` module to manage the connection options for you, which we call a "self managed client". To achieve this you can use the `Supabase.Client` module in your module: defmodule MyApp.Supabase.Client do use Supabase.Client, otp_app: :my_app end This will automatically start an [Agent](https://hexdocs.pm/elixir/Agent.html) process to manage the state for you. But for that to work, you need to configure your Supabase client options in your application configuration, either in compile-time (`config.exs`) or runtime (`runtime.exs`): # config/runtime.exs or config/config.exs config :my_app, MyApp.Supabase.Client, base_url: "https://.supabase.co", api_key: "", # any additional options access_token: "", db: [schema: "another"], auth: [debug: true] # optional Another alternative would be to configure your Supabase Client in code, while starting your application: defmodule MyApp.Application do use Application def start(_type, _args) do children = [ {MyApp.Supabase.Client, [ base_url: "https://.supabase.co", api_key: "" ]} ] opts = [strategy: :one_for_one, name: MyApp.Supabase.Client.Supervisor] Supervisor.start_link(children, opts) end end For more information on how to configure your Supabase Client with additional options, please refer to the `Supabase.Client.t()` typespec. ## Examples %Supabase.Client{ base_url: "https://.supabase.io", api_key: "", 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_key: "sb--auth-token" } } """ use Ecto.Schema import Ecto.Changeset alias Supabase.Client.Auth alias Supabase.Client.Db alias Supabase.Client.Global @typedoc """ The type of the `Supabase.Client` that will be returned from `Supabase.init_client/3`. ## Source https://supabase.com/docs/reference/javascript/initializing """ @type t :: %__MODULE__{ base_url: String.t(), access_token: String.t(), api_key: String.t(), # helper fields realtime_url: String.t(), auth_url: String.t(), functions_url: String.t(), database_url: String.t(), storage_url: String.t(), # "public" options db: Db.t(), global: Global.t(), auth: Auth.t() } @typedoc """ The type for the available additional options that can be passed to `Supabase.init_client/3` to configure the Supabase client. Note that these options can be passed to `Supabase.init_client/3` as `Enumerable`, which means it can be either a `Keyword.t()` or a `Map.t()`, but internally it will be passed as a map. """ @type options :: %{ optional(:db) => Db.params(), optional(:global) => Global.params(), optional(:auth) => Auth.params() } defmacro __using__(otp_app: otp_app) do module = __CALLER__.module quote do use Agent import Supabase.Client, only: [update_access_token: 2] alias Supabase.MissingSupabaseConfig @behaviour Supabase.Client.Behaviour @otp_app unquote(otp_app) @doc """ Start an Agent process to manage the Supabase client instance. ## Usage First, define your client module and use the `Supabase.Client` module: defmodule MyApp.Supabase.Client do use Supabase.Client, otp_app: :my_app end Note that you need to configure it with your Supabase project details. You can do this by setting the `base_url` and `api_key` in your `config.exs` file: config :#{@otp_app}, #{inspect(unquote(module))}, base_url: "https://.supabase.co", api_key: "", # additional options access_token: "", db: [schema: "another"], auth: [debug: true] Then, on your `application.ex` file, you can start the agent process by adding your defined client into the Supervision tree of your project: def start(_type, _args) do children = [ #{inspect(unquote(module))} ] Supervisor.init(children, strategy: :one_for_one) end For alternatives on how to start and define your Supabase client instance, please refer to the [Supabase.Client module documentation](https://hexdocs.pm/supabase_potion/Supabase.Client.html). For more information on how to start an Agent process, please refer to the [Agent module documentation](https://hexdocs.pm/elixir/Agent.html). """ def start_link(opts \\ []) def start_link(opts) when is_list(opts) and opts == [] do config = Application.get_env(@otp_app, __MODULE__) if is_nil(config) do raise MissingSupabaseConfig, key: :config, client: __MODULE__, otp_app: @otp_app end base_url = Keyword.get(config, :base_url) api_key = Keyword.get(config, :api_key) name = Keyword.get(config, :name, __MODULE__) params = Map.new(config) if is_nil(base_url) do raise MissingSupabaseConfig, key: :url, client: __MODULE__, otp_app: @otp_app end if is_nil(api_key) do raise MissingSupabaseConfig, key: :key, client: __MODULE__, otp_app: @otp_app end Agent.start_link(fn -> Supabase.init_client!(base_url, api_key, params) end, name: name) end def start_link(opts) when is_list(opts) do base_url = Keyword.get(opts, :base_url) api_key = Keyword.get(opts, :api_key) if is_nil(base_url) do raise MissingSupabaseConfig, key: :url, client: __MODULE__, otp_app: @otp_app end if is_nil(api_key) do raise MissingSupabaseConfig, key: :key, client: __MODULE__, otp_app: @otp_app end name = Keyword.get(opts, :name, __MODULE__) params = Map.new(opts) Agent.start_link( fn -> Supabase.init_client!(base_url, api_key, params) end, name: name ) end @doc """ This function is an alias for `start_link/1` with no arguments. """ @impl Supabase.Client.Behaviour def init, do: start_link([]) @doc """ Retrieve the client instance from the Agent process, so you can use it to interact with the Supabase API. """ @impl Supabase.Client.Behaviour def get_client(pid \\ __MODULE__) do case Agent.get(pid, & &1) do nil -> {:error, :not_found} client -> {:ok, client} end end @doc """ This function updates the `access_token` field of client that will then be used by the integrations as the `Authorization` header in requests, by default the `access_token` have the same value as the `api_key`. """ @impl Supabase.Client.Behaviour def set_auth(pid \\ __MODULE__, token) when is_binary(token) do Agent.update(pid, &update_access_token(&1, token)) end end end @primary_key false embedded_schema do field(:api_key, :string) field(:access_token, :string) field(:base_url, :string) field(:realtime_url, :string) field(:auth_url, :string) field(:storage_url, :string) field(:functions_url, :string) field(:database_url, :string) embeds_one(:db, Db, defaults_to_struct: true, on_replace: :update) embeds_one(:global, Global, defaults_to_struct: true, on_replace: :update) embeds_one(:auth, Auth, defaults_to_struct: true, on_replace: :update) end @spec changeset(attrs :: map) :: Ecto.Changeset.t() def changeset(%{base_url: base_url, api_key: api_key} = attrs) do %__MODULE__{} |> cast(attrs, [:api_key, :base_url, :access_token]) |> put_change(:access_token, attrs[:access_token] || api_key) |> cast_embed(:db, required: false) |> cast_embed(:global, required: false) |> cast_embed(:auth, required: false) |> validate_required([:access_token, :base_url, :api_key]) |> put_change(:auth_url, Path.join(base_url, "auth/v1")) |> put_change(:functions_url, Path.join(base_url, "functions/v1")) |> put_change(:database_url, Path.join(base_url, "rest/v1")) |> put_change(:storage_url, Path.join(base_url, "storage/v1")) |> put_change(:realtime_url, Path.join(base_url, "realtime/v1")) end @doc """ Helper function to swap the current acccess token being used in the Supabase client instance. Note that this functions shoudln't be used directly if you are using a self managed client (aka started it into your supervision tree as the `Supabase.Client` moduledoc says), since it will return the updated client but it **won't** update the inner client in the `Agent` process. To update the access token for a self managed client, you can use the `set_auth/2` function that is generated when you configure your client module. If you're managing your own Supabase client state (aka one off clients) you can use this helper function. """ @spec update_access_token(t, String.t()) :: t def update_access_token(%__MODULE__{} = client, access_token) do %{client | access_token: access_token} end defimpl Inspect, for: Supabase.Client do import Inspect.Algebra def inspect(%Supabase.Client{} = client, opts) do concat([ "#Supabase.Client<", nest( concat([ line(), "base_url: ", to_doc(client.base_url, opts), ",", line(), "schema: ", to_doc(client.db.schema, opts), ",", line(), "auth: (", "flow_type: ", to_doc(client.auth.flow_type, opts), ", ", "persist_session: ", to_doc(client.auth.persist_session, opts), ")" ]), 2 ), line(), ">" ]) end end end