use std::collections::HashMap;
use toasty::{
Executor,
stmt::{List, Query},
};
use topcoat::{Result, context::Cx};
use crate::form::{FieldErrors, Posted, RecordForm};
mod action;
mod commit;
mod def;
mod link;
mod mounted;
mod relation;
mod write;
pub use action::Action;
pub(crate) use action::{ActionEntry, Actions};
pub(crate) use commit::run_after_commit;
pub use commit::{Committed, Mutation};
pub use def::ResourceDef;
pub use link::PublicLink;
pub(crate) use mounted::{MountScope, Mounted, Mounts, mounted, require_mounted};
pub use relation::{ForeignKey, Relation};
pub use write::{write_create, write_update};
pub trait Resource: Sized + Send + Sync + 'static {
type Model: toasty::schema::Model
+ toasty::stmt::IntoExpr<Self::Model>
+ Send
+ Sync
+ Clone
+ 'static;
type Form: RecordForm<Model = Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
}
fn view_content<'a>(_cx: &'a Cx, _record: &Self::Model) -> Option<topcoat::view::BoxView<'a>> {
None
}
fn record_label(_cx: &Cx, _record: &Self::Model) -> Option<String> {
None
}
fn public_link(_cx: &Cx, _record: &Self::Model) -> Option<PublicLink> {
None
}
fn query(_cx: &Cx) -> toasty::stmt::Query<List<Self::Model>> {
toasty::stmt::Query::<List<Self::Model>>::all()
}
fn view_query(cx: &Cx) -> toasty::stmt::Query<List<Self::Model>> {
Self::query(cx)
}
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(()) }
}
fn view_values(_cx: &Cx, _record: &Self::Model) -> HashMap<String, String> {
HashMap::new()
}
}
pub fn scoped_query<R: Resource>(cx: &Cx) -> Result<Query<List<R::Model>>> {
require_mounted::<R>(cx)?.scoped_query(cx)
}
pub fn scoped_view_query<R: Resource>(cx: &Cx) -> Result<Query<List<R::Model>>> {
require_mounted::<R>(cx)?.scoped_view_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))
}
pub(crate) fn scoped_view_query(&self, cx: &Cx) -> Result<Query<List<R::Model>>> {
self.tenant_scope(cx, R::view_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)?.table.search_expr(term)
}
fn order_by(cx: &Cx) -> Option<toasty::stmt::OrderByExpr> {
mounted::<R>(cx)?.table.order_by(false)
}
fn available(cx: &Cx) -> bool {
mounted::<R>(cx).is_some()
}
}
#[cfg(test)]
mod tests;