use topcoat::{
context::Cx,
router::{
Body, RouteFuture,
error::{bad_request, forbidden, not_found, see_other},
path_param_segment,
},
view::BoxView,
};
use super::{
super::{
forms::{FormChrome, parse_form_body},
gate::{gate, landing_url},
relations::{ATTACH, DETACH, linked_relation},
write::commit_write,
},
fetch::find_by_key,
mutation::{InputPage, Pending, parse_bulk_ids, read_input},
options::input_options,
};
use crate::{
db::{WriteTx, db},
notification::{Notification, set_notification},
panel::url::{action_options_url, relation_url},
policy::Ability,
resource::{Change, Links, Mounted, Resource},
topcoat_compat::async_page,
};
pub(crate) fn relation_list_action<R: Resource>(cx: &Cx, body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
let links = linked_relation(cx, &resource).ok_or_else(not_found)?;
match path_param_segment(cx, "action") {
ATTACH => attach(cx, &resource, links, body).await,
DETACH => detach(cx, &resource, links, body, None).await,
_ => Err(not_found().into()),
}
})
}
pub(crate) fn relation_record_action<R: Resource>(cx: &Cx, body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
let links = linked_relation(cx, &resource).ok_or_else(not_found)?;
if path_param_segment(cx, "action") != DETACH {
return Err(not_found().into());
}
let related = path_param_segment(cx, "related").to_string();
detach(cx, &resource, links, body, Some(related)).await
})
}
pub(crate) fn relation_action_options<R: Resource>(cx: &Cx, _body: Body) -> RouteFuture<'_> {
Box::pin(async move {
let resource = gate::<R>(cx)?;
let links = linked_relation(cx, &resource).ok_or_else(not_found)?;
if path_param_segment(cx, "action") != ATTACH {
return Err(not_found().into());
}
owner(cx, &resource, &mut db(cx)?).await?;
input_options(cx, links.input).await
})
}
async fn owner<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
ex: &mut dyn toasty::Executor,
) -> topcoat::Result<R::Model> {
let id = path_param_segment(cx, "id").to_string();
let owner = find_by_key(cx, resource, &id, ex).await?;
if !resource.can(cx, Ability::View(&owner)) || !resource.can(cx, Ability::Update(&owner)) {
return Err(forbidden().into());
}
Ok(owner)
}
async fn attach<'a, R: Resource>(
cx: &'a Cx,
resource: &Mounted<R>,
links: &Links<R::Model>,
body: Body,
) -> Result<BoxView<'a>, topcoat::Error> {
let parts = parse_form_body(cx, body).await?;
crate::csrf::verify(cx, parts.last(crate::csrf::FIELD_NAME))?;
owner(cx, resource, &mut db(cx)?).await?;
let pending = read_input(cx, ATTACH, &links.input, &parts.pairs).await?;
let id = path_param_segment(cx, "id").to_string();
let home = format!(
"{}/{}",
resource.url,
crate::topcoat_compat::href::encode_path_segment(&id)
);
let chrome = || {
let url = relation_url(&resource.url, &id, path_param_segment(cx, "relation"));
FormChrome::action(
crate::t!(cx, "tablo-relation-attach"),
crate::t!(cx, "tablo-relation-attach"),
false,
Vec::new(),
landing_url(cx, &home),
action_options_url(&url, ATTACH),
)
};
let (input, values) = match pending {
Pending::Submitted { input, values } => (input, values),
Pending::Page(page) => return page.render(cx, &links.input, chrome()).await,
Pending::Ready(_) => return Err(bad_request("attach takes a record").into()),
};
let mut db = db(cx)?;
let mut tx = WriteTx::begin(cx, &mut db)
.await
.map_err(crate::error::unavailable)?;
let owner = owner(cx, resource, &mut *tx).await?;
let errors = (links.input.schema)()
.recheck_relationships(cx, &values, &crate::form::FormValues::new(), &mut *tx)
.await;
if !errors.is_empty() {
drop(tx);
return InputPage { values, errors }
.render(cx, &links.input, chrome())
.await;
}
let key = *input
.downcast::<String>()
.expect("the attach input parses the key it posts");
let written = (links.attach)(cx, &owner, key, &mut *tx)
.await
.map(|key| (owner, key));
commit_write(
cx,
resource,
tx,
written.map(|value| (value, crate::Outcome::done())),
|(owner, key)| Change::Attach {
owner,
relation: links.field.clone(),
key,
},
crate::t!(cx, "tablo-relation-attached"),
"tablo-failed-attach",
)
.await
}
async fn detach<'a, R: Resource>(
cx: &'a Cx,
resource: &Mounted<R>,
links: &Links<R::Model>,
body: Body,
related: Option<String>,
) -> Result<BoxView<'a>, topcoat::Error> {
let parts = parse_form_body(cx, body).await?;
crate::csrf::verify(cx, parts.last(crate::csrf::FIELD_NAME))?;
let keys = match related {
Some(key) => vec![key],
None => {
let max = crate::limits::of(cx).bulk_records;
let ids = parse_bulk_ids(parts.last("ids").unwrap_or_default(), max);
if ids.is_empty() {
set_notification(
cx,
Notification::error(crate::t!(cx, "tablo-list-select-first")),
);
return Err(see_other(landing_url(cx, &resource.url)).into());
}
if ids.len() > max {
return Err(bad_request(format!("too many ids (max {max})")).into());
}
ids
}
};
let mut db = db(cx)?;
let mut tx = WriteTx::begin(cx, &mut db)
.await
.map_err(crate::error::unavailable)?;
let owner = owner(cx, resource, &mut *tx).await?;
let count = keys.len();
let written = (links.detach)(cx, &owner, keys, &mut *tx)
.await
.map(|keys| (owner, keys));
commit_write(
cx,
resource,
tx,
written.map(|value| (value, crate::Outcome::done())),
|(owner, keys)| Change::Detach {
owner,
relation: links.field.clone(),
keys,
},
crate::t!(cx, "tablo-relation-detached", count = count),
"tablo-failed-detach",
)
.await
}