Skip to main content

karbon_framework/db/
pagination.rs

1use serde::{Deserialize, Serialize};
2
3/// Pagination query parameters (from URL: ?page=1&per_page=20&sort=id&order=desc)
4#[derive(Debug, Clone, Deserialize)]
5pub struct PaginationParams {
6    #[serde(default = "default_page")]
7    pub page: u32,
8    #[serde(default = "default_per_page")]
9    pub per_page: u32,
10    #[serde(default)]
11    pub sort: Option<String>,
12    #[serde(default = "default_order")]
13    pub order: String,
14    #[serde(default)]
15    pub search: Option<String>,
16}
17
18fn default_page() -> u32 {
19    1
20}
21fn default_per_page() -> u32 {
22    20
23}
24fn default_order() -> String {
25    "desc".to_string()
26}
27
28/// Maximum allowed per_page to prevent DoS via huge result sets
29const MAX_PER_PAGE: u32 = 200;
30
31impl PaginationParams {
32    /// Returns per_page clamped to MAX_PER_PAGE (200)
33    pub fn safe_per_page(&self) -> u32 {
34        self.per_page.min(MAX_PER_PAGE).max(1)
35    }
36
37    /// Calculate SQL OFFSET (uses safe_per_page)
38    pub fn offset(&self) -> u32 {
39        (self.page.saturating_sub(1)) * self.safe_per_page()
40    }
41
42    /// Validated ORDER direction (only asc/desc)
43    pub fn order_direction(&self) -> &str {
44        match self.order.to_lowercase().as_str() {
45            "asc" => "ASC",
46            _ => "DESC",
47        }
48    }
49
50    /// Validate and return the sort column against an allow-list
51    pub fn sort_column<'a>(&'a self, allowed: &[&'a str], default: &'a str) -> &'a str {
52        match &self.sort {
53            Some(col) if allowed.contains(&col.as_str()) => col.as_str(),
54            _ => default,
55        }
56    }
57}
58
59/// Paginated response wrapper
60#[derive(Debug, Serialize)]
61pub struct Paginated<T: Serialize> {
62    pub data: Vec<T>,
63    pub meta: PaginationMeta,
64}
65
66#[derive(Debug, Serialize)]
67pub struct PaginationMeta {
68    pub page: u32,
69    pub per_page: u32,
70    pub total: u64,
71    pub total_pages: u32,
72    pub has_next: bool,
73    pub has_prev: bool,
74}
75
76impl<T: Serialize> Paginated<T> {
77    /// Create a paginated response from data and total count
78    pub fn new(data: Vec<T>, total: u64, params: &PaginationParams) -> Self {
79        let total_pages = ((total as f64) / (params.per_page as f64)).ceil() as u32;
80        Self {
81            data,
82            meta: PaginationMeta {
83                page: params.page,
84                per_page: params.per_page,
85                total,
86                total_pages,
87                has_next: params.page < total_pages,
88                has_prev: params.page > 1,
89            },
90        }
91    }
92}