use std::{any::TypeId, sync::Arc};
use toasty::stmt::{Expr, IntoExpr, List, Path};
use topcoat::{Result, context::Cx};
use super::{ErasedInput, InputResult, InputSpec, Resource, can, scoped_query};
use crate::{
BoxFuture,
form::{FieldError, FormScalar, FormValues},
naming::{sentence_case, type_short_name},
policy::Ability,
schema::{Field, Schema},
toasty_compat::{
join::{JoinTable, linked_to},
model::{self, AppSchema, ModelPath},
pk,
},
};
pub struct Relation<P> {
pub(crate) child: TypeId,
pub(crate) child_name: &'static str,
pub(crate) label: Option<String>,
pub(crate) kind: RelationKind<P>,
bind: BindFn<P>,
pub(crate) misdeclared: Option<crate::DeclarationErrorKind>,
}
pub(crate) enum RelationKind<P> {
HasMany { foreign_key: String },
ManyToMany(Links<P>),
}
type BindFn<P> = Arc<dyn Fn(&P) -> (Expr<bool>, String) + Send + Sync>;
pub(crate) struct Links<P> {
pub(crate) field: String,
pub(crate) input: InputSpec,
pub(crate) attach: LinkFn<P, String>,
pub(crate) detach: LinkFn<P, Vec<String>>,
}
type LinkFn<P, K> = Arc<
dyn for<'a> Fn(&'a Cx, &'a P, K, &'a mut dyn toasty::Executor) -> BoxFuture<'a, Result<()>>
+ Send
+ Sync,
>;
const ATTACH_KEY: &str = "record";
pub trait ForeignKey<K>:
sealed::Sealed<K> + IntoExpr<Self> + FormScalar + Send + Sync + 'static
{
}
impl<K> ForeignKey<K> for K where K: IntoExpr<K> + FormScalar + Send + Sync + 'static {}
impl<K> ForeignKey<K> for Option<K> where
Option<K>: IntoExpr<Self> + FormScalar + Send + Sync + 'static
{
}
mod sealed {
pub trait Sealed<K> {}
impl<K> Sealed<K> for K {}
impl<K> Sealed<K> for Option<K> {}
}
impl<P> Relation<P>
where
P: toasty::schema::Model + IntoExpr<P> + 'static,
{
pub fn has_many<C>(foreign_key: Path<C::Model, impl ForeignKey<P::PrimaryKey>>) -> Self
where
C: Resource,
{
Self::bound_by::<C, _>(foreign_key)
}
fn bound_by<C, T>(foreign_key: Path<C::Model, T>) -> Self
where
C: Resource,
T: ForeignKey<P::PrimaryKey>,
{
let (column, misdeclared) = match model::field::<C::Model>(&ModelPath::of(&foreign_key)) {
Ok(field) => (field.name, None),
Err(error) => (String::new(), Some(error)),
};
Self {
child: TypeId::of::<C>(),
child_name: std::any::type_name::<C>(),
label: None,
kind: RelationKind::HasMany {
foreign_key: column,
},
misdeclared,
bind: Arc::new(move |owner| {
let scope = foreign_key.clone().eq(pk::pk_expr::<P, T>(owner));
(scope, pk::pk_text(owner))
}),
}
}
pub fn belongs_to_many<C>(via: impl Into<Path<P, List<C::Model>>>) -> Self
where
C: Resource,
P: Send + Sync,
{
let via: Path<P, List<C::Model>> = via.into();
let (field, misdeclared) = match model::field::<P>(&ModelPath::of(&via)) {
Ok(field) => (field.name, None),
Err(error) => (String::new(), Some(error)),
};
let attach_via = via.clone();
let attach_field = field.clone();
let detach_via = via.clone();
let detach_field = field.clone();
Self {
child: TypeId::of::<C>(),
child_name: std::any::type_name::<C>(),
label: None,
kind: RelationKind::ManyToMany(Links {
field,
input: InputSpec {
schema: attach_schema::<C>,
takes_input: true,
parse: parse_attach,
},
attach: Arc::new(move |cx, owner, key, ex| {
let (via, field) = (attach_via.clone(), attach_field.clone());
Box::pin(async move { attach::<P, C>(cx, owner, via, &field, key, ex).await })
}),
detach: Arc::new(move |cx, owner, keys, ex| {
let (via, field) = (detach_via.clone(), detach_field.clone());
Box::pin(async move { detach::<P, C>(cx, owner, via, &field, keys, ex).await })
}),
}),
misdeclared,
bind: Arc::new(move |owner| (linked_to(owner, via.clone()), pk::pk_text(owner))),
}
}
}
fn attach_schema<C: Resource>() -> Schema {
Schema::new(
Field::choice_input(ATTACH_KEY)
.label(sentence_case(type_short_name::<C::Model>()))
.relationship::<C>()
.searchable(),
)
}
fn parse_attach(_cx: &Cx, values: &FormValues) -> InputResult {
match values.text(ATTACH_KEY).map(str::trim) {
Some(key) if !key.is_empty() => Ok(Box::new(key.to_string()) as ErasedInput),
_ => Err(vec![FieldError::required(ATTACH_KEY)]),
}
}
fn join_table<P: toasty::schema::Model>(cx: &Cx, field: &str) -> Result<JoinTable> {
let schema = AppSchema::of(cx).ok_or_else(|| {
crate::error::declaration(crate::DeclarationErrorKind::MissingDb.to_string())
})?;
JoinTable::of::<P>(&schema, field).map_err(|fault| {
crate::error::declaration(
crate::DeclarationErrorKind::ManyToMany {
field: field.to_string(),
fault,
}
.to_string(),
)
})
}
async fn attach<P, C>(
cx: &Cx,
owner: &P,
via: Path<P, List<C::Model>>,
field: &str,
key: String,
ex: &mut dyn toasty::Executor,
) -> Result<()>
where
P: toasty::schema::Model + IntoExpr<P>,
C: Resource,
{
let join = join_table::<P>(cx, field)?;
let Some(record) = pk::pk_eq_expr::<C::Model>(&key) else {
return Err(topcoat::router::error::not_found().into());
};
let linked = scoped_query::<C>(cx)?
.filter(record)
.filter(linked_to(owner, via))
.first()
.exec(&mut *ex)
.await
.map_err(crate::error::unavailable)?;
if linked.is_none() {
join.link(owner, &[key], ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
}
Ok(())
}
async fn detach<P, C>(
cx: &Cx,
owner: &P,
via: Path<P, List<C::Model>>,
field: &str,
keys: Vec<String>,
ex: &mut dyn toasty::Executor,
) -> Result<()>
where
P: toasty::schema::Model + IntoExpr<P>,
C: Resource,
{
let join = join_table::<P>(cx, field)?;
let ids: Vec<&str> = keys.iter().map(String::as_str).collect();
let Some(records) = pk::pk_in_expr::<C::Model>(&ids) else {
return Err(topcoat::router::error::not_found().into());
};
let linked = scoped_query::<C>(cx)?
.filter(records)
.filter(linked_to(owner, via))
.exec(&mut *ex)
.await
.map_err(crate::error::unavailable)?;
if linked.len() != keys.len() {
return Err(topcoat::router::error::not_found().into());
}
if linked
.iter()
.any(|record| !can::<C>(cx, Ability::View(record)))
{
return Err(topcoat::router::error::forbidden().into());
}
join.unlink(owner, &keys, ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })
}
impl<P> Relation<P> {
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub(crate) fn bind(&self, owner: &P) -> (Expr<bool>, String) {
(self.bind)(owner)
}
}
impl<P> Relation<P> {
pub(crate) fn links(&self) -> Option<&Links<P>> {
match &self.kind {
RelationKind::ManyToMany(links) => Some(links),
RelationKind::HasMany { .. } => None,
}
}
}
impl<P> std::fmt::Debug for Relation<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("Relation");
debug
.field("child", &self.child_name)
.field("label", &self.label);
match &self.kind {
RelationKind::HasMany { foreign_key } => debug.field("foreign_key", foreign_key),
RelationKind::ManyToMany(links) => debug.field("via", &links.field),
};
debug.finish_non_exhaustive()
}
}