Skip to content

Commit 94f2d2b

Browse files
authored
feat: port tRPC bounty listing API endpoint (#107)
1 parent 3b69ff4 commit 94f2d2b

File tree

6 files changed

+249
-0
lines changed

6 files changed

+249
-0
lines changed

lib/algora/bounties/bounties.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,9 @@ defmodule Algora.Bounties do
10071007
{:owner_id, owner_id}, query ->
10081008
from([b, r: r] in query, where: b.owner_id == ^owner_id or r.user_id == ^owner_id)
10091009

1010+
{:owner_handle, owner_handle}, query ->
1011+
from([b, o: o] in query, where: o.handle == ^owner_handle)
1012+
10101013
{:status, status}, query ->
10111014
query = where(query, [b], b.status == ^status)
10121015

@@ -1114,8 +1117,10 @@ defmodule Algora.Bounties do
11141117
status: b.status,
11151118
owner: %{
11161119
id: o.id,
1120+
inserted_at: o.inserted_at,
11171121
name: o.name,
11181122
handle: o.handle,
1123+
provider_login: o.provider_login,
11191124
avatar_url: o.avatar_url,
11201125
tech_stack: o.tech_stack
11211126
},
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
defmodule AlgoraWeb.API.BountyController do
2+
use AlgoraWeb, :controller
3+
4+
alias Algora.Bounties
5+
alias AlgoraWeb.API.FallbackController
6+
7+
action_fallback FallbackController
8+
9+
@doc """
10+
Get a list of bounties with optional filtering parameters.
11+
12+
Query Parameters:
13+
- status: string (optional) - Filter by status (open, paid)
14+
- org: string (optional) - Filter by organization handle
15+
- limit: integer (optional) - Limit the number of bounties returned
16+
"""
17+
def index(conn, %{"batch" => _batch, "input" => input} = _params) do
18+
with {:ok, decoded} <- Jason.decode(input),
19+
%{"0" => %{"json" => json}} <- decoded do
20+
criteria = to_criteria(json)
21+
bounties = Bounties.list_bounties(criteria)
22+
render(conn, :index, bounties: bounties)
23+
end
24+
end
25+
26+
def index(conn, params) do
27+
criteria = to_criteria(params)
28+
bounties = Bounties.list_bounties(criteria)
29+
render(conn, :index, bounties: bounties)
30+
end
31+
32+
# Convert JSON/map parameters to keyword list criteria
33+
defp to_criteria(params) when is_map(params) do
34+
params
35+
|> Enum.map(&parse_params/1)
36+
|> Enum.reject(&is_nil/1)
37+
|> Keyword.new()
38+
end
39+
40+
defp to_criteria(_), do: []
41+
42+
defp parse_params({"status", status}) do
43+
{:status, parse_status(status)}
44+
end
45+
46+
defp parse_params({"org", org_handle}) do
47+
{:owner_handle, org_handle}
48+
end
49+
50+
defp parse_params({"limit", limit}) do
51+
{:limit, limit}
52+
end
53+
54+
defp parse_params(_), do: nil
55+
56+
# Parse status string to corresponding enum atom
57+
defp parse_status(status) when is_binary(status) do
58+
case String.downcase(status) do
59+
"paid" -> :paid
60+
_ -> :open
61+
end
62+
end
63+
64+
defp parse_status(_), do: :open
65+
end
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
defmodule AlgoraWeb.API.BountyJSON do
2+
alias Algora.Bounties.Bounty
3+
4+
def index(%{bounties: bounties}) do
5+
[
6+
%{
7+
result: %{
8+
data: %{
9+
json: %{
10+
next_cursor: nil,
11+
items: for(bounty <- bounties, do: data(bounty))
12+
}
13+
}
14+
}
15+
}
16+
]
17+
end
18+
19+
defp data(%{} = bounty) do
20+
%{
21+
id: bounty.id,
22+
point_reward: nil,
23+
reward: %{
24+
amount: Algora.MoneyUtils.to_minor_units(bounty.amount),
25+
currency: bounty.amount.currency
26+
},
27+
reward_formatted: Money.to_string!(bounty.amount, no_fraction_if_integer: true),
28+
reward_tiers: [],
29+
tech: bounty.owner.tech_stack,
30+
status: bounty.status,
31+
is_external: false,
32+
org: org_data(bounty.owner),
33+
task: task_data(bounty),
34+
type: "standard",
35+
kind: "dev",
36+
reward_type: "cash",
37+
visibility: "public",
38+
bids: [],
39+
autopay_disabled: false,
40+
timeouts_disabled: false,
41+
manual_assignments: false,
42+
created_at: bounty.inserted_at,
43+
updated_at: bounty.inserted_at
44+
}
45+
end
46+
47+
defp task_data(bounty) do
48+
%{
49+
id: bounty.ticket.id,
50+
forge: "github",
51+
repo_owner: bounty.repository.owner.provider_login,
52+
repo_name: bounty.repository.name,
53+
number: bounty.ticket.number,
54+
source: %{
55+
type: "github",
56+
data: %{
57+
id: bounty.ticket.id,
58+
html_url: bounty.ticket.url,
59+
title: bounty.ticket.title,
60+
body: "",
61+
user: %{
62+
id: 0,
63+
login: "",
64+
avatar_url: "",
65+
html_url: "",
66+
name: "",
67+
company: "",
68+
location: "",
69+
twitter_username: ""
70+
}
71+
}
72+
},
73+
status: "open",
74+
title: bounty.ticket.title,
75+
url: bounty.ticket.url,
76+
body: "",
77+
type: "issue",
78+
hash: Bounty.path(bounty),
79+
tech: []
80+
}
81+
end
82+
83+
defp org_data(nil), do: nil
84+
85+
defp org_data(org) do
86+
%{
87+
id: org.id,
88+
created_at: org.inserted_at,
89+
handle: org.handle,
90+
name: org.name,
91+
display_name: org.name,
92+
description: "",
93+
avatar_url: org.avatar_url,
94+
website_url: "",
95+
twitter_url: "",
96+
youtube_url: "",
97+
discord_url: "",
98+
slack_url: "",
99+
stargazers_count: 0,
100+
tech: org.tech_stack,
101+
accepts_sponsorships: false,
102+
members: members_data(org),
103+
enabled_expert_recs: false,
104+
enabled_private_bounties: false,
105+
days_until_timeout: nil,
106+
github_handle: org.provider_login
107+
}
108+
end
109+
110+
defp members_data(_org) do
111+
[]
112+
end
113+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
defmodule AlgoraWeb.API.ErrorJSON do
2+
def error(%{changeset: changeset}) do
3+
%{
4+
errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
5+
}
6+
end
7+
8+
def error(%{message: message}) do
9+
%{error: message}
10+
end
11+
12+
defp translate_error({msg, opts}) do
13+
Enum.reduce(opts, msg, fn
14+
{key, value}, acc when is_binary(value) ->
15+
String.replace(acc, "%{#{key}}", value)
16+
17+
{key, value}, acc when is_integer(value) ->
18+
String.replace(acc, "%{#{key}}", Integer.to_string(value))
19+
20+
{key, value}, acc ->
21+
String.replace(acc, "%{#{key}}", inspect(value))
22+
end)
23+
end
24+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
defmodule AlgoraWeb.API.FallbackController do
2+
use AlgoraWeb, :controller
3+
4+
alias AlgoraWeb.API.ErrorJSON
5+
6+
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
7+
conn
8+
|> put_status(:unprocessable_entity)
9+
|> put_view(json: ErrorJSON)
10+
|> render(:error, changeset: changeset)
11+
end
12+
13+
def call(conn, {:error, :not_found}) do
14+
conn
15+
|> put_status(:not_found)
16+
|> put_view(json: ErrorJSON)
17+
|> render(:error, message: "Not found")
18+
end
19+
20+
def call(conn, {:error, :unauthorized}) do
21+
conn
22+
|> put_status(:unauthorized)
23+
|> put_view(json: ErrorJSON)
24+
|> render(:error, message: "Unauthorized")
25+
end
26+
27+
# Catch-all error handler
28+
def call(conn, {:error, error}) do
29+
conn
30+
|> put_status(:bad_request)
31+
|> put_view(json: ErrorJSON)
32+
|> render(:error, message: to_string(error))
33+
end
34+
end

lib/algora_web/router.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ defmodule AlgoraWeb.Router do
158158
end
159159
end
160160

161+
scope "/api", AlgoraWeb.API do
162+
pipe_through :api
163+
164+
# Legacy tRPC endpoints
165+
get "/trpc/bounty.list", BountyController, :index
166+
post "/trpc/bounty.list", BountyController, :index
167+
end
168+
161169
# Other scopes may use custom stacks.
162170
# scope "/api", AlgoraWeb do
163171
# pipe_through :api

0 commit comments

Comments
 (0)