1use toasty::{Executor, stmt::IntoInsert};
4use topcoat::{Result, context::Cx};
5
6use super::{Mounted, Resource, Trashed, WriteError, 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
13pub async fn write_create<R: Resource>(
27 cx: &Cx,
28 form: R::Form,
29 ex: &mut dyn Executor,
30) -> Result<R::Model, WriteError<<R::Form as RecordForm>::Field>> {
31 let resource = require_mounted::<R>(cx)?;
32 let links = form.links();
33 let mut insert = form.into_create().into_insert();
34 match resource.tenancy.column_field() {
35 Some(Ok(column)) => {
36 insert.set(
37 column.index,
38 toasty::stmt::Value::from(require_tenant(cx)?.get()),
39 );
40 }
41 Some(Err(error)) => {
42 return Err(crate::error::declaration(
43 crate::DeclarationError::of::<R>(crate::Site::Tenancy, error).to_string(),
44 )
45 .into());
46 }
47 None => {}
48 }
49 let record: R::Model = ex
50 .exec(insert.into())
51 .await
52 .map_err(|error| -> topcoat::Error { error.into() })?;
53 for (field, keys) in links {
54 let join = join_table(cx, &resource, field)?;
55 join.link(&record, &keys, &mut *ex)
56 .await
57 .map_err(|error| -> topcoat::Error { error.into() })?;
58 }
59 Ok(record)
60}
61
62pub async fn write_update<R: Resource>(
72 cx: &Cx,
73 mut record: R::Model,
74 posted: Posted<R::Form>,
75 ex: &mut dyn Executor,
76) -> Result<R::Model, WriteError<<R::Form as RecordForm>::Field>> {
77 let links: Vec<_> = posted
78 .links()
79 .into_iter()
80 .filter(|(field, _)| posted.named(*field))
81 .collect();
82 if !links.is_empty() {
83 let resource = require_mounted::<R>(cx)?;
84 let mut stored = <R::Form as RecordForm>::hydrate(cx, &record);
85 for (field, keys) in links {
86 let join = join_table(cx, &resource, field)?;
87 let key = form_key(&resource, field);
88 if !stored.contains(key) {
89 stored = linked_now::<R>(cx, &record, &mut *ex).await?;
91 }
92 let held = stored.list(key).unwrap_or_default();
93 let removed: Vec<String> = held
94 .iter()
95 .filter(|key| !keys.contains(key))
96 .cloned()
97 .collect();
98 let added: Vec<String> = keys.into_iter().filter(|key| !held.contains(key)).collect();
99 join.unlink(&record, &removed, &mut *ex)
100 .await
101 .map_err(|error| -> topcoat::Error { error.into() })?;
102 join.link(&record, &added, &mut *ex)
103 .await
104 .map_err(|error| -> topcoat::Error { error.into() })?;
105 }
106 }
107 {
110 let pending = posted
111 .into_update(&mut record)
112 .map(|update| <R::Form as RecordForm>::exec_update(update, &mut *ex));
113 if let Some(pending) = pending {
114 pending
115 .await
116 .map_err(|error| -> topcoat::Error { error.into() })?;
117 }
118 }
119 Ok(record)
120}
121
122impl<R: Resource> Mounted<R> {
123 pub(crate) async fn create(
125 &self,
126 cx: &Cx,
127 form: R::Form,
128 ex: &mut dyn Executor,
129 ) -> Result<R::Model, WriteError<<R::Form as RecordForm>::Field>> {
130 let written = R::create_record(cx, form, &mut *ex).await?;
131 ensure_in_tenant(cx, self, &written, "create_record", ex).await?;
132 Ok(written)
133 }
134
135 pub(crate) async fn update(
137 &self,
138 cx: &Cx,
139 record: R::Model,
140 posted: Posted<R::Form>,
141 ex: &mut dyn Executor,
142 ) -> Result<R::Model, WriteError<<R::Form as RecordForm>::Field>> {
143 let written = R::update_record(cx, record, posted, &mut *ex).await?;
144 ensure_in_tenant(cx, self, &written, "update_record", ex).await?;
145 Ok(written)
146 }
147}
148
149async fn ensure_in_tenant<R: Resource>(
158 cx: &Cx,
159 resource: &Mounted<R>,
160 written: &R::Model,
161 record_fn: &str,
162 ex: &mut dyn Executor,
163) -> Result<()> {
164 if !resource.tenancy.is_scoped() {
165 return Ok(());
166 }
167 let query = resource
168 .tenant_scope(cx, toasty::stmt::Query::all())?
169 .filter(pk_filter(written));
170 let held = crate::toasty_compat::count(query, ex)
171 .await
172 .map_err(|error| -> topcoat::Error { error.into() })?;
173 if held == 0 {
174 return Err(crate::error::declaration(format!(
175 "resource '{}': `{record_fn}` wrote a row outside the request's tenant, so the \
176 write rolled back. An overriding `create_record` stamps the tenant by delegating \
177 to `write_create`.",
178 resource.slug
179 )));
180 }
181 Ok(())
182}
183
184pub async fn write_delete<R: Resource>(
219 cx: &Cx,
220 record: &R::Model,
221 ex: &mut dyn Executor,
222) -> Result<(), WriteError> {
223 let resource = require_mounted::<R>(cx)?;
224 if resource.soft_delete.is_some() {
225 let now = toasty::stmt::Value::from(jiff::Timestamp::now());
226 return Ok(stamp(cx, &resource, record, Trashed::Without, now, ex).await?);
227 }
228 Ok(remove(cx, &resource, record, Trashed::Without, ex).await?)
229}
230
231pub async fn write_restore<R: Resource>(
239 cx: &Cx,
240 record: &R::Model,
241 ex: &mut dyn Executor,
242) -> Result<(), WriteError> {
243 let resource = require_mounted::<R>(cx)?;
244 Ok(stamp(
245 cx,
246 &resource,
247 record,
248 Trashed::Only,
249 toasty::stmt::Value::Null,
250 ex,
251 )
252 .await?)
253}
254
255pub async fn write_force_delete<R: Resource>(
263 cx: &Cx,
264 record: &R::Model,
265 ex: &mut dyn Executor,
266) -> Result<(), WriteError> {
267 let resource = require_mounted::<R>(cx)?;
268 Ok(remove(cx, &resource, record, Trashed::Only, ex).await?)
269}
270
271async fn stamp<R: Resource>(
278 cx: &Cx,
279 resource: &Mounted<R>,
280 record: &R::Model,
281 trashed: Trashed,
282 value: toasty::stmt::Value,
283 ex: &mut dyn Executor,
284) -> Result<()> {
285 let index = match resource.soft_delete.as_ref().map(super::SoftDelete::field) {
286 Some(Ok((index, _))) => *index,
287 Some(Err(kind)) => {
288 return Err(crate::error::declaration(
289 crate::DeclarationError::of::<R>(crate::Site::Registration, kind.clone())
290 .to_string(),
291 ));
292 }
293 None => {
294 return Err(crate::error::declaration(format!(
295 "resource '{}' declares no soft-delete column to restore through",
296 resource.slug
297 )));
298 }
299 };
300 let query = resource
301 .scoped_query_in(cx, trashed)?
302 .filter(pk_filter(record));
303 let mut update = toasty::stmt::Update::<toasty::stmt::List<R::Model>>::new(query);
304 update.as_untyped_mut().returning = None;
305 update.set(index, value);
306 toasty::stmt::Statement::<()>::from_untyped_stmt(update.into_untyped())
307 .exec(ex)
308 .await
309 .map_err(|error| -> topcoat::Error { error.into() })?;
310 Ok(())
311}
312
313async fn remove<R: Resource>(
315 cx: &Cx,
316 resource: &Mounted<R>,
317 record: &R::Model,
318 trashed: Trashed,
319 ex: &mut dyn Executor,
320) -> Result<()> {
321 resource
322 .scoped_query_in(cx, trashed)?
323 .filter(pk_filter(record))
324 .delete()
325 .exec(ex)
326 .await
327 .map_err(|error| -> topcoat::Error { error.into() })?;
328 Ok(())
329}
330
331fn form_key<R: Resource>(resource: &Mounted<R>, field: <R::Form as RecordForm>::Field) -> &str {
333 resource
334 .fields
335 .iter()
336 .find(|claim| claim.field == field)
337 .and_then(|claim| claim.keys.first())
338 .map_or("", String::as_str)
339}
340
341fn join_table<R: Resource>(
347 cx: &Cx,
348 resource: &Mounted<R>,
349 field: <R::Form as RecordForm>::Field,
350) -> Result<JoinTable> {
351 let key = form_key(resource, field);
352 let schema = AppSchema::of(cx).ok_or_else(|| {
353 crate::error::declaration(crate::DeclarationErrorKind::MissingDb.to_string())
354 })?;
355 JoinTable::of::<R::Model>(&schema, key).map_err(|fault| {
356 crate::error::declaration(
357 crate::DeclarationError::of::<R>(
358 crate::Site::Form,
359 crate::DeclarationErrorKind::ManyToMany {
360 field: key.to_string(),
361 fault,
362 },
363 )
364 .to_string(),
365 )
366 })
367}
368
369async fn linked_now<R: Resource>(
371 cx: &Cx,
372 record: &R::Model,
373 ex: &mut dyn Executor,
374) -> Result<FormValues> {
375 let reloaded =
376 toasty::stmt::Query::<toasty::stmt::List<R::Model>>::all().filter(pk_filter(record));
377 let reloaded = <R::Form as RecordForm>::includes()
378 .into_vec()
379 .into_iter()
380 .fold(reloaded, |query, include| query.include(include))
381 .first()
382 .exec(ex)
383 .await
384 .map_err(|error| -> topcoat::Error { error.into() })?;
385 Ok(reloaded
386 .map(|reloaded| <R::Form as RecordForm>::hydrate(cx, &reloaded))
387 .unwrap_or_default())
388}