use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::Operation;
pub const MAX_PAGE_ROWS: u32 = 1_000;
pub const MAX_PINNED_ROWS: u32 = 10 * MAX_PAGE_ROWS;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct PageArgs {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub until: Option<String>,
pub limit: u32,
}
impl PageArgs {
#[must_use]
pub fn first(limit: u32) -> Self {
Self {
after: None,
until: None,
limit,
}
}
#[must_use]
pub fn after(end: Option<String>, limit: u32) -> Self {
Self {
after: end,
until: None,
limit,
}
}
#[must_use]
pub fn pinned(mut self, end: Option<String>) -> Self {
self.until = end;
self
}
#[must_use]
pub fn is_pinned(&self) -> bool {
self.until.is_some()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Page<T> {
pub items: Vec<T>,
pub end: Option<String>,
pub has_more: bool,
}
impl<T> Default for Page<T> {
fn default() -> Self {
Self {
items: Vec::new(),
end: None,
has_more: false,
}
}
}
pub trait Paginated: Operation<Output = Page<Self::Item>> {
type Item: Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static;
#[must_use]
fn with_page(self, page: PageArgs) -> Self;
}
#[cfg(any(feature = "postgres", feature = "sqlite"))]
pub async fn paginate<T, V>(
db: &impl leptoskit_db::Read,
query: leptoskit_db::Query<T>,
page: &PageArgs,
order: impl FnOnce(&T::Columns) -> leptoskit_db::Order,
key: impl Fn(&T) -> V,
) -> crate::Result<Page<T>>
where
T: leptoskit_db::Table,
V: leptoskit_db::Column + Serialize + serde::de::DeserializeOwned,
{
let order = order(T::columns());
let column = leptoskit_db::Col::<T, V>::new(order.column());
let descending = order.is_descending();
let cursor = |text: &str| {
serde_json::from_str::<V>(text)
.map_err(|_| crate::Error::invalid(format!("`{text}` isn't a cursor of this query")))
};
let mut query = query.order_by(|_| order);
if let Some(after) = page.after.as_deref() {
let after = cursor(after)?;
query = query.filter(|_| {
if descending {
column.lt(after)
} else {
column.gt(after)
}
});
}
if let Some(until) = page.until.as_deref() {
let until_value = cursor(until)?;
query = query.filter(|_| {
if descending {
column.gte(until_value)
} else {
column.lte(until_value)
}
});
let items = query.limit(u64::from(MAX_PINNED_ROWS)).all(db).await?;
return Ok(Page {
items,
end: Some(until.to_owned()),
has_more: true,
});
}
let rows = page.limit.clamp(1, MAX_PAGE_ROWS);
let limit = usize::try_from(rows).unwrap_or(usize::MAX);
let mut items = query.limit(u64::from(rows) + 1).all(db).await?;
let has_more = items.len() > limit;
items.truncate(limit);
let end = match items.last() {
Some(row) => {
Some(
serde_json::to_string(&key(row))
.map_err(|_| crate::Error::server("a page's cursor didn't serialize"))?,
)
}
None => page.after.clone(),
};
Ok(Page {
items,
end,
has_more,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pages_follow_and_pin() {
let first = PageArgs::first(20);
assert!(!first.is_pinned());
let pinned = first.clone().pinned(Some("\"b\"".into()));
assert_eq!(pinned.until.as_deref(), Some("\"b\""));
assert!(pinned.is_pinned());
let next = PageArgs::after(pinned.until.clone(), 20);
assert_eq!(next.after.as_deref(), Some("\"b\""));
assert_eq!(
serde_json::to_value(&first).unwrap(),
serde_json::json!({ "limit": 20 })
);
assert_eq!(Page::<u8>::default().items, Vec::<u8>::new());
}
}