1use std::collections::BTreeSet;
2use std::future::Future;
3use std::marker::PhantomData;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use teaql_core::{Entity, MutationValues, Value};
8
9use crate::{DataServiceError, GraphNode, GraphOperation, RuntimeError, UserContext};
10
11pub(crate) trait DynGraphSaver: Send + Sync {
21 fn save_graph_dyn<'a>(
22 &'a self,
23 context: &'a UserContext,
24 node: GraphNode,
25 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
26
27 fn save_ledger_dyn<'a>(
28 &'a self,
29 context: &'a UserContext,
30 node: GraphNode,
31 root: crate::EntityRuntimeState,
32 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
33}
34
35pub(crate) struct GraphSaverFor<E> {
41 _marker: PhantomData<fn() -> E>,
42}
43
44impl<E> GraphSaverFor<E> {
45 pub(crate) fn new() -> Self {
46 Self {
47 _marker: PhantomData,
48 }
49 }
50}
51
52impl<E> DynGraphSaver for GraphSaverFor<E>
53where
54 E: teaql_data_service::QueryExecutor
55 + teaql_data_service::MutationExecutor
56 + Send
57 + Sync
58 + 'static,
59{
60 fn save_graph_dyn<'a>(
61 &'a self,
62 context: &'a UserContext,
63 node: GraphNode,
64 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
65 Box::pin(async move {
66 let entity = node.entity.clone();
67 let eds = context
68 .entity_data_service::<E>(entity)
69 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
70 eds.save_graph_internal(node).await.map_err(|e| match e {
71 DataServiceError::Runtime(r) => r,
72 other => RuntimeError::Graph(other.to_string()),
73 })
74 })
75 }
76
77 fn save_ledger_dyn<'a>(
78 &'a self,
79 context: &'a UserContext,
80 mut node: GraphNode,
81 root: crate::EntityRuntimeState,
82 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
83 Box::pin(async move {
84 let entity = node.entity.clone();
85 let eds = context
86 .entity_data_service::<E>(&entity)
87 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
88 let descriptor = context.require_entity(&entity)?;
89 let id_prop = descriptor.id_property().ok_or_else(|| {
90 RuntimeError::Graph(format!("entity {entity} has no id property"))
91 })?;
92 let current_id = node
93 .values
94 .get(&id_prop.name)
95 .cloned()
96 .unwrap_or(Value::I64(0));
97 let root_key = crate::EntityKey::new(entity.clone(), current_id);
98 let was_new = root.new_keys().contains(&root_key);
99 let was_deleted = root.deleted_keys().contains(&root_key);
100 let original_version = root.get_original_version(&root_key);
101 let generated_ids = eds
102 .execute_ledger_plan_internal(root.clone())
103 .await
104 .map_err(|e| match e {
105 DataServiceError::Runtime(r) => r,
106 other => RuntimeError::Graph(other.to_string()),
107 })?;
108
109 if let Some(new_id) = generated_ids.get(&root_key) {
110 node.values.insert(id_prop.name.clone(), new_id.clone());
111 }
112 if let Some(changes) = root.current_change_set().changes().get(&root_key) {
113 for (field, value) in changes {
114 node.values.insert(field.clone(), value.clone());
115 }
116 }
117 if let Some(version_prop) = descriptor.version_property() {
118 let authoritative_version = saved_version(was_new, was_deleted, original_version);
119 if let Some(version) = authoritative_version {
120 node.values
121 .insert(version_prop.name.clone(), Value::I64(version));
122 }
123 }
124 root.clear_committed();
125 Ok(node)
126 })
127 }
128}
129
130fn saved_version(was_new: bool, was_deleted: bool, original_version: Option<i64>) -> Option<i64> {
131 if was_new {
132 Some(1)
133 } else if was_deleted {
134 original_version.map(|version| -(version.abs() + 1))
135 } else {
136 original_version.map(|version| version + 1)
137 }
138}
139
140#[cfg(test)]
141mod saved_version_tests {
142 use super::saved_version;
143
144 #[test]
145 fn create_returns_initial_version() {
146 assert_eq!(saved_version(true, false, None), Some(1));
147 }
148
149 #[test]
150 fn update_returns_incremented_version() {
151 assert_eq!(saved_version(false, false, Some(7)), Some(8));
152 }
153
154 #[test]
155 fn delete_returns_next_negative_version() {
156 assert_eq!(saved_version(false, true, Some(7)), Some(-8));
157 }
158}
159
160pub fn graph_node_from_entity<T: Entity>(
170 context: &UserContext,
171 entity: T,
172) -> Result<GraphNode, RuntimeError> {
173 let descriptor = T::entity_descriptor();
174 let dirty_fields = entity.dirty_fields();
175 let original_values = entity.original_values();
176 let is_new = entity.is_new();
177 let is_deleted = entity.is_marked_as_delete();
178 let comment = entity.get_comment();
179 let mut node = graph_node_from_values(context, &descriptor.name, entity.into_values())?;
180 node.dirty_fields = dirty_fields;
181 node.original_values = original_values.map(Into::into);
182 if is_new {
183 node.operation = GraphOperation::Create;
184 }
185 if is_deleted {
186 node.operation = GraphOperation::Remove;
187 node.relations.clear();
188 }
189 if let Some(c) = comment {
190 node.set_comment(c);
191 }
192 Ok(node)
193}
194
195fn graph_node_from_values(
199 context: &UserContext,
200 entity: &str,
201 values: MutationValues,
202) -> Result<GraphNode, RuntimeError> {
203 let descriptor = context.require_entity(entity)?;
204 let mut node = GraphNode::new(entity);
205
206 for (field, value) in values {
207 if field == "_comment" {
208 if let Value::Text(comment) = value {
209 node.set_comment(comment);
210 }
211 continue;
212 }
213 if field == "_dirty_fields" {
214 if let Value::List(fields) = value {
215 let mut dirty = BTreeSet::new();
216 for f in fields {
217 if let Value::Text(t) = f {
218 dirty.insert(t);
219 }
220 }
221 node.dirty_fields = Some(dirty);
222 }
223 continue;
224 }
225 if field == "_original_values" {
226 if let Value::Object(orig) = value {
227 node.original_values = Some(orig.into());
228 }
229 continue;
230 }
231 if field == "_is_new" {
232 if matches!(value, Value::Bool(true)) {
233 node.operation = GraphOperation::Create;
234 }
235 continue;
236 }
237 if field == "_is_deleted" {
238 if matches!(value, Value::Bool(true)) {
239 node.operation = GraphOperation::Remove;
240 }
241 continue;
242 }
243 let Some(relation) = descriptor.relation_by_name(&field) else {
244 node.values.insert(field, value);
245 continue;
246 };
247
248 match value {
249 Value::Null => {
250 node.relations.entry(field).or_default();
251 }
252 Value::Object(record) => {
253 let child =
254 graph_node_from_values(context, &relation.target_entity, record.into())?;
255 node.relations.entry(field).or_default().push(child);
256 }
257 Value::List(values) => {
258 let children = node.relations.entry(field.clone()).or_default();
259 for value in values {
260 let Value::Object(record) = value else {
261 return Err(RuntimeError::Graph(format!(
262 "relation {}.{} expects object children, got {:?}",
263 entity, field, value
264 )));
265 };
266 children.push(graph_node_from_values(
267 context,
268 &relation.target_entity,
269 record.into(),
270 )?);
271 }
272 }
273 other => {
274 return Err(RuntimeError::Graph(format!(
275 "relation {}.{} expects object/list/null, got {:?}",
276 entity, field, other
277 )));
278 }
279 }
280 }
281
282 Ok(node)
283}
284
285fn merge_relation_mutations_into_root(
286 root: &crate::EntityRuntimeState,
287 node: &GraphNode,
288) -> Result<(), RuntimeError> {
289 for children in node.relations.values() {
290 for child in children {
291 let id = child.values.get("id").cloned().ok_or_else(|| {
292 RuntimeError::Graph(format!(
293 "related mutation {} is missing its id",
294 child.entity
295 ))
296 })?;
297 let key = crate::EntityKey::new(child.entity.clone(), id);
298
299 match child.operation {
300 GraphOperation::Create => {
301 root.mark_as_new(key.clone());
302 for (field, value) in &child.values {
303 root.set(key.clone(), field, value.clone());
304 }
305 }
306 GraphOperation::Upsert => {
307 if let Some(fields) = &child.dirty_fields {
308 for field in fields {
309 if let Some(value) = child.values.get(field) {
310 root.set(key.clone(), field, value.clone());
311 }
312 }
313 }
314 }
315 GraphOperation::Remove => root.mark_as_delete(key.clone()),
316 GraphOperation::Reference => {}
317 }
318
319 if let Some(version) = child
320 .original_values
321 .as_ref()
322 .and_then(|values| values.get("version"))
323 .and_then(Value::try_i64)
324 {
325 root.set_original_version(key, version);
326 }
327 merge_relation_mutations_into_root(root, child)?;
328 }
329 }
330 Ok(())
331}
332
333pub trait AuditedSaveExt {
346 type Entity;
347
348 fn save<'a>(
349 self,
350 context: &'a UserContext,
351 ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>>;
352}
353
354impl<T> AuditedSaveExt for teaql_core::Audited<T>
355where
356 T: Entity + Send + 'static,
357{
358 type Entity = T;
359
360 fn save<'a>(
361 self,
362 context: &'a UserContext,
363 ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>> {
364 Box::pin(async move {
365 let entity_name = T::entity_descriptor().name;
366 let entity = self.into_entity(); let node = graph_node_from_entity(context, entity)?;
368 let saver = context
369 .require_resource::<Arc<dyn DynGraphSaver>>()
370 .map_err(|e| {
371 RuntimeError::Graph(format!(
372 "no DynGraphSaver registered — did you call register_executor()? ({})",
373 e
374 ))
375 })?;
376 let saved = saver.save_graph_dyn(context, node).await?;
377 T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
378 .map_err(|e| RuntimeError::Graph(e.to_string()))
379 })
380 }
381}
382
383#[doc(hidden)]
390pub async fn save_audited_ledger_entity<T>(
391 audited: teaql_core::Audited<T>,
392 context: &UserContext,
393) -> Result<T, RuntimeError>
394where
395 T: crate::LedgerEntity + Send + 'static,
396{
397 let entity_name = T::entity_descriptor().name;
398 let entity = audited.into_entity();
399 let root = entity.entity_runtime_state();
400 let node = graph_node_from_entity(context, entity)?;
401 let saver = context
402 .require_resource::<Arc<dyn DynGraphSaver>>()
403 .map_err(|e| {
404 RuntimeError::Graph(format!(
405 "no DynGraphSaver registered — did you call register_executor()? ({e})"
406 ))
407 })?;
408
409 if let Some(root) = root {
410 merge_relation_mutations_into_root(&root, &node)?;
411 let has_ledger_changes = !root.current_change_set().changes().is_empty()
412 || !root.deleted_keys().is_empty()
413 || !root.new_keys().is_empty();
414 if has_ledger_changes {
415 let saved = saver.save_ledger_dyn(context, node, root).await?;
416 return T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
417 .map_err(|e| RuntimeError::Graph(e.to_string()));
418 }
419 }
420
421 let saved = saver.save_graph_dyn(context, node).await?;
422 T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
423 .map_err(|e| RuntimeError::Graph(e.to_string()))
424}