Skip to content

Commit 7b4a62e

Browse files
authored
Fix script and device pagination (#2400)
Turns out the device controller was broken as well Fixes #2392
1 parent 1118ecc commit 7b4a62e

File tree

6 files changed

+222
-16
lines changed

6 files changed

+222
-16
lines changed

lib/nerves_hub_web/controllers/api/device_controller.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule NervesHubWeb.API.DeviceController do
1111
alias NervesHub.Products
1212
alias NervesHub.Repo
1313

14+
alias NervesHubWeb.API.PaginationHelpers
1415
alias NervesHubWeb.Endpoint
1516
alias NervesHubWeb.Helpers.RoleValidateHelpers
1617

@@ -34,18 +35,16 @@ defmodule NervesHubWeb.API.DeviceController do
3435

3536
def index(%{assigns: %{org: org, product: product}} = conn, params) do
3637
opts = %{
37-
pagination: Map.get(params, "pagination", %{}),
38+
pagination: PaginationHelpers.atomize_pagination_params(Map.get(params, "pagination", %{})),
3839
filters: Map.get(params, "filters", %{})
3940
}
4041

4142
{devices, page} =
4243
Devices.get_devices_by_org_id_and_product_id_with_pager(org.id, product.id, opts)
4344

44-
pagination = Map.take(page, [:page_number, :page_size, :total_entries, :total_pages])
45-
4645
conn
4746
|> assign(:devices, devices)
48-
|> assign(:pagination, pagination)
47+
|> assign(:pagination, PaginationHelpers.format_pagination_meta(page))
4948
|> render(:index)
5049
end
5150

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
defmodule NervesHubWeb.API.PaginationHelpers do
2+
@moduledoc """
3+
Shared helper functions for handling pagination in API controllers.
4+
"""
5+
6+
@doc """
7+
Converts pagination parameters from string keys and values (as they come from URL params)
8+
to atom keys with integer values (as expected by Flop and internal APIs).
9+
10+
## Examples
11+
12+
iex> atomize_pagination_params(%{"page" => "1", "page_size" => "25"})
13+
%{page: 1, page_size: 25}
14+
15+
iex> atomize_pagination_params(%{"page" => 2, "page_size" => 10})
16+
%{page: 2, page_size: 10}
17+
18+
iex> atomize_pagination_params(%{})
19+
%{}
20+
21+
iex> atomize_pagination_params(nil)
22+
%{}
23+
"""
24+
@spec atomize_pagination_params(map() | nil) :: map()
25+
def atomize_pagination_params(pagination) when is_map(pagination) do
26+
pagination
27+
|> Enum.reduce(%{}, fn
28+
{"page", value}, acc when is_binary(value) ->
29+
Map.put(acc, :page, String.to_integer(value))
30+
31+
{"page", value}, acc when is_integer(value) ->
32+
Map.put(acc, :page, value)
33+
34+
{"page_size", value}, acc when is_binary(value) ->
35+
Map.put(acc, :page_size, String.to_integer(value))
36+
37+
{"page_size", value}, acc when is_integer(value) ->
38+
Map.put(acc, :page_size, value)
39+
40+
_other, acc ->
41+
acc
42+
end)
43+
end
44+
45+
def atomize_pagination_params(_), do: %{}
46+
47+
@doc """
48+
Converts Flop.Meta struct fields to the standard pagination response format
49+
used by the API.
50+
51+
Maps:
52+
- current_page -> page_number
53+
- total_count -> total_entries
54+
- page_size -> page_size (unchanged)
55+
- total_pages -> total_pages (unchanged)
56+
57+
## Examples
58+
59+
iex> meta = %Flop.Meta{current_page: 2, page_size: 10, total_count: 45, total_pages: 5}
60+
iex> format_pagination_meta(meta)
61+
%{page_number: 2, page_size: 10, total_entries: 45, total_pages: 5}
62+
"""
63+
@spec format_pagination_meta(Flop.Meta.t()) :: map()
64+
def format_pagination_meta(%Flop.Meta{} = meta) do
65+
%{
66+
page_number: meta.current_page,
67+
page_size: meta.page_size,
68+
total_entries: meta.total_count,
69+
total_pages: meta.total_pages
70+
}
71+
end
72+
end

lib/nerves_hub_web/controllers/api/script_controller.ex

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule NervesHubWeb.API.ScriptController do
33
use OpenApiSpex.ControllerSpecs
44

55
alias NervesHub.Scripts
6+
alias NervesHubWeb.API.PaginationHelpers
67

78
security([%{}, %{"bearer_auth" => []}])
89
tags(["Support Scripts"])
@@ -55,18 +56,15 @@ defmodule NervesHubWeb.API.ScriptController do
5556
into: %{},
5657
do: {String.to_existing_atom(key), val}
5758

58-
opts = %{
59-
pagination: Map.get(params, "pagination", %{}),
60-
filters: filters
61-
}
62-
63-
{scripts, page} = Scripts.filter(product, opts)
64-
65-
pagination = Map.take(page, [:page_number, :page_size, :total_entries, :total_pages])
59+
{scripts, page} =
60+
Scripts.filter(product, %{
61+
pagination: PaginationHelpers.atomize_pagination_params(Map.get(params, "pagination", %{})),
62+
filters: filters
63+
})
6664

6765
conn
6866
|> assign(:scripts, scripts)
69-
|> assign(:pagination, pagination)
67+
|> assign(:pagination, PaginationHelpers.format_pagination_meta(page))
7068
|> render(:index)
7169
end
7270
end

lib/nerves_hub_web/controllers/api/script_json.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
defmodule NervesHubWeb.API.ScriptJSON do
22
@moduledoc false
33

4-
def index(%{scripts: scripts}) do
4+
def index(%{scripts: scripts, pagination: pagination}) do
55
%{
6-
data: for(script <- scripts, do: script(script))
6+
data: for(script <- scripts, do: script(script)),
7+
pagination: pagination
78
}
89
end
910

test/nerves_hub_web/controllers/api/device_controller_test.exs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,45 @@ defmodule NervesHubWeb.API.DeviceControllerTest do
4545

4646
conn = get(conn, Routes.api_device_path(conn, :index, org.name, product.name))
4747

48-
assert json_response(conn, 200)["data"]
48+
assert json_response(conn, 200) == %{
49+
"data" => [
50+
%{
51+
"connection_status" => "not_seen",
52+
"deleted" => false,
53+
"deployment_group" => nil,
54+
"description" => nil,
55+
"firmware_metadata" => %{
56+
"architecture" => "x86_64",
57+
"author" => "me",
58+
"description" => "D ",
59+
"fwup_version" => "1.0.0",
60+
"id" => device.firmware_metadata.id,
61+
"misc" => nil,
62+
"platform" => "platform",
63+
"product" => "valid product",
64+
"uuid" => firmware.uuid,
65+
"vcs_identifier" => nil,
66+
"version" => "1.0.0"
67+
},
68+
"identifier" => device.identifier,
69+
"last_communication" => "never",
70+
"online" => "not_seen",
71+
"org_name" => "Test-Org",
72+
"priority_updates" => false,
73+
"product_name" => "valid product",
74+
"tags" => ["beta", "beta-edge"],
75+
"updates_blocked_until" => nil,
76+
"updates_enabled" => true,
77+
"version" => "1.0.0"
78+
}
79+
],
80+
"pagination" => %{
81+
"page_number" => 1,
82+
"page_size" => nil,
83+
"total_entries" => 1,
84+
"total_pages" => 1
85+
}
86+
}
4987

5088
assert Enum.find(conn.assigns.devices, fn %{identifier: identifier} ->
5189
device.identifier == identifier

test/nerves_hub_web/controllers/api/script_controller_test.exs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,104 @@ defmodule NervesHubWeb.API.ScriptControllerTest do
4444
assert Enum.count(data) == 1
4545
assert script_response["id"] == script_with_tags.id
4646
end
47+
48+
test "returns pagination metadata", %{conn: conn, product: product, device: device, user: user} do
49+
_script = Fixtures.support_script_fixture(product, user)
50+
51+
conn = get(conn, Routes.api_script_path(conn, :index, device))
52+
response = json_response(conn, 200)
53+
54+
assert Map.has_key?(response, "data")
55+
assert Map.has_key?(response, "pagination")
56+
57+
pagination = response["pagination"]
58+
assert pagination["page_number"] == 1
59+
assert pagination["page_size"] == 25
60+
assert pagination["total_entries"] == 1
61+
assert pagination["total_pages"] == 1
62+
end
63+
64+
test "respects pagination parameters with device scope", %{conn: conn, product: product, device: device, user: user} do
65+
# Create 5 scripts
66+
_scripts = for i <- 1..5, do: Fixtures.support_script_fixture(product, user, %{name: "script#{i}"})
67+
68+
# Get first page with page_size=2
69+
conn =
70+
get(conn, Routes.api_script_path(conn, :index, device), %{
71+
pagination: %{page: 1, page_size: 2}
72+
})
73+
74+
response = json_response(conn, 200)
75+
76+
assert length(response["data"]) == 2
77+
assert response["pagination"]["page_number"] == 1
78+
assert response["pagination"]["page_size"] == 2
79+
assert response["pagination"]["total_entries"] == 5
80+
assert response["pagination"]["total_pages"] == 3
81+
82+
# Get second page with page_size=2
83+
conn =
84+
get(conn, Routes.api_script_path(conn, :index, device), %{
85+
pagination: %{page: 2, page_size: 2}
86+
})
87+
88+
response = json_response(conn, 200)
89+
90+
assert length(response["data"]) == 2
91+
assert response["pagination"]["page_number"] == 2
92+
assert response["pagination"]["page_size"] == 2
93+
assert response["pagination"]["total_entries"] == 5
94+
assert response["pagination"]["total_pages"] == 3
95+
96+
# Get third page with page_size=2 (should have 1 item)
97+
conn =
98+
get(conn, Routes.api_script_path(conn, :index, device), %{
99+
pagination: %{page: 3, page_size: 2}
100+
})
101+
102+
response = json_response(conn, 200)
103+
104+
assert length(response["data"]) == 1
105+
assert response["pagination"]["page_number"] == 3
106+
assert response["pagination"]["total_entries"] == 5
107+
end
108+
109+
test "respects pagination parameters with product scope", %{conn: conn, org: org, product: product, user: user} do
110+
# Create 3 scripts
111+
_scripts = for i <- 1..3, do: Fixtures.support_script_fixture(product, user, %{name: "script#{i}"})
112+
113+
# Get page with custom page_size
114+
conn =
115+
get(conn, Routes.api_script_path(conn, :index, org.name, product.name), %{
116+
pagination: %{page: 1, page_size: 2}
117+
})
118+
119+
response = json_response(conn, 200)
120+
121+
assert length(response["data"]) == 2
122+
assert response["pagination"]["page_number"] == 1
123+
assert response["pagination"]["page_size"] == 2
124+
assert response["pagination"]["total_entries"] == 3
125+
assert response["pagination"]["total_pages"] == 2
126+
end
127+
128+
test "handles string pagination parameters", %{conn: conn, product: product, device: device, user: user} do
129+
# Create 3 scripts
130+
_scripts = for i <- 1..3, do: Fixtures.support_script_fixture(product, user, %{name: "script#{i}"})
131+
132+
# Send pagination params as strings (as they come from URL query params)
133+
conn =
134+
get(conn, Routes.api_script_path(conn, :index, device), %{
135+
pagination: %{"page" => "2", "page_size" => "1"}
136+
})
137+
138+
response = json_response(conn, 200)
139+
140+
assert length(response["data"]) == 1
141+
assert response["pagination"]["page_number"] == 2
142+
assert response["pagination"]["page_size"] == 1
143+
assert response["pagination"]["total_entries"] == 3
144+
end
47145
end
48146

49147
describe "send" do

0 commit comments

Comments
 (0)