Skip to content

Commit afaa7c4

Browse files
committed
Admin API: parameter to include total number of items
This allows removing the count calculation when not needed, or to skip the list of items entirely.
1 parent dd7d401 commit afaa7c4

File tree

11 files changed

+455
-144
lines changed

11 files changed

+455
-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,
@@ -121,7 +138,23 @@ impl<T: Resource> PaginatedResponse<T> {
121138

122139
Self {
123140
meta: PaginationMeta { count },
124-
data,
141+
data: Some(data),
142+
links,
143+
}
144+
}
145+
146+
pub fn for_count_only(count: usize, base: &str) -> Self {
147+
let links = PaginationLinks {
148+
self_: base.to_owned(),
149+
first: None,
150+
last: None,
151+
next: None,
152+
prev: None,
153+
};
154+
155+
Self {
156+
meta: PaginationMeta { count: Some(count) },
157+
data: None,
125158
links,
126159
}
127160
}

crates/handlers/src/admin/v1/compat_sessions/list.rs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::{
2121
admin::{
2222
call_context::CallContext,
2323
model::{CompatSession, Resource},
24-
params::Pagination,
24+
params::{IncludeCount, Pagination},
2525
response::{ErrorResponse, PaginatedResponse},
2626
},
2727
impl_from_error_for_route,
@@ -143,10 +143,10 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p
143143
};
144144

145145
t.description("Paginated response of compatibility sessions")
146-
.example(PaginatedResponse::new(
146+
.example(PaginatedResponse::for_page(
147147
page,
148148
pagination,
149-
42,
149+
Some(42),
150150
CompatSession::PATH,
151151
))
152152
})
@@ -159,10 +159,11 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p
159159
#[tracing::instrument(name = "handler.admin.v1.compat_sessions.list", skip_all)]
160160
pub async fn handler(
161161
CallContext { mut repo, .. }: CallContext,
162-
Pagination(pagination): Pagination,
162+
Pagination(pagination, include_count): Pagination,
163163
params: FilterParams,
164164
) -> Result<Json<PaginatedResponse<CompatSession>>, RouteError> {
165165
let base = format!("{path}{params}", path = CompatSession::PATH);
166+
let base = include_count.add_to_base(&base);
166167
let filter = CompatSessionFilter::default();
167168

168169
// Load the user from the filter
@@ -206,15 +207,31 @@ pub async fn handler(
206207
None => filter,
207208
};
208209

209-
let page = repo.compat_session().list(filter, pagination).await?;
210-
let count = repo.compat_session().count(filter).await?;
210+
let response = match include_count {
211+
IncludeCount::True => {
212+
let page = repo
213+
.compat_session()
214+
.list(filter, pagination)
215+
.await?
216+
.map(CompatSession::from);
217+
let count = repo.compat_session().count(filter).await?;
218+
PaginatedResponse::for_page(page, pagination, Some(count), &base)
219+
}
220+
IncludeCount::False => {
221+
let page = repo
222+
.compat_session()
223+
.list(filter, pagination)
224+
.await?
225+
.map(CompatSession::from);
226+
PaginatedResponse::for_page(page, pagination, None, &base)
227+
}
228+
IncludeCount::Only => {
229+
let count = repo.compat_session().count(filter).await?;
230+
PaginatedResponse::for_count_only(count, &base)
231+
}
232+
};
211233

212-
Ok(Json(PaginatedResponse::new(
213-
page.map(CompatSession::from),
214-
pagination,
215-
count,
216-
&base,
217-
)))
234+
Ok(Json(response))
218235
}
219236

220237
#[cfg(test)]

crates/handlers/src/admin/v1/oauth2_sessions/list.rs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::{
2525
admin::{
2626
call_context::CallContext,
2727
model::{OAuth2Session, Resource},
28-
params::Pagination,
28+
params::{IncludeCount, Pagination},
2929
response::{ErrorResponse, PaginatedResponse},
3030
},
3131
impl_from_error_for_route,
@@ -198,10 +198,10 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p
198198
};
199199

200200
t.description("Paginated response of OAuth 2.0 sessions")
201-
.example(PaginatedResponse::new(
201+
.example(PaginatedResponse::for_page(
202202
page,
203203
pagination,
204-
42,
204+
Some(42),
205205
OAuth2Session::PATH,
206206
))
207207
})
@@ -218,10 +218,11 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p
218218
#[tracing::instrument(name = "handler.admin.v1.oauth2_sessions.list", skip_all)]
219219
pub async fn handler(
220220
CallContext { mut repo, .. }: CallContext,
221-
Pagination(pagination): Pagination,
221+
Pagination(pagination, include_count): Pagination,
222222
params: FilterParams,
223223
) -> Result<Json<PaginatedResponse<OAuth2Session>>, RouteError> {
224224
let base = format!("{path}{params}", path = OAuth2Session::PATH);
225+
let base = include_count.add_to_base(&base);
225226
let filter = OAuth2SessionFilter::default();
226227

227228
// Load the user from the filter
@@ -300,15 +301,31 @@ pub async fn handler(
300301
None => filter,
301302
};
302303

303-
let page = repo.oauth2_session().list(filter, pagination).await?;
304-
let count = repo.oauth2_session().count(filter).await?;
304+
let response = match include_count {
305+
IncludeCount::True => {
306+
let page = repo
307+
.oauth2_session()
308+
.list(filter, pagination)
309+
.await?
310+
.map(OAuth2Session::from);
311+
let count = repo.oauth2_session().count(filter).await?;
312+
PaginatedResponse::for_page(page, pagination, Some(count), &base)
313+
}
314+
IncludeCount::False => {
315+
let page = repo
316+
.oauth2_session()
317+
.list(filter, pagination)
318+
.await?
319+
.map(OAuth2Session::from);
320+
PaginatedResponse::for_page(page, pagination, None, &base)
321+
}
322+
IncludeCount::Only => {
323+
let count = repo.oauth2_session().count(filter).await?;
324+
PaginatedResponse::for_count_only(count, &base)
325+
}
326+
};
305327

306-
Ok(Json(PaginatedResponse::new(
307-
page.map(OAuth2Session::from),
308-
pagination,
309-
count,
310-
&base,
311-
)))
328+
Ok(Json(response))
312329
}
313330

314331
#[cfg(test)]

0 commit comments

Comments
 (0)