use std::{any::TypeId, sync::Arc};
use toasty::stmt::{Expr, IntoExpr, List, Path};
use topcoat::{Result, context::Cx};
use super::{
ErasedInput, InputResult, InputSpec, Resource, Trashed, can, scoped_query, scoped_query_in,
};
use crate::{
BoxFuture,
form::{FieldError, FormValues},
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<crate::Text>,
pub(crate) tab: bool,
pub(crate) kind: RelationKind<P>,
bind: BindFn<P>,
pub(crate) misdeclared: Option<crate::DeclarationErrorKind>,
}
pub(crate) enum RelationKind<P> {
HasMany { foreign_key: String },
HasOne { 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<K>>
+ Send
+ Sync,
>;
const ATTACH_KEY: &str = "record";
pub trait ForeignKey<M, K>: sealed::Field<M, K> {}
impl<M, K, F: sealed::Field<M, K>> ForeignKey<M, K> for F {}
mod sealed {
use toasty::stmt::{IntoExpr, Path};
use crate::{Lens, form::FormScalar};
pub trait Field<M, K> {
type Column: Column<K>;
fn into_path(self) -> Path<M, Self::Column>;
}
impl<M, K, T: Column<K>> Field<M, K> for Path<M, T> {
type Column = T;
fn into_path(self) -> Path<M, T> {
self
}
}
impl<M, K, V, T: Column<K>> Field<M, K> for Lens<M, V, T> {
type Column = T;
fn into_path(self) -> Path<M, T> {
self.into()
}
}
pub trait Column<K>: IntoExpr<Self> + FormScalar + Send + Sync + 'static {}
impl<K> Column<K> for K where K: IntoExpr<K> + FormScalar + Send + Sync + 'static {}
impl<K> Column<K> for Option<K> where Option<K>: IntoExpr<Self> + FormScalar + Send + Sync + 'static {}
}
impl<P> Relation<P>
where
P: toasty::schema::Model + IntoExpr<P> + 'static,
{
pub fn has_many<C>(foreign_key: impl ForeignKey<C::Model, P::PrimaryKey>) -> Self
where
C: Resource,
{
Self::bound_by::<C, _>(foreign_key)
}
pub fn has_one<C>(foreign_key: impl ForeignKey<C::Model, P::PrimaryKey>) -> Self
where
C: Resource,
{
let mut relation = Self::bound_by::<C, _>(foreign_key);
if let RelationKind::HasMany { foreign_key } = relation.kind {
relation.kind = RelationKind::HasOne { foreign_key };
}
relation
}
fn bound_by<C, F>(foreign_key: F) -> Self
where
C: Resource,
F: ForeignKey<C::Model, P::PrimaryKey>,
{
let foreign_key = foreign_key.into_path();
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,
tab: false,
kind: RelationKind::HasMany {
foreign_key: column,
},
misdeclared,
bind: Arc::new(move |owner| {
let scope = foreign_key.clone().eq(pk::pk_expr::<P, F::Column>(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,
tab: false,
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(crate::Text::model::<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<String>
where
P: toasty::schema::Model + IntoExpr<P>,
C: Resource,
{
let join = join_table::<P>(cx, field)?;
let (Some(key), Some(record)) = (
pk::canonical_key::<C::Model>(&key),
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, std::slice::from_ref(&key), ex)
.await
.map_err(|error| -> topcoat::Error { error.into() })?;
}
Ok(key)
}
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<Vec<String>>
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_in::<C>(cx, Trashed::With)?
.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() })?;
Ok(linked.iter().map(pk::pk_text).collect())
}
impl<P> Relation<P> {
#[must_use]
pub fn label(mut self, label: impl Into<crate::Text>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn tab(mut self) -> Self {
self.tab = true;
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 { .. } | RelationKind::HasOne { .. } => None,
}
}
pub(crate) fn has_one_key(&self) -> Option<&str> {
match &self.kind {
RelationKind::HasOne { foreign_key } => Some(foreign_key),
RelationKind::HasMany { .. } | RelationKind::ManyToMany(_) => 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 } | RelationKind::HasOne { foreign_key } => {
debug.field("foreign_key", foreign_key)
}
RelationKind::ManyToMany(links) => debug.field("via", &links.field),
};
debug.finish_non_exhaustive()
}
}