diff --git a/src/leaderboard.rs b/src/leaderboard.rs index 596d3fa..bbf0af3 100644 --- a/src/leaderboard.rs +++ b/src/leaderboard.rs @@ -10,21 +10,39 @@ use sqlx::PgPool; use crate::{ AppState, - model::{ActivityCrossing, LeaderboardGroup, LeaderboardRow}, + model::{ActivityCrossing, LeaderboardGroup, LeaderboardPage, LeaderboardRow}, }; #[derive(Debug, Deserialize, Default)] pub struct LeaderboardQuery { pub group_id: Option, + #[serde(default)] + pub offset: i64, + #[serde(default = "default_page_size")] + pub limit: i64, +} + +fn default_page_size() -> i64 { + 50 } pub async fn api( State(state): State, Query(query): Query, -) -> Result>, (axum::http::StatusCode, String)> { - build(&state.db, query.group_id) +) -> Result, (axum::http::StatusCode, String)> { + let offset = query.offset.max(0); + let limit = query.limit.clamp(1, 200); + + build(&state.db, query.group_id, offset, limit) .await - .map(Json) + .map(|groups| { + let returned = groups.iter().map(|group| group.rows.len() as i64).sum::(); + Json(LeaderboardPage { + groups, + next_offset: offset + returned, + has_more: returned == limit, + }) + }) .map_err(|error| { tracing::error!(%error, "leaderboard query failed"); ( @@ -34,7 +52,7 @@ pub async fn api( }) } -pub async fn build(db: &PgPool, group_id: Option) -> Result> { +pub async fn build(db: &PgPool, group_id: Option, offset: i64, limit: i64) -> Result> { let rows = sqlx::query( r#" WITH bucketed AS ( @@ -114,9 +132,13 @@ pub async fn build(db: &PgPool, group_id: Option) -> Result, } +#[derive(Debug, Serialize, Clone)] +pub struct LeaderboardPage { + pub groups: Vec, + pub next_offset: i64, + pub has_more: bool, +} + #[derive(Debug, Serialize, Clone)] pub struct ActivityCrossing { pub crossing_time: DateTime,