1use std::{marker::PhantomData, num::NonZeroUsize, sync::Arc};
8
9use toasty::stmt::{Expr, List, OrderByExpr};
10
11use self::{column::BoxColumn, filter::BoxFilter};
12use crate::{
13 DeclarationErrorKind, Lens,
14 form::FormScalar,
15 schema::{Binding, FieldResolver},
16};
17
18mod column;
19mod export;
20mod filter;
21mod page;
22mod render;
23mod state;
24mod wiring;
25
26pub use self::{
27 column::{
28 BooleanColumn, Column, ColumnWidth, ComputedColumn, Includes, IntoColumns, TextColumn,
29 contains_expr,
30 },
31 filter::{
32 DateFilter, Filter, FilterInput, IntoFilters, QueryFilter, SelectFilter, TernaryFilter,
33 },
34 page::TablePage,
35 state::{Cursor, Sort, TableState},
36 wiring::WiredTable,
37};
38pub(crate) use self::{
39 page::{Past, row_exists_past},
40 state::{
41 ACTION_ROUTE_PARAM, ACTIONS_ROUTE_SEGMENT, BULK_DELETE_ROUTE_SEGMENT, CREATE_ROUTE_SEGMENT,
42 DASH_ROUTE_SEGMENT, DELETE_ROUTE_SEGMENT, EDIT_ROUTE_SEGMENT, RECORD_ROUTE_PARAM,
43 RETURN_PARAM, create_page_url, with_return,
44 },
45};
46
47pub(crate) type GroupKey<M> = Arc<dyn Fn(&M) -> String + Send + Sync>;
49
50pub(crate) type RowPolicy<M> = Arc<dyn Fn(&M) -> RowActions + Send + Sync>;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub(crate) struct RowActions {
58 pub view: bool,
60 pub edit: bool,
62 pub delete: bool,
64}
65
66impl RowActions {
67 pub const ALL: Self = Self {
69 view: true,
70 edit: true,
71 delete: true,
72 };
73}
74
75pub(crate) struct GroupDef<M> {
77 binding: Binding,
78 key: GroupKey<M>,
79}
80
81impl<M> Clone for GroupDef<M> {
82 fn clone(&self) -> Self {
83 Self {
84 binding: self.binding.clone(),
85 key: Arc::clone(&self.key),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
92pub(crate) struct TableChrome {
93 pub(crate) delete: bool,
95 pub(crate) edit: bool,
97 pub(crate) view: bool,
99 pub(crate) actions: bool,
101}
102
103pub(crate) struct TableAction<M> {
105 pub(crate) name: &'static str,
106 pub(crate) label: String,
107 pub(crate) row: bool,
108 pub(crate) bulk: bool,
109 pub(crate) confirm: bool,
110 pub(crate) allowed: Arc<dyn Fn(&M) -> bool + Send + Sync>,
111}
112
113pub const DEFAULT_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(25).unwrap();
115
116pub struct Table<M> {
118 columns: Vec<BoxColumn<M>>,
119 misdeclared: Vec<DeclarationErrorKind>,
121 filters: Vec<BoxFilter<M>>,
122 group_by: Option<GroupDef<M>>,
123 key: fn(&M) -> String,
125 addressable: bool,
128 page_size: NonZeroUsize,
129 hide_search: bool,
130 hide_filter_bar: bool,
131 _marker: PhantomData<M>,
132}
133
134impl<M> Clone for Table<M> {
136 fn clone(&self) -> Self {
137 Self {
138 columns: self.columns.clone(),
139 misdeclared: self.misdeclared.clone(),
140 filters: self.filters.clone(),
141 group_by: self.group_by.clone(),
142 key: self.key,
143 addressable: self.addressable,
144 page_size: self.page_size,
145 hide_search: self.hide_search,
146 hide_filter_bar: self.hide_filter_bar,
147 _marker: PhantomData,
148 }
149 }
150}
151
152impl<M> std::fmt::Debug for Table<M> {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.debug_struct("Table")
155 .field(
156 "columns",
157 &self.columns.iter().map(|c| c.name()).collect::<Vec<_>>(),
158 )
159 .field("filters", &self.filters.len())
160 .field("group_by", &self.group_by.is_some())
161 .field("page_size", &self.page_size)
162 .field("hide_search", &self.hide_search)
163 .field("hide_filter_bar", &self.hide_filter_bar)
164 .finish()
165 }
166}
167
168impl<M> Table<M> {
169 pub fn new(cols: impl IntoColumns<M>) -> Self
173 where
174 M: toasty::schema::Model + toasty::stmt::IntoExpr<M>,
175 {
176 Self {
177 columns: cols.into_columns(),
178 misdeclared: Vec::new(),
179 filters: Vec::new(),
180 group_by: None,
181 key: crate::toasty_compat::pk::pk_text::<M>,
182 addressable: !crate::toasty_compat::pk::pk_is_composite::<M>(),
183 page_size: DEFAULT_PAGE_SIZE,
184 hide_search: false,
185 hide_filter_bar: false,
186 _marker: PhantomData,
187 }
188 }
189
190 pub fn column(mut self, column: impl Column<M> + 'static) -> Self {
192 self.columns.push(Arc::new(column));
193 self
194 }
195
196 pub fn filters(mut self, filters: impl IntoFilters<M>) -> Self
198 where
199 M: toasty::schema::Model,
200 {
201 let filters = filters.into_filters();
202 self.filters = filters;
203 self
204 }
205
206 pub(crate) fn include_relations(
208 &self,
209 mut query: toasty::stmt::Query<List<M>>,
210 ) -> toasty::stmt::Query<List<M>>
211 where
212 M: toasty::schema::Model,
213 {
214 let mut seen: Vec<toasty_core::stmt::Include> = Vec::new();
215 for include in self.columns.iter().flat_map(|c| c.includes().into_vec()) {
216 if !seen.contains(&include) {
217 query = query.include(include.clone());
218 seen.push(include);
219 }
220 }
221 query
222 }
223
224 pub(crate) fn filter_expr(&self, state: &TableState) -> Option<Expr<bool>>
226 where
227 M: toasty::schema::Model,
228 {
229 let mut exprs = Vec::new();
230 for f in &self.filters {
231 if let Some(v) = state.filters.get(f.name())
232 && let Some(e) = f.to_expr(v)
233 {
234 exprs.push(e);
235 }
236 }
237 if exprs.is_empty() {
238 None
239 } else {
240 let mut iter = exprs.into_iter();
241 let first = iter.next().unwrap();
242 Some(iter.fold(first, |acc, e| acc.and(e)))
243 }
244 }
245
246 pub(crate) fn unapplied_filters(&self, state: &TableState) -> Vec<(String, String)>
248 where
249 M: toasty::schema::Model,
250 {
251 let mut out = Vec::new();
252 for (key, value) in &state.filters {
253 match self.filters.iter().find(|f| f.name() == key) {
254 None => out.push((format!("{key}:{value}"), "unknown filter".to_string())),
255 Some(f) if f.to_expr(value).is_none() && !f.is_noop_value(value) => {
256 out.push((format!("{key}:{value}"), "invalid value".to_string()))
257 }
258 Some(_) => {}
259 }
260 }
261 if state.filters_dropped {
262 out.push((
263 "dropped filters".to_string(),
264 format!(
265 "more than {}, over {} bytes, or the retired filters= form",
266 state::MAX_FILTERS,
267 state::MAX_FILTER_LEN
268 ),
269 ));
270 }
271 out.sort();
272 out
273 }
274
275 pub fn group_by<T>(mut self, lens: Lens<M, T>) -> Self
277 where
278 M: toasty::schema::Model + Send + Sync + 'static,
279 T: FormScalar + Send + Sync + 'static,
280 {
281 self.group_by = Some(GroupDef {
282 binding: Binding::of(lens.path()),
283 key: Arc::new(move |record| lens.read(record).to_form()),
284 });
285 self
286 }
287
288 fn effective_group_key(&self, state: &TableState) -> Option<GroupKey<M>> {
290 match (&self.group_by, &state.group_by) {
291 (Some(def), Some(want)) if def.binding.name() == want => Some(def.key.clone()),
292 _ => None,
293 }
294 }
295
296 pub(crate) fn normalize_state(&self, state: &TableState) -> TableState {
298 let mut out = state.clone();
299 if self.group_by.as_ref().map(|def| def.binding.name()) != out.group_by.as_deref() {
300 out.group_by = None;
301 }
302 out
303 }
304
305 pub fn paginate(mut self, per_page: usize) -> Self {
307 match NonZeroUsize::new(per_page) {
308 Some(size) => self.page_size = size,
309 None => self.misdeclared.push(DeclarationErrorKind::ZeroPageSize),
310 }
311 self
312 }
313
314 pub fn declaration_errors(&self) -> Vec<DeclarationErrorKind> {
316 let mut errors = self.misdeclared.clone();
317 errors.extend(
318 self.group_by
319 .as_ref()
320 .and_then(|def| def.binding.misdeclared()),
321 );
322 if self.columns.is_empty() {
323 errors.push(DeclarationErrorKind::NoColumns);
324 }
325 let mut seen = std::collections::HashSet::with_capacity(self.columns.len());
326 for column in &self.columns {
327 match column.misdeclared() {
328 Some(error) => errors.push(error),
329 None if !seen.insert(column.name()) => {
330 errors.push(DeclarationErrorKind::DuplicateColumn {
331 name: column.name().to_string(),
332 });
333 }
334 None => {}
335 }
336 }
337 let mut seen = std::collections::HashSet::with_capacity(self.filters.len());
338 for filter in &self.filters {
339 match filter.misdeclared() {
340 Some(error) => errors.push(error),
341 None if !seen.insert(filter.name()) => {
342 errors.push(DeclarationErrorKind::DuplicateFilter {
343 name: filter.name().to_string(),
344 });
345 }
346 None => {}
347 }
348 }
349 errors
350 }
351
352 pub fn bind(self, db: &toasty::Db) -> Self {
358 self.bind_with(&FieldResolver::of_db(db));
359 self
360 }
361
362 pub(crate) fn bind_with(&self, resolver: &FieldResolver) {
364 for column in &self.columns {
365 column.bind(resolver);
366 }
367 for filter in &self.filters {
368 filter.bind(resolver);
369 }
370 if let Some(def) = &self.group_by {
371 def.binding.bind(resolver);
372 }
373 }
374
375 pub fn page_size(&self) -> usize {
377 self.page_size.get()
378 }
379
380 pub(crate) fn key_of(&self, record: &M) -> String {
382 (self.key)(record)
383 }
384
385 pub fn hide_search(mut self) -> Self {
387 self.hide_search = true;
388 self
389 }
390
391 pub fn hide_filter_bar(mut self) -> Self {
393 self.hide_filter_bar = true;
394 self
395 }
396
397 pub(crate) fn search_expr(&self, term: &str) -> Option<Expr<bool>>
399 where
400 M: toasty::schema::Model,
401 {
402 let t = term.trim();
403 if t.is_empty() {
404 return None;
405 }
406 let mut exprs = self.columns.iter().filter_map(|c| c.search_expr(t));
407 let first = exprs.next()?;
408 Some(exprs.fold(first, |acc, e| acc.or(e)))
409 }
410
411 pub(crate) fn order_by(&self, descending: bool) -> Option<OrderByExpr>
413 where
414 M: toasty::schema::Model,
415 {
416 self.columns.iter().find_map(|c| c.order_by(descending))
417 }
418
419 fn pk_order_bys() -> Vec<OrderByExpr>
421 where
422 M: toasty::schema::Model,
423 {
424 let app_model = M::schema();
425 let Some(root) = app_model.as_root() else {
426 debug_assert!(
427 false,
428 "pk_order_bys: {} is not a root model; deterministic pagination needs its primary key",
429 std::any::type_name::<M>()
430 );
431 return Vec::new();
432 };
433 root.primary_key
434 .fields
435 .iter()
436 .map(|fid| M::path_field::<toasty::stmt::Value>(fid.index).asc())
437 .collect()
438 }
439
440 pub(crate) fn order_bys_for(&self, state: &TableState) -> Vec<OrderByExpr>
442 where
443 M: toasty::schema::Model,
444 {
445 if let Some(sort) = &state.sort
446 && let Some(col) = self
447 .columns
448 .iter()
449 .find(|c| c.is_sortable() && c.name() == sort.column)
450 && let Some(ord) = col.order_by(sort.descending)
451 {
452 return vec![ord];
453 }
454 let out: Vec<OrderByExpr> = self.order_by(false).into_iter().collect();
455 if out.is_empty() {
456 return Self::pk_order_bys();
457 }
458 out
459 }
460
461 pub(crate) fn apply_declaration(
463 &self,
464 mut query: toasty::stmt::Query<List<M>>,
465 state: &TableState,
466 ) -> toasty::stmt::Query<List<M>>
467 where
468 M: toasty::schema::Model,
469 {
470 if let Some(term) = &state.search
471 && let Some(expr) = self.search_expr(term)
472 {
473 query = query.filter(expr);
474 }
475 if let Some(expr) = self.filter_expr(state) {
476 query = query.filter(expr);
477 }
478 for ord in self.order_bys_for(state) {
479 query = query.order_by(ord);
480 }
481 query
482 }
483
484 pub(crate) fn search_enabled(&self) -> bool
486 where
487 M: toasty::schema::Model,
488 {
489 !self.hide_search && self.columns.iter().any(|c| c.is_searchable())
490 }
491
492 pub(crate) fn filter_bar_enabled(&self) -> bool {
494 !self.hide_filter_bar && !self.filters.is_empty()
495 }
496}
497
498#[cfg(test)]
499mod tests;