Files
imks/repo/pagination.rs
T
zhenyi 821537186e refactor(tests): reformat code and update dependency management
- Reorganized import statements in adapter tests for better readability
- Replaced or_insert_with(Vec::new) with or_default() in test closures
- Updated Cargo.lock with new dependency versions and checksums
- Added TLS features to tonic dependency configuration
- Included sqlx, chrono, and uuid dependencies with specific features
- Added jsonwebtoken and arc-swap as project dependencies
- Reformatted assertion statements to comply with line length limits
- Adjusted base64 import order in engine codec module
- Updated protobuf include statement formatting
2026-06-11 12:11:05 +08:00

115 lines
3.2 KiB
Rust

//! Cursor-based pagination helpers for repository queries.
//!
//! UUID v7 IDs are time-ordered, so `WHERE id < $cursor ORDER BY id DESC`
//! naturally yields reverse-chronological pages without OFFSET.
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Default number of items per page.
pub const DEFAULT_PAGE_SIZE: i64 = 50;
/// Hard upper bound on page size to prevent abuse.
pub const MAX_PAGE_SIZE: i64 = 100;
/// Generic cursor-based page response.
///
/// Returned by list operations in the repo layer. The `next_cursor`
/// is the last item's UUID — pass it as `before` in the next request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CursorPage<T> {
/// Items in this page (ordered by `id DESC`).
pub items: Vec<T>,
/// Opaque cursor for the next page. `None` when no more results exist.
pub next_cursor: Option<Uuid>,
/// Whether there are more results beyond this page.
pub has_more: bool,
}
impl<T> CursorPage<T> {
/// Build a page from a raw result set that may contain one extra row.
///
/// If `raw_items.len() > limit`, the extra row is dropped and
/// `has_more` is set to `true`.
pub fn from_raw(mut raw_items: Vec<T>, limit: i64, get_id: impl Fn(&T) -> Uuid) -> Self {
let has_more = raw_items.len() > limit as usize;
if has_more {
raw_items.truncate(limit as usize);
}
let next_cursor = if has_more {
raw_items.last().map(get_id)
} else {
None
};
Self {
items: raw_items,
next_cursor,
has_more,
}
}
/// Empty page (no results).
pub fn empty() -> Self {
Self {
items: Vec::new(),
next_cursor: None,
has_more: false,
}
}
}
/// Clamp a caller-requested limit to `[1, MAX_PAGE_SIZE]`, defaulting to `DEFAULT_PAGE_SIZE`.
pub fn clamp_limit(limit: Option<i64>) -> i64 {
match limit {
Some(n) if n < 1 => DEFAULT_PAGE_SIZE,
Some(n) if n > MAX_PAGE_SIZE => MAX_PAGE_SIZE,
Some(n) => n,
None => DEFAULT_PAGE_SIZE,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clamp_limit_none() {
assert_eq!(clamp_limit(None), DEFAULT_PAGE_SIZE);
}
#[test]
fn test_clamp_limit_zero() {
assert_eq!(clamp_limit(Some(0)), DEFAULT_PAGE_SIZE);
}
#[test]
fn test_clamp_limit_negative() {
assert_eq!(clamp_limit(Some(-5)), DEFAULT_PAGE_SIZE);
}
#[test]
fn test_clamp_limit_over_max() {
assert_eq!(clamp_limit(Some(200)), MAX_PAGE_SIZE);
}
#[test]
fn test_clamp_limit_valid() {
assert_eq!(clamp_limit(Some(25)), 25);
}
#[test]
fn test_cursor_page_empty() {
let page: CursorPage<String> = CursorPage::empty();
assert!(page.items.is_empty());
assert!(!page.has_more);
assert!(page.next_cursor.is_none());
}
#[test]
fn test_cursor_page_from_raw_no_overflow() {
let items = vec!["a".to_string(), "b".to_string()];
let page = CursorPage::from_raw(items, 5, |_| Uuid::nil());
assert_eq!(page.items.len(), 2);
assert!(!page.has_more);
}
}