Skip to main content

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}