use toasty::{
Executor,
stmt::{List, Query},
};
use topcoat::{Result, context::Cx};
use crate::form::{FieldErrors, Posted, RecordForm};
mod action;
mod commit;
mod def;
mod header_action;
mod mounted;
mod relation;
mod write;
#[doc(hidden)]
pub use action::required_input;
pub use action::{Action, ActionInput, Places};
pub(crate) use action::{
ActionEntry, Actions, ErasedInput, InputResult, InputSpec, RESERVED_KEYS, parse_validated,
};
pub(crate) use commit::run_after_commit;
pub use commit::{Committed, Mutation};
pub use def::{PublicLink, ResourceDef};
pub(crate) use header_action::HeaderEntry;
pub use header_action::{HeaderAction, HeaderActions};
pub(crate) use mounted::{MountScope, Mounted, Mounts, mounted, require_mounted};
pub use relation::{ForeignKey, Relation};
pub(crate) use relation::{Links, RelationKind};
pub use write::{write_create, write_update};
pub trait Resource: Sized + Send + Sync + 'static {
type Model: crate::ModelColumns
+ toasty::stmt::IntoExpr<Self::Model>
+ Send
+ Sync
+ Clone
+ 'static;
type Form: RecordForm<Model = Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
}
fn query(_cx: &Cx) -> toasty::stmt::Query<List<Self::Model>> {
toasty::stmt::Query::<List<Self::Model>>::all()
}
fn validate_record(
_cx: &Cx,
_form: &Self::Form,
) -> FieldErrors<<Self::Form as RecordForm>::Field> {
FieldErrors::new()
}
fn create_record(
cx: &Cx,
form: Self::Form,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<Self::Model>> + Send {
write_create::<Self>(cx, form, ex)
}
fn update_record(
cx: &Cx,
record: Self::Model,
posted: Posted<Self::Form>,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<Self::Model>> + Send {
write_update::<Self>(cx, record, posted, ex)
}
fn delete_record(
cx: &Cx,
record: &Self::Model,
ex: &mut dyn toasty::Executor,
) -> impl std::future::Future<Output = Result<()>> + Send
where
Self: Sized,
{
let filter = crate::toasty_compat::pk::pk_filter(record);
let query = scoped_query::<Self>(cx).map(|query| query.filter(filter));
async move {
query?
.delete()
.exec(ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
Ok(())
}
}
fn bulk_delete_records(
cx: &Cx,
records: &[Self::Model],
ex: &mut dyn toasty::Executor,
) -> impl std::future::Future<Output = Result<()>> + Send
where
Self: Sized,
{
async move {
for record in records {
Self::delete_record(cx, record, &mut *ex).await?;
}
Ok(())
}
}
fn after_commit(
_cx: &Cx,
_committed: Committed<Self::Model>,
) -> impl std::future::Future<Output = Result<()>> + Send
where
Self: Sized,
{
async move { Ok(()) }
}
}
pub fn scoped_query<R: Resource>(cx: &Cx) -> Result<Query<List<R::Model>>> {
require_mounted::<R>(cx)?.scoped_query(cx)
}
impl<R: Resource> Mounted<R> {
pub(crate) fn scoped_query(&self, cx: &Cx) -> Result<Query<List<R::Model>>> {
self.tenant_scope(cx, R::query(cx))
}
fn tenant_scope(&self, cx: &Cx, query: Query<List<R::Model>>) -> Result<Query<List<R::Model>>> {
if !self.tenancy.is_scoped() {
return Ok(query);
}
if let Some(Err(error)) = self.tenancy.column_field() {
return Err(crate::error::declaration(
crate::DeclarationError::of::<R>(crate::Site::Tenancy, error).to_string(),
));
}
let tenant = crate::tenancy::require_tenant(cx)?;
Ok(match self.tenancy.filter(tenant) {
Some(filter) => query.filter(filter),
None => query,
})
}
}
pub fn can<R: Resource>(cx: &Cx, ability: crate::Ability<'_, R::Model>) -> bool {
mounted::<R>(cx).is_some_and(|resource| resource.can(cx, ability))
}
impl<R: Resource> crate::schema::OptionSource for R {
type Model = R::Model;
fn scoped_query(cx: &Cx) -> Result<Query<List<R::Model>>> {
scoped_query::<R>(cx)
}
fn allows(cx: &Cx, ability: crate::Ability<'_, R::Model>) -> bool {
can::<R>(cx, ability)
}
fn requires_tenant(cx: &Cx) -> bool {
mounted::<R>(cx).is_some_and(|mounted| mounted.tenancy.is_scoped())
}
fn search_expr(cx: &Cx, term: &str) -> Option<toasty::stmt::Expr<bool>> {
mounted::<R>(cx)?.option_search_expr(term)
}
fn order_by(cx: &Cx) -> Option<toasty::stmt::OrderByExpr> {
mounted::<R>(cx)?.table.order_by(false)
}
fn label(cx: &Cx, record: &R::Model) -> String {
let key = crate::toasty_compat::pk::pk_text(record);
match mounted::<R>(cx) {
Some(mounted) => mounted.record_title(record, &key),
None => key,
}
}
fn available(cx: &Cx) -> bool {
mounted::<R>(cx).is_some()
}
}
#[cfg(test)]
mod tests;