Current section
Files
Jump to
Current section
Files
lib/supabase/functions.ex
defmodule Supabase.Functions do
@moduledoc """
This module provides integration with the Supabase Edge Functions API, enabling developers
to invoke their serverless functions.
## Usage
iex> client = Supabase.init_client!("SUPABASE_URL", "SUPABASE_KEY")
iex> Supabase.Functions.invoke(client, "function_name", %{})
"""
alias Supabase.Client
alias Supabase.Fetcher
alias Supabase.Fetcher.Request
alias Supabase.Fetcher.Response
@type opts :: Enumerable.t(opt)
@typedoc """
Available options for invoking a function:
- `body`: The body of the request.
- `headers`: The additional custom headers of the request.
- `method`: The HTTP method of the request.
- `region`: The Region to invoke the function in.
- `on_response`: The custom response handler for response streaming.
"""
@type opt ::
{:body, Fetcher.body()}
| {:headers, Fetcher.headers()}
| {:method, Fetcher.method()}
| {:region, region}
| {:on_response, on_response}
@type on_response :: ({Fetcher.status(), Fetcher.headers(), body :: Enumerable.t()} ->
Supabase.result(Response.t()))
@type region ::
:any
| :"us-west-1"
| :"us-west-2"
| :"us-east-1"
| :"eu-west-1"
| :"eu-west-2"
| :"eu-west-3"
| :"ap-south-1"
| :"ap-southeast-1"
| :"ap-southeast-2"
| :"ap-northeast-1"
| :"ap-northeast-2"
| :"sa-east-1"
| :"ca-central-1"
| :"eu-central-1"
@doc """
Invokes a function
Invoke a Supabase Edge Function.
- 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.
"""
@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] || %{}
client
|> Request.new(decode_body?: false)
|> Request.with_functions_url(name)
|> Request.with_method(method)
|> maybe_define_region(opts[:region])
|> then(&if body = opts[:body], do: Request.with_body(&1, body), else: &1)
|> then(&if c = opts[:http_client], do: Request.with_http_client(&1, c), else: &1)
|> Request.with_body_decoder(nil)
|> maybe_define_content_type(opts[:body])
|> Request.with_headers(custom_headers)
|> execute_request(opts[:on_response])
|> maybe_decode_body()
end
defp maybe_define_region(req, nil), do: req
defp maybe_define_region(req, :any), do: req
defp maybe_define_region(req, region) do
Request.with_headers(req, %{"x-region" => region})
end
defp maybe_define_content_type(req, nil), do: req
defp maybe_define_content_type(req, string) when is_binary(string) do
if raw_binary?(string) do
Request.with_headers(req, %{"content-type" => "application/octet-stream"})
else
Request.with_headers(req, %{"content-type" => "text/plain"})
end
end
defp maybe_define_content_type(req, %{}) do
Request.with_headers(req, %{"content-type" => "application/json"})
end
defp raw_binary?(bin), do: not String.printable?(bin)
defp execute_request(req, on_response) do
if on_response do
Fetcher.stream(req, on_response)
else
# consume all the response, answers eagerly
Fetcher.stream(req)
end
end
defp maybe_decode_body(:ok), do: :ok
defp maybe_decode_body({:error, _} = err), do: err
defp maybe_decode_body({:ok, %Response{} = resp}) do
content_type = Response.get_header(resp, "content-type") || "text/plain"
decoder = decoder_from_content_type(content_type)
with {:ok, body} <- decoder.(resp.body), do: {:ok, %{resp | body: body}}
end
defp maybe_decode_body({:ok, _body} = ok), do: ok
defp decoder_from_content_type("application/json"), do: &JSON.decode/1
defp decoder_from_content_type(_) do
fn body -> {:ok, Function.identity(body)} end
end
end