use toasty::stmt::{List, Query};
use toasty_core::stmt::Value;
use topcoat::{Result, context::Cx};
use super::{Cursor, Table, TableState};
#[derive(Debug, Clone)]
pub struct TablePage<M> {
pub rows: Vec<M>,
pub next_cursor: Option<String>,
pub prev_cursor: Option<String>,
}
impl<M> From<Vec<M>> for TablePage<M> {
fn from(rows: Vec<M>) -> Self {
Self {
rows,
next_cursor: None,
prev_cursor: None,
}
}
}
impl<M: toasty::schema::Model> TablePage<M> {
pub fn from_toasty_page(page: toasty::stmt::Page<M>) -> Result<Self> {
Ok(Self {
rows: page.items,
next_cursor: page
.next_cursor
.as_ref()
.map(crate::toasty_compat::cursor::encode)
.transpose()?,
prev_cursor: page
.prev_cursor
.as_ref()
.map(crate::toasty_compat::cursor::encode)
.transpose()?,
})
}
}
impl<M> TablePage<M>
where
M: toasty::schema::Model + Send + Sync + 'static,
{
pub async fn load(
cx: &Cx,
table: &Table<M>,
query: Query<List<M>>,
state: &TableState,
) -> Result<Self> {
let base_query = table.apply_declaration(query, state);
let query = table.include_relations(base_query.clone());
let mut db = crate::db::db(cx);
let per_page = table.page_size();
let mut paginated = toasty::stmt::Paginate::new(query, per_page);
match &state.cursor {
Some(Cursor::After(token)) => {
paginated = paginated.after(crate::toasty_compat::cursor::decode(token)?);
}
Some(Cursor::Before(token)) => {
paginated = paginated.before(crate::toasty_compat::cursor::decode(token)?);
}
None => {}
}
let loaded = paginated
.exec(&mut db)
.await
.map_err(|error| reject_cursor(error.into(), state))?;
let mut page = TablePage::from_toasty_page(loaded)?;
if matches!(state.cursor, Some(Cursor::Before(_))) {
if let Some(cursor) = page.prev_cursor.clone() {
let past = Past::Before(crate::toasty_compat::cursor::decode(&cursor)?);
if !row_exists_past(&mut db, base_query, past)
.await
.map_err(crate::error::unavailable)?
{
page.prev_cursor = None;
}
}
} else if page.rows.len() == per_page {
if let Some(cursor) = page.next_cursor.clone() {
let past = Past::After(crate::toasty_compat::cursor::decode(&cursor)?);
if !row_exists_past(&mut db, base_query, past)
.await
.map_err(crate::error::unavailable)?
{
page.next_cursor = None;
}
}
} else {
page.next_cursor = None;
}
Ok(page)
}
}
pub(crate) enum Past {
After(Value),
Before(Value),
}
pub(crate) async fn row_exists_past<M>(
db: &mut toasty::Db,
query: Query<List<M>>,
past: Past,
) -> toasty::Result<bool>
where
M: toasty::schema::Model + Send + Sync + 'static,
{
let probe = toasty::stmt::Paginate::new(query, 1);
let probe = match past {
Past::After(cursor) => probe.after(cursor),
Past::Before(cursor) => probe.before(cursor),
};
Ok(!probe.exec(db).await?.items.is_empty())
}
fn reject_cursor(error: topcoat::Error, state: &TableState) -> topcoat::Error {
let cursored = state.cursor.is_some();
let rejected = error
.downcast_ref::<toasty::Error>()
.is_some_and(toasty::Error::is_invalid_statement);
if cursored && rejected {
crate::toasty_compat::cursor::rejected(&error)
} else {
crate::error::unavailable(error)
}
}