cratestack_core/page.rs
1//! Generic paginated-page envelope used by every `list` route. The shape
2//! mirrors what generated clients consume.
3
4use serde::{Deserialize, Serialize};
5
6/// Hard ceiling on the `limit` query parameter (REST) / RPC list-input
7/// field every generated list route accepts, regardless of whether the
8/// model is `@@paged`. Requests above this are rejected with a `400`,
9/// the same way negative `limit`/`offset` already are — see
10/// `handle_list_<plural>_dispatch` in the generated code, shared
11/// byte-for-byte between REST and RPC dispatch.
12///
13/// Without this, a caller can request an arbitrarily large `limit` and
14/// force the generated handler to fetch (and, for `@@paged` models,
15/// separately COUNT) an unbounded number of rows in one request — a
16/// resource-exhaustion vector with no framework-level mitigation.
17/// Chosen as a generous-but-real ceiling rather than a small one: it
18/// should never trip on realistic paginated-UI or batch-export usage,
19/// only on pathological/abusive requests.
20pub const MAX_LIST_LIMIT: i64 = 1000;
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
23#[serde(rename_all = "camelCase")]
24pub struct PageInfo {
25 pub limit: Option<i64>,
26 pub offset: Option<i64>,
27 pub has_next_page: bool,
28 pub has_previous_page: bool,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct Page<T> {
34 pub items: Vec<T>,
35 pub total_count: Option<i64>,
36 pub page_info: PageInfo,
37}
38
39impl<T> Page<T> {
40 pub fn new(items: Vec<T>, page_info: PageInfo) -> Self {
41 Self {
42 items,
43 total_count: None,
44 page_info,
45 }
46 }
47
48 pub fn with_total_count(mut self, total_count: Option<i64>) -> Self {
49 self.total_count = total_count;
50 self
51 }
52}