use toasty::{Executor, stmt::IntoInsert};
use topcoat::{Result, context::Cx};
use super::{Mounted, Resource, require_mounted};
use crate::{
form::{Posted, RecordForm, decode_list},
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(key) {
stored = linked_now::<R>(cx, &record, &mut *ex).await?;
}
let held = stored
.get(key)
.and_then(|value| decode_list(value))
.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)
}
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<std::collections::HashMap<String, String>> {
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())
}