Skip to content

Commit 8a48c1d

Browse files
authored
Add dynamic auth token updates (#11)
thanks for contribution!
1 parent 9d8338b commit 8a48c1d

File tree

4 files changed

+193
-2
lines changed

4 files changed

+193
-2
lines changed

CHANGELOG.md

Whitespace-only changes.

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,3 @@ This feature provides:
123123
- **Request cancellation**: Long-running requests will timeout and be cancelled
124124
- **Better resource management**: Prevents hanging connections
125125
- **Comprehensive timeout coverage**: Sets both receive timeout (per-chunk) and request timeout (complete response)
126-
- **Feature parity with JS client**: Matches timeout functionality in the JavaScript SDK

lib/supabase/functions.ex

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule Supabase.Functions do
2525
- `region`: The Region to invoke the function in.
2626
- `on_response`: The custom response handler for response streaming.
2727
- `timeout`: The timeout in milliseconds for the request. Defaults to 15 seconds.
28+
- `access_token`: Override the authorization token for this request.
2829
"""
2930
@type opt ::
3031
{:body, Fetcher.body()}
@@ -33,6 +34,7 @@ defmodule Supabase.Functions do
3334
| {:region, region}
3435
| {:on_response, on_response}
3536
| {:timeout, pos_integer()}
37+
| {:access_token, String.t()}
3638

3739
@type on_response :: ({Fetcher.status(), Fetcher.headers(), body :: Enumerable.t()} ->
3840
Supabase.result(Response.t()))
@@ -54,6 +56,24 @@ defmodule Supabase.Functions do
5456
| :"ca-central-1"
5557
| :"eu-central-1"
5658

59+
@doc """
60+
Updates the access token for a client
61+
62+
Creates a new client instance with the updated access token. This provides
63+
feature parity with the JavaScript client's `setAuth(token)` method.
64+
65+
## Examples
66+
67+
# Update auth token functionally
68+
new_client = Supabase.Functions.update_auth(client, "new_token")
69+
{:ok, response} = Supabase.Functions.invoke(new_client, "my-function")
70+
71+
"""
72+
@spec update_auth(Client.t(), String.t()) :: Client.t()
73+
def update_auth(%Client{} = client, token) when is_binary(token) do
74+
%{client | access_token: token}
75+
end
76+
5777
@doc """
5878
Invokes a function
5979
@@ -62,6 +82,19 @@ defmodule Supabase.Functions do
6282
- 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.
6383
- Responses are automatically parsed as json depending on the Content-Type header sent by your function. Responses are parsed as text by default.
6484
85+
## Authentication
86+
87+
You can override the authorization token for a specific request using the `access_token` option:
88+
89+
# Use a different token for this request
90+
{:ok, response} = Supabase.Functions.invoke(client, "my-function", access_token: "new_token")
91+
92+
Alternatively, you can update the client's auth token functionally:
93+
94+
# Update the client's token
95+
new_client = Supabase.Functions.update_auth(client, "new_token")
96+
{:ok, response} = Supabase.Functions.invoke(new_client, "my-function")
97+
6598
## Timeout Support
6699
67100
You can set a timeout for function invocations using the `timeout` option. This sets both the
@@ -84,14 +117,24 @@ defmodule Supabase.Functions do
84117
{:ok, response} = Supabase.Functions.invoke(client, "my-function",
85118
body: %{data: "value"},
86119
timeout: 30_000)
120+
121+
# With custom access token
122+
{:ok, response} = Supabase.Functions.invoke(client, "my-function", access_token: "custom_token")
87123
"""
88124
@spec invoke(Client.t(), function :: String.t(), opts) :: Supabase.result(Response.t())
89125
def invoke(%Client{} = client, name, opts \\ []) when is_binary(name) do
90126
method = opts[:method] || :post
91127
custom_headers = opts[:headers] || %{}
92128
timeout = opts[:timeout] || 15_000
93129

94-
client
130+
effective_client =
131+
if opts[:access_token] do
132+
update_auth(client, opts[:access_token])
133+
else
134+
client
135+
end
136+
137+
effective_client
95138
|> Request.new(decode_body?: false)
96139
|> Request.with_functions_url(name)
97140
|> Request.with_method(method)

test/supabase/functions_test.exs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,153 @@ defmodule Supabase.FunctionsTest do
257257
assert response == "chunk1chunk2"
258258
end
259259
end
260+
261+
describe "update_auth/2" do
262+
test "returns a new client with updated access token", %{client: client} do
263+
new_token = "new_test_token"
264+
updated_client = Functions.update_auth(client, new_token)
265+
266+
assert updated_client.access_token == new_token
267+
# Original client should remain unchanged
268+
assert client.access_token != new_token
269+
# All other fields should remain the same
270+
assert updated_client.base_url == client.base_url
271+
assert updated_client.api_key == client.api_key
272+
end
273+
274+
test "updated client works with invoke/3", %{client: client} do
275+
new_token = "updated_token"
276+
277+
expect(@mock, :stream, fn request, _opts ->
278+
assert Request.get_header(request, "authorization") == "Bearer #{new_token}"
279+
280+
{:ok,
281+
%Finch.Response{
282+
status: 200,
283+
headers: %{"content-type" => "application/json"},
284+
body: ~s({"success": true})
285+
}}
286+
end)
287+
288+
updated_client = Functions.update_auth(client, new_token)
289+
290+
assert {:ok, response} =
291+
Functions.invoke(updated_client, "test-function", http_client: @mock)
292+
293+
assert response.body == %{"success" => true}
294+
end
295+
end
296+
297+
describe "access_token option in invoke/3" do
298+
test "overrides authorization header with custom token", %{client: client} do
299+
custom_token = "custom_auth_token"
300+
301+
expect(@mock, :stream, fn request, _opts ->
302+
assert Request.get_header(request, "authorization") == "Bearer #{custom_token}"
303+
304+
{:ok,
305+
%Finch.Response{
306+
status: 200,
307+
headers: %{"content-type" => "application/json"},
308+
body: ~s({"authorized": true})
309+
}}
310+
end)
311+
312+
assert {:ok, response} =
313+
Functions.invoke(client, "test-function",
314+
access_token: custom_token,
315+
http_client: @mock
316+
)
317+
318+
assert response.body == %{"authorized" => true}
319+
end
320+
321+
test "works with other options combined", %{client: client} do
322+
custom_token = "combined_auth_token"
323+
custom_headers = %{"x-custom" => "value"}
324+
body_data = %{test: "data"}
325+
326+
expect(@mock, :stream, fn request, _opts ->
327+
assert Request.get_header(request, "authorization") == "Bearer #{custom_token}"
328+
assert Request.get_header(request, "x-custom") == "value"
329+
assert Request.get_header(request, "content-type") == "application/json"
330+
331+
{:ok,
332+
%Finch.Response{
333+
status: 200,
334+
headers: %{"content-type" => "application/json"},
335+
body: ~s({"success": true})
336+
}}
337+
end)
338+
339+
assert {:ok, response} =
340+
Functions.invoke(client, "test-function",
341+
access_token: custom_token,
342+
headers: custom_headers,
343+
body: body_data,
344+
http_client: @mock
345+
)
346+
347+
assert response.body == %{"success" => true}
348+
end
349+
350+
test "original client remains unchanged after access_token override", %{client: client} do
351+
original_token = client.access_token
352+
custom_token = "temporary_override_token"
353+
354+
expect(@mock, :stream, fn request, _opts ->
355+
assert Request.get_header(request, "authorization") == "Bearer #{custom_token}"
356+
357+
{:ok,
358+
%Finch.Response{
359+
status: 200,
360+
headers: %{"content-type" => "application/json"},
361+
body: ~s({"success": true})
362+
}}
363+
end)
364+
365+
assert {:ok, _response} =
366+
Functions.invoke(client, "test-function",
367+
access_token: custom_token,
368+
http_client: @mock
369+
)
370+
371+
# Original client should be unchanged
372+
assert client.access_token == original_token
373+
end
374+
375+
test "nil access_token option uses original client token", %{client: client} do
376+
expect(@mock, :stream, fn request, _opts ->
377+
assert Request.get_header(request, "authorization") == "Bearer #{client.access_token}"
378+
379+
{:ok,
380+
%Finch.Response{
381+
status: 200,
382+
headers: %{"content-type" => "application/json"},
383+
body: ~s({"success": true})
384+
}}
385+
end)
386+
387+
assert {:ok, response} =
388+
Functions.invoke(client, "test-function", access_token: nil, http_client: @mock)
389+
390+
assert response.body == %{"success" => true}
391+
end
392+
393+
test "empty access_token option uses original client token", %{client: client} do
394+
expect(@mock, :stream, fn request, _opts ->
395+
assert Request.get_header(request, "authorization") == "Bearer #{client.access_token}"
396+
397+
{:ok,
398+
%Finch.Response{
399+
status: 200,
400+
headers: %{"content-type" => "application/json"},
401+
body: ~s({"success": true})
402+
}}
403+
end)
404+
405+
assert {:ok, response} = Functions.invoke(client, "test-function", http_client: @mock)
406+
assert response.body == %{"success" => true}
407+
end
408+
end
260409
end

0 commit comments

Comments
 (0)