use std::{
collections::{HashMap, HashSet},
sync::{Arc, LazyLock, Mutex},
};
use toasty::{Deferred, Executor};
use topcoat::{Result, context::Cx};
use super::ToOneRelation;
use crate::{
BoxFuture,
schema::{OptionSource, ensure_option_access},
toasty_compat::pk,
};
pub(super) type Restrict<M> = Arc<
dyn for<'a> Fn(&'a Cx, &'a [M], &'a mut dyn Executor) -> BoxFuture<'a, Result<()>>
+ Send
+ Sync,
>;
pub(super) fn restrict_one<M, S, F>(read: fn(&M) -> &F) -> Restrict<M>
where
M: Send + Sync + 'static,
S: OptionSource,
F: ToOneRelation<S::Model> + 'static,
{
Arc::new(move |cx, rows, ex| {
Box::pin(async move {
let loaded: Vec<&S::Model> = rows
.iter()
.filter_map(|row| read(row).loaded().flatten())
.collect();
check::<S>(cx, &loaded, ex).await
})
})
}
pub(super) fn restrict_many<M, S>(read: fn(&M) -> &Deferred<Vec<S::Model>>) -> Restrict<M>
where
M: Send + Sync + 'static,
S: OptionSource,
{
Arc::new(move |cx, rows, ex| {
Box::pin(async move {
let loaded: Vec<&S::Model> = rows
.iter()
.map(read)
.filter(|records| !records.is_unloaded())
.flat_map(|records| records.get())
.collect();
check::<S>(cx, &loaded, ex).await
})
})
}
pub(super) fn shows<S: OptionSource>(cx: &Cx, record: &S::Model) -> bool {
checked::<S>(cx, crate::tenancy::tenant_id(cx))
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&pk::pk_text(record))
.copied()
.unwrap_or(false)
}
#[derive(Default)]
pub(super) struct Checked(Mutex<HashMap<String, bool>>);
#[topcoat::context::memoize]
fn checked<S: OptionSource>(cx: &Cx, _tenant: Option<crate::TenantId>) -> Arc<Checked> {
let _ = cx;
Arc::default()
}
fn warn_denied(source: &'static str) {
if first_denial(source) {
tracing::warn!(
source,
"a relation column's source refuses the request, so its cells render empty: let the \
source list for the user, or use `RelationColumn::unscoped`"
);
}
}
fn first_denial(source: &'static str) -> bool {
static DENIED: LazyLock<Mutex<HashSet<&'static str>>> = LazyLock::new(Mutex::default);
DENIED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(source)
}
const KEYS_PER_QUERY: usize = 500;
async fn check<S: OptionSource>(
cx: &Cx,
records: &[&S::Model],
ex: &mut dyn Executor,
) -> Result<()> {
let store = &checked::<S>(cx, crate::tenancy::tenant_id(cx)).0;
let lock = || {
store
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let unchecked: Vec<&S::Model> = {
let known = lock();
let mut seen = HashSet::new();
records
.iter()
.copied()
.filter(|record| {
let key = pk::pk_text(*record);
!known.contains_key(&key) && seen.insert(key)
})
.collect()
};
if unchecked.is_empty() {
return Ok(());
}
let mut answers: HashMap<String, bool> = unchecked
.iter()
.map(|record| (pk::pk_text(*record), false))
.collect();
if ensure_option_access::<S>(cx).is_err() {
warn_denied(std::any::type_name::<S>());
} else {
for chunk in unchecked.chunks(KEYS_PER_QUERY) {
let rows = S::scoped_query(cx)?
.filter(pk::pk_set_filter(chunk))
.exec(&mut *ex)
.await
.map_err(crate::error::unavailable)?;
for record in &rows {
answers.insert(pk::pk_text(record), true);
}
}
}
lock().extend(answers);
Ok(())
}
#[cfg(test)]
mod tests;