use std::{collections::HashSet, sync::Arc};
use topcoat::{
Result,
context::Cx,
router::{Body, error::forbidden},
view::BoxView,
};
use super::{
super::{actions::load_editable, gate::gate, write::commit_write},
common::{
FormParts, blank, drop_client_typed_uploads, file_flags, reject_unknown_form_keys,
rerender_invalid_form, restore_pending_uploads,
},
decode::parse_form_body,
render::FormChrome,
unique::check_unique,
};
use crate::{
db::db,
form::{FieldError, FieldErrors, FormValues, Posted, RecordForm},
policy::Ability,
resource::{Committed, Mounted, Resource},
schema::Schema,
topcoat_compat::async_page,
};
const WRITE_CREATE: &str = "create the record";
const WRITE_UPDATE: &str = "save the changes";
struct Submission {
schema: Arc<Schema>,
values: FormValues,
errors: FieldErrors,
carried: HashSet<String>,
named: HashSet<String>,
}
async fn prepare_submission<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
parts: FormParts,
advisory: Option<&R::Model>,
) -> Result<Submission, topcoat::Error> {
let schema = Arc::clone(&resource.form);
let (mut values, unclaimed) = schema
.read_unclaimed(&parts.pairs)
.map_err(topcoat::router::error::bad_request)?;
reject_unknown_form_keys(&schema, unclaimed)?;
let mut flags = file_flags(&schema, &parts);
let FormParts {
mut files,
file_part_names,
..
} = parts;
let stored = advisory
.map(|advisory| <R::Form as RecordForm>::hydrate(cx, advisory))
.unwrap_or_default();
let editing = advisory.is_some();
for key in schema.disabled_keys(editing) {
files.remove(key);
flags.remove(key);
}
schema.drop_disabled(&mut values, editing);
let mut read = stored.clone();
read.extend(values.clone());
for key in &schema.condition_hidden(&read) {
values.remove(key);
files.remove(key);
flags.remove(key);
}
drop_client_typed_uploads(&schema, &file_part_names, &mut values);
let (upload_errors, mut carried) =
crate::upload::store_uploads(cx, &schema, &files, &mut values).await;
carried.extend(restore_pending_uploads(cx, &flags, &mut values).await);
for (name, flags) in &flags {
if !flags.clear && blank(&values, name) && !blank(&stored, name) {
values.remove(name);
}
}
let named: HashSet<String> = values.keys().map(str::to_string).collect();
complete(&schema, &mut values, &named, &stored);
let mut errors = FieldErrors::new();
schema
.check_controls(cx, &values, &stored, &mut errors)
.await;
errors.replace(upload_errors);
Ok(Submission {
schema,
values,
errors,
carried,
named,
})
}
fn complete(
schema: &Schema,
values: &mut FormValues,
named: &HashSet<String>,
stored: &FormValues,
) {
for field in schema.fields() {
let name = field.name();
if named.contains(name) {
continue;
}
match stored.get(name) {
Some(value) => values.insert(name, value.clone()),
None => values.remove(name),
};
}
}
fn parse_form<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
values: &FormValues,
errors: &mut FieldErrors,
) -> Result<Option<R::Form>, topcoat::Error> {
let unbound = |source: &str, key: &dyn std::fmt::Debug| {
crate::error::declaration(format!(
"{source} refused {key:?}, which `{}` binds to no form key",
std::any::type_name::<R::Form>()
))
};
match <R::Form as RecordForm>::parse(cx, values) {
Ok(form) => {
for FieldError { key, kind } in R::validate_record(cx, &form) {
let Some(first) = resource
.fields
.iter()
.find(|field| field.field == key)
.and_then(|field| field.keys.first())
else {
return Err(unbound("validate_record", &key));
};
errors.push(FieldError {
key: first.clone(),
kind,
});
}
Ok(Some(form))
}
Err(failures) => {
for failure in failures {
if !resource.fields.iter().any(|field| {
field
.keys
.iter()
.any(|key| renders_under(&resource.form, key, &failure.key))
}) {
return Err(unbound("the parse", &failure.key));
}
if !errors.contains_key(&failure.key) {
errors.push(failure);
}
}
Ok(None)
}
}
}
fn renders_under(form: &Schema, key: &str, failed: &str) -> bool {
match failed.strip_prefix(key) {
Some("") => true,
Some(rest) => {
rest.starts_with('.')
&& form
.fields()
.any(|field| field.name() == key && field.as_repeater().is_some())
}
None => false,
}
}
async fn recheck_relationships(
cx: &Cx,
schema: &Schema,
values: &FormValues,
stored: &FormValues,
errors: &mut FieldErrors,
ex: &mut dyn toasty::Executor,
) {
if errors.is_empty() {
errors.extend(schema.recheck_relationships(cx, values, stored, ex).await);
}
}
fn named_fields<R: Resource>(
resource: &Mounted<R>,
named: &HashSet<String>,
) -> Vec<<R::Form as RecordForm>::Field> {
resource
.fields
.iter()
.filter(|field| field.keys.iter().any(|key| named.contains(key)))
.map(|field| field.field)
.collect()
}
pub(crate) fn resource_create_post<R: Resource>(cx: &Cx, body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
if !resource.can(cx, Ability::Create) {
return Err(forbidden().into());
}
let parts = parse_form_body(cx, body).await?;
crate::csrf::verify(cx, parts.last(crate::csrf::FIELD_NAME))?;
let Submission {
schema,
values,
mut errors,
carried,
..
} = prepare_submission(cx, &resource, parts, None).await?;
let mut db = db(cx);
let mut tx = db.transaction().await.map_err(crate::error::unavailable)?;
errors.extend(
check_unique(cx, &resource, &schema, &values, &FormValues::new(), &mut tx).await?,
);
let form = parse_form(cx, &resource, &values, &mut errors)?;
recheck_relationships(
cx,
&schema,
&values,
&FormValues::new(),
&mut errors,
&mut tx,
)
.await;
let Some(form) = form.filter(|_| errors.is_empty()) else {
return rerender_invalid_form(
cx,
&resource,
tx,
FormChrome::create(&resource),
&values,
&errors,
&carried,
)
.await;
};
let written = R::create_record(cx, form, &mut tx).await;
commit_write(
cx,
&resource,
tx,
written,
Committed::created,
"Created",
WRITE_CREATE,
)
.await
})
}
pub(crate) fn resource_edit_post<R: Resource>(cx: &Cx, body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
let parts = parse_form_body(cx, body).await?;
crate::csrf::verify(cx, parts.last(crate::csrf::FIELD_NAME))?;
let mut db0 = db(cx);
let advisory = load_editable(cx, &resource, &mut db0).await?;
if !resource.can(cx, Ability::Update(&advisory)) {
return Err(forbidden().into());
}
let Submission {
schema,
mut values,
mut errors,
carried,
named,
} = prepare_submission(cx, &resource, parts, Some(&advisory)).await?;
let mut db = db(cx);
let mut tx = db.transaction().await.map_err(crate::error::unavailable)?;
let record = load_editable(cx, &resource, &mut tx).await?;
if !resource.can(cx, Ability::Update(&record)) {
return Err(forbidden().into());
}
let stored = <R::Form as RecordForm>::hydrate(cx, &record);
complete(&schema, &mut values, &named, &stored);
schema
.keep_unseen_links(cx, &mut values, &stored, &mut tx)
.await;
errors.extend(check_unique(cx, &resource, &schema, &values, &stored, &mut tx).await?);
let form = parse_form(cx, &resource, &values, &mut errors)?;
recheck_relationships(cx, &schema, &values, &stored, &mut errors, &mut tx).await;
let Some(form) = form.filter(|_| errors.is_empty()) else {
return rerender_invalid_form(
cx,
&resource,
tx,
FormChrome::edit(cx, &resource, &record),
&values,
&errors,
&carried,
)
.await;
};
let posted = Posted::new(form, named_fields(&resource, &named));
let written = R::update_record(cx, record, posted, &mut tx).await;
commit_write(
cx,
&resource,
tx,
written,
Committed::updated,
"Updated",
WRITE_UPDATE,
)
.await
})
}
#[cfg(test)]
mod tests;