chore: remove duplicated code for pagination
This commit is contained in:
parent
d41cf7a86c
commit
dd0f716437
10 changed files with 172 additions and 245 deletions
|
|
@ -13,6 +13,8 @@ pub type DatabasePool = sqlx::SqlitePool;
|
||||||
type PoolOptions = sqlx::sqlite::SqlitePoolOptions;
|
type PoolOptions = sqlx::sqlite::SqlitePoolOptions;
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
pub type DatabaseTransaction<'a> = sqlx::Transaction<'a, sqlx::Sqlite>;
|
pub type DatabaseTransaction<'a> = sqlx::Transaction<'a, sqlx::Sqlite>;
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
pub type DatabaseRow = sqlx::sqlite::SqliteRow;
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
pub type DatabasePool = sqlx::PgPool;
|
pub type DatabasePool = sqlx::PgPool;
|
||||||
|
|
@ -20,6 +22,8 @@ pub type DatabasePool = sqlx::PgPool;
|
||||||
type PoolOptions = sqlx::postgres::PgPoolOptions;
|
type PoolOptions = sqlx::postgres::PgPoolOptions;
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
pub type DatabaseTransaction<'a> = sqlx::Transaction<'a, sqlx::Postgres>;
|
pub type DatabaseTransaction<'a> = sqlx::Transaction<'a, sqlx::Postgres>;
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
pub type DatabaseRow = sqlx::postgres::PgRow;
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pool: DatabasePool,
|
pool: DatabasePool,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod pagination;
|
||||||
pub mod roasters;
|
pub mod roasters;
|
||||||
pub mod roasts;
|
pub mod roasts;
|
||||||
pub mod sessions;
|
pub mod sessions;
|
||||||
|
|
|
||||||
76
src/infrastructure/repositories/pagination.rs
Normal file
76
src/infrastructure/repositories/pagination.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
use sqlx::{FromRow, query_as, query_scalar};
|
||||||
|
|
||||||
|
use crate::domain::RepositoryError;
|
||||||
|
use crate::domain::listing::{ListRequest, Page, PageSize, SortKey};
|
||||||
|
use crate::infrastructure::database::{DatabasePool, DatabaseRow};
|
||||||
|
|
||||||
|
pub async fn paginate<K, R, T, MapFn>(
|
||||||
|
pool: &DatabasePool,
|
||||||
|
request: &ListRequest<K>,
|
||||||
|
base_query: &str,
|
||||||
|
count_query: &str,
|
||||||
|
order_clause: &str,
|
||||||
|
map_fn: MapFn,
|
||||||
|
) -> Result<Page<T>, RepositoryError>
|
||||||
|
where
|
||||||
|
K: SortKey,
|
||||||
|
R: for<'r> FromRow<'r, DatabaseRow> + Send + Unpin,
|
||||||
|
T: Send,
|
||||||
|
MapFn: Fn(R) -> Result<T, RepositoryError> + Send + Sync,
|
||||||
|
{
|
||||||
|
match request.page_size() {
|
||||||
|
PageSize::All => {
|
||||||
|
let query = format!("{} ORDER BY {}", base_query, order_clause);
|
||||||
|
let records = query_as::<_, R>(&query)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
|
let mut items = Vec::with_capacity(records.len());
|
||||||
|
for record in records {
|
||||||
|
items.push(map_fn(record)?);
|
||||||
|
}
|
||||||
|
let total = items.len() as u64;
|
||||||
|
let page_size = total.min(u64::from(u32::MAX)) as u32;
|
||||||
|
Ok(Page::new(items, 1, page_size.max(1), total, true))
|
||||||
|
}
|
||||||
|
PageSize::Limited(page_size) => {
|
||||||
|
let limit = page_size as i64;
|
||||||
|
let mut page = request.page();
|
||||||
|
let offset = ((page - 1) as i64).saturating_mul(limit);
|
||||||
|
|
||||||
|
let query_sql = format!("{} ORDER BY {} LIMIT ? OFFSET ?", base_query, order_clause);
|
||||||
|
|
||||||
|
let mut records = query_as::<_, R>(&query_sql)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
|
let total: i64 = query_scalar(count_query)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
|
if page > 1 && records.is_empty() && total > 0 {
|
||||||
|
let last_page = ((total + limit - 1) / limit) as u32;
|
||||||
|
page = last_page.max(1);
|
||||||
|
let offset = ((page - 1) as i64).saturating_mul(limit);
|
||||||
|
records = query_as::<_, R>(&query_sql)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut items = Vec::with_capacity(records.len());
|
||||||
|
for record in records {
|
||||||
|
items.push(map_fn(record)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Page::new(items, page, page_size, total as u64, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use sqlx::{QueryBuilder, query, query_as, query_scalar};
|
use sqlx::{QueryBuilder, query, query_as};
|
||||||
|
|
||||||
use crate::domain::RepositoryError;
|
use crate::domain::RepositoryError;
|
||||||
use crate::domain::ids::RoasterId;
|
use crate::domain::ids::RoasterId;
|
||||||
use crate::domain::listing::{ListRequest, Page, PageSize, SortDirection};
|
use crate::domain::listing::{ListRequest, Page, SortDirection};
|
||||||
use crate::domain::repositories::RoasterRepository;
|
use crate::domain::repositories::RoasterRepository;
|
||||||
use crate::domain::roasters::{NewRoaster, Roaster, RoasterSortKey, UpdateRoaster};
|
use crate::domain::roasters::{NewRoaster, Roaster, RoasterSortKey, UpdateRoaster};
|
||||||
use crate::domain::timeline::TimelineEventDetail;
|
use crate::domain::timeline::TimelineEventDetail;
|
||||||
|
|
@ -159,77 +159,19 @@ impl RoasterRepository for SqlRoasterRepository {
|
||||||
request: &ListRequest<RoasterSortKey>,
|
request: &ListRequest<RoasterSortKey>,
|
||||||
) -> Result<Page<Roaster>, RepositoryError> {
|
) -> Result<Page<Roaster>, RepositoryError> {
|
||||||
let order_clause = Self::sort_clause(request);
|
let order_clause = Self::sort_clause(request);
|
||||||
|
let base_query =
|
||||||
|
"SELECT id, name, country, city, homepage, notes, created_at FROM roasters";
|
||||||
|
let count_query = "SELECT COUNT(*) FROM roasters";
|
||||||
|
|
||||||
match request.page_size() {
|
crate::infrastructure::repositories::pagination::paginate(
|
||||||
PageSize::All => {
|
&self.pool,
|
||||||
let query = format!(
|
request,
|
||||||
"SELECT id, name, country, city, homepage, notes, created_at FROM roasters ORDER BY {}",
|
base_query,
|
||||||
order_clause
|
count_query,
|
||||||
);
|
&order_clause,
|
||||||
|
|record| Ok(Self::into_domain(record)),
|
||||||
let records = query_as::<_, RoasterRecord>(&query)
|
)
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
|
|
||||||
let items = records
|
|
||||||
.into_iter()
|
|
||||||
.map(Self::into_domain)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let total = items.len() as u64;
|
|
||||||
let page_size = total.min(u64::from(u32::MAX)) as u32;
|
|
||||||
|
|
||||||
Ok(Page::new(items, 1, page_size.max(1), total, true))
|
|
||||||
}
|
|
||||||
PageSize::Limited(page_size) => {
|
|
||||||
let limit = page_size as i64;
|
|
||||||
let mut page_number = request.page();
|
|
||||||
let offset = ((page_number - 1) as i64).saturating_mul(limit);
|
|
||||||
|
|
||||||
let query = format!(
|
|
||||||
"SELECT id, name, country, city, homepage, notes, created_at FROM roasters ORDER BY {} LIMIT ? OFFSET ?",
|
|
||||||
order_clause
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut records = query_as::<_, RoasterRecord>(&query)
|
|
||||||
.bind(limit)
|
|
||||||
.bind(offset)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
|
|
||||||
let total: i64 = query_scalar("SELECT COUNT(*) FROM roasters")
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
|
|
||||||
if page_number > 1 && records.is_empty() && total > 0 {
|
|
||||||
let last_page = ((total + limit - 1) / limit) as u32;
|
|
||||||
page_number = last_page.max(1);
|
|
||||||
let offset = ((page_number - 1) as i64).saturating_mul(limit);
|
|
||||||
|
|
||||||
records = query_as::<_, RoasterRecord>(&query)
|
|
||||||
.bind(limit)
|
|
||||||
.bind(offset)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let items = records
|
|
||||||
.into_iter()
|
|
||||||
.map(Self::into_domain)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Ok(Page::new(
|
|
||||||
items,
|
|
||||||
page_number,
|
|
||||||
page_size,
|
|
||||||
total as u64,
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(
|
async fn update(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use sqlx::{Error as SqlxError, QueryBuilder, query, query_as, query_scalar};
|
||||||
|
|
||||||
use crate::domain::RepositoryError;
|
use crate::domain::RepositoryError;
|
||||||
use crate::domain::ids::{RoastId, RoasterId};
|
use crate::domain::ids::{RoastId, RoasterId};
|
||||||
use crate::domain::listing::{ListRequest, Page, PageSize, SortDirection};
|
use crate::domain::listing::{ListRequest, Page, SortDirection};
|
||||||
use crate::domain::repositories::RoastRepository;
|
use crate::domain::repositories::RoastRepository;
|
||||||
use crate::domain::roasts::{NewRoast, Roast, RoastSortKey, RoastWithRoaster, UpdateRoast};
|
use crate::domain::roasts::{NewRoast, Roast, RoastSortKey, RoastWithRoaster, UpdateRoast};
|
||||||
use crate::domain::timeline::TimelineEventDetail;
|
use crate::domain::timeline::TimelineEventDetail;
|
||||||
|
|
@ -195,77 +195,18 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
request: &ListRequest<RoastSortKey>,
|
request: &ListRequest<RoastSortKey>,
|
||||||
) -> Result<Page<RoastWithRoaster>, RepositoryError> {
|
) -> Result<Page<RoastWithRoaster>, RepositoryError> {
|
||||||
let order_clause = Self::order_clause(request);
|
let order_clause = Self::order_clause(request);
|
||||||
|
let base_query = "SELECT r.id, r.roaster_id, r.name, r.origin, r.region, r.producer, r.process, r.tasting_notes, r.created_at, ro.name AS roaster_name \n FROM roasts r \n JOIN roasters ro ON ro.id = r.roaster_id";
|
||||||
|
let count_query = "SELECT COUNT(*) FROM roasts";
|
||||||
|
|
||||||
match request.page_size() {
|
crate::infrastructure::repositories::pagination::paginate(
|
||||||
PageSize::All => {
|
&self.pool,
|
||||||
let query = format!(
|
request,
|
||||||
"SELECT r.id, r.roaster_id, r.name, r.origin, r.region, r.producer, r.process, r.tasting_notes, r.created_at, ro.name AS roaster_name \n FROM roasts r \n JOIN roasters ro ON ro.id = r.roaster_id \n ORDER BY {}",
|
base_query,
|
||||||
order_clause
|
count_query,
|
||||||
);
|
&order_clause,
|
||||||
|
|record: RoastWithRoasterRecord| record.into_with_roaster(),
|
||||||
let records = query_as::<_, RoastWithRoasterRecord>(&query)
|
)
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
|
|
||||||
let items = records
|
|
||||||
.into_iter()
|
|
||||||
.map(|record| record.into_with_roaster())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
let total = items.len() as u64;
|
|
||||||
let page_size = total.min(u64::from(u32::MAX)) as u32;
|
|
||||||
Ok(Page::new(items, 1, page_size.max(1), total, true))
|
|
||||||
}
|
|
||||||
PageSize::Limited(page_size) => {
|
|
||||||
let limit = page_size as i64;
|
|
||||||
let mut page_number = request.page();
|
|
||||||
let offset = ((page_number - 1) as i64).saturating_mul(limit);
|
|
||||||
|
|
||||||
let query = format!(
|
|
||||||
"SELECT r.id, r.roaster_id, r.name, r.origin, r.region, r.producer, r.process, r.tasting_notes, r.created_at, ro.name AS roaster_name \n FROM roasts r \n JOIN roasters ro ON ro.id = r.roaster_id \n ORDER BY {} \n LIMIT ? OFFSET ?",
|
|
||||||
order_clause
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut records = query_as::<_, RoastWithRoasterRecord>(&query)
|
|
||||||
.bind(limit)
|
|
||||||
.bind(offset)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
|
|
||||||
let total: i64 = query_scalar("SELECT COUNT(*) FROM roasts")
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
|
|
||||||
if page_number > 1 && records.is_empty() && total > 0 {
|
|
||||||
let last_page = ((total + limit - 1) / limit) as u32;
|
|
||||||
page_number = last_page.max(1);
|
|
||||||
let offset = ((page_number - 1) as i64).saturating_mul(limit);
|
|
||||||
|
|
||||||
records = query_as::<_, RoastWithRoasterRecord>(&query)
|
|
||||||
.bind(limit)
|
|
||||||
.bind(offset)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let items = records
|
|
||||||
.into_iter()
|
|
||||||
.map(|record| record.into_with_roaster())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
Ok(Page::new(
|
|
||||||
items,
|
|
||||||
page_number,
|
|
||||||
page_size,
|
|
||||||
total as u64,
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_by_roaster(
|
async fn list_by_roaster(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
use sqlx::{query_as, query_scalar};
|
|
||||||
|
|
||||||
use crate::domain::RepositoryError;
|
use crate::domain::RepositoryError;
|
||||||
use crate::domain::ids::TimelineEventId;
|
use crate::domain::ids::TimelineEventId;
|
||||||
use crate::domain::listing::{ListRequest, Page, PageSize, SortDirection};
|
use crate::domain::listing::{ListRequest, Page, SortDirection};
|
||||||
use crate::domain::repositories::TimelineEventRepository;
|
use crate::domain::repositories::TimelineEventRepository;
|
||||||
use crate::domain::timeline::{TimelineEvent, TimelineEventDetail, TimelineSortKey};
|
use crate::domain::timeline::{TimelineEvent, TimelineEventDetail, TimelineSortKey};
|
||||||
use crate::infrastructure::database::DatabasePool;
|
use crate::infrastructure::database::DatabasePool;
|
||||||
|
|
@ -33,73 +31,19 @@ impl TimelineEventRepository for SqlTimelineEventRepository {
|
||||||
};
|
};
|
||||||
|
|
||||||
let order_clause = format!("occurred_at {direction_sql}, id DESC");
|
let order_clause = format!("occurred_at {direction_sql}, id DESC");
|
||||||
|
let base_query = "SELECT id, entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json \
|
||||||
|
FROM timeline_events";
|
||||||
|
let count_query = "SELECT COUNT(*) FROM timeline_events";
|
||||||
|
|
||||||
match request.page_size() {
|
crate::infrastructure::repositories::pagination::paginate(
|
||||||
PageSize::All => {
|
&self.pool,
|
||||||
let query = format!(
|
request,
|
||||||
"SELECT id, entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json \
|
base_query,
|
||||||
FROM timeline_events \
|
count_query,
|
||||||
ORDER BY {order_clause}"
|
&order_clause,
|
||||||
);
|
|record: TimelineEventRecord| record.into_domain(),
|
||||||
|
)
|
||||||
let records = query_as::<_, TimelineEventRecord>(&query)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
|
|
||||||
let mut events = Vec::with_capacity(records.len());
|
|
||||||
for record in records {
|
|
||||||
events.push(record.into_domain()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let total = events.len() as u64;
|
|
||||||
let page_size = total.min(u64::from(u32::MAX)) as u32;
|
|
||||||
Ok(Page::new(events, 1, page_size.max(1), total, true))
|
|
||||||
}
|
|
||||||
PageSize::Limited(page_size) => {
|
|
||||||
let limit = page_size as i64;
|
|
||||||
let mut page = request.page();
|
|
||||||
let offset = ((page - 1) as i64).saturating_mul(limit);
|
|
||||||
|
|
||||||
let query = format!(
|
|
||||||
"SELECT id, entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json \
|
|
||||||
FROM timeline_events \
|
|
||||||
ORDER BY {order_clause} \
|
|
||||||
LIMIT ? OFFSET ?"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut records = query_as::<_, TimelineEventRecord>(&query)
|
|
||||||
.bind(limit)
|
|
||||||
.bind(offset)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
|
|
||||||
let total: i64 = query_scalar("SELECT COUNT(*) FROM timeline_events")
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
|
|
||||||
if page > 1 && records.is_empty() && total > 0 {
|
|
||||||
let last_page = ((total + limit - 1) / limit) as u32;
|
|
||||||
page = last_page.max(1);
|
|
||||||
let offset = ((page - 1) as i64).saturating_mul(limit);
|
|
||||||
records = query_as::<_, TimelineEventRecord>(&query)
|
|
||||||
.bind(limit)
|
|
||||||
.bind(offset)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut events = Vec::with_capacity(records.len());
|
|
||||||
for record in records {
|
|
||||||
events.push(record.into_domain()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Page::new(events, page, page_size, total as u64, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::server::auth::AuthenticatedUser;
|
||||||
use crate::server::errors::{ApiError, AppError, map_app_error};
|
use crate::server::errors::{ApiError, AppError, map_app_error};
|
||||||
use crate::server::routes::render_html;
|
use crate::server::routes::render_html;
|
||||||
use crate::server::routes::support::{
|
use crate::server::routes::support::{
|
||||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, set_datastar_patch_headers,
|
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
||||||
};
|
};
|
||||||
use crate::server::server::AppState;
|
use crate::server::server::AppState;
|
||||||
|
|
||||||
|
|
@ -31,12 +31,13 @@ async fn load_roaster_page(
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
let normalized_request = crate::server::routes::support::normalize_request(request, &page);
|
Ok(crate::server::routes::support::build_page_view(
|
||||||
let roasters = Paginated::from_page(page, RoasterView::from);
|
page,
|
||||||
let navigator =
|
request,
|
||||||
ListNavigator::new(ROASTER_PAGE_PATH, ROASTER_FRAGMENT_PATH, normalized_request);
|
RoasterView::from,
|
||||||
|
ROASTER_PAGE_PATH,
|
||||||
Ok((roasters, navigator))
|
ROASTER_FRAGMENT_PATH,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn roasters_page(
|
pub(crate) async fn roasters_page(
|
||||||
|
|
@ -204,10 +205,5 @@ async fn render_roaster_list_fragment(
|
||||||
navigator,
|
navigator,
|
||||||
};
|
};
|
||||||
|
|
||||||
let html = crate::presentation::templates::render_template(template)
|
crate::server::routes::support::render_fragment(template, "#roaster-list")
|
||||||
.map_err(|err| AppError::unexpected(format!("failed to render roaster list: {err}")))?;
|
|
||||||
|
|
||||||
let mut response = Html(html).into_response();
|
|
||||||
set_datastar_patch_headers(response.headers_mut(), "#roaster-list");
|
|
||||||
Ok(response)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::server::auth::AuthenticatedUser;
|
||||||
use crate::server::errors::{ApiError, AppError, map_app_error};
|
use crate::server::errors::{ApiError, AppError, map_app_error};
|
||||||
use crate::server::routes::render_html;
|
use crate::server::routes::render_html;
|
||||||
use crate::server::routes::support::{
|
use crate::server::routes::support::{
|
||||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, set_datastar_patch_headers,
|
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
||||||
};
|
};
|
||||||
use crate::server::server::AppState;
|
use crate::server::server::AppState;
|
||||||
|
|
||||||
|
|
@ -31,11 +31,13 @@ async fn load_roast_page(
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
let normalized_request = crate::server::routes::support::normalize_request(request, &page);
|
Ok(crate::server::routes::support::build_page_view(
|
||||||
let roasts = Paginated::from_page(page, RoastView::from_list_item);
|
page,
|
||||||
let navigator = ListNavigator::new(ROAST_PAGE_PATH, ROAST_FRAGMENT_PATH, normalized_request);
|
request,
|
||||||
|
RoastView::from_list_item,
|
||||||
Ok((roasts, navigator))
|
ROAST_PAGE_PATH,
|
||||||
|
ROAST_FRAGMENT_PATH,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn roasts_page(
|
pub(crate) async fn roasts_page(
|
||||||
|
|
@ -268,10 +270,5 @@ async fn render_roast_list_fragment(
|
||||||
|
|
||||||
let template = RoastListTemplate { roasts, navigator };
|
let template = RoastListTemplate { roasts, navigator };
|
||||||
|
|
||||||
let html = crate::presentation::templates::render_template(template)
|
crate::server::routes::support::render_fragment(template, "#roast-list")
|
||||||
.map_err(|err| AppError::unexpected(format!("failed to render roast list: {err}")))?;
|
|
||||||
|
|
||||||
let mut response = Html(html).into_response();
|
|
||||||
set_datastar_patch_headers(response.headers_mut(), "#roast-list");
|
|
||||||
Ok(response)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
use axum::async_trait;
|
use axum::async_trait;
|
||||||
use axum::extract::{Form, FromRequest, Json as JsonPayload, Request};
|
use axum::extract::{Form, FromRequest, Json as JsonPayload, Request};
|
||||||
use axum::http::{HeaderMap, HeaderValue, header::CONTENT_TYPE};
|
use axum::http::{HeaderMap, HeaderValue, header::CONTENT_TYPE};
|
||||||
|
use axum::response::{Html, IntoResponse, Response};
|
||||||
|
use askama::Template;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::domain::listing::{
|
use crate::domain::listing::{
|
||||||
DEFAULT_PAGE_SIZE, ListRequest, Page, PageSize, SortDirection, SortKey,
|
DEFAULT_PAGE_SIZE, ListRequest, Page, PageSize, SortDirection, SortKey,
|
||||||
};
|
};
|
||||||
|
use crate::presentation::views::{ListNavigator, Paginated};
|
||||||
use crate::server::errors::{ApiError, AppError};
|
use crate::server::errors::{ApiError, AppError};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
|
@ -90,6 +93,34 @@ where
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_page_view<K, T, V>(
|
||||||
|
page: Page<T>,
|
||||||
|
request: ListRequest<K>,
|
||||||
|
view_mapper: impl FnMut(T) -> V,
|
||||||
|
base_path: &'static str,
|
||||||
|
fragment_path: &'static str,
|
||||||
|
) -> (Paginated<V>, ListNavigator<K>)
|
||||||
|
where
|
||||||
|
K: SortKey,
|
||||||
|
{
|
||||||
|
let normalized_request = normalize_request(request, &page);
|
||||||
|
let view_page = Paginated::from_page(page, view_mapper);
|
||||||
|
let navigator = ListNavigator::new(base_path, fragment_path, normalized_request);
|
||||||
|
(view_page, navigator)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_fragment<T: Template>(
|
||||||
|
template: T,
|
||||||
|
selector: &'static str,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
let html = crate::presentation::templates::render_template(template)
|
||||||
|
.map_err(|err| AppError::unexpected(format!("failed to render fragment: {err}")))?;
|
||||||
|
|
||||||
|
let mut response = Html(html).into_response();
|
||||||
|
set_datastar_patch_headers(response.headers_mut(), selector);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
fn page_size_from_text(value: &str) -> PageSize {
|
fn page_size_from_text(value: &str) -> PageSize {
|
||||||
if value.eq_ignore_ascii_case("all") {
|
if value.eq_ignore_ascii_case("all") {
|
||||||
PageSize::All
|
PageSize::All
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use axum::extract::{Query, State};
|
use axum::extract::{Query, State};
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{Html, IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
|
|
||||||
use crate::domain::listing::ListRequest;
|
use crate::domain::listing::ListRequest;
|
||||||
use crate::domain::timeline::{TimelineEvent, TimelineSortKey};
|
use crate::domain::timeline::{TimelineEvent, TimelineSortKey};
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::presentation::views::{ListNavigator, Paginated, TimelineEventView, Ti
|
||||||
use crate::server::errors::{AppError, map_app_error};
|
use crate::server::errors::{AppError, map_app_error};
|
||||||
use crate::server::routes::render_html;
|
use crate::server::routes::render_html;
|
||||||
use crate::server::routes::support::{
|
use crate::server::routes::support::{
|
||||||
ListQuery, is_datastar_request, normalize_request, set_datastar_patch_headers,
|
ListQuery, is_datastar_request, normalize_request,
|
||||||
};
|
};
|
||||||
use crate::server::server::AppState;
|
use crate::server::server::AppState;
|
||||||
|
|
||||||
|
|
@ -65,12 +65,7 @@ async fn render_timeline_chunk(
|
||||||
months: data.months,
|
months: data.months,
|
||||||
};
|
};
|
||||||
|
|
||||||
let html = crate::presentation::templates::render_template(template)
|
crate::server::routes::support::render_fragment(template, "#timeline-loader")
|
||||||
.map_err(|err| AppError::unexpected(format!("failed to render timeline chunk: {err}")))?;
|
|
||||||
|
|
||||||
let mut response = Html(html).into_response();
|
|
||||||
set_datastar_patch_headers(response.headers_mut(), "#timeline-loader");
|
|
||||||
Ok(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TimelinePageData {
|
struct TimelinePageData {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue