Skip to main content

tablo_core/resource/
write.rs

1//! The derived record writes a resource's create and update default to.
2
3use toasty::{Executor, stmt::IntoInsert};
4use topcoat::{Result, context::Cx};
5
6use super::{Mounted, Resource, require_mounted};
7use crate::{
8    form::{FormValues, Posted, RecordForm},
9    tenancy::require_tenant,
10    toasty_compat::{join::JoinTable, model::AppSchema, pk::pk_filter},
11};
12
13/// The derived create: the form's builder, the request tenant stamped on the
14/// resource's [`Tenancy::column`](crate::Tenancy::column), executed through
15/// `ex`, then a join row linking the record to each record its many-to-many fields hold.
16///
17/// A resource whose tenant is inherited ([`Tenancy::via`](crate::Tenancy::via))
18/// has nothing to stamp.
19///
20/// # Errors
21///
22/// A tenantless request on a tenant-scoped resource (the handler answers 403 first),
23/// a `column` tenancy whose lens names no field of the model (the mount refuses it first), a
24/// resource the context's panel does not mount, or the driver's error.
25pub async fn write_create<R: Resource>(
26    cx: &Cx,
27    form: R::Form,
28    ex: &mut dyn Executor,
29) -> Result<R::Model> {
30    let resource = require_mounted::<R>(cx)?;
31    let links = form.links();
32    let mut insert = form.into_create().into_insert();
33    match resource.tenancy.column_field() {
34        Some(Ok(column)) => {
35            insert.set(column.index, toasty::stmt::Value::from(require_tenant(cx)?));
36        }
37        Some(Err(error)) => {
38            return Err(crate::error::declaration(
39                crate::DeclarationError::of::<R>(crate::Site::Tenancy, error).to_string(),
40            ));
41        }
42        None => {}
43    }
44    let record: R::Model = ex
45        .exec(insert.into())
46        .await
47        .map_err(|error| -> topcoat::Error { error.into() })?;
48    for (field, keys) in links {
49        let join = join_table(cx, &resource, field)?;
50        join.link(&record, &keys, &mut *ex)
51            .await
52            .map_err(|error| -> topcoat::Error { error.into() })?;
53    }
54    Ok(record)
55}
56
57/// The derived update: assign every named field and execute through `ex`, then link the record
58/// to the records each named many-to-many field holds, and unlink it from the others.
59///
60/// A submission that names no field writes nothing. The instance update
61/// reloads `record`, so the returned row is the written one.
62///
63/// # Errors
64///
65/// The driver's error.
66pub async fn write_update<R: Resource>(
67    cx: &Cx,
68    mut record: R::Model,
69    posted: Posted<R::Form>,
70    ex: &mut dyn Executor,
71) -> Result<R::Model> {
72    let links: Vec<_> = posted
73        .links()
74        .into_iter()
75        .filter(|(field, _)| posted.named(*field))
76        .collect();
77    if !links.is_empty() {
78        let resource = require_mounted::<R>(cx)?;
79        let mut stored = <R::Form as RecordForm>::hydrate(cx, &record);
80        for (field, keys) in links {
81            let join = join_table(cx, &resource, field)?;
82            let key = form_key(&resource, field);
83            if !stored.contains(key) {
84                // A record loaded without its links, by an app's own update: read them now.
85                stored = linked_now::<R>(cx, &record, &mut *ex).await?;
86            }
87            let held = stored.list(key).unwrap_or_default();
88            let removed: Vec<String> = held
89                .iter()
90                .filter(|key| !keys.contains(key))
91                .cloned()
92                .collect();
93            let added: Vec<String> = keys.into_iter().filter(|key| !held.contains(key)).collect();
94            join.unlink(&record, &removed, &mut *ex)
95                .await
96                .map_err(|error| -> topcoat::Error { error.into() })?;
97            join.link(&record, &added, &mut *ex)
98                .await
99                .map_err(|error| -> topcoat::Error { error.into() })?;
100        }
101    }
102    // Build the execution future before awaiting: the builder itself is not
103    // known to be `Send`, the future `exec_update` returns is.
104    {
105        let pending = posted
106            .into_update(&mut record)
107            .map(|update| <R::Form as RecordForm>::exec_update(update, &mut *ex));
108        if let Some(pending) = pending {
109            pending
110                .await
111                .map_err(|error| -> topcoat::Error { error.into() })?;
112        }
113    }
114    Ok(record)
115}
116
117/// The form key the record form binds `field` to.
118fn form_key<R: Resource>(resource: &Mounted<R>, field: <R::Form as RecordForm>::Field) -> &str {
119    resource
120        .fields
121        .iter()
122        .find(|claim| claim.field == field)
123        .and_then(|claim| claim.keys.first())
124        .map_or("", String::as_str)
125}
126
127/// The join model behind the many-to-many field `field`.
128///
129/// # Errors
130///
131/// A declaration error: no `Db` in `cx`, or a field the mount would have refused.
132fn join_table<R: Resource>(
133    cx: &Cx,
134    resource: &Mounted<R>,
135    field: <R::Form as RecordForm>::Field,
136) -> Result<JoinTable> {
137    let key = form_key(resource, field);
138    let schema = AppSchema::of(cx).ok_or_else(|| {
139        crate::error::declaration(crate::DeclarationErrorKind::MissingDb.to_string())
140    })?;
141    JoinTable::of::<R::Model>(&schema, key).map_err(|fault| {
142        crate::error::declaration(
143            crate::DeclarationError::of::<R>(
144                crate::Site::Form,
145                crate::DeclarationErrorKind::ManyToMany {
146                    field: key.to_string(),
147                    fault,
148                },
149            )
150            .to_string(),
151        )
152    })
153}
154
155/// `record` as the form spells it, reloaded with the links its many-to-many fields hold.
156async fn linked_now<R: Resource>(
157    cx: &Cx,
158    record: &R::Model,
159    ex: &mut dyn Executor,
160) -> Result<FormValues> {
161    let reloaded =
162        toasty::stmt::Query::<toasty::stmt::List<R::Model>>::all().filter(pk_filter(record));
163    let reloaded = <R::Form as RecordForm>::includes()
164        .into_vec()
165        .into_iter()
166        .fold(reloaded, |query, include| query.include(include))
167        .first()
168        .exec(ex)
169        .await
170        .map_err(|error| -> topcoat::Error { error.into() })?;
171    Ok(reloaded
172        .map(|reloaded| <R::Form as RecordForm>::hydrate(cx, &reloaded))
173        .unwrap_or_default())
174}