Skip to content

Commit

Permalink
Improve and fix client options parsing (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe authored Jan 9, 2025
1 parent 534f2fb commit 1143708
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 284 deletions.
50 changes: 35 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ end
This library per si is the base foundation to user Supabase services from Elixir, so to integrate with specific services you need to add each client library you want to use.

Available client services are:
- [PostgREST](https://github.com/zoedsoupe/postgres-ex)
- [Storage](https://github.com/zoedsoupe/storage-ex)
- [Auth/GoTrue](https://github.com/zoedsoupe/gotrue-ex)
- [PostgREST](https://github.com/supabase-community/postgres-ex)
- [Storage](https://github.com/supabase-community/storage-ex)
- [Auth/GoTrue](https://github.com/supabase-community/auth-ex)

So if you wanna use the Storage and Auth/GoTrue services, your `mix.exs` should look like that:

Expand All @@ -70,10 +70,9 @@ A `Supabase.Client` holds general information about Supabase, that can be used t

`Supabase.Client` is defined as:

- `:conn` - connection information, the only required option as it is vital to the `Supabase.Client`.
- `:base_url` - The base url of the Supabase API, it is usually in the form `https://<app-name>.supabase.io`.
- `:api_key` - The API key used to authenticate requests to the Supabase API.
- `:access_token` - Token with specific permissions to access the Supabase API, it is usually the same as the API key.
- `:base_url` - The base url of the Supabase API, it is usually in the form `https://<app-name>.supabase.io`.
- `:api_key` - The API key used to authenticate requests to the Supabase API.
- `:access_token` - Token with specific permissions to access the Supabase API, it is usually the same as the API key.
- `:db` - default database options
- `:schema` - default schema to use, defaults to `"public"`
- `:global` - global options config
Expand All @@ -84,7 +83,6 @@ A `Supabase.Client` holds general information about Supabase, that can be used t
- `:detect_session_in_url` - detect session in URL, defaults to `true`
- `:flow_type` - authentication flow type, defaults to `"web"`
- `:persist_session` - persist session, defaults to `true`
- `:storage` - storage type
- `:storage_key` - storage key

### Usage
Expand All @@ -102,13 +100,19 @@ iex> Supabase.init_client("https://<supabase-url>", "<supabase-api-key>")
iex> {:ok, %Supabase.Client{}}
```

Any additional config can be passed as the third argument:
Any additional config can be passed as the third argument as an [Enumerable](https://hexdocs.pm/elixir/Enumerable.html):

```elixir
iex> Supabase.init_client("https://<supabase-url>", "<supabase-api-key>", %{db: %{schema: "another"}}})
iex> Supabase.init_client("https://<supabase-url>", "<supabase-api-key>",
db: [schema: "another"],
auth: [flow_type: :pkce],
global: [headers: %{"custom-header" => "custom-value"}]
)
iex> {:ok, %Supabase.Client{}}
```

> Note that one off clients are just raw elixir structs and therefore don't manage any state
For more information on the available options, see the [Supabase.Client](https://hexdocs.pm/supabase_potion/Supabase.Client.html) module documentation.

> There's also a bang version of `Supabase.init_client/3` that will raise an error if the client can't be created.
Expand Down Expand Up @@ -148,27 +152,31 @@ If you don't have experience with processes or is a Elixir begginner, you should
- [GenServer getting started](https://hexdocs.pm/elixir/genservers.html)
- [Supervison trees getting started](https://hexdocs.pm/elixir/supervisor-and-application.html)

So, to define a self managed client, you need to define a module that will hold the client state and the client process.
So, to define a self managed client, you need to define a module that will hold the client state and the client process as an [Agent](https://hexdocs.pm/elixir/Agent.html).

```elixir
defmodule MyApp.Supabase.Client do
use Supabase.Client, otp_app: :my_app
end
```

For that to work, you also need to configure the client in your `config.exs`:
For that to work, you also need to configure the client in your app configuration, it can be a compile-time config on `config.exs` or a runtime config in `runtime.exs`:

```elixir
import Config

# `:my_app` here is the same `otp_app` option you passed
config :my_app, MyApp.Supabase.Client,
base_url: "https://<supabase-url>", # required
api_key: "<supabase-api-key>", # required
conn: %{access_token: "<supabase-token>"}, # optional
db: %{schema: "another"} # additional options
access_token: "<supabase-token>", # optional
# additional options
db: [schema: "another"],
auth: [flow_type: :implicit, debug: true],
global: [headers: %{"custom-header" => "custom-value"}]
```

Then, you can start the client process in your application supervision tree:
Then, you can start the client process in your application supervision tree, generally in your `application.ex` module:

```elixir
defmodule MyApp.Application do
Expand All @@ -185,13 +193,25 @@ defmodule MyApp.Application do
end
```

> Of course, you can spawn as many clients you wanna, with different configurations if you need
Now you can interact with the client process:

```elixir
iex> {:ok, %Supabase.Client{} = client} = MyApp.Supabase.Client.get_client()
iex> Supabase.GoTrue.sign_in_with_password(client, email: "", password: "")
```

You can also update the `access_token` for it:

```elixir
iex> {:ok, %Supabase.Client{} = client} = MyApp.Supabase.Client.get_client()
iex> client.access_token == client.api_key
iex> :ok = MyApp.Supabase.Client.set_auth("new-access-token")
iex> {:ok, %Supabase.Client{} = client} = MyApp.Supabase.Client.get_client()
iex> client.access_token == "new-access-token"
```

For more examples on how to use the client, check clients implementations docs:
- [Supabase.GoTrue](https://hexdocs.pm/supabase_gotrue)
- [Supabase.Storage](https://hexdocs.pm/supabase_storage)
Expand Down
91 changes: 63 additions & 28 deletions lib/supabase.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ defmodule Supabase do
This package represents the base SDK for Supabase. That means
that it not includes all of the functionality of the Supabase client integrations, so you need to install each feature separetely, as:
- [Auth/GoTrue](https://github.com/zoedsoupe/gotrue-ex)
- [Storage](https://github.com/zoedsoupe/storage-ex)
- [PostgREST](https://github.com/zoedsoupe/postgrest-ex)
- [Supabase.GoTrue](https://hexdocs.pm/supabase_gotrue)
- [Supabase.Storage](https://hexdocs.pm/supabase_storage)
- [Supabase.PostgREST](https://hexdocs.pm/supabase_postgrest)
- `Realtime` - TODO
- `UI` - TODO
Expand Down Expand Up @@ -47,30 +47,80 @@ defmodule Supabase do

@typep changeset :: Ecto.Changeset.t()

@spec init_client(String.t(), String.t(), Client.params() | %{}) :: {:ok, Client.t()} | {:error, changeset}
@doc """
Creates a new one off Supabase client, you you wanna a self managed client, that
levarages an [Agent][https://hexdocs.pm/elixir/Agent.html] instance that can
started in your application supervision tree, check the `Supabase.Client` module docs.
## Parameters
- `base_url`: The unique Supabase URL which is supplied when you create a new project in your project dashboard.
- `api_key`: The unique Supabase Key which is supplied when you create a new project in your project dashboard.
- `options`: Additional options to configure the client behaviour, check `Supabase.Client.options()` typespec to check all available options.
## Examples
iex> Supabase.init_client("https://<supabase-url>", "<supabase-api-key>")
iex> {:ok, %Supabase.Client{}}
iex> Supabase.init_client("https://<supabase-url>", "<supabase-api-key>",
db: [schema: "another"],
auth: [flow_type: :pkce],
global: [headers: %{"custom-header" => "custom-value"}]
)
iex> {:ok, %Supabase.Client{}}
"""
@spec init_client(supabase_url, supabase_key, options) ::
{:ok, Client.t()} | {:error, changeset}
when supabase_url: String.t(),
supabase_key: String.t(),
options: Enumerable.t()
def init_client(url, api_key, opts \\ %{})
when is_binary(url) and is_binary(api_key) do
when is_binary(url) and is_binary(api_key) do
opts
|> Map.put(:conn, %{base_url: url, api_key: api_key})
|> Map.update(:conn, opts, &Map.merge(&1, opts[:conn] || %{}))
|> Client.parse()
|> Map.new()
|> Map.put(:base_url, url)
|> Map.put(:api_key, api_key)
|> then(&Client.changeset(%Client{}, &1))
|> Ecto.Changeset.apply_action(:parse)
|> then(&maybe_put_storage_key/1)
end

defp maybe_put_storage_key({:ok, %Client{base_url: base_url} = client}) do
maybe_default = &(Function.identity(&1) || default_storage_key(base_url))
{:ok, update_in(client.auth.storage_key, maybe_default)}
end

@spec init_client!(String.t, String.t, Client.params | %{}) :: Client.t() | no_return
def init_client!(url, api_key, %{} = opts \\ %{})
when is_binary(url) and is_binary(api_key) do
defp maybe_put_storage_key(other), do: other

defp default_storage_key(base_url) when is_binary(base_url) do
base_url
|> URI.parse()
|> then(&String.split(&1.host, ".", trim: true))
|> List.first()
|> then(&"sb-#{&1}-auth-token")
end

@doc """
Same as `Supabase.init_client/3` but raises if any errors occurs while
parsing the client options.
"""
@spec init_client!(supabase_url, supabase_key, options) :: Client.t()
when supabase_url: String.t(),
supabase_key: String.t(),
options: Enumerable.t()
def init_client!(url, api_key, opts \\ %{})
when is_binary(url) and is_binary(api_key) do
case init_client(url, api_key, opts) do
{:ok, client} ->
client

{:error, changeset} ->
errors = errors_on_changeset(changeset)

if "can't be blank" in (get_in(errors, [:conn, :api_key]) || []) do
if "can't be blank" in (errors[:api_key] || []) do
raise MissingSupabaseConfig, key: :key, client: nil
end

if "can't be blank" in (get_in(errors, [:conn, :base_url]) || []) do
if "can't be blank" in (errors[:base_url] || []) do
raise MissingSupabaseConfig, key: :url, client: nil
end

Expand All @@ -89,19 +139,4 @@ defmodule Supabase do
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end

def schema do
quote do
use Ecto.Schema
import Ecto.Changeset
alias __MODULE__

@opaque changeset :: Ecto.Changeset.t()

@callback changeset(__MODULE__.t(), map) :: changeset
@callback parse(map) :: {:ok, __MODULE__.t()} | {:error, changeset}

@optional_callbacks changeset: 2, parse: 1
end
end
end
Loading

0 comments on commit 1143708

Please sign in to comment.