1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//! [`ResourceDef::query`]: a resource's own row scoping within its panel.
use std::sync::Arc;
use toasty::stmt::{List, Query};
use topcoat::context::Cx;
use super::{Resource, ResourceDef};
/// One [`ResourceDef::query`] narrowing, applied to the query the ones before it returned.
pub(crate) type QueryFn<M> = Arc<dyn Fn(&Cx, Query<List<M>>) -> Query<List<M>> + Send + Sync>;
impl<R: Resource> ResourceDef<R> {
/// Narrows the rows the resource serves within the panel: its own row scoping, such as row
/// visibility. Every loader reads through it: the list and its counts, the export, the
/// detail, edit and record pages, every write, relationship options and relation tables, the
/// global search, widgets and the sidebar badge. A row it leaves out does not exist for the
/// request: its pages answer 404.
///
/// ```rust
/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
/// # struct Post { #[key] #[auto] id: uuid::Uuid, author: String }
/// # struct PostResource;
/// # impl tablo_core::Resource for PostResource {
/// # type Model = Post;
/// # type Form = tablo_core::NoForm<Post>;
/// # }
/// # fn current_user(_cx: &topcoat::context::Cx) -> String { String::new() }
/// use tablo_core::Panel;
///
/// // Each author lists and opens their own posts only.
/// Panel::new("portal").resource_with::<PostResource>(|def| {
/// def.query(|cx, query| query.filter(Post::fields().author().eq(current_user(cx))))
/// });
/// ```
///
/// Each call receives the query the calls before it return, so a panel's
/// [`resource_with`](crate::Panel::resource_with) narrows what the resource's own
/// [`declare`](Resource::declare) narrows.
///
/// **Tenancy and soft deletes are not this method's job.** The framework ANDs the
/// [`tenancy`](Self::tenancy) filter and leaves out the [trashed](Self::soft_delete) records
/// after every narrowing, so none can drop them. Code outside the framework's loaders starts
/// from [`scoped_query`](crate::scoped_query), which reads through the same narrowings.
///
/// **Relations are not its job either.** The list, the export and the detail page load the
/// relations their columns declare ([`Column::includes`](crate::extend::Column::includes)):
/// a [`RelationColumn`](crate::RelationColumn) declares its own. Include a relation here
/// only when a closure no column covers reads it on every loader's rows: one the
/// [`policy`](Self::policy) reads, or one a table's `group_by` or row key reads without a
/// column including it.
///
/// A list and its export replace any ordering a narrowing sets with the table's own sort:
/// order a list with [`Table::default_sort`](crate::Table::default_sort).
///
/// # Keep unique constraints in step with the narrowed rows
///
/// The app-side unique pre-check probes through the same query, so a `#[unique]` index
/// *broader* than it is invisible: the probe misses the colliding row and the user gets a 500
/// instead of the inline "has already been taken". Scope the constraint to match —
/// `#[unique(tenant_id, email)]` — or, when the index must stay broader, probe it in
/// [`create_record`](Resource::create_record) and [`update_record`](Resource::update_record)
/// and refuse with [`WriteError::Refused`](crate::WriteError::Refused). Not checkable at
/// declaration time, since a query's filters are not introspectable; tablo#117 tracks the fix.
#[must_use]
pub fn query<F>(mut self, query: F) -> Self
where
F: Fn(&Cx, Query<List<R::Model>>) -> Query<List<R::Model>> + Send + Sync + 'static,
{
self.queries.push(Arc::new(query));
self
}
}