use std::future::Future;
use topcoat::{context::Cx, router::error::see_other, view::BoxView};
use super::{forms::AfterCommit, gate::landing_url, state::uploader};
use crate::{
db::WriteTx,
flash::{notify_write_failure, set_notification},
form::RecordForm,
notification::Notification,
outcome::Outcome,
resource::{Change, Mounted, Resource},
};
pub(crate) async fn commit_write<'a, R: Resource, T>(
cx: &'a Cx,
resource: &Mounted<R>,
tx: WriteTx<'_>,
written: Result<(T, Outcome), topcoat::Error>,
change: impl FnOnce(T) -> Change<R::Model>,
note: impl Into<String>,
failure: &'static str,
) -> Result<BoxView<'a>, topcoat::Error> {
commit_write_then(cx, resource, tx, written, change, note, failure, Vec::new()).await
}
#[expect(
clippy::too_many_arguments,
reason = "commit_write's arguments and the related forms' hooks"
)]
pub(crate) async fn commit_write_then<'a, R: Resource, T>(
cx: &'a Cx,
resource: &Mounted<R>,
mut tx: WriteTx<'_>,
written: Result<(T, Outcome), topcoat::Error>,
change: impl FnOnce(T) -> Change<R::Model>,
note: impl Into<String>,
failure: &'static str,
also: Vec<AfterCommit>,
) -> Result<BoxView<'a>, topcoat::Error> {
let written = match written {
Ok((value, outcome)) => {
let change = change(value);
match super::commit_hook::before_commit::<R>(cx, &change, &mut *tx).await {
Ok(()) => match end_changed_credentials(cx, &mut *tx, &change).await {
Ok(()) => keep_uploads(cx, resource, &change)
.await
.map(|()| (change, outcome)),
Err(error) => Err(error),
},
Err(error) => Err(error),
}
}
Err(error) => Err(error),
};
commit_then(
cx,
tx,
written,
|change| async move {
discard_uploads(cx, resource, &change).await;
crate::resource::run_after_commit::<R>(cx, change).await;
for hook in also {
hook(cx).await;
}
},
note,
failure,
&resource.url,
)
.await
}
pub(crate) async fn end_changed_credentials<M: Send + Sync + 'static>(
cx: &Cx,
tx: &mut dyn toasty::Executor,
change: &Change<M>,
) -> topcoat::Result<()> {
match change {
Change::Update { before, after } => {
crate::auth::end_changed_credentials(cx, tx, before, after).await
}
_ => Ok(()),
}
}
async fn keep_uploads<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
change: &Change<R::Model>,
) -> Result<(), topcoat::Error> {
let hydrate = |record| <R::Form as RecordForm>::hydrate(cx, record);
let (before, after) = match change {
Change::Create(record) => (None, hydrate(record)),
Change::Update { before, after } => (Some(hydrate(before)), hydrate(after)),
_ => return Ok(()),
};
crate::upload::keep(uploader(cx), &resource.form, before.as_ref(), &after)
.await
.map_err(crate::error::unavailable)
}
async fn discard_uploads<R: Resource>(cx: &Cx, resource: &Mounted<R>, change: &Change<R::Model>) {
let hydrate = |record| <R::Form as RecordForm>::hydrate(cx, record);
let (before, after) = match change {
Change::Update { before, after } => (vec![hydrate(before)], Some(hydrate(after))),
Change::Delete(records) if resource.soft_delete.is_none() => {
(records.iter().map(hydrate).collect(), None)
}
Change::ForceDelete(records) => (records.iter().map(hydrate).collect(), None),
_ => return,
};
crate::upload::discard(uploader(cx), &resource.form, &before, after.as_ref()).await;
}
pub(crate) async fn commit_then<'a, T, F: Future<Output = ()>>(
cx: &'a Cx,
tx: WriteTx<'_>,
written: Result<(T, Outcome), topcoat::Error>,
after: impl FnOnce(T) -> F,
note: impl Into<String>,
failure: &'static str,
home: &str,
) -> Result<BoxView<'a>, topcoat::Error> {
match written {
Ok((value, outcome)) => match tx.commit().await {
Ok(()) => {
after(value).await;
Err(redirect_after_write(cx, home, note.into(), outcome))
}
Err(error) => {
notify_write_failure(cx, failure);
Err(crate::error::unavailable(error))
}
},
Err(error) => Err(failed_write(cx, failure, error)),
}
}
pub(crate) fn declined_write(cx: &Cx, message: &crate::Text, home: &str) -> topcoat::Error {
set_notification(cx, Notification::error(message.localize(cx)));
see_other(landing_url(cx, home)).into()
}
pub(crate) fn failed_write(
cx: &Cx,
failure: &'static str,
error: topcoat::Error,
) -> topcoat::Error {
notify_write_failure(cx, failure);
crate::error::driver_failure(error, crate::error::DATABASE_UNAVAILABLE)
}
fn redirect_after_write(cx: &Cx, home: &str, note: String, outcome: Outcome) -> topcoat::Error {
let notification = outcome
.notification()
.cloned()
.unwrap_or_else(|| Notification::success(note));
set_notification(cx, notification);
let target = match outcome.landing() {
Some(landing) => match landing.url() {
Some(url) => url.to_string(),
None => {
tracing::error!(
?landing,
"an action's outcome lands where no browser should go; ignored"
);
landing_url(cx, home)
}
},
None => landing_url(cx, home),
};
see_other(target).into()
}