Skip to main content

rango_core/
model.rs

1//! Model trait and repository over the store.
2
3use std::{marker::PhantomData, sync::Arc};
4
5use axum::{extract::FromRequestParts, http::request::Parts};
6
7use crate::{
8    error::Error,
9    store::{Cells, Gather, Reader, Row, Slots, Store, StoreError, Value, Widget, Writer},
10};
11
12pub use crate::store::{
13    Action, Check, Field, Filter, Key, Link, Mass, Name, Only, Op, Order, Page, Pick, Query, Rule,
14    Run, Schema, Sort, Table, Tree,
15};
16
17/// A domain type mapped to a store table.
18pub trait Model: Clone + Send + Sync + 'static {
19    /// Table this model maps to.
20    fn table() -> Table;
21    /// Field definitions for this model's table.
22    fn fields() -> Vec<Field>;
23    /// Write all non-id values to the given writer.
24    fn write(&self, w: &mut dyn Writer);
25    /// Read a new value from the given reader.
26    fn read(r: &mut dyn Reader) -> Result<Self, StoreError>;
27    /// Write just the id values to the given writer.
28    fn write_id(&self, w: &mut dyn Writer);
29    /// Read just the id values from the given reader.
30    fn read_id(&mut self, r: &mut dyn Reader) -> Result<(), StoreError>;
31
32    /// Names of the plain cell columns.
33    fn columns() -> Vec<Name> {
34        Self::fields()
35            .into_iter()
36            .filter(|field| !field.id && !field.keyed && !field.many)
37            .map(|field| field.name)
38            .collect()
39    }
40
41    /// Names of the text columns, used for free-text search.
42    fn search() -> Vec<Name> {
43        Self::fields()
44            .into_iter()
45            .filter(|field| field.load() == Widget::Text)
46            .map(|field| field.name)
47            .collect()
48    }
49
50    /// Names of the columns the UI shows as read-only.
51    fn readonly() -> Vec<Name> {
52        Vec::new()
53    }
54
55    /// Actions the UI offers, defaulting to wipe.
56    fn actions() -> Vec<Action> {
57        vec![Action::wipe()]
58    }
59
60    /// Schema derived from this model's table and fields.
61    fn schema() -> Schema {
62        Schema {
63            table: Self::table(),
64            fields: Self::fields(),
65            rules: Vec::new(),
66        }
67    }
68
69    /// Full schema including any overriding rules.
70    fn spec() -> Schema {
71        Self::schema()
72    }
73
74    /// Cell values of this model in field order.
75    fn row(&self) -> Vec<Value> {
76        let mut slots = Slots::new();
77        self.write(&mut slots);
78        slots.values()
79    }
80
81    /// Key value of this model.
82    fn id(&self) -> Value {
83        let mut gather = Gather::new();
84        self.write_id(&mut gather);
85        gather.value()
86    }
87}
88
89/// Name of the model's key column.
90pub fn id_column<M: Model>() -> Name {
91    M::schema().key()
92}
93
94/// Key value parsed from a raw string using the model's schema.
95pub fn key<M: Model>(raw: &str) -> Value {
96    Key::parse(raw, &M::schema()).value()
97}
98
99/// Rows linked to this model through a many-to-many `via` field.
100pub async fn related<M: Model>(
101    store: &Arc<dyn Store>,
102    schemas: &[Schema],
103    model: &M,
104    field: Name,
105) -> Result<Vec<Row>, StoreError> {
106    let bad = |msg: &str| StoreError::Value(msg.into());
107    let here = M::schema();
108    let many = here
109        .fields
110        .iter()
111        .find(|entry| entry.name == field)
112        .ok_or_else(|| bad("unknown field"))?;
113    if !many.many {
114        return Err(StoreError::Unsupported("single field needs filter".into()));
115    }
116    let (through_table, mine, theirs) = match &many.link {
117        Some(Link::Via(through, mine, theirs)) => (*through, *mine, *theirs),
118        _ => return Err(bad("many field needs via")),
119    };
120    let through = schemas
121        .iter()
122        .find(|spec| spec.table == through_table)
123        .ok_or_else(|| bad("unknown through"))?;
124    let their = through
125        .fields
126        .iter()
127        .find(|entry| entry.name == theirs)
128        .ok_or_else(|| bad("unknown through column"))?;
129    let (target_table, target_col) = match &their.link {
130        Some(Link::To(table, name)) => (*table, *name),
131        _ => return Err(bad("through column needs references")),
132    };
133    let target = schemas
134        .iter()
135        .find(|spec| spec.table == target_table)
136        .ok_or_else(|| bad("unknown target"))?;
137    store.define(through).await?;
138    store.define(target).await?;
139    let links = store
140        .scan_query(
141            through,
142            &Query {
143                tree: Tree::Leaf(Filter {
144                    field: mine,
145                    op: Op::Eq,
146                    value: model.id(),
147                }),
148                sort: Vec::new(),
149                page: Page::all(),
150                only: Only::Some(vec![theirs]),
151                mass: None,
152            },
153        )
154        .await?;
155    let mut ids = Vec::new();
156    for row in &links {
157        if let Some(id) = row.get(0)
158            && *id != Value::Null
159            && !ids.contains(id)
160        {
161            ids.push(id.clone());
162        }
163    }
164    if ids.is_empty() {
165        return Ok(Vec::new());
166    }
167    let leaves = ids
168        .into_iter()
169        .map(|id| {
170            Tree::Leaf(Filter {
171                field: target_col,
172                op: Op::Eq,
173                value: id,
174            })
175        })
176        .collect();
177    store
178        .scan_query(
179            target,
180            &Query {
181                tree: Tree::Or(leaves),
182                sort: vec![Sort {
183                    field: target.key(),
184                    order: Order::Asc,
185                }],
186                page: Page::all(),
187                only: Only::All,
188                mass: None,
189            },
190        )
191        .await
192}
193
194/// Full CRUD access to one model over a store; usable as an axum extractor.
195pub struct Repository<M = ()> {
196    store: Arc<dyn Store>,
197    marker: PhantomData<M>,
198}
199
200fn pairs<M: Model>(model: &M) -> Vec<(Name, Value)> {
201    let mut values = model.row().into_iter();
202    M::fields()
203        .into_iter()
204        .filter(|field| !field.id && !field.many)
205        .map(|field| {
206            let value = values.next().unwrap_or(Value::Null);
207            let value = match value {
208                Value::Null => field.initial(),
209                other => other,
210            };
211            (field.name, value)
212        })
213        .collect()
214}
215
216impl<M: Model> Repository<M> {
217    /// Repository over the given store.
218    pub fn new(store: Arc<dyn Store>) -> Self {
219        Self {
220            store,
221            marker: PhantomData,
222        }
223    }
224
225    async fn ensure(&self) -> Result<(), StoreError> {
226        self.store.define(&M::schema()).await
227    }
228
229    /// Insert the model and store its generated key.
230    pub async fn save(&self, model: &mut M) -> Result<(), StoreError> {
231        self.save_many(std::slice::from_mut(model)).await
232    }
233
234    /// Insert a batch of models in one call.
235    pub async fn save_many(&self, models: &mut [M]) -> Result<(), StoreError> {
236        if models.is_empty() {
237            return Ok(());
238        }
239        self.ensure().await?;
240        let batch = models.iter().map(pairs).collect::<Vec<_>>();
241        let keys = self.store.create(&M::schema(), &batch).await?;
242        for (model, key) in models.iter_mut().zip(keys) {
243            let value = key.value();
244            model.read_id(&mut Cells::new(std::slice::from_ref(&value)))?;
245        }
246        Ok(())
247    }
248
249    /// Load one model by key value.
250    pub async fn get(&self, id: &Value) -> Result<Option<M>, StoreError> {
251        let schema = M::schema();
252        let mut rows = self
253            .scan_query(&Query {
254                tree: Tree::Leaf(Filter {
255                    field: schema.key(),
256                    op: Op::Eq,
257                    value: id.clone(),
258                }),
259                sort: vec![Sort {
260                    field: schema.key(),
261                    order: Order::Asc,
262                }],
263                page: Page::all(),
264                only: Only::Lone,
265                mass: None,
266            })
267            .await?;
268        Ok(rows.pop())
269    }
270
271    /// All models ordered by key.
272    pub async fn all(&self) -> Result<Vec<M>, StoreError> {
273        let schema = M::schema();
274        self.scan_query(&Query {
275            tree: Tree::And(Vec::new()),
276            sort: vec![Sort {
277                field: schema.key(),
278                order: Order::Asc,
279            }],
280            page: Page::all(),
281            only: Only::All,
282            mass: None,
283        })
284        .await
285    }
286
287    /// Models whose given field equals the value.
288    pub async fn filter(&self, field: Name, value: &Value) -> Result<Vec<M>, StoreError> {
289        let schema = M::schema();
290        self.scan_query(&Query {
291            tree: Tree::Leaf(Filter {
292                field,
293                op: Op::Eq,
294                value: value.clone(),
295            }),
296            sort: vec![Sort {
297                field: schema.key(),
298                order: Order::Asc,
299            }],
300            page: Page::all(),
301            only: Only::All,
302            mass: None,
303        })
304        .await
305    }
306
307    /// All models with the given sort applied.
308    pub async fn ordered(&self, sort: Sort) -> Result<Vec<M>, StoreError> {
309        self.scan_query(&Query {
310            tree: Tree::And(Vec::new()),
311            sort: vec![sort],
312            page: Page::all(),
313            only: Only::All,
314            mass: None,
315        })
316        .await
317    }
318
319    /// Models matched by the query, refusing projected rows.
320    pub async fn scan_query(&self, query: &Query) -> Result<Vec<M>, StoreError> {
321        if matches!(query.only, Only::Some(_)) {
322            return Err(StoreError::Unsupported("projected rows need rows()".into()));
323        }
324        let rows = self.rows(query).await?;
325        rows.iter().map(|row| M::read(&mut row.cells())).collect()
326    }
327
328    /// Raw rows matched by the query.
329    pub async fn rows(&self, query: &Query) -> Result<Vec<Row>, StoreError> {
330        self.ensure().await?;
331        self.store.scan_query(&M::schema(), query).await
332    }
333
334    /// Count of rows matched by the query.
335    pub async fn total_query(&self, query: &Query) -> Result<usize, StoreError> {
336        self.ensure().await?;
337        self.store.total_query(&M::schema(), query).await
338    }
339
340    /// Aggregate value computed by the query's mass action.
341    pub async fn mass(&self, query: &Query) -> Result<Value, StoreError> {
342        self.ensure().await?;
343        self.store.mass(&M::schema(), query).await
344    }
345
346    /// Replace the row for this model's key.
347    pub async fn update(&self, model: &M) -> Result<(), StoreError> {
348        self.ensure().await?;
349        self.store
350            .replace(&M::schema(), &Key::of(&model.id())?, &pairs(model))
351            .await
352    }
353
354    /// Delete the row with the given key value.
355    pub async fn delete(&self, id: &Value) -> Result<(), StoreError> {
356        self.ensure().await?;
357        self.store.remove(&M::schema(), &Key::of(id)?).await
358    }
359}
360
361impl<M: Model> FromRequestParts<()> for Repository<M> {
362    type Rejection = Error;
363
364    async fn from_request_parts(parts: &mut Parts, _state: &()) -> Result<Self, Self::Rejection> {
365        let store = parts
366            .extensions
367            .get::<Arc<dyn Store>>()
368            .cloned()
369            .ok_or_else(|| Error::Server("no store configured".into()))?;
370        Ok(Self::new(store))
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use crate::store::Storable;
378
379    #[derive(Clone)]
380    struct Post {
381        id: i64,
382        title: String,
383    }
384
385    impl Model for Post {
386        fn table() -> Table {
387            Table("posts")
388        }
389
390        fn fields() -> Vec<Field> {
391            vec![Field::id(), Field::str("title")]
392        }
393
394        fn write(&self, w: &mut dyn Writer) {
395            Storable::put(&self.title, w);
396        }
397
398        fn read(r: &mut dyn Reader) -> Result<Self, StoreError> {
399            Ok(Self {
400                id: Storable::take(r)?,
401                title: Storable::take(r)?,
402            })
403        }
404
405        fn write_id(&self, w: &mut dyn Writer) {
406            Storable::put(&self.id, w);
407        }
408
409        fn read_id(&mut self, r: &mut dyn Reader) -> Result<(), StoreError> {
410            self.id = Storable::take(r)?;
411            Ok(())
412        }
413    }
414
415    #[derive(Clone)]
416    struct Product {
417        sku: String,
418        price: rust_decimal::Decimal,
419    }
420
421    impl Model for Product {
422        fn table() -> Table {
423            Table("products")
424        }
425
426        fn fields() -> Vec<Field> {
427            vec![
428                Field::key::<String>("sku"),
429                Field::cell::<rust_decimal::Decimal>("price"),
430            ]
431        }
432
433        fn write(&self, w: &mut dyn Writer) {
434            Storable::put(&self.sku, w);
435            Storable::put(&self.price, w);
436        }
437
438        fn read(r: &mut dyn Reader) -> Result<Self, StoreError> {
439            Ok(Self {
440                sku: Storable::take(r)?,
441                price: Storable::take(r)?,
442            })
443        }
444
445        fn write_id(&self, w: &mut dyn Writer) {
446            Storable::put(&self.sku, w);
447        }
448
449        fn read_id(&mut self, r: &mut dyn Reader) -> Result<(), StoreError> {
450            self.sku = Storable::take(r)?;
451            Ok(())
452        }
453    }
454
455    #[test]
456    fn spec() {
457        let spec = Post::spec();
458        assert_eq!(spec.table, Table("posts"));
459        assert_eq!(spec.rules, Vec::new());
460        assert_eq!(Post::spec().key(), Name("id"));
461        assert_eq!(Product::spec().key(), Name("sku"));
462        assert_eq!(Post::columns(), vec![Name("title")]);
463        assert_eq!(Post::search(), vec![Name("title")]);
464    }
465
466    #[test]
467    fn sorts() {
468        let schema = Post::spec();
469        assert_eq!(
470            Sort::parse("title", &schema),
471            Sort {
472                field: Name("title"),
473                order: Order::Asc,
474            }
475        );
476        assert_eq!(
477            Sort::parse("-title", &schema),
478            Sort {
479                field: Name("title"),
480                order: Order::Desc,
481            }
482        );
483        assert_eq!(
484            Sort::parse("junk", &schema),
485            Sort {
486                field: Name("id"),
487                order: Order::Asc,
488            }
489        );
490    }
491
492    #[test]
493    fn keys() {
494        assert_eq!(Key::parse("7", &Post::spec()).value(), Value::int(7));
495        assert_eq!(Key::parse("7", &Product::spec()).value(), Value::str("7"));
496        assert_eq!(Key::parse("x", &Post::spec()).value(), Value::str("x"));
497    }
498
499    #[tokio::test]
500    async fn gets() {
501        let path =
502            std::env::temp_dir().join(format!("rango-test-{}-gets.sqlite", std::process::id()));
503        let _ = std::fs::remove_file(&path);
504        let store = crate::store::sqlite::open(&path).await.unwrap();
505        let repo = Repository::<Post>::new(store);
506        let mut post = Post {
507            id: 0,
508            title: "hello".into(),
509        };
510        repo.save(&mut post).await.unwrap();
511        let found = repo.get(&Value::int(post.id)).await.unwrap().unwrap();
512        assert_eq!(found.title, "hello");
513        assert!(repo.get(&Value::int(999)).await.unwrap().is_none());
514        let projected = repo
515            .scan_query(&Query {
516                tree: Tree::And(Vec::new()),
517                sort: Vec::new(),
518                page: Page::all(),
519                only: Only::Some(vec![Name("title")]),
520                mass: None,
521            })
522            .await;
523        assert!(matches!(projected, Err(StoreError::Unsupported(_))));
524    }
525}