Current section

Files

Jump to
supabase_potion lib supabase client.ex
Raw

lib/supabase/client.ex

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://<app-name>.supabase.io"
iex> api_key = "<supabase-api-key>"
iex> Supabase.init_client(base_url, api_key, %{})
{:ok, %Supabase.Client{}}
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://<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_key: "sb-<host>-auth-token"
},
storage: %Supabase.Client.Storage{
use_new_hostname: false
}
}
"""
use Ecto.Schema
import Ecto.Changeset
alias Supabase.Client.Auth
alias Supabase.Client.Db
alias Supabase.Client.Global
alias Supabase.Client.Storage
@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(),
storage: Storage.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(),
optional(:storage) => Storage.params()
}
@deprecated """
The self-managed client pattern using Agents is deprecated and will be removed in v1.0.
This pattern causes race conditions and security vulnerabilities in multi-user server
environments where multiple requests share the same Agent state. User tokens can become mixed, allowing User A to access User B's data.
Instead, one can still use the macro utility to better organize the client
initialization:
defmodule MyApp.Supabase do
# this will define both `get_client/0` and `set_auth/1`
use Supabase.Client, otp_app: :my_app
end
And pass client struct explicitly to Supabase functions, even for Plug/Live View helpers generated by `supabase.gen.auth`, like:
# Before
MyApp.Auth.log_in_with_password(conn, %{email: "", password: ""})
# After
MyApp.Auth.log_in_with_password(client, conn, %{email: "", password: ""})
"""
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://<app-name>.supabase.co",
api_key: "<supabase-api-key>",
# additional options
access_token: "<supabase-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)
embeds_one(:storage, Storage, 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)
|> cast_embed(:storage, 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_storage_url()
|> put_change(:realtime_url, Path.join(base_url, "realtime/v1"))
end
@spec put_storage_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp put_storage_url(%Ecto.Changeset{} = changeset) do
base_url = get_field(changeset, :base_url)
if is_binary(base_url) and base_url != "" do
default_storage_url = Path.join(base_url, "storage/v1")
storage_url = Storage.Hostname.transform_storage_url(default_storage_url)
put_change(changeset, :storage_url, storage_url)
else
changeset
end
end
@doc """
Helper function to swap the current acccess token being used in
the Supabase client instance.
"""
@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