diff --git a/README.md b/README.md index 2bac89f..c3bbdd1 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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://.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://.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 @@ -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 @@ -102,13 +100,19 @@ iex> Supabase.init_client("https://", "") 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://", "", %{db: %{schema: "another"}}}) +iex> Supabase.init_client("https://", "", + 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. @@ -148,7 +152,7 @@ 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 @@ -156,19 +160,23 @@ defmodule MyApp.Supabase.Client do 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://", # required api_key: "", # required - conn: %{access_token: ""}, # optional - db: %{schema: "another"} # additional options + access_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 @@ -185,6 +193,8 @@ 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 @@ -192,6 +202,16 @@ 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) diff --git a/lib/supabase.ex b/lib/supabase.ex index ec9fbb5..0995953 100644 --- a/lib/supabase.ex +++ b/lib/supabase.ex @@ -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 @@ -47,18 +47,68 @@ 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://", "") + iex> {:ok, %Supabase.Client{}} + + iex> Supabase.init_client("https://", "", + 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 @@ -66,11 +116,11 @@ defmodule Supabase do {: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 @@ -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 diff --git a/lib/supabase/client.ex b/lib/supabase/client.ex index 5a5ce9e..4565ff4 100644 --- a/lib/supabase/client.ex +++ b/lib/supabase/client.ex @@ -12,9 +12,9 @@ defmodule Supabase.Client do iex> Supabase.init_client(base_url, api_key, %{}) {:ok, %Supabase.Client{}} - > That way of initialisation is useful when you want to manage the connection options yourself or create one off clients. + > 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 connection options yourself. To make it easier, you can use the `Supabase.Client` module to manage the connection options for you. + 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: @@ -22,16 +22,19 @@ defmodule Supabase.Client do use Supabase.Client, otp_app: :my_app end - This will automatically start an Agent process to manage the connection options for you. But for that to work, you need to configure your defined Supabase client in your `config.exs`: + 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: "", - conn: %{access_token: ""}, # optional - db: %{schema: "another"}, # default to public - auth: %{debug: true} # optional + # any additional options + access_token: "", + db: [schema: "another"], + auth: [debug: true] # optional - Another alternative would be to configure your Supabase Client at runtime, while starting your application: + Another alternative would be to configure your Supabase Client in code, while starting your application: defmodule MyApp.Application do use Application @@ -49,16 +52,14 @@ defmodule Supabase.Client do end end - For more information on how to configure your Supabase Client with additional options, please refer to the [Supabase official documentation](https://supabase.com/docs/reference/javascript/initializing) + For more information on how to configure your Supabase Client with additional options, please refer to the `Supabase.Client.t()` typespec. ## Examples %Supabase.Client{ - conn: %{ - base_url: "https://.supabase.io", - api_key: "", - access_token: "" - }, + base_url: "https://.supabase.io", + api_key: "", + access_token: "", db: %Supabase.Client.Db{ schema: "public" }, @@ -71,17 +72,9 @@ defmodule Supabase.Client do detect_session_in_url: true, flow_type: :implicit, persist_session: true, - storage: nil, storage_key: "sb--auth-token" } } - - iex> Supabase.Client.retrieve_connection(%Supabase.Client{}) - %Supabase.Client.Conn{ - base_url: "https://.supabase.io", - api_key: "", - access_token: "" - } """ use Ecto.Schema @@ -89,35 +82,52 @@ defmodule Supabase.Client do import Ecto.Changeset alias Supabase.Client.Auth - alias Supabase.Client.Conn 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__{ - conn: Conn.t(), + 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() } - @type params :: %{ - conn: Conn.params(), + @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 :: %{ db: Db.params(), global: Global.params(), 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, - retrieve_connection: 1, - retrieve_base_url: 1, - retrieve_auth_url: 2, - retrieve_storage_url: 2 - ] + import Supabase.Client, only: [update_access_token: 2] alias Supabase.MissingSupabaseConfig @@ -138,18 +148,19 @@ defmodule Supabase.Client do 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}, MyApp.Supabase.Client, + config :#{@otp_app}, #{inspect(unquote(module))}, base_url: "https://.supabase.co", api_key: "", - conn: %{access_token: ""}, # optional - db: %{schema: "another"}, # default to public - auth: %{debug: true} # optional + # 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 = [ - MyApp.Supabase.Client + #{inspect(unquote(module))} ] Supervisor.init(children, strategy: :one_for_one) @@ -199,9 +210,12 @@ defmodule Supabase.Client do 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) + Agent.start_link( + fn -> + Supabase.init_client!(base_url, api_key, params) + end, + name: name + ) end @doc """ @@ -220,128 +234,78 @@ defmodule Supabase.Client do 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 - embeds_one(:conn, Conn) - embeds_one(:db, Db) - embeds_one(:global, Global) - embeds_one(:auth, Auth) + 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 parse(params) :: {:ok, t} | {:error, Ecto.Changeset.t()} - def parse(attrs) do - %__MODULE__{} - |> cast(attrs, []) - |> cast_embed(:conn, required: true) + @spec changeset(source, attrs) :: {:ok, t} | {:error, changeset} + when source: t(), + changeset: Ecto.Changeset.t(), + attrs: %{ + base_url: String.t(), + api_key: String.t(), + db: Db.params(), + global: Global.params(), + auth: Auth.params() + } + def changeset(%__MODULE__{} = source, %{base_url: base_url, api_key: api_key} = attrs) do + source + |> 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) - |> maybe_put_assocs() - |> validate_required([:conn]) - |> 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 - - @spec update_access_token(t, String.t()) :: t - def update_access_token(%__MODULE__{} = client, access_token) do - path = [Access.key(:conn), Access.key(:access_token)] - put_in(client, path, access_token) - end - - @doc """ - Given a `Supabase.Client`, return the connection informations. - - ## Examples - - iex> Supabase.Client.retrieve_connection(%Supabase.Client{}) - %Supabase.Client.Conn{} - """ - @spec retrieve_connection(t) :: Conn.t() - def retrieve_connection(%__MODULE__{conn: conn}), do: conn - - @doc """ - Given a `Supabase.Client`, return the raw the base url for the Supabase project. - - ## Examples - - iex> Supabase.Client.retrieve_base_url(%Supabase.Client{}) - "https://.supabase.co" - """ - @spec retrieve_base_url(t) :: String.t() - def retrieve_base_url(%__MODULE__{conn: conn}) do - conn.base_url - end - - @spec retrieve_url(t, String.t()) :: URI.t() - defp retrieve_url(%__MODULE__{} = client, uri) do - client - |> retrieve_base_url() - |> URI.merge(uri) + |> 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 """ - Given a `Supabase.Client`, mounts the base url for the Auth/GoTrue feature. - - ## Examples - - iex> Supabase.Client.retrieve_auth_url(%Supabase.Client{}) - "https://.supabase.co/auth/v1" - """ - @spec retrieve_auth_url(t, String.t()) :: String.t() - def retrieve_auth_url(%__MODULE__{auth: auth} = client, uri \\ "/") do - client - |> retrieve_url(auth.uri) - |> URI.append_path(uri) - |> URI.to_string() - end + Helper function to swap the current acccess token being used in + the Supabase client instance. - @storage_endpoint "/storage/v1" + 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. - @doc """ - Given a `Supabase.Client`, mounts the base url for the Storage feature. + 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. - ## Examples - - iex> Supabase.Client.retrieve_storage_url(%Supabase.Client{}) - "https://.supabase.co/storage/v1" + If you're managing your own Supabase client state (aka one off clients) you can + use this helper function. """ - @spec retrieve_storage_url(t, String.t()) :: String.t() - def retrieve_storage_url(%__MODULE__{} = client, uri \\ "/") do - client - |> retrieve_url(@storage_endpoint) - |> URI.append_path(uri) - |> URI.to_string() + @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 @@ -354,7 +318,7 @@ defmodule Supabase.Client do concat([ line(), "base_url: ", - to_doc(client.conn.base_url, opts), + to_doc(client.base_url, opts), ",", line(), "schema: ", diff --git a/lib/supabase/client/auth.ex b/lib/supabase/client/auth.ex index 9258393..86a0bd2 100644 --- a/lib/supabase/client/auth.ex +++ b/lib/supabase/client/auth.ex @@ -22,13 +22,11 @@ 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(), flow_type: String.t(), persist_session: boolean(), - storage: String.t(), storage_key: String.t() } @@ -38,47 +36,27 @@ defmodule Supabase.Client.Auth do detect_session_in_url: boolean(), flow_type: String.t(), persist_session: boolean(), - storage: String.t(), storage_key: String.t() } - @storage_key_template "sb-$host-auth-token" + @flow_types ~w[implicit pkce magicLink]a @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) - field(:flow_type, Ecto.Enum, values: ~w[implicit pkce magicLink]a, default: :implicit) + field(:flow_type, Ecto.Enum, values: @flow_types, default: :implicit) field(:persist_session, :boolean, default: true) - field(:storage, :string) field(:storage_key, :string) end - def changeset(schema, params, supabase_url) do - schema - |> cast( - params, - ~w[auto_refresh_token debug detect_session_in_url persist_session flow_type storage]a - ) - |> validate_required( - ~w[auto_refresh_token debug detect_session_in_url persist_session flow_type]a - ) - |> put_storage_key(supabase_url) - end - - defp put_storage_key(%{valid?: false} = changeset, _), do: changeset + @fields ~w[auto_refresh_token debug detect_session_in_url persist_session flow_type storage_key]a - defp put_storage_key(changeset, url) do - host = - url - |> URI.new!() - |> Map.get(:host) - |> String.split(".") - |> List.first() - - storage_key = String.replace(@storage_key_template, "$host", host) - put_change(changeset, :storage_key, storage_key) + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(schema, params) do + schema + |> cast(params, @fields) + |> validate_required(@fields -- [:storage_key]) end end diff --git a/lib/supabase/client/behaviour.ex b/lib/supabase/client/behaviour.ex index 02975c4..87bacd4 100644 --- a/lib/supabase/client/behaviour.ex +++ b/lib/supabase/client/behaviour.ex @@ -12,6 +12,7 @@ defmodule Supabase.Client.Behaviour do @callback init :: {:ok, Client.t()} | {:error, Ecto.Changeset.t()} @callback get_client :: {:ok, Client.t()} | {:error, :not_found} @callback get_client(pid | atom) :: {:ok, Client.t()} | {:error, :not_found} + @callback set_auth(pid | atom, access_token :: String.t()) :: :ok - @optional_callbacks get_client: 0, get_client: 1 + @optional_callbacks get_client: 0, get_client: 1, set_auth: 2 end diff --git a/lib/supabase/client/conn.ex b/lib/supabase/client/conn.ex deleted file mode 100644 index e01a538..0000000 --- a/lib/supabase/client/conn.ex +++ /dev/null @@ -1,54 +0,0 @@ -defmodule Supabase.Client.Conn do - @moduledoc """ - Conn configuration for Supabase Client. This schema is used to configure - the connection options. This schema is embedded in the `Supabase.Client`. - - ## Fields - - - `:base_url` - The Supabase Project URL to use. This option is required. - - `:api_key` - The Supabase ProjectAPI Key to use. This option is required. - - `:access_token` - The access token to use. Default to the API key. - - For more information about the connection options, see the documentation for - the [client](https://supabase.com/docs/reference/javascript/initializing). - """ - - use Supabase, :schema - - @type t :: %__MODULE__{ - api_key: String.t(), - access_token: String.t(), - base_url: String.t() - } - - @type params :: %{ - api_key: String.t(), - access_token: String.t(), - base_url: String.t() - } - - @primary_key false - embedded_schema do - field(:api_key, :string) - field(:access_token, :string) - field(:base_url, :string) - end - - def changeset(schema \\ %__MODULE__{}, params) do - schema - |> cast(params, ~w[api_key access_token base_url]a) - |> maybe_put_access_token() - |> validate_required(~w[api_key base_url]a) - end - - defp maybe_put_access_token(changeset) do - api_key = get_change(changeset, :api_key) - token = get_change(changeset, :access_token) - - cond do - not changeset.valid? -> changeset - not is_nil(token) -> changeset - true -> put_change(changeset, :access_token, api_key) - end - end -end diff --git a/lib/supabase/client/global.ex b/lib/supabase/client/global.ex index 22b616b..02cc64b 100644 --- a/lib/supabase/client/global.ex +++ b/lib/supabase/client/global.ex @@ -19,6 +19,7 @@ defmodule Supabase.Client.Global do field(:headers, {:map, :string}, default: %{}) end + @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(schema, params) do schema |> cast(params, [:headers]) diff --git a/lib/supabase/fetcher.ex b/lib/supabase/fetcher.ex index 3d411af..84c49ec 100644 --- a/lib/supabase/fetcher.ex +++ b/lib/supabase/fetcher.ex @@ -286,8 +286,8 @@ defmodule Supabase.Fetcher do 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) + client.api_key + |> apply_headers(token || client.access_token, client.global.headers) |> merge_headers(headers) end diff --git a/test/supabase/client_test.exs b/test/supabase/client_test.exs new file mode 100644 index 0000000..ef391ef --- /dev/null +++ b/test/supabase/client_test.exs @@ -0,0 +1,60 @@ +defmodule Supabase.ClientTest do + use ExUnit.Case, async: true + + alias Supabase.Client + + @valid_base_url "https://test.supabase.co" + @valid_api_key "test_api_key" + + describe "Client struct defaults" do + test "has default values for db, global, and auth fields" do + client = %Client{} + + assert client.db.schema == "public" + assert client.global.headers == %{} + assert client.auth.auto_refresh_token == true + assert client.auth.debug == false + assert client.auth.detect_session_in_url == true + assert client.auth.flow_type == :implicit + assert client.auth.persist_session == true + assert client.auth.storage_key == nil + end + end + + defmodule TestClient do + use Supabase.Client, otp_app: :supabase_potion + end + + describe "Agent behavior" do + setup do + config = [ + base_url: @valid_base_url, + api_key: @valid_api_key, + access_token: "123", + auth: %{storage_key: "test-key", debug: true} + ] + + Application.put_env(:supabase_potion, TestClient, config) + pid = start_supervised!(TestClient) + {:ok, pid: pid} + end + + test "retrieves client from Agent", %{pid: pid} do + assert {:ok, %Client{} = client} = TestClient.get_client(pid) + assert client.base_url == @valid_base_url + assert client.api_key == @valid_api_key + assert client.access_token == "123" + assert client.auth.debug + assert client.auth.storage_key == "test-key" + end + + test "updates access token in client", %{pid: pid} do + new_access_token = "new_access_token" + assert {:ok, %Client{} = client} = TestClient.get_client(pid) + assert client.access_token == "123" + assert :ok = TestClient.set_auth(pid, new_access_token) + assert {:ok, %Client{} = client} = TestClient.get_client(pid) + assert client.access_token == new_access_token + end + end +end diff --git a/test/supabase_test.exs b/test/supabase_test.exs index 3db0cba..8441b35 100644 --- a/test/supabase_test.exs +++ b/test/supabase_test.exs @@ -6,21 +6,36 @@ defmodule SupabaseTest do describe "init_client/1" do test "should return a valid client on valid attrs" do - {:ok, %Client{} = client} = - Supabase.init_client("https://test.supabase.co", "test") + assert {:ok, %Client{} = client} = + Supabase.init_client("https://test.supabase.co", "test") - assert client.conn.base_url == "https://test.supabase.co" - assert client.conn.api_key == "test" + assert client.base_url == "https://test.supabase.co" + assert client.api_key == "test" + end + + test "should return a valid client on valid attrs and additional attrs" do + assert {:ok, %Client{} = client} = + Supabase.init_client("https://test.supabase.co", "test", + auth: [debug: true, storage_key: "test-key"], + db: [schema: "custom"] + ) + + assert client.base_url == "https://test.supabase.co" + assert client.api_key == "test" + assert client.auth.debug + assert client.auth.storage_key == "test-key" + assert client.db.schema == "custom" end end describe "init_client!/1" do test "should return a valid client on valid attrs" do - assert %Client{} = client = - Supabase.init_client!("https://test.supabase.co", "test") + assert %Client{} = + client = + Supabase.init_client!("https://test.supabase.co", "test") - assert client.conn.base_url == "https://test.supabase.co" - assert client.conn.api_key == "test" + assert client.base_url == "https://test.supabase.co" + assert client.api_key == "test" end test "should raise MissingSupabaseConfig on missing base_url" do