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