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::{Posted, RecordForm, decode_list},
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(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
88                .get(key)
89                .and_then(|value| decode_list(value))
90                .unwrap_or_default();
91            let removed: Vec<String> = held
92                .iter()
93                .filter(|key| !keys.contains(key))
94                .cloned()
95                .collect();
96            let added: Vec<String> = keys.into_iter().filter(|key| !held.contains(key)).collect();
97            join.unlink(&record, &removed, &mut *ex)
98                .await
99                .map_err(|error| -> topcoat::Error { error.into() })?;
100            join.link(&record, &added, &mut *ex)
101                .await
102                .map_err(|error| -> topcoat::Error { error.into() })?;
103        }
104    }
105    // Build the execution future before awaiting: the builder itself is not
106    // known to be `Send`, the future `exec_update` returns is.
107    {
108        let pending = posted
109            .into_update(&mut record)
110            .map(|update| <R::Form as RecordForm>::exec_update(update, &mut *ex));
111        if let Some(pending) = pending {
112            pending
113                .await
114                .map_err(|error| -> topcoat::Error { error.into() })?;
115        }
116    }
117    Ok(record)
118}
119
120/// The form key the record form binds `field` to.
121fn form_key<R: Resource>(resource: &Mounted<R>, field: <R::Form as RecordForm>::Field) -> &str {
122    resource
123        .fields
124        .iter()
125        .find(|claim| claim.field == field)
126        .and_then(|claim| claim.keys.first())
127        .map_or("", String::as_str)
128}
129
130/// The join model behind the many-to-many field `field`.
131///
132/// # Errors
133///
134/// A declaration error: no `Db` in `cx`, or a field the mount would have refused.
135fn join_table<R: Resource>(
136    cx: &Cx,
137    resource: &Mounted<R>,
138    field: <R::Form as RecordForm>::Field,
139) -> Result<JoinTable> {
140    let key = form_key(resource, field);
141    let schema = AppSchema::of(cx).ok_or_else(|| {
142        crate::error::declaration(crate::DeclarationErrorKind::MissingDb.to_string())
143    })?;
144    JoinTable::of::<R::Model>(&schema, key).map_err(|fault| {
145        crate::error::declaration(
146            crate::DeclarationError::of::<R>(
147                crate::Site::Form,
148                crate::DeclarationErrorKind::ManyToMany {
149                    field: key.to_string(),
150                    fault,
151                },
152            )
153            .to_string(),
154        )
155    })
156}
157
158/// `record` as the form spells it, reloaded with the links its many-to-many fields hold.
159async fn linked_now<R: Resource>(
160    cx: &Cx,
161    record: &R::Model,
162    ex: &mut dyn Executor,
163) -> Result<std::collections::HashMap<String, String>> {
164    let reloaded =
165        toasty::stmt::Query::<toasty::stmt::List<R::Model>>::all().filter(pk_filter(record));
166    let reloaded = <R::Form as RecordForm>::includes()
167        .into_vec()
168        .into_iter()
169        .fold(reloaded, |query, include| query.include(include))
170        .first()
171        .exec(ex)
172        .await
173        .map_err(|error| -> topcoat::Error { error.into() })?;
174    Ok(reloaded
175        .map(|reloaded| <R::Form as RecordForm>::hydrate(cx, &reloaded))
176        .unwrap_or_default())
177}