Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed CHANGELOG.md
Empty file.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
45 changes: 44 additions & 1 deletion lib/supabase/functions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()}
Expand All @@ -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()))
Expand All @@ -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

Expand All @@ -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
Expand All @@ -84,14 +117,24 @@ 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
method = opts[:method] || :post
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)
Expand Down
149 changes: 149 additions & 0 deletions test/supabase/functions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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