Skip to content

Commit 58864ac

Browse files
authored
Admin API: add control over the page & total count in the list endpoints (#5065)
2 parents 4cccc06 + 0d71448 commit 58864ac

File tree

11 files changed

+1752
-144
lines changed

11 files changed

+1752
-144
lines changed

crates/handlers/src/admin/params.rs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Generated code from schemars violates this rule
88
#![allow(clippy::str_to_string)]
99

10-
use std::num::NonZeroUsize;
10+
use std::{borrow::Cow, num::NonZeroUsize};
1111

1212
use aide::OperationIo;
1313
use axum::{
@@ -64,6 +64,34 @@ impl std::ops::Deref for UlidPathParam {
6464
/// The default page size if not specified
6565
const DEFAULT_PAGE_SIZE: usize = 10;
6666

67+
#[derive(Deserialize, JsonSchema, Clone, Copy, Default, Debug)]
68+
pub enum IncludeCount {
69+
/// Include the total number of items (default)
70+
#[default]
71+
#[serde(rename = "true")]
72+
True,
73+
74+
/// Do not include the total number of items
75+
#[serde(rename = "false")]
76+
False,
77+
78+
/// Only include the total number of items, skip the items themselves
79+
#[serde(rename = "only")]
80+
Only,
81+
}
82+
83+
impl IncludeCount {
84+
pub(crate) fn add_to_base(self, base: &str) -> Cow<'_, str> {
85+
let separator = if base.contains('?') { '&' } else { '?' };
86+
match self {
87+
// This is the default, don't add anything
88+
Self::True => Cow::Borrowed(base),
89+
Self::False => format!("{base}{separator}count=false").into(),
90+
Self::Only => format!("{base}{separator}count=only").into(),
91+
}
92+
}
93+
}
94+
6795
#[derive(Deserialize, JsonSchema, Clone, Copy)]
6896
struct PaginationParams {
6997
/// Retrieve the items before the given ID
@@ -83,6 +111,10 @@ struct PaginationParams {
83111
/// Retrieve the last N items
84112
#[serde(rename = "page[last]")]
85113
last: Option<NonZeroUsize>,
114+
115+
/// Include the total number of items. Defaults to `true`.
116+
#[serde(rename = "count")]
117+
include_count: Option<IncludeCount>,
86118
}
87119

88120
#[derive(Debug, thiserror::Error)]
@@ -107,7 +139,7 @@ impl IntoResponse for PaginationRejection {
107139
/// An extractor for pagination parameters in the query string
108140
#[derive(OperationIo, Debug, Clone, Copy)]
109141
#[aide(input_with = "Query<PaginationParams>")]
110-
pub struct Pagination(pub mas_storage::Pagination);
142+
pub struct Pagination(pub mas_storage::Pagination, pub IncludeCount);
111143

112144
impl<S: Send + Sync> FromRequestParts<S> for Pagination {
113145
type Rejection = PaginationRejection;
@@ -130,11 +162,14 @@ impl<S: Send + Sync> FromRequestParts<S> for Pagination {
130162
(None, Some(last)) => (PaginationDirection::Backward, last.into()),
131163
};
132164

133-
Ok(Self(mas_storage::Pagination {
134-
before: params.before,
135-
after: params.after,
136-
direction,
137-
count,
138-
}))
165+
Ok(Self(
166+
mas_storage::Pagination {
167+
before: params.before,
168+
after: params.after,
169+
direction,
170+
count,
171+
},
172+
params.include_count.unwrap_or_default(),
173+
))
139174
}
140175
}

crates/handlers/src/admin/response.rs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ struct PaginationLinks {
2121
self_: String,
2222

2323
/// The link to the first page of results
24-
first: String,
24+
#[serde(skip_serializing_if = "Option::is_none")]
25+
first: Option<String>,
2526

2627
/// The link to the last page of results
27-
last: String,
28+
#[serde(skip_serializing_if = "Option::is_none")]
29+
last: Option<String>,
2830

2931
/// The link to the next page of results
3032
///
@@ -42,17 +44,26 @@ struct PaginationLinks {
4244
#[derive(Serialize, JsonSchema)]
4345
struct PaginationMeta {
4446
/// The total number of results
45-
count: usize,
47+
#[serde(skip_serializing_if = "Option::is_none")]
48+
count: Option<usize>,
49+
}
50+
51+
impl PaginationMeta {
52+
fn is_empty(&self) -> bool {
53+
self.count.is_none()
54+
}
4655
}
4756

4857
/// A top-level response with a page of resources
4958
#[derive(Serialize, JsonSchema)]
5059
pub struct PaginatedResponse<T> {
5160
/// Response metadata
61+
#[serde(skip_serializing_if = "PaginationMeta::is_empty")]
5262
meta: PaginationMeta,
5363

5464
/// The list of resources
55-
data: Vec<SingleResource<T>>,
65+
#[serde(skip_serializing_if = "Option::is_none")]
66+
data: Option<Vec<SingleResource<T>>>,
5667

5768
/// Related links
5869
links: PaginationLinks,
@@ -87,16 +98,22 @@ fn url_with_pagination(base: &str, pagination: Pagination) -> String {
8798
}
8899

89100
impl<T: Resource> PaginatedResponse<T> {
90-
pub fn new(
101+
pub fn for_page(
91102
page: mas_storage::Page<T>,
92103
current_pagination: Pagination,
93-
count: usize,
104+
count: Option<usize>,
94105
base: &str,
95106
) -> Self {
96107
let links = PaginationLinks {
97108
self_: url_with_pagination(base, current_pagination),
98-
first: url_with_pagination(base, Pagination::first(current_pagination.count)),
99-
last: url_with_pagination(base, Pagination::last(current_pagination.count)),
109+
first: Some(url_with_pagination(
110+
base,
111+
Pagination::first(current_pagination.count),
112+
)),
113+
last: Some(url_with_pagination(
114+
base,
115+
Pagination::last(current_pagination.count),
116+
)),
100117
next: page.has_next_page.then(|| {
101118
url_with_pagination(
102119
base,
@@ -125,7 +142,23 @@ impl<T: Resource> PaginatedResponse<T> {
125142

126143
Self {
127144
meta: PaginationMeta { count },
128-
data,
145+
data: Some(data),
146+
links,
147+
}
148+
}
149+
150+
pub fn for_count_only(count: usize, base: &str) -> Self {
151+
let links = PaginationLinks {
152+
self_: base.to_owned(),
153+
first: None,
154+
last: None,
155+
next: None,
156+
prev: None,
157+
};
158+
159+
Self {
160+
meta: PaginationMeta { count: Some(count) },
161+
data: None,
129162
links,
130163
}
131164
}

0 commit comments

Comments
 (0)