1use 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
13pub 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
57pub 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 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 {
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
117pub(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
132pub(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
150pub(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
160async 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
202async 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
220fn 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
230fn 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
258async 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}