Skip to content

Commit 2454654

Browse files
authored
chore: solve on_response unatural return (#60)
## Problem N/A ## Solution fixes #59 ## Rationale N/A
1 parent bbfd034 commit 2454654

File tree

13 files changed

+214
-21
lines changed

13 files changed

+214
-21
lines changed

.wakatime-project

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
supabase-ex

lib/supabase/fetcher.ex

+1-4
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,7 @@ defmodule Supabase.Fetcher do
218218

219219
def stream(%Request{http_client: http_client} = builder, on_response, opts)
220220
when not is_nil(builder.url) do
221-
with {:ok, resp} <- http_client.stream(builder, on_response, opts) do
222-
{:ok, ResponseAdapter.from(resp)}
223-
end
224-
|> handle_response(builder)
221+
http_client.stream(builder, on_response, opts)
225222
rescue
226223
exception -> handle_exception(exception, __STACKTRACE__, builder)
227224
end

lib/supabase/fetcher/adapter/finch.ex

+6-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,12 @@ defmodule Supabase.Fetcher.Adapter.Finch do
8989
end
9090

9191
if is_function(on_response, 1) do
92-
on_response.({status, headers, stream})
92+
case on_response.({status, headers, stream}) do
93+
:ok -> :ok
94+
{:ok, body} -> {:ok, body}
95+
{:error, %Supabase.Error{} = err} -> {:error, err}
96+
unexpected -> Supabase.Error.new(service: b.service, metadata: %{raw_error: unexpected})
97+
end
9398
else
9499
%Finch.Response{
95100
status: status,

lib/supabase/fetcher/behaviour.ex

+11-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@ defmodule Supabase.Fetcher.Behaviour do
44
alias Supabase.Fetcher.Request
55
alias Supabase.Fetcher.Response
66

7+
@type on_response_input ::
8+
{Supabase.Fetcher.status(), Supabase.Fetcher.headers(), body :: Enumerable.t()}
9+
@typedoc """
10+
The response handler for streaming responses. It receives the response status, headers, and body as input.
11+
12+
Note that here only the status and headers are consumed from the stream and so the body reamins unconsumed for custom operations, receiving each chunk of the body as it arrives.
13+
14+
It needs to return either `:ok` or `{:ok, body}` or `{:error, Supabase.Error}`.
15+
"""
16+
@type on_response :: (on_response_input -> :ok | {:ok, term} | {:error, Supabase.Error.t()})
17+
718
@callback request(Request.t()) :: Supabase.result(Response.t())
819
@callback request_async(Request.t()) :: Supabase.result(Response.t())
920
@callback upload(Request.t(), filepath :: Path.t()) :: Supabase.result(Response.t())
1021
@callback stream(Request.t()) :: Supabase.result(Response.t())
1122
@callback stream(Request.t(), on_response) :: Supabase.result(Response.t())
12-
when on_response: ({Supabase.Fetcher.status(), Supabase.Fetcher.headers(),
13-
body :: Enumerable.t()} ->
14-
Supabase.result(Response.t()))
1523
end

mix.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Supabase.MixProject do
22
use Mix.Project
33

4-
@version "0.6.0"
4+
@version "0.6.1"
55
@source_url "https://github.com/supabase-community/supabase-ex"
66

77
def project do

supabase/config.toml

+31-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ enabled = false
242242
# user_pool_region = "us-east-1"
243243

244244
[edge_runtime]
245-
enabled = false
245+
enabled = true
246246
# Configure one of the supported request policies: `oneshot`, `per_worker`.
247247
# Use `oneshot` for hot reload, or `per_worker` for load testing.
248248
policy = "oneshot"
@@ -258,6 +258,36 @@ inspector_port = 8083
258258
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
259259
# entrypoint = "./functions/MY_FUNCTION_NAME/index.ts"
260260

261+
[functions.binary-data]
262+
enabled = true
263+
verify_jwt = false
264+
entrypoint = "./functions/binary-data/index.ts"
265+
266+
[functions.error-cases]
267+
enabled = true
268+
verify_jwt = false
269+
entrypoint = "./functions/error-cases/index.ts"
270+
271+
[functions.headers-demo]
272+
enabled = true
273+
verify_jwt = false
274+
entrypoint = "./functions/headers-demo/index.ts"
275+
276+
[functions.json-echo]
277+
enabled = true
278+
verify_jwt = false
279+
entrypoint = "./functions/json-echo/index.ts"
280+
281+
[functions.simple-text]
282+
enabled = true
283+
verify_jwt = false
284+
entrypoint = "./functions/simple-text/index.ts"
285+
286+
[functions.stream-data]
287+
enabled = true
288+
verify_jwt = false
289+
entrypoint = "./functions/stream-data/index.ts"
290+
261291
[analytics]
262292
enabled = false
263293
port = 54327
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Deno.serve(async (req) => {
2+
const data = await req.arrayBuffer();
3+
// Reverse the binary data as an example transformation
4+
const reversed = new Uint8Array(data).reverse();
5+
6+
return new Response(reversed, {
7+
headers: { "Content-Type": "application/octet-stream" }
8+
});
9+
});
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Deno.serve(async (req) => {
2+
const { error_type } = await req.json();
3+
4+
switch (error_type) {
5+
case "validation":
6+
return new Response(JSON.stringify({
7+
error: "Validation failed",
8+
details: ["Field 'name' is required"]
9+
}), {
10+
status: 400,
11+
headers: { "Content-Type": "application/json" }
12+
});
13+
14+
case "unauthorized":
15+
return new Response(JSON.stringify({
16+
error: "Unauthorized access"
17+
}), {
18+
status: 401,
19+
headers: { "Content-Type": "application/json" }
20+
});
21+
22+
default:
23+
return new Response(JSON.stringify({
24+
error: "Internal server error"
25+
}), {
26+
status: 500,
27+
headers: { "Content-Type": "application/json" }
28+
});
29+
}
30+
});
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Deno.serve(async (req) => {
2+
const headers = Object.fromEntries(req.headers);
3+
const customHeader = req.headers.get("x-custom-header") || "no custom header";
4+
5+
return new Response(JSON.stringify({
6+
headers,
7+
customHeader
8+
}), {
9+
headers: {
10+
"Content-Type": "application/json",
11+
"X-Response-Header": "test-value"
12+
}
13+
});
14+
});

supabase/functions/json-echo/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Deno.serve(async (req) => {
2+
const body = await req.json();
3+
return new Response(JSON.stringify(body), {
4+
headers: { "Content-Type": "application/json" }
5+
});
6+
});
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Deno.serve(async (req) => {
2+
return new Response("Hello from Deno!", {
3+
headers: { "Content-Type": "text/plain" }
4+
});
5+
});
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Deno.serve(async (req) => {
2+
const stream = new ReadableStream({
3+
start(controller) {
4+
let count = 0;
5+
const interval = setInterval(() => {
6+
if (count >= 5) {
7+
clearInterval(interval);
8+
controller.close();
9+
return;
10+
}
11+
const message = `Event ${count}\n`;
12+
controller.enqueue(new TextEncoder().encode(message));
13+
count++;
14+
}, 1000);
15+
}
16+
});
17+
18+
return new Response(stream, {
19+
headers: {
20+
"Content-Type": "text/event-stream",
21+
"Cache-Control": "no-cache",
22+
"Connection": "keep-alive"
23+
}
24+
});
25+
});

test/supabase/fetcher/adapter/finch_test.exs

+74-11
Original file line numberDiff line numberDiff line change
@@ -60,31 +60,94 @@ defmodule Supabase.Fetcher.Adapter.FinchTest do
6060
describe "dealing with streams" do
6161
test "streams a response successfully", %{builder: builder} do
6262
@mock
63-
|> expect(:stream, fn %Request{}, on_response, _opts ->
63+
|> expect(:stream, fn %Request{}, _opts ->
6464
status = 200
6565
headers = [{"content-length", 80_543}]
6666
stream = Stream.cycle(["chunk1", "chunk2"])
67-
on_response.({status, headers, stream})
67+
body = Enum.take(stream, 2) |> Enum.to_list() |> Enum.join(",")
68+
{:ok, %Finch.Response{status: status, headers: headers, body: body}}
6869
end)
6970

70-
on_response = fn {status, headers, body} ->
71-
body = Enum.take(body, 2) |> Enum.to_list() |> Enum.join(",")
72-
73-
{:ok,
74-
[status: status, headers: headers, body: body]
75-
|> then(&struct(Finch.Response, &1))}
76-
end
77-
7871
builder =
7972
Request.with_body_decoder(builder, fn %{body: body}, _opts ->
8073
{:ok, String.split(body, ",", trim: true)}
8174
end)
8275

83-
assert {:ok, %Response{} = resp} = Fetcher.stream(builder, on_response)
76+
assert {:ok, %Response{} = resp} = Fetcher.stream(builder)
8477
assert resp.status == 200
8578
assert Response.get_header(resp, "content-length") == 80_543
8679
assert resp.body == ["chunk1", "chunk2"]
8780
end
81+
82+
test "streams a response successfully with custom on_response handler with no body", %{
83+
builder: builder
84+
} do
85+
@mock
86+
|> expect(:stream, fn %Request{}, on_response, _opts ->
87+
status = 200
88+
headers = [{"content-length", 80_543}]
89+
stream = Stream.cycle(["chunk1", "chunk2"])
90+
on_response.({status, headers, stream})
91+
end)
92+
93+
on_response = fn {status, headers, body} ->
94+
assert status == 200
95+
assert List.keyfind(headers, "content-length", 0) == {"content-length", 80_543}
96+
97+
# very expensing body consuming
98+
_ = Enum.take(body, 2) |> Enum.to_list()
99+
100+
:ok
101+
end
102+
103+
assert :ok = Fetcher.stream(builder, on_response)
104+
end
105+
106+
test "streams a response successfully with custom on_response handler returning body", %{
107+
builder: builder
108+
} do
109+
@mock
110+
|> expect(:stream, fn %Request{}, on_response, _opts ->
111+
status = 200
112+
headers = [{"content-length", 80_543}]
113+
stream = Stream.cycle(["chunk1", "chunk2"])
114+
on_response.({status, headers, stream})
115+
end)
116+
117+
on_response = fn {status, headers, body} ->
118+
assert status == 200
119+
assert List.keyfind(headers, "content-length", 0) == {"content-length", 80_543}
120+
121+
{:ok,
122+
body
123+
|> Enum.take(2)
124+
|> Enum.to_list()}
125+
end
126+
127+
assert {:ok, body} = Fetcher.stream(builder, on_response)
128+
assert body == ["chunk1", "chunk2"]
129+
end
130+
131+
test "returns an error from response streaming with custom on_response handler", %{
132+
builder: builder
133+
} do
134+
@mock
135+
|> expect(:stream, fn %Request{}, on_response, _opts ->
136+
status = 404
137+
headers = [{"content-length", 80_543}]
138+
stream = Stream.cycle([])
139+
on_response.({status, headers, stream})
140+
end)
141+
142+
on_response = fn {status, headers, _body} ->
143+
assert status == 404
144+
assert List.keyfind(headers, "content-length", 0) == {"content-length", 80_543}
145+
146+
{:error, Error.new(code: :not_found, metadata: %{status: status})}
147+
end
148+
149+
assert {:error, %Error{}} = Fetcher.stream(builder, on_response)
150+
end
88151
end
89152

90153
describe "dealing with errors" do

0 commit comments

Comments
 (0)