use topcoat::{Result, context::Cx};
use crate::{
error::TabloError,
form::RecordForm,
policy::Ability,
resource::{Mounted, Resource, Trashed},
};
pub(crate) async fn find_by_key<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
id: &str,
ex: &mut dyn toasty::Executor,
) -> Result<R::Model> {
find_trashed_by_key(cx, resource, id, Trashed::Without, ex).await
}
pub(crate) async fn find_trashed_by_key<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
id: &str,
trashed: Trashed,
ex: &mut dyn toasty::Executor,
) -> Result<R::Model> {
find_by_key_in(resource, id, ex, || resource.scoped_query_in(cx, trashed)).await
}
pub(super) fn composite_pk_error<R: Resource>(resource: &Mounted<R>) -> Option<topcoat::Error> {
if !crate::toasty_compat::pk::pk_is_composite::<R::Model>() {
return None;
}
tracing::error!(
resource = resource.slug,
"composite primary key has no URL representation"
);
Some(
TabloError::Declaration(format!(
"resource '{}' has a composite primary key, which has no URL representation",
resource.slug
))
.into(),
)
}
async fn find_by_key_in<R: Resource>(
resource: &Mounted<R>,
id: &str,
ex: &mut dyn toasty::Executor,
seed: impl FnOnce() -> Result<toasty::stmt::Query<toasty::stmt::List<R::Model>>>,
) -> Result<R::Model> {
let Some(expr) = crate::toasty_compat::pk::pk_eq_expr::<R::Model>(id) else {
if let Some(error) = composite_pk_error(resource) {
return Err(error);
}
return Err(topcoat::router::error::not_found().into());
};
seed()?
.filter(expr)
.first()
.exec(&mut *ex)
.await
.map_err(crate::error::unavailable)?
.ok_or_else(topcoat::router::error::not_found)
.map_err(Into::into)
}
pub(crate) async fn load_editable<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
ex: &mut dyn toasty::Executor,
) -> Result<R::Model> {
let id = topcoat::router::path_param_segment(cx, "id").to_string();
let record = find_by_key_in(resource, &id, ex, || {
resource.scoped_query(cx).map(|query| {
<R::Form as RecordForm>::includes()
.into_vec()
.into_iter()
.fold(query, |query, include| query.include(include))
})
})
.await?;
check_viewable(cx, resource, record)
}
pub(crate) async fn load_detail<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
ex: &mut dyn toasty::Executor,
) -> Result<R::Model> {
let id = topcoat::router::path_param_segment(cx, "id").to_string();
check_viewable(cx, resource, find_detail(cx, resource, &id, ex).await?)
}
async fn find_detail<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
id: &str,
ex: &mut dyn toasty::Executor,
) -> Result<R::Model> {
let record = find_by_key_in(resource, id, &mut *ex, || {
resource
.scoped_query_in(cx, Trashed::With)
.map(|query| resource.detail.include_relations(query))
})
.await?;
resource
.detail
.restrict_relations(cx, std::slice::from_ref(&record), ex)
.await?;
Ok(record)
}
fn check_viewable<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
record: R::Model,
) -> Result<R::Model> {
if !resource.can(cx, Ability::View(&record)) {
return Err(topcoat::router::error::forbidden().into());
}
Ok(record)
}
#[cfg(test)]
mod tests;