Skip to content

Commit

Permalink
feat: add url helpers and handle more API errors
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe committed Nov 18, 2023
1 parent ee22e9e commit 22b9b6a
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 33 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ result

# Secrets files
.env

/.lexical/
12 changes: 5 additions & 7 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
{
description = "A complete Supabase SDK for Elixir alchemists";

inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";

outputs = {nixpkgs, ...}: let
system = "aarch64-darwin";
pkgs = import nixpkgs {inherit system;};
Expand All @@ -15,7 +13,7 @@
name = "supabase-potion";
shellHook = "mkdir -p $PWD/.nix-mix";
packages = with pkgs;
[beam.packages.erlangR26.elixir postgresql_15]
[beam.packages.erlangR26.elixir_1_15]
++ lib.optional stdenv.isDarwin [
darwin.apple_sdk.frameworks.CoreServices
darwin.apple_sdk.frameworks.CoreFoundation
Expand Down
33 changes: 31 additions & 2 deletions lib/supabase.ex
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,16 @@ defmodule Supabase do
@spec init_client(params) :: {:ok, pid} | {:error, changeset}
when params: Client.params()
def init_client(%{} = opts) do
conn = Map.get(opts, :conn, %{})
opts = maybe_merge_config_from_application(conn, opts)

with {:ok, opts} <- Client.parse(opts) do
name = ClientRegistry.named(opts.name)
client_opts = [name: name, client_info: opts]
ClientSupervisor.start_child({Client, client_opts})
end
rescue
_ -> {:error, :missing_config}
end

def init_client!(%{} = opts) do
Expand All @@ -133,9 +138,33 @@ defmodule Supabase do
defp maybe_merge_config_from_application(%{base_url: _, api_key: _}, opts), do: opts

defp maybe_merge_config_from_application(%{}, opts) do
base_url = Application.get_env(:supabase, :supabase_url) || raise MissingSupabaseConfig, :url
api_key = Application.get_env(:supabase, :supabase_key) || raise MissingSupabaseConfig, :key
base_url =
Application.get_env(:supabase_potion, :supabase_base_url) ||
raise MissingSupabaseConfig, :url

api_key =
Application.get_env(:supabase_potion, :supabase_api_key) ||
raise MissingSupabaseConfig, :key

Map.put(opts, :conn, %{base_url: base_url, api_key: api_key})
end

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
43 changes: 32 additions & 11 deletions lib/supabase/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ defmodule Supabase.Client do

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

@type client :: atom | pid

@type t :: %__MODULE__{
name: atom,
conn: Conn.t(),
Expand Down Expand Up @@ -159,21 +161,40 @@ defmodule Supabase.Client do
defp maybe_parse(%__MODULE__{} = client), do: client
defp maybe_parse(params), do: parse!(params)

@spec retrieve_client(name) :: Supabase.Client.t() | nil
@spec retrieve_client(name) :: {:ok, Supabase.Client.t()} | {:error, :client_not_started}
when name: atom | pid
def retrieve_client(name) when is_atom(name) do
pid = ClientRegistry.lookup(name)
pid && Agent.get(pid, & &1)
def retrieve_client(source) do
if is_atom(source) do
pid = ClientRegistry.lookup(source)
{:ok, Agent.get(pid, & &1)}
else
{:ok, Agent.get(source, & &1)}
end
rescue
_ -> {:error, :client_not_started}
end

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

@spec retrieve_connection(name) :: Conn.t() | nil
@spec retrieve_connection(name) :: {:ok, Conn.t()} | {:error, :client_not_started}
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))
def retrieve_connection(source) do
with {:ok, client} <- retrieve_client(source) do
client.conn
end
end

def retrieve_connection(pid) when is_pid(pid), do: Agent.get(pid, &Map.get(&1, :conn))
def retrieve_base_url(%__MODULE__{conn: conn}) do
conn.base_url
end

def retrieve_url(%__MODULE__{} = client, uri) do
client
|> retrieve_base_url()
|> URI.merge(uri)
end

def retrieve_auth_url(%__MODULE__{auth: auth} = client, uri \\ "/") do
client
|> retrieve_url(auth.uri)
|> URI.append_path(uri)
end
end
2 changes: 2 additions & 0 deletions lib/supabase/client/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ defmodule Supabase.Client.Auth do
import Ecto.Changeset

@type t :: %__MODULE__{
uri: String.t(),
auto_refresh_token: boolean(),
debug: boolean(),
detect_session_in_url: boolean(),
Expand All @@ -45,6 +46,7 @@ defmodule Supabase.Client.Auth do

@primary_key false
embedded_schema do
field(:uri, :string, default: "/auth/v1")
field(:auto_refresh_token, :boolean, default: true)
field(:debug, :boolean, default: false)
field(:detect_session_in_url, :boolean, default: true)
Expand Down
5 changes: 2 additions & 3 deletions lib/supabase/client/conn.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ defmodule Supabase.Client.Conn do
the [client](https://supabase.com/docs/reference/javascript/initializing).
"""

use Ecto.Schema
import Ecto.Changeset
use Supabase, :schema

@type t :: %__MODULE__{
api_key: String.t(),
Expand All @@ -35,7 +34,7 @@ defmodule Supabase.Client.Conn do
field(:base_url, :string)
end

def changeset(schema, params) do
def changeset(schema \\ %__MODULE__{}, params) do
schema
|> cast(params, ~w[api_key access_token base_url]a)
|> maybe_put_access_token()
Expand Down
55 changes: 48 additions & 7 deletions lib/supabase/fetcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,19 @@ defmodule Supabase.Fetcher do
end

defp merge_headers(some, other) do
some = if is_list(some), do: some, else: Map.to_list(some)
other = if is_list(other), do: other, else: Map.to_list(other)

some
|> Kernel.++(other)
|> Enum.dedup_by(fn {name, _} -> name end)
|> Enum.reject(fn {_, v} -> is_nil(v) end)
end

def apply_client_headers(%Supabase.Client{} = client, token \\ nil, headers \\ []) do
client.conn.api_key
|> apply_headers(token || client.conn.access_token, client.global.headers)
|> merge_headers(headers)
end

defp format_response({:error, %{reason: reason}}) do
Expand All @@ -241,6 +251,14 @@ defmodule Supabase.Fetcher do
{:error, :not_found}
end

defp format_response({:ok, %{status: 401}}) do
{:error, :unauthorized}
end

defp format_response({:ok, %{status: 204}}) do
{:ok, :no_body}
end

defp format_response({:ok, %{status: s, body: body}}) when s in 200..300 do
result =
case Jason.decode(body) do
Expand All @@ -252,18 +270,41 @@ defmodule Supabase.Fetcher do
end

defp format_response({:ok, %{status: s, body: body}}) when s in 400..499 do
msg = Jason.decode!(body)["message"]
{:error, format_bad_request_error(Jason.decode!(body))}
end

reason =
defp format_response({:ok, %{status: s}}) when s >= 500 do
{:error, :server_error}
end

defp format_bad_request_error(%{"message" => msg}) do
case msg do
"The resource was not found" -> :not_found
_ -> msg
end
end

defp format_bad_request_error(%{"code" => 429, "msg" => msg}) do
if String.starts_with?(msg, "For security purposes,") do
[seconds] = ~r/\d+/ |> Regex.run(msg) |> String.to_integer()
{:error, {:rate_limit_until_seconds, seconds}}
else
case msg do
"The resource was not found" -> :not_found
_ -> msg
"Email rate limit exceeded" -> :email_rate_limit
end
end
end

{:error, reason}
defp format_bad_request_error(%{"error" => err, "error_description" => desc}) do
case {err, desc} do
{"invalid_grant", nil} -> :invalid_grant
{"invalid_grant", "Invalid login credentials"} -> {:invalid_grant, :invalid_credentials}
{"invalid_grant", "Email not confirmed"} -> {:invalid_grant, :email_not_confirmed}
{"invalid_grant", err} -> {:invalid_grant, err}
end
end

defp format_response({:ok, %{status: s}}) when s >= 500 do
{:error, :server_error}
defp format_bad_request_error(err) do
err
end
end

0 comments on commit 22b9b6a

Please sign in to comment.