use toasty::{Executor, stmt::IntoInsert};
use topcoat::{Result, context::Cx};
use super::{Mounted, Resource, Trashed, require_mounted};
use crate::{
form::{FormValues, Posted, RecordForm},
tenancy::require_tenant,
toasty_compat::{join::JoinTable, model::AppSchema, pk::pk_filter},
};
pub async fn write_create<R: Resource>(
cx: &Cx,
form: R::Form,
ex: &mut dyn Executor,
) -> Result<R::Model> {
let resource = require_mounted::<R>(cx)?;
let links = form.links();
let mut insert = form.into_create().into_insert();
match resource.tenancy.column_field() {
Some(Ok(column)) => {
insert.set(column.index, toasty::stmt::Value::from(require_tenant(cx)?));
}
Some(Err(error)) => {
return Err(crate::error::declaration(
crate::DeclarationError::of::<R>(crate::Site::Tenancy, error).to_string(),
));
}
None => {}
}
let record: R::Model = ex
.exec(insert.into())
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
for (field, keys) in links {
let join = join_table(cx, &resource, field)?;
join.link(&record, &keys, &mut *ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
}
Ok(record)
}
pub async fn write_update<R: Resource>(
cx: &Cx,
mut record: R::Model,
posted: Posted<R::Form>,
ex: &mut dyn Executor,
) -> Result<R::Model> {
let links: Vec<_> = posted
.links()
.into_iter()
.filter(|(field, _)| posted.named(*field))
.collect();
if !links.is_empty() {
let resource = require_mounted::<R>(cx)?;
let mut stored = <R::Form as RecordForm>::hydrate(cx, &record);
for (field, keys) in links {
let join = join_table(cx, &resource, field)?;
let key = form_key(&resource, field);
if !stored.contains(key) {
stored = linked_now::<R>(cx, &record, &mut *ex).await?;
}
let held = stored.list(key).unwrap_or_default();
let removed: Vec<String> = held
.iter()
.filter(|key| !keys.contains(key))
.cloned()
.collect();
let added: Vec<String> = keys.into_iter().filter(|key| !held.contains(key)).collect();
join.unlink(&record, &removed, &mut *ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
join.link(&record, &added, &mut *ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
}
}
{
let pending = posted
.into_update(&mut record)
.map(|update| <R::Form as RecordForm>::exec_update(update, &mut *ex));
if let Some(pending) = pending {
pending
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
}
}
Ok(record)
}
pub(super) async fn delete<R: Resource>(
cx: &Cx,
record: &R::Model,
ex: &mut dyn Executor,
) -> Result<()> {
let resource = require_mounted::<R>(cx)?;
if resource.soft_delete.is_some() {
let now = toasty::stmt::Value::from(jiff::Timestamp::now());
return stamp(cx, &resource, record, Trashed::Without, now, ex).await;
}
remove(cx, &resource, record, Trashed::Without, ex).await
}
pub(super) async fn restore<R: Resource>(
cx: &Cx,
record: &R::Model,
ex: &mut dyn Executor,
) -> Result<()> {
let resource = require_mounted::<R>(cx)?;
stamp(
cx,
&resource,
record,
Trashed::Only,
toasty::stmt::Value::Null,
ex,
)
.await
}
pub(super) async fn force_delete<R: Resource>(
cx: &Cx,
record: &R::Model,
ex: &mut dyn Executor,
) -> Result<()> {
let resource = require_mounted::<R>(cx)?;
remove(cx, &resource, record, Trashed::Only, ex).await
}
async fn stamp<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
record: &R::Model,
trashed: Trashed,
value: toasty::stmt::Value,
ex: &mut dyn Executor,
) -> Result<()> {
let index = match resource.soft_delete.as_ref().map(super::SoftDelete::field) {
Some(Ok((index, _))) => *index,
Some(Err(kind)) => {
return Err(crate::error::declaration(
crate::DeclarationError::of::<R>(crate::Site::Registration, kind.clone())
.to_string(),
));
}
None => {
return Err(crate::error::declaration(format!(
"resource '{}' declares no soft-delete column to restore through",
resource.slug
)));
}
};
let query = resource
.scoped_query_in(cx, trashed)?
.filter(pk_filter(record));
let mut update = toasty::stmt::Update::<toasty::stmt::List<R::Model>>::new(query);
update.as_untyped_mut().returning = None;
update.set(index, value);
toasty::stmt::Statement::<()>::from_untyped_stmt(update.into_untyped())
.exec(ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
Ok(())
}
async fn remove<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
record: &R::Model,
trashed: Trashed,
ex: &mut dyn Executor,
) -> Result<()> {
resource
.scoped_query_in(cx, trashed)?
.filter(pk_filter(record))
.delete()
.exec(ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
Ok(())
}
fn form_key<R: Resource>(resource: &Mounted<R>, field: <R::Form as RecordForm>::Field) -> &str {
resource
.fields
.iter()
.find(|claim| claim.field == field)
.and_then(|claim| claim.keys.first())
.map_or("", String::as_str)
}
fn join_table<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
field: <R::Form as RecordForm>::Field,
) -> Result<JoinTable> {
let key = form_key(resource, field);
let schema = AppSchema::of(cx).ok_or_else(|| {
crate::error::declaration(crate::DeclarationErrorKind::MissingDb.to_string())
})?;
JoinTable::of::<R::Model>(&schema, key).map_err(|fault| {
crate::error::declaration(
crate::DeclarationError::of::<R>(
crate::Site::Form,
crate::DeclarationErrorKind::ManyToMany {
field: key.to_string(),
fault,
},
)
.to_string(),
)
})
}
async fn linked_now<R: Resource>(
cx: &Cx,
record: &R::Model,
ex: &mut dyn Executor,
) -> Result<FormValues> {
let reloaded =
toasty::stmt::Query::<toasty::stmt::List<R::Model>>::all().filter(pk_filter(record));
let reloaded = <R::Form as RecordForm>::includes()
.into_vec()
.into_iter()
.fold(reloaded, |query, include| query.include(include))
.first()
.exec(ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
Ok(reloaded
.map(|reloaded| <R::Form as RecordForm>::hydrate(cx, &reloaded))
.unwrap_or_default())
}