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, Trashed, 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 default [`Resource::delete_record`]: stamps the soft-delete column of a resource that
118/// declares one, and removes the row of any other, through the tenant-scoped query.
119pub(super) async fn delete<R: Resource>(
120    cx: &Cx,
121    record: &R::Model,
122    ex: &mut dyn Executor,
123) -> Result<()> {
124    let resource = require_mounted::<R>(cx)?;
125    if resource.soft_delete.is_some() {
126        let now = toasty::stmt::Value::from(jiff::Timestamp::now());
127        return stamp(cx, &resource, record, Trashed::Without, now, ex).await;
128    }
129    remove(cx, &resource, record, Trashed::Without, ex).await
130}
131
132/// The default [`Resource::restore_record`]: empties the soft-delete column of a trashed record.
133pub(super) async fn restore<R: Resource>(
134    cx: &Cx,
135    record: &R::Model,
136    ex: &mut dyn Executor,
137) -> Result<()> {
138    let resource = require_mounted::<R>(cx)?;
139    stamp(
140        cx,
141        &resource,
142        record,
143        Trashed::Only,
144        toasty::stmt::Value::Null,
145        ex,
146    )
147    .await
148}
149
150/// The default [`Resource::force_delete_record`]: removes a trashed record's row.
151pub(super) async fn force_delete<R: Resource>(
152    cx: &Cx,
153    record: &R::Model,
154    ex: &mut dyn Executor,
155) -> Result<()> {
156    let resource = require_mounted::<R>(cx)?;
157    remove(cx, &resource, record, Trashed::Only, ex).await
158}
159
160/// Sets `record`'s soft-delete column to `value`, if `trashed` admits it.
161///
162/// # Errors
163///
164/// A declaration error on a resource that does not soft-delete, or one whose lens names no
165/// column (the mount refuses both first), or the driver's error.
166async fn stamp<R: Resource>(
167    cx: &Cx,
168    resource: &Mounted<R>,
169    record: &R::Model,
170    trashed: Trashed,
171    value: toasty::stmt::Value,
172    ex: &mut dyn Executor,
173) -> Result<()> {
174    let index = match resource.soft_delete.as_ref().map(super::SoftDelete::field) {
175        Some(Ok((index, _))) => *index,
176        Some(Err(kind)) => {
177            return Err(crate::error::declaration(
178                crate::DeclarationError::of::<R>(crate::Site::Registration, kind.clone())
179                    .to_string(),
180            ));
181        }
182        None => {
183            return Err(crate::error::declaration(format!(
184                "resource '{}' declares no soft-delete column to restore through",
185                resource.slug
186            )));
187        }
188    };
189    let query = resource
190        .scoped_query_in(cx, trashed)?
191        .filter(pk_filter(record));
192    let mut update = toasty::stmt::Update::<toasty::stmt::List<R::Model>>::new(query);
193    update.as_untyped_mut().returning = None;
194    update.set(index, value);
195    toasty::stmt::Statement::<()>::from_untyped_stmt(update.into_untyped())
196        .exec(ex)
197        .await
198        .map_err(|error| -> topcoat::Error { error.into() })?;
199    Ok(())
200}
201
202/// Removes `record`'s row, if `trashed` admits it.
203async fn remove<R: Resource>(
204    cx: &Cx,
205    resource: &Mounted<R>,
206    record: &R::Model,
207    trashed: Trashed,
208    ex: &mut dyn Executor,
209) -> Result<()> {
210    resource
211        .scoped_query_in(cx, trashed)?
212        .filter(pk_filter(record))
213        .delete()
214        .exec(ex)
215        .await
216        .map_err(|error| -> topcoat::Error { error.into() })?;
217    Ok(())
218}
219
220/// The form key the record form binds `field` to.
221fn form_key<R: Resource>(resource: &Mounted<R>, field: <R::Form as RecordForm>::Field) -> &str {
222    resource
223        .fields
224        .iter()
225        .find(|claim| claim.field == field)
226        .and_then(|claim| claim.keys.first())
227        .map_or("", String::as_str)
228}
229
230/// The join model behind the many-to-many field `field`.
231///
232/// # Errors
233///
234/// A declaration error: no `Db` in `cx`, or a field the mount would have refused.
235fn join_table<R: Resource>(
236    cx: &Cx,
237    resource: &Mounted<R>,
238    field: <R::Form as RecordForm>::Field,
239) -> Result<JoinTable> {
240    let key = form_key(resource, field);
241    let schema = AppSchema::of(cx).ok_or_else(|| {
242        crate::error::declaration(crate::DeclarationErrorKind::MissingDb.to_string())
243    })?;
244    JoinTable::of::<R::Model>(&schema, key).map_err(|fault| {
245        crate::error::declaration(
246            crate::DeclarationError::of::<R>(
247                crate::Site::Form,
248                crate::DeclarationErrorKind::ManyToMany {
249                    field: key.to_string(),
250                    fault,
251                },
252            )
253            .to_string(),
254        )
255    })
256}
257
258/// `record` as the form spells it, reloaded with the links its many-to-many fields hold.
259async fn linked_now<R: Resource>(
260    cx: &Cx,
261    record: &R::Model,
262    ex: &mut dyn Executor,
263) -> Result<FormValues> {
264    let reloaded =
265        toasty::stmt::Query::<toasty::stmt::List<R::Model>>::all().filter(pk_filter(record));
266    let reloaded = <R::Form as RecordForm>::includes()
267        .into_vec()
268        .into_iter()
269        .fold(reloaded, |query, include| query.include(include))
270        .first()
271        .exec(ex)
272        .await
273        .map_err(|error| -> topcoat::Error { error.into() })?;
274    Ok(reloaded
275        .map(|reloaded| <R::Form as RecordForm>::hydrate(cx, &reloaded))
276        .unwrap_or_default())
277}