diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index b5f7d71..2e17d72 100644 --- a/README.md +++ b/README.md @@ -123,4 +123,3 @@ This feature provides: - **Request cancellation**: Long-running requests will timeout and be cancelled - **Better resource management**: Prevents hanging connections - **Comprehensive timeout coverage**: Sets both receive timeout (per-chunk) and request timeout (complete response) -- **Feature parity with JS client**: Matches timeout functionality in the JavaScript SDK diff --git a/lib/supabase/functions.ex b/lib/supabase/functions.ex index 01c1a28..8309806 100644 --- a/lib/supabase/functions.ex +++ b/lib/supabase/functions.ex @@ -25,6 +25,7 @@ defmodule Supabase.Functions do - `region`: The Region to invoke the function in. - `on_response`: The custom response handler for response streaming. - `timeout`: The timeout in milliseconds for the request. Defaults to 15 seconds. + - `access_token`: Override the authorization token for this request. """ @type opt :: {:body, Fetcher.body()} @@ -33,6 +34,7 @@ defmodule Supabase.Functions do | {:region, region} | {:on_response, on_response} | {:timeout, pos_integer()} + | {:access_token, String.t()} @type on_response :: ({Fetcher.status(), Fetcher.headers(), body :: Enumerable.t()} -> Supabase.result(Response.t())) @@ -54,6 +56,24 @@ defmodule Supabase.Functions do | :"ca-central-1" | :"eu-central-1" + @doc """ + Updates the access token for a client + + Creates a new client instance with the updated access token. This provides + feature parity with the JavaScript client's `setAuth(token)` method. + + ## Examples + + # Update auth token functionally + new_client = Supabase.Functions.update_auth(client, "new_token") + {:ok, response} = Supabase.Functions.invoke(new_client, "my-function") + + """ + @spec update_auth(Client.t(), String.t()) :: Client.t() + def update_auth(%Client{} = client, token) when is_binary(token) do + %{client | access_token: token} + end + @doc """ Invokes a function @@ -62,6 +82,19 @@ defmodule Supabase.Functions do - When you pass in a body to your function, we automatically attach the `Content-Type` header automatically. If it doesn't match any of these types we assume the payload is json, serialize it and attach the `Content-Type` header as `application/json`. You can override this behavior by passing in a `Content-Type` header of your own. - Responses are automatically parsed as json depending on the Content-Type header sent by your function. Responses are parsed as text by default. + ## Authentication + + You can override the authorization token for a specific request using the `access_token` option: + + # Use a different token for this request + {:ok, response} = Supabase.Functions.invoke(client, "my-function", access_token: "new_token") + + Alternatively, you can update the client's auth token functionally: + + # Update the client's token + new_client = Supabase.Functions.update_auth(client, "new_token") + {:ok, response} = Supabase.Functions.invoke(new_client, "my-function") + ## Timeout Support You can set a timeout for function invocations using the `timeout` option. This sets both the @@ -84,6 +117,9 @@ defmodule Supabase.Functions do {:ok, response} = Supabase.Functions.invoke(client, "my-function", body: %{data: "value"}, timeout: 30_000) + + # With custom access token + {:ok, response} = Supabase.Functions.invoke(client, "my-function", access_token: "custom_token") """ @spec invoke(Client.t(), function :: String.t(), opts) :: Supabase.result(Response.t()) def invoke(%Client{} = client, name, opts \\ []) when is_binary(name) do @@ -91,7 +127,14 @@ defmodule Supabase.Functions do custom_headers = opts[:headers] || %{} timeout = opts[:timeout] || 15_000 - client + effective_client = + if opts[:access_token] do + update_auth(client, opts[:access_token]) + else + client + end + + effective_client |> Request.new(decode_body?: false) |> Request.with_functions_url(name) |> Request.with_method(method) diff --git a/test/supabase/functions_test.exs b/test/supabase/functions_test.exs index a619ff5..3e17d66 100644 --- a/test/supabase/functions_test.exs +++ b/test/supabase/functions_test.exs @@ -257,4 +257,153 @@ defmodule Supabase.FunctionsTest do assert response == "chunk1chunk2" end end + + describe "update_auth/2" do + test "returns a new client with updated access token", %{client: client} do + new_token = "new_test_token" + updated_client = Functions.update_auth(client, new_token) + + assert updated_client.access_token == new_token + # Original client should remain unchanged + assert client.access_token != new_token + # All other fields should remain the same + assert updated_client.base_url == client.base_url + assert updated_client.api_key == client.api_key + end + + test "updated client works with invoke/3", %{client: client} do + new_token = "updated_token" + + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{new_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + updated_client = Functions.update_auth(client, new_token) + + assert {:ok, response} = + Functions.invoke(updated_client, "test-function", http_client: @mock) + + assert response.body == %{"success" => true} + end + end + + describe "access_token option in invoke/3" do + test "overrides authorization header with custom token", %{client: client} do + custom_token = "custom_auth_token" + + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{custom_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"authorized": true}) + }} + end) + + assert {:ok, response} = + Functions.invoke(client, "test-function", + access_token: custom_token, + http_client: @mock + ) + + assert response.body == %{"authorized" => true} + end + + test "works with other options combined", %{client: client} do + custom_token = "combined_auth_token" + custom_headers = %{"x-custom" => "value"} + body_data = %{test: "data"} + + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{custom_token}" + assert Request.get_header(request, "x-custom") == "value" + assert Request.get_header(request, "content-type") == "application/json" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + assert {:ok, response} = + Functions.invoke(client, "test-function", + access_token: custom_token, + headers: custom_headers, + body: body_data, + http_client: @mock + ) + + assert response.body == %{"success" => true} + end + + test "original client remains unchanged after access_token override", %{client: client} do + original_token = client.access_token + custom_token = "temporary_override_token" + + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{custom_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + assert {:ok, _response} = + Functions.invoke(client, "test-function", + access_token: custom_token, + http_client: @mock + ) + + # Original client should be unchanged + assert client.access_token == original_token + end + + test "nil access_token option uses original client token", %{client: client} do + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{client.access_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + assert {:ok, response} = + Functions.invoke(client, "test-function", access_token: nil, http_client: @mock) + + assert response.body == %{"success" => true} + end + + test "empty access_token option uses original client token", %{client: client} do + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{client.access_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + assert {:ok, response} = Functions.invoke(client, "test-function", http_client: @mock) + assert response.body == %{"success" => true} + end + end end